Разработка консольного симулятора баттл-арены на C++ с «умными» ботами
Приветствую, 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; // ... прочие методы };Полный код проекта с детальными комментариями по каждому классу доступен в репозитории:
Буду рад обратной связи и любым предложениям по улучшению архитектуры или алгоритмов! Поскольку я еще только начинаю свой путь в программировании, советы профессионалов придутся как нельзя кстати.
Джон Уоттс снимет новый эпизод «Звёздных войн» по данным СМИ
Продажи ПК в Японии выросли более чем вдвое на фоне роста популярности компьютеров среди местных игроков
По слухам, Microsoft готовит масштабный перезапуск всей серии Halo
Руководство Obsidian пока останется прежним, несмотря на изменения со стороны Bethesda
В Insider Gaming прогнозируют, что оценки GTA VI на агрегаторах будут ниже 90 баллов из-за завышенных ожиданий игроков
СМИ сообщили новые детали об игре Physint Хидео Кодзимы и возможном бюджете Xbox в 400 млн долларов
Разработка Armed Fantasia, духовной наследницы Wild Arms, прекращена спустя несколько лет
Квест-директор Cyberpunk 2077 заявил, что успех дополнения не сотрет провальный запуск игры, но выразил гордость за Phantom Liberty