Привет, Хабр! Как‑то появилась у меня идея сделать свой симулятор бойцовского клуба, но чтобы бой был не кулачный, а с элементами интересных механик, так как я люблю фэнтези и фантастику и моими любимыми сагами являются:«Ведьмак» и «Властелин колец»(да Азог из другой книги, но это ведь одна вселенная), то я решил написать этот небольшой проектик для усвоения теории, полученной при создании таких мейнстримных консольных игр как змейка и морской бой.
Проект написан полностью на чистом С++ без применения специфических библиотек, единственная «экзотика» которая может встретиться это #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);//вывести информацию о параметрах класса
};Кто хочет более подробно разобрать код данного проекта, вот ссылка на данный проект, в описании рассказано более подробно о логике работы каждого класса:
Надеюсь вам было интересно читать данную статью и у вас появились идеи как улучшить данный проект или создать свой! Делитесь своими идеями в комментариях, пожалуйста, будет интересно почитать как можно улучшить код, поскольку я не силен в алгоритмах, да и в программировании я ещё зелёный.




















