В этой статье я хотел бы рассказать об особенностях создания спортивного режима в разрабатываемой нашей командой игре. Я думаю данная информация окажется полезной разработчикам игр, которым предстоит подобная работа.
Коротко об игре. Разработка ведется с ноября 2020 года. Над игрой работает два человека. В качестве игрового движка используется cocos2d-x версии 3.17.2. В качестве основного редактора анимации используется Cocostudio и частично Dragonbones. Уровни создаются при помощи Tiled Map Editor.
Об особенностях использования двух редакторов анимации в игровом проекте, я писал ранее. Вот ссылки на первую часть статьи и ее продолжение.
Сюжет игры проходит в вымышленной вселенной, населенной антропоморфными зверями. Одно из развлечений жителей – телевизионное шоу, в котором участники разного социального уровня пытаются пройти все препятствия и обойти других участников.
Участники шоу-игры, как и в реальной жизни, сражаются друг с другом за деньги. У антропоморфных зверей победил капитализм, приносящий «частички свободы и независимости» кому надо, одновременно подталкивая низшие слои населения к участию в беспощадных телевизионных шоу, в которых, чтобы получить главный приз необходимо выжить.
Игра сочетает в себе такие жанры как платформер, файтинг, beat'em up, party game. Другими словами геймплей игры наполнен драками, перестрелками, движением и соперничеством.
В игре мы хотели видеть различные режимы, в которые будет интересно играть любым игрокам. По этой причине решили добавить в нее спортивный режим.
В боевых режимах целью любого бота является другой персонаж из команды соперника. И не важно, это игрок или другой бот. В спортивном режиме цель у бота совсем иная. Ему необходимо своими ударами сделать так, чтобы мяч попал в ворота или кольцо команды соперника.
Исходя из этого, мы понимали, что текущие настройки интеллекта, реализованного для боевых режимов, не совсем будут подходить под спортивный режим.
Если углубиться в небольшие подробности, интеллект реализован при помощи настроек более чем 50 основных параметров. Эти параметры могут различаться от игры или предпочтений разработчика.
Я приведу часть основных параметров, которые используются у нас в игре:
вероятность смены цели на ближайшего персонажа;
вероятность смены цели на атакующего персонажа;
вероятность использования ударов в стойке;
вероятность использования ударов в беге;
вероятность использования ударов в рывке;
вероятность использования ударов в воздухе;
вероятность использования оружия;
вероятность использования рывка;
вероятность использования бега;
вероятность использования блока, чтобы обороняться.
На основании этих параметров, в дело вступают дополнительные параметры, отвечающие за проведение серий ударов, смену направления движения, или поиск пути по которому бот будет направляться к персонажу и т.д.
После первых тестов стало очевидно, что для интеллекта персонажей в спортивном режиме необходимо использовать совсем другие настройки, чем те, которые используются в боевых режимах. А главной целью у персонажа должен быть мяч, который необходимо бить по направлению к воротам или кольцу соперника.
Изменение настроек позволило нам добиться нужного результата. Персонажи начали совершать обдуманные действия и забивать мяч в ворота.
Далее мы решили добавить в спортивный режим аналог баскетбола. В результате мы столкнулись с проблемой. Боты били мяч под кольцом и не подбрасывали его вверх.
Каждый персонаж имеет свои особенности. Чтобы ему подбросить мяч, необходимо использовать только определенные удары. Поэтому унифицированные типы ударов подходящие под боевые режимы и аналог футбола не очень подходили под аналог баскетбола.
Поэтому мы сделали дополнительную доработку интеллекта, добавив возможность персонажам использовать свои удары, при помощи которых можно подбрасывать мяч вверх. Получилось довольно хорошо. Боты стали намного чаще забивать мяч в ворота или кольцо.
Как оказалось, это была не финальная доработка, необходимая для спортивных режимов. В течение спортивного состязания, если в одной команде играло четыре и более персонажей, было слишком заметно, как звери всей толпой бегают туда-сюда за мячом. При этом пытаясь ударить его в нужную им сторону.
Чтобы исключить подобную беготню по игровому полю. Мы решили, что персонажи должны быть распределены внутри своей команды по типу игроков:
нападающий
защитник;
полузащитник.
Количество персонажей в игре может быть различным. Игрок может собрать команду от одного до одиннадцати персонажей. Поэтому классическое распределение ролей внутри команды нам не походит.
В итоге решено было использовать специальные спортивные точки. Для каждой команды были созданы по две. Одна спортивная точка в команде для нападающих, другая для защитников. Эти точки описывают ограничения персонажа на поле, тем самым объясняя ему его примерное место и модель поведения на спортивной площадке.
Теперь, при наличии в уровне спортивных точек в начале игры происходит перераспределение персонажей по их ролям внутри команды.
В команде может быть только один персонаж. В этом случае, он не должен быть ограничен какой-то определенной моделью поведения, описываемой спортивной точкой. По этой причине первый персонаж из команды, всегда становится полузащитником. Выбирая свою роль в команде, персонажи-боты могут об этом сообщить игроку.
Далее точка, имеющая наивысший приоритет задает модель поведения следующему персонажу. Обычно это точка описывающая модель поведения нападающего. Далее точка описывающая модель поведения защитника. Если в игровом уровне персонажей больше чем спортивных точек, алгоритм повторяется, пока все персонажи не распределяться внутри своей команды.
Внутри уровня для команды может не быть ни одной точки. Может быть всего одна, или точек может быть десять. Персонажи будут перераспределяться по этим точкам. Таким образом при проектировании уровня можно создать необходимую схему игры для персонажей в команде. Распределение осуществляется при помощи двух функций:
функции распределения игроков внутри команды;
// функция устанавливает персонажу настройки спортивнй точки
void JCModeRabbidBall::_setGameScheme(float dt)
{
delayScheme_ -= dt;
if (delayScheme_ > 0)
return;
Director::getInstance()->getScheduler()->unschedule(CC_SCHEDULE_SELECTOR(JCModeRabbidBall::_setGameScheme), this);
JCStatistics *statistics = this->_getStatistics();
if (!statistics)
return;
std::vector<JCSportPointSetting> sportPointSetting = statistics->getSportPointSettings();
std::sort(sportPointSetting.begin(), sportPointSetting.end(),
[](const JCSportPointSetting &sportPoint1, const JCSportPointSetting &sportPoint2)
{
return sportPoint1.priority < sportPoint2.priority;
}
);
std::vector<JCSportPointSetting> pointPlayer, pointOpponent;
float add = 0;
for (auto isPoint = sportPointSetting.begin(); isPoint != sportPointSetting.end(); ++isPoint)
{
if ((*isPoint).groupTag == JCPersonTeams::TeamTag::PLAYER)
{
pointPlayer.push_back(*isPoint);
pointPlayer.back().radiusMove += add;
}
if ((*isPoint).groupTag == JCPersonTeams::TeamTag::OPPONENT)
{
pointOpponent.push_back(*isPoint);
pointPlayer.back().radiusMove += add;
}
add += RadiusAdd;
}
size_t sizePlayer = pointPlayer.size();
size_t sizeOpponent = pointOpponent.size();
size_t playerTag = sizePlayer;
size_t opponentTag = sizeOpponent;
std::vector<std::pair<Node*, int> > armatures = statistics->getArmatures();
for (auto itPerson = armatures.begin(); itPerson != armatures.end(); ++itPerson)
{
JCSportBot *person = dynamic_cast<JCSportBot*>((*itPerson).first->getUserObject());
if (person)
{
this->_setSportPoinToPerson(sizePlayer, pointPlayer, person, JCPersonTeams::TeamTag::PLAYER, playerTag);
this->_setSportPoinToPerson(sizeOpponent, pointOpponent, person, JCPersonTeams::TeamTag::OPPONENT, opponentTag);
}
}
}
функции установки настроек спортивных точек.
// функция устанавливает персонажу настройки спортивнй точки
void JCModeRabbidBall::_setSportPoinToPerson(size_t size, const std::vector<JCSportPointSetting> &pointSetting, JCPerson *person, int groupTag, size_t &tag)
{
if (size)
{
if (tag < size)
{
if (person && person->getGroupTag() == groupTag)
{
person->setSportPointSetting(pointSetting[tag]);
person->showPersonSportTalks(pointSetting[tag].type);
if (tag)
--tag;
}
}
else if (tag)
{
person->showPersonSportTalks(JCSportTypes::Tag::HALFBACK);
--tag;
}
}
}
Результат получился ожидаемый. Боты перестали бегать по площадке взад-вперед, пытаясь догнать катящийся по полю мяч. Модель поведения бота позволяла одним персонажам находиться большую часть времени возле ворот соперника, другим возле своих ворот. Количество результативных ударов увеличилось.
Однако проделанной нами работы даже в этом случае оказалось недостаточно. Увеличилось количество авто голов. Боты, которые должны защищать свои ворота или кольцо производили удары, которыми забивали мяч в свои ворота или кольцо.
Стало понятно, что кроме модели поведения, ботам необходимо менять набор ударов в зависимости от того какую роль в команде они для себя выбрали. Подобная доработка позволила значительно снизить количество авто голов в спортивном режиме.
На данном этапе игра ботов в спортивном режиме нас устраивает. Выбравшие роль защитников, основную часть времени проводят в защите, а нападающие в нападении. Итоговый результат нашей работы можно посмотреть на прикрепленном видео.
Возможно, что-то еще мы будем улучшать в поведении ботов, но я думаю, что эти изменения будут незначительными.
В завершение приведу ссылку на страницу нашей игры в Steam. Желающие могут добавить нашу игру в список желаемого, нам будет приятно.
BugM
Жаль. Ожидалось описание решений для правильной обработки фреймдаты через сеть. Там должно быть интересно. Нужна синхронизация до кадра при типовой задержке до 100мс.
useful Автор
У нас возникла проблема с настройкой интеллекта. Я подумал, что это будет интересно. Вы описываете реализацию сетевого мультиплеера. Его мы еще не реализовали. Хотя у нас заложены его основы. Мы предварительно реализовывали запись действий персонажей. Даже при воспроизведение записанных действий сталкиваешься с проблемой синхронизации кадров анимации ударов при небольшом отклонении FPS при воспроизведении от FPS при записи. А проблемы задержки до 100мс планируется решать предсказанием событий на клиенте. Но к реализации мультиплеера и накопления материала для статьи, к сожалению еще довольно далеко.
BugM
Начав читать я понял что "спортивный режим" можно интерпретировать по разному
BioLogIn
Синхронизация до кадра - это немного устаревший подход. За последние 10 лет в файтингах де-факто стандартом стал код, поддерживающий откат состояний (rollback), который пришел на смену delay-based коду.
Есть очень приличные по качеству опен-сорс решения (прежде всего GGPO - https://github.com/pond3r/ggpo, https://en.wikipedia.org/wiki/GGPO), включающиеся в том числе в топовые коммерческие релизы в этом жанре (из последнего - SF6, MK1). Есть даже примеры крупных релизов (GG Strive), которые изначально использовали delay-based подход, но перешли, к радости их игровых сообществ, на роллбэк, добавив его патчем.
Так что необходимости изобретать тут велосипед, имхо, нет. Если, конечно, вы не автор статьи, которую мы тут обсуждаем - у них, судя по тексте, чем креативнее, тем лучше =)