Написание ИИ для хоккея. Часть 1

Существуют различные способы создать какую-нибудь особенную игру. Чаще всего разработчик для получения лучшего результата выбирает такую игру, которую он уже в состоянии написать. Сегодня мы попробуем прыгнуть выше наших голов — создадим искусственный интеллект для игры в хоккей!

Ключевым моментом будет использование рулевого поведения (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”