Существуют различные способы создать какую-нибудь особенную игру. Чаще всего разработчик для получения лучшего результата выбирает такую игру, которую он уже в состоянии написать. Сегодня мы попробуем прыгнуть выше наших голов — создадим искусственный интеллект для игры в хоккей!
Ключевым моментом будет использование рулевого поведения (steering behaviors), о котором я рассказывал в одной из своих прошлых статей (рассказ от лица автора — прим. переводчика).
Примечание Несмотря на то, что эти уроки написаны с использованием Action Script 3 и Flash, вам не должно составить труда реализовать игру в любой другой среде разработки на любом языке программирования.
Введение
Хоккей — одна из самых популярных спортивных игр. О нем написано множество статей, охватывающих и тактику игры, и поведение игроков в атаке и в защите, и такую тонкую вещь, как работа в команде. И, разумеется, искусственный интеллект. Реализация хоккея отлично подходит для демонстрации сочетания некоторых полезных приемов и методов.
Хоккей — динамичная игра. Если движения игроков будут предопределены, то играть в нее станет совсем скучно и неинтересно. Но как же нам создать динамичную игру, да при том, чтобы игроки вели себя адекватно и осознанно? Ответ прост — с помощью механики рулевого поведения.
Использование рулевого поведения направлено на создание реалистичных моделей передвижения объектов. Они основаны на простых силах, вследствие чего — чрезвычайно динамичны по своей природе. Это делает их идеальным выбором для реализации сложных и реалистичных движений, которые встречаются в футболе или в том же хоккее.
Обзор предстоящей работы
Как уже говорилось выше, хоккей — очень сложная игра. В ней присутствует огромное количество правил, нарушений и т.д. Для сокращения времени обучения, мы несколько упростим игру и сохраним лишь небольшой набор оригинальных правил этого вида спорта: у нас не будет вратарей (все игроки на катке будут двигаться), а также мы не будем учитывать всевозможные штрафы.
Ворота в нашей игре тоже будут своеобразные — сетка будет отсутствовать, а для того, чтобы забить гол, достаточно шайбе коснуться «ворот» с любой стороны. После забитого гола все игроки встают на свои позиции, шайба перемещается в центр, и через несколько секунд игра начинается заново.
Касаемо обработки шайбы: если игрок А находится с шайбой, а игрок B сталкивается с ним, то шайба переходит к игроку B, который становится недвижимым на некоторое время.
Для вывода графики я буду использовать графический движок Flixel. Однако в прилагаемом коде я буду опускать все связанное с графикой и максимально обращать ваше внимание на механику игры.
Базовые классы
Давайте начнем с основ — катка, который представляет собой прямоугольник, игроков и двух ворот. Каток имеет физические границы, поэтому ничего не выйдет за пределы поля. Хоккеист будет описываться классом Athlete
:
public class Athlete
{
private var mBoid :Boid; // контролирует физическое тело хоккеиста
private var mId :int; // уникальный идентификатор хоккеиста
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
mBoid = new Boid(thePosX, thePosY, theTotalMass);
}
public function update():void {
// очистка всех действующих сил
mBoid.steering = null;
// метод блуждания по катку
wanderInTheRink();
// главный метод обновления физического тела
mBoid.update();
}
private function wanderInTheRink() :void {
var aRinkCenter :Vector3D = getRinkCenter();
// Если расстояние до центра катка больше 80
// вернуться в центр, иначе бродить
if (Utils.distance(this, aRinkCenter) >= 80) {
mBoid.steering = mBoid.steering + mBoid.seek(aRinkCenter);
} else {
mBoid.steering = mBoid.steering + mBoid.wander();
}
}
}
Поле mBoid
является объектом класса Boid
, более подробно о котором вы можете прочитать в серии уроков про рулевое поведение. Он имеет, среди прочих элементов, вектор направления, вектор силы, а также текущее положение игрока.
Метод update()
будет вызываться каждый раз, пока запущена игра. Сейчас в этом методе очищается любое активное усилие в рулевом поведении, добавляется эффект блуждания игрока, а также вызывается метод mBoid.update()
.
Класс, ответственный за саму игру, называется PlayState
. Среди его полей есть каток, две группы хоккеистов, а также двое ворот.
public class PlayState
{
private var mAthletes :FlxGroup;
private var mRightGoal :Goal;
private var mLeftGoal :Goal;
public function create():void {
// здесь будут создаваться все игровые элементы
}
override public function update():void {
// включить коллизию всех хоккеистов с бортиками катка
collide(mRink, mAthletes);
// проверка того, что все хоккеисты в пределах катка
applyRinkContraints();
}
private function applyRinkContraints() :void {
}
}
Если мы добавим одного хоккеиста на поле, то увидим такой результат:
//
Следуй за мышью
Мышь имеет координаты на экране, а потому мы можем использовать их в качестве пункта назначения для игрока. Мы можем использовать метод arrival
объекта mBoid
. Он задает цель, которую будет преследовать игрок. Движение будет плавным, а по мере приближения скорость хоккеиста будет падать и, в конце концов, станет равна 0.
Давайте заменим блуждающий метод в классе Athlete
на движение к курсору мыши:
public class Athlete
{
// (...)
public function update():void {
// очистка всех действующих сил
mBoid.steering = null;
// игрок управляет хоккеистом,
// поэтому он будет следовать за курсором мыши
followMouseCursor();
// главный метод обновления физического тела
mBoid.update();
}
private function followMouseCursor() :void {
var aMouse :Vector3D = getMouseCursorPosition();
mBoid.steering = mBoid.steering + mBoid.arrive(aMouse, 50);
}
}
В результате мы получим возможность управлять нашим игроком мышью, а движение получится плавным и реалистичным:
//
Добавим шайбу
Шайба будет описываться классом Puck
. Самое важное здесь — метод update()
и поле mOwner
.
public class Puck
{
public var velocity :Vector3D;
public var position :Vector3D;
private var mOwner :Athlete; // хоккеист, который владеет шайбой
public function setOwner(theOwner :Athlete) :void {
if (mOwner != theOwner) {
mOwner = theOwner;
velocity = null;
}
}
public function update():void {
}
public function get owner() :Athlete { return mOwner; }
}
Следуя той же логике, что и выше, метод update()
будет выполняться каждый раз, пока запущена игра. Поле mOwner
будет хранить того хоккеиста, который владеет шайбой. Если же mOwner
равно null
, то шайба никому не принадлежит и она свободно скользит по катку.
Если же mOwner
не равно null
, то владелец у шайбы имеется. В этом случае шайба насильно ставится перед своим владельцем. Для этого используется вектор скорости хоккеиста, который также соответствует и вектору направления. Вот наглядная иллюстрация:
А вот и код:
public class Puck
{
// (...)
private function placeAheadOfOwner() :void {
var ahead :Vector3D = mOwner.boid.velocity.clone();
ahead = normalize(ahead) * 30;
position = mOwner.boid.position + ahead;
}
override public function update():void {
if (mOwner != null) {
placeAheadOfOwner();
}
}
// (...)
}
В классе PlayState
есть проверка на коллизию с шайбой. Если какой-то хоккеист перекроет шайбу, то он становится новым владельцем. Вы можете протестировать это в демо-режиме, представленном ниже:
//
Удар по шайбе
Реализуем удар по шайбе клюшкой. Независимо от того, кто является владельцем шайбы, нам нужно сымитировать удар по ней и вычислить направление этого удара. Затем отправить шайбу по найденному вектору. Вычислить этот вектор несложно:
А вот и реализация удара по шайбе:
public class Puck
{
// (...)
public function goFromStickHit(theAthlete :Athlete, theDestination :Vector3D, theSpeed :Number = 160) :void {
// установка шайбы перед хоккеистом во избежании неожиданных столкновений
placeAheadOfOwner();
// очистка владельца шайбы (т.к. был совершен удар)
setOwner(null);
// вычисление траектории шайбы
var new_velocity :Vector3D = theDestination - position;
velocity = normalize(new_velocity) * theSpeed;
}
}
В классе PlayState
метод goFromStickHit()
выполняется каждый раз, когда игрок нажимаете на экран. Координата клика используется в качестве конечной точки для удара. Результат виден ниже:
//
Добавление ИИ
До сих пор у нас был только один хоккеист на катке. Поскольку в хоккее по 6 человек на команду, а это уже целая дюжина игроков, то нам стоит задуматься о создании искусственного интеллекта для них. Да причем такого, чтобы игроки вели себя естественно и рационально.
Для этого мы будем использовать конечный автомат (FSM — finite state machine). Как было написано ранее, FSM является универсальным и полезным инструментом для реализации ИИ в играх.
Немного изменим наш класс Athlete
:
public class Athlete
{
// (...)
private var mBrain :StackFSM; // конечный автомат, контролирующий ИИ
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
// (...)
mBrain = new StackFSM();
}
// (...)
}
Поле mBrain
является экземпляром класса StackFSM
, о котором более подробно вы можете узнать из этой статьи. Он использует стек для управления состоянием ИИ. Каждое состояние описывается методом, и, когда состояние кладется в стек, оно становится активным и вызывается каждый раз при вызове основного метода update()
.
Все состояния игрока будут выполнять строго определенную функцию: взять шайбу, отобрать шайбу, патрулировать зону и т.д.
Теперь хоккеист может быть как под нашим контролем, так и под контролем ИИ. Обновим наш класс:
public class Athlete
{
// (...)
public function update():void {
// очистка всех действующих сил
mBoid.steering = null;
if (mControlledByAI) {
// хоккеист управляем ИИ
// обновление логики конечного автомата
mBrain.update();
} else {
// хоккеист управляем игроком,
// поэтому он будет следовать за курсором мыши
followMouseCursor();
}
// главный метод обновления физического тела
mBoid.update();
}
}
Если хоккеист находится под контролем искусственного интеллекта, то мы обновляем ее логику, вызывая mBrain.update()
. Если же хоккеист под управлением игрока, то логика ИИ игнорируется, а спортсмен следует за мышью.
Что касается самих состояний, посылаемых искусственному интеллекту, то мы реализуем два из них. Первое будет отвечать за подготовку игроков к матчу, т.е. они переместятся к своим стартовым позициям и будут смотреть на шайбу. Второе состояние просто будет заставлять хоккеиста стоять и следить за шайбой.
Состояние покоя
public class Athlete
{
// (...)
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) {
// (...)
// указание ИИ состояния 'idle'
mBrain.pushState(idle);
}
private function idle() :void {
var aPuck :Puck = getPuck();
stopAndlookAt(aPuck.position);
}
private function stopAndlookAt(thePoint :Vector3D) :void {
mBoid.velocity = thePoint - mBoid.position;
mBoid.velocity = normalize(mBoid.velocity) * 0.01;
}
}
На данный момент это состояние будет активным всегда. Но в дальнейшем, оно заменит собой другие состояния, например, attack
.
Метод stopAndlookAt()
высчитывает нужное направление по тому же алгоритму, по которому мы вычисляли направление удара. Вектор, начинающийся с позиции хоккеиста и заканчивающийся позицией шайбы, измеряется по формуле thePoint - mBoid.position
и используется для указания направления взгляда спортсмена.
Если применить полученный вектор к хоккеисту, то он устремиться к шайбе. Для того, чтобы он оставался на месте, мы умножаем вектор на число, близкое к нулю, т.е. на 0.01. Это удерживает спортсмена на месте, однако, смотреть он будет на шайбу.
Подготовка к матчу
Это состояние ответственно за то, чтобы игроки возвращались на свои позиции и останавливались там. Обновим наш класс Athlete
:
public class Athlete
{
// (...)
private var mInitialPosition :Vector3D; // стартовая позиция хоккеиста
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) {
// (...)
mInitialPosition = new Vector3D(thePosX, thePosY);
// указание ИИ состояния 'idle'
mBrain.pushState(idle);
}
private function prepareForMatch() :void {
mBoid.steering = mBoid.steering + mBoid.arrive(mInitialPosition, 80);
// нахожусь ли я в своей начальной позиции?
if (distance(mBoid.position, mInitialPosition) <= 5) {
// я в своей стартовой позиции
mBrain.popState();
mBrain.pushState(idle);
}
}
// (...)
}
Ниже вы сможете увидеть результат добавления ИИ в игру. Нажмите клавишу G и игроки переместятся в случайные позиции. Затем они встанут на нужные места:
//
Вывод
Этот урок дает вам основу для реализации хоккея, используя рулевое поведение и конечный автомат. Комбинируя эти концепции, игроки могут двигаться как самостоятельно, под управлением ИИ, так и следуя за курсором вашей мыши. Также хоккеист может ударить по шайбе.
Используя конечный автомат с двумя состояниями мы научили игроков готовиться к матчу и занимать положенные места. В следующем уроке вы узнаете, как организовать нападение и научить игроков забивать голы!
Перевод статьи «Create a Hockey Game AI Using Steering Behaviors: Foundation»
Наши тесты для вас:
— Тест на знание сленга веб-разработчиков.
— Что вы знаете о работе мозга?
— А вы точно программист?