Приветствую, SE7EN! Некоторое время назад мне пришла идея создать собственный симулятор бойцовского клуба. Вместо банальных кулачных боев захотелось добавить глубокие игровые механики. Будучи фанатом фэнтези и научной фантастики (особенно «Ведьмака» и «Властелина колец»), я решил реализовать этот небольшой проект, чтобы закрепить навыки, полученные при создании классических «Змейки» и «Морского боя».
Весь код написан на «чистом» C++ без сторонних зависимостей. Единственное исключение — библиотека <windows.h>, которая здесь используется исключительно для организации задержек, чтобы отрисовка на экране оставалась читаемой для пользователя (при желании можно заменить на кроссплатформенные альтернативы).
Инициализация и визуализация арены
Для создания игрового поля традиционно используется двумерный массив char.
const int HEIGHT = 14;
const int WIDTH = 14;
char MAP[HEIGHT][WIDTH] =
{
'#','#','#','#','#','#','#','#','#','#','#','#','#','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',
'#','#','#','#','#','#','#','#','#','#','#','#','#','#'
};
За отрисовку карты отвечает функция showMap(...), размещенная в main.cpp. На вход она принимает указатели на базовый класс Character.
void showMap(Character* ch1, Character* ch2)
{
for (int i = 0; i < HEIGHT; i++)
{
for (int j = 0; j < WIDTH; j++)
{
if (i == ch1->getPosY() && j == ch1->getPosX())
{
cout << ch1->getAppearance();
}
else if (i == ch2->getPosY() && j == ch2->getPosX())
{
cout << ch2->getAppearance();
}
else cout << MAP[i][j];
}
cout << endl;
}
}
Виртуальный базовый класс
Давайте разберем архитектуру класса Character. Пропуская тривиальные геттеры и сеттеры, сфокусируемся на функционале:
class Character { protected: string name; string feature; char appearance; int HP; int damage; string weapon; int posX, posY; public: Character(string C = "Unknown", int h = 100, int d = 10, int x = 5, int y = 5, string f = "close combat", char ch="W",string w="sword"); virtual ~Character() {}string getName() const { return name; } int getHP() const { return HP; } int getDamage() const { return damage; } char getAppearance() const { return appearance; } string getFeature() const { return feature; } string getWeapon()const { return weapon; } int getPosX()const { return posX; }; int getPosY()const { return posY; }; int setHP(int hp=0) { return HP=hp; }; void move(int dx, int dy, Character& o); bool inBorders(int d)const; bool isOccupied(int y, int x, Character& o)const; void toRun(Character& o, bool isUnderAttack); void toPursue(Character& o); bool isAllowedToAttack(Character& o)const; void showCloseAttack(Character& o)const; void animateRemoteAttack(Character& o, char symb); void showRemoteAttack(Character& o, int x, int y, char& symb); virtual void Attack(Character& obj) = 0; virtual bool isOnSight(Character& obj) = 0; virtual void Character_info(Character& obj) = 0; friend ostream& operator<<(ostream& os, Character& o);};
Метод
isOccupied(...)проверяет, не занята ли клетка другим бойцом, чтобы исключить наложение персонажей друг на друга.bool Character::isOccupied(int y, int x,Character& o) const { return o.getPosY() == y && o.getPosX() == x; }Функция
move(...)реализует логику перемещения с проверкой на коллизии. Внешние функцииmove_chиmove_ch_oppositeуправляют передачей направления.void Character::move(int dx, int dy,Character& o) { if (isOccupied(posY + dy, posX + dx, o)) { int ddx = -dx; int ddy = -dy; int newX = posX + ddx; int newY = posY + ddy; if (MAP[newY][newX] != '#') { posX = newX; posY = newY; } } else { posX += dx; posY += dy; } }Метод
inBorders(...)гарантирует, что персонаж не покинет пределы арены и не пройдет сквозь стены.bool Character::inBorders(int d) const { int newX = posX, newY = posY; switch (d) { case UP: newY--; break; case DOWN: newY++; break; case LEFT: newX--; break; case RIGHT: newX++; break; default: return false; } if (newX < 1 || newX >= WIDTH-1 || newY < 1 || newY >= HEIGHT-1) return false; if (MAP[newY][newX] == '#') return false; return true; }Логика ИИ реализована через
toRun(...)— если персонаж получает урон от более сильного врага, он пытается разорвать дистанцию.void Character::toRun(Character& o, bool isUnderAttack) { if (!isUnderAttack) return; int x_enemy = o.getPosX(), y_enemy = o.getPosY(); int dx = (posX < x_enemy) ? -1 : (posX > x_enemy) ? 1 : 0; int dy = (dx == 0) ? ((posY < y_enemy) ? -1 : (posY > y_enemy) ? 1 : 0) : 0;auto isWalkable = [](int x, int y) { return (x >= 1 && x < WIDTH - 1 && y >= 1 && y < HEIGHT - 1 && MAP[y][x] != '#'); }; if (isWalkable(posX + dx, posY + dy)) move(dx, dy, o);}
Метод
toPursue(...)заставляет агрессора преследовать жертву.void Character::toPursue(Character& o) { double dist = sqrt(pow(o.getPosX() - posX, 2) + pow(o.getPosY() - posY, 2)); if (dist <= 1) return; int dx = (posX < o.getPosX()) ? 1 : -1; int dy = (posY < o.getPosY()) ? 1 : -1; move(dx, dy, o); }Метод
isAllowedToAttackпроверяет, позволяет ли расстояние нанести удар. Порог 1.5 выбран для корректной работы с диагональными атаками (sqrt(2) ≈ 1.41).bool Character::isAllowedToAttack(Character& o)const { return sqrt(pow(posX - o.getPosX(), 2) + pow(posY - o.getPosY(), 2)) <= 1.5; }Остальные функции отрисовки, включая анимацию дальних атак, работают по схожему принципу с обновлением экрана через
system("cls"). В реализацииanimateRemoteAttack(...)есть нюансы при расчете траектории снаряда по диагонали — буду рад конструктивным советам от сообщества по оптимизации алгоритма.Производные классы
Для создания разнообразия я реализовал классы
Warrior,OrcиMagicianс уникальными характеристиками и способностями (например, магический урон или бонусы к защите).class Warrior : virtual public Character { string ArmorName; int remote_damage, defense, SwordSharpness; public: Warrior(...) : Character(...), defense(def), SwordSharpness(SS), remote_damage(rd) { HP += defense; } void Attack(Character& o) override; // ... прочие методы };Полный код проекта с детальными комментариями по каждому классу доступен в репозитории:
Буду рад обратной связи и любым предложениям по улучшению архитектуры или алгоритмов! Поскольку я еще только начинаю свой путь в программировании, советы профессионалов придутся как нельзя кстати.


