Привет, Хабр! Как‑то появилась у меня идея сделать свой симулятор бойцовского клуба, но чтобы бой был не кулачный, а с элементами интересных механик, так как я люблю фэнтези и фантастику и моими любимыми сагами являются:«Ведьмак» и «Властелин колец»(да Азог из другой книги, но это ведь одна вселенная), то я решил написать этот небольшой проектик для усвоения теории, полученной при создании таких мейнстримных консольных игр как змейка и морской бой.
Проект написан полностью на чистом С++ без применения специфических библиотек, единственная «экзотика» которая может встретиться это #include <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();//вывести отображение бойца 1 на экран } else if (i == ch2->getPosY() && j == ch2->getPosX()) { cout << ch2->getAppearance();//вывести отображение бойца 2 на экран } else cout << MAP[i][j];//вывести границы арены и незанятые клетки на экран } cout << endl; } }
Базовый виртуальный класс
Пробежимся по основному функционалу класса, не принимая во внимание геттеры и сеттеры, основная задача которых заложена в их названиях get-выдать, set-установить.
class Character { protected: string name;//имя персонажа string feature;//его особенность(преимущество) char appearance;//оторажение на экране int HP;//уровень здоровья int damage;//сколько HP снимает ближняя атака 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(...) проверяет не занята ли клетка, куда хочет шагнуть боец 1 бойцом 2.
bool Character::isOccupied(int y, int x,Character& o) const { return o.getPosY() == y&&o.getPosX()==x; }
Функция move(...) позволяет перемещать бойца по полю, проверяя каждую клетку на занятость другим бойцом. Данная функция используется в функциях, объявленных в файле main.cpp: void move_ch(int& d, Character& o, Character& check) и void move_ch_opposite(int& d, Character& o, Character& check) в которых и реализована логика выбора направления и передачи координат.
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 += 0; posY += 0; } } else { posX += dx; posY += dy; } }
Функция inBorders(...) проверяет не вышел ли боец за границы арены. Если вышел функция возвращает false, иначе true.
bool Character::inBorders(int d) const { int newX = posX; int 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(...) позволяет добавить логику побега от более сильного физически бойца. Если боец 1 сильнее бойца 2 в ближнем бою и боец 1 наносит урон бойцу 2, то боец 2 старается отдалиться от противника.
void Character::toRun(Character& o, bool isUnderAttack) { if (!isUnderAttack) return; // убегаем только после получения удара int x_enemy = o.getPosX(); int y_enemy = o.getPosY(); int x_me = posX; int y_me = posY; // Направление ОТ врага int dx = 0, dy = 0; if (x_me < x_enemy) dx = -1; // враг справа - бежим влево else if (x_me > x_enemy) dx = 1; // враг слева – бежим вправо if (dx == 0) { if (y_me < y_enemy) dy = -1; // враг снизу – бежим вверх else if (y_me > y_enemy) dy = 1; // враг сверху – бежим вниз } int newX = x_me + dx; int newY = y_me + dy; // Функция проверки проходимости (стены + границы) auto isWalkable = [](int x, int y) -> bool { return (x >= 1 && x < WIDTH - 1 && y >= 1 && y < HEIGHT - 1 && MAP[y][x] != '#'); }; if (isWalkable(newX, newY)) { move(dx, dy, o); return; } }
Функция toPursue(...) противоположна по логике функции toRun(...): более сильный боец преследует более слабого пока не сблизится с ним.
void Character::toPursue(Character& o) { int x_enemy = o.getPosX(); int y_enemy = o.getPosY(); int x_me = posX; int y_me = posY; int dist = sqrt(pow(x_enemy - x_me, 2) + pow(y_enemy - y_me, 2)); if (dist <= 1) return; // уже рядом – атакуем в основном цикле // Направление к врагу int dx = 0, dy = 0; if (x_me < x_enemy) dx = 1; else if (x_me > x_enemy) dx = -1; if (dx == 0) { if (y_me < y_enemy) dy = 1; else if (y_me > y_enemy) dy = -1; } int newX = x_me + dx; int newY = y_me + dy; // Проверка на стены bool walkable = (newX >= 1 && newX < WIDTH - 1 && newY >= 1 && newY < HEIGHT - 1 && MAP[newY][newX] != '#'); // Запрещаем занимать клетку врага if (walkable && (newX != x_enemy || newY != y_enemy)) move(dx, dy, o); }
Функция isAllowedToAttack(...) позволяет проверить достаточно ли близко подошел боец 1 для того чтобы атаковать бойца 2 в ближнем бою. Число 1.5 выбрано по причине того что sqrt(2) это приблизительно 1,41 , соответственно бить по диагонали можно.
bool Character::isAllowedToAttack(Character& o)const { int x_enemy = o.getPosX(); int y_enemy = o.getPosY(); int x_me = posX; int y_me = posY; double distance = sqrt(pow(x_me - x_enemy, 2) + pow(y_me - y_enemy, 2)); return distance <= 1.5; }
Функция showCloseAttack(...) обновляет карту и отображает бойца по которому наносится урон в виде 'X'.
void Character::showCloseAttack(Character& o) const { system("cls"); for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { if (i == posY && j == posX) { cout << getAppearance(); } else if (i == o.getPosY() && j == o.getPosX()) { cout << 'X'; } else cout << MAP[i][j]; } cout << endl; } }
Функция showRemoteAttack(...) отображает позицию снаряда в данном кадре. Сама анимация полета снаряда происходит в функции animateRemoteAttack(...).
void Character::showRemoteAttack(Character& o, int x, int y, char& symb) { for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { if (i == posY && j == posX) { cout << getAppearance(); } else if (i == o.getPosY() && j == o.getPosX()) { cout << o.getAppearance(); } else if (i == y && j == x)cout << symb; else cout << MAP[i][j]; } cout << endl; } }
Функция animateRemoteAttack(...), как было сказано выше реализует отрисовку полета снаряда. При расчете количества шагов до цели я просто вычислил длину вектора и данный способ оказался неверным, поскольку по диагонали не всегда получается целое число и при такой отрисовке снаряд отображается некорректно для выстрела по диагонали. Я надеюсь более опытные программисты подправят данную функцию и скажут о решении, которое они нашли в комментариях.
void Character::animateRemoteAttack(Character& o, char symb) { system("cls"); int x_target = o.getPosX(); int y_target = o.getPosY(); int dx = (x_target > posX) ? 1 : (x_target < posX) ? -1 : 0; int dy = (y_target > posY) ? 1 : (y_target < posY) ? -1 : 0; double realDistance = sqrt(dx * dx + dy * dy); int steps = int(realDistance); for (int i = 0; i <= steps; i++) { int cx = posX + dx * i, cy = posY + dy * i; showRemoteAttack(o, cx, cy, symb); Sleep(100); system("cls"); } showCloseAttack(o); Sleep(100); }
Остальные функции являются чисто виртуальными и реализуются в каждом производном классе в зависимости от параметров, переданных ему.
Класс Warrior
Я не буду подробно расписывать каждую функцию для дальнейших классов, просто приведу объявление для каждого класса.
class Warrior : virtual public Character { string ArmorName;//название брони int remote_damage;//урон от дальней атаки int defense;//добавляет хп в зависимости от брони int SwordSharpness;//добавляет урон в зависимости от остроты клинка public: Warrior(string cl, int hp, int dam, int x, int y, string f, char a,string w, string A = "Wolf school armor", int def = 25, int SS = 10, int rd = 10) : Character(cl, hp, dam, x, y, f, a,w), ArmorName(A), defense(def), SwordSharpness(SS),remote_damage(rd) { HP += defense; } virtual ~Warrior() {} //геттеры string getArmorName()const { return ArmorName; } int getRemote_Damage()const { return remote_damage; } int get_defense()const { return defense; } int getSwordSharpness()const { return SwordSharpness; } //функционал void Attack(Character& o)override;//виртуальная функция атаки(здесь происходит логика отнимания HP при ударе),т.к. добавляется доп.урон в зависимости от остроты клинка void Remote_Attack(Character& o);//атака на расстоянии с помощью арбалета bool isAllowedRemoteAttack(Character& o);//проверка достаточно ли расстояния для того чтобы атаковать из арбалета, если слишком близко или далеко - не атаковать bool isOnSight(Character& obj)override;//вирутальная функция позволяющая обнаружить врага в поле зрения void Character_info(Character& obj)override;//виртуальная функция, выводит информацию об атаке:кто кого атаковал,тип атаки(вблизи), сколько HP снесла атака и сколько HP осталось у противника void Character_info_remote(Character& obj);//вывести информацию об атаке:кто кого атаковал,тип атаки(на расстоянии), сколько HP снесла атака и сколько HP осталось у противника friend ostream& operator<<(ostream& os, const Warrior& o); //вывести информацию о параметрах класса };
Класс Orc
class Orc :virtual public Character { private: string race;//раса орка int buff;//доп.урон в зависимости от расы public: Orc(string cl, int hp, int dam, int x, int y, string f, char a,string w, string r="Grey orc", int b=20) :Character(cl, hp, dam, x, y, f, a,w), race(r), buff(b) { } virtual ~Orc() {} void Attack(Character& o)override;//виртуальная функция атаки(здесь происходит логика отнимания HP при ударе),т.к. добавляется доп.урон в зависимости от расы орка(физ.сила) bool isOnSight(Character& o)override;//вирутальная функция позволяющая обнаружить врага в поле зрения(каждый класс имеет свою дальность) void Character_info(Character& obj)override;//виртуальная функция, выводит информацию об атаке:кто кого атаковал,тип атаки(вблизи), сколько HP снесла атака и сколько HP осталось у противника friend ostream& operator<<(ostream& os, const Orc& o);//вывести информацию о параметрах класса };
Класс Magician
class Magician :public Character { private: string power; int power_damage; public: Magician(string cl, int hp, int dam, int x, int y, string f, char a,string w, string p = "Fire", int pd = 25) :Character(cl, hp, dam, x, y, f, a,w),power(p),power_damage(pd){} virtual ~Magician() {} void Attack(Character& o)override;//ближняя атака стальным шестом void Magic_Attack(Character& o);//атака магией на расстоянии bool isAllowedMagicAttack(Character& o);//проверка достаточно ли расстояния для того чтобы атаковать магией, если слишком близко или далеко - не атаковать bool isOnSight(Character& obj)override;//видно ли врага, если расстояние до врага меньше или равно 15.0, то true, иначе false void Character_info(Character& obj)override;//вывести информацию об атаке:кто кого атаковал,тип атаки(вблизи), сколько HP снесла атака и сколько HP осталось у противника void Character_info_remote(Character& obj)const;//вывести информацию об атаке:кто кого атаковал,тип атаки(на расстоянии), сколько HP снесла атака и сколько HP осталось у противника friend ostream& operator<<(ostream& os, Magician& o);//вывести информацию о параметрах класса };
Кто хочет более подробно разобрать код данного проекта, вот ссылка на данный проект, в описании рассказано более подробно о логике работы каждого класса:
Надеюсь вам было интересно читать данную статью и у вас появились идеи как улучшить данный проект или создать свой! Делитесь своими идеями в комментариях, пожалуйста, будет интересно почитать как можно улучшить код, поскольку я не силен в алгоритмах, да и в программировании я ещё зелёный.
code_panik
Посмотрите https://ru.wikipedia.org/wiki/Алгоритмы_построения_отрезка, например, алгоритм Брезенхэма https://ru.wikipedia.org/wiki/Алгоритм_Брезенхэма.