Сбер AIJ 11.12.24
Сбер AIJ 11.12.24
Сбер AIJ 11.12.24

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

Аватар Типичный программист
Отредактировано

4К открытий4К показов

Прежде чем читать эту часть, советуем вам взглянуть на предыдущий урок. А в этой статье мы продолжим реализацию искусственного интеллекта для игры в хоккей с использованием «рулевого поведения» (steering behaviors), основанного на конечном автомате (FSM – finite state machine). Сегодня мы будем акцентировать наше внимание на атаке, не забудем о перехвате шайбы и научим наших хоккеистов забивать гол в ворота соперников.

Несколько слов об атаке

Скоординировать и грамотно провести атаку в любой командной игре достаточно сложная задача. В реальной игре хоккеисты частенько проводят сложнейшие комбинации.

Однако все действия игроков возможны лишь при анализе текущей ситуации на катке. Хоккеисты постоянно просчитывают возможные ходы на игровом поле. Человек в большинстве случаев может объяснить действия одного игрока относительно другого. Например, “этот хоккеист занял более выгодное положение, проанализировав ситуацию на катке”. Хоть для нас это и очевидно, объяснить такую тактику компьютеру совсем нетривиальная задача.

Как следствие, если мы попытаемся научить искусственный интеллект думать также, как и человек, то в итоге получим огромную и ужасную кучу кода. К тому же такой код будет трудночитаемым и уж точно сложно модифицируемым.

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

Организация атаки с помощью состояний

Мы разобьем всю атаку на несколько мелких частей, каждая из которых будет выполнять какую-то свою специфическую задачу. Все эти куски будут являться состояниями конечного автомата, основанного на стеке. Как было сказано ранее, каждое состояние будет прикладывать силу в «рулевом управлении» (steering force), вследствие чего хоккеист будет вести себя соответственно.

Управление этими состояниями, а также переключение между ними и определяет такое сложное явление, как атака. Изображение ниже иллюстрирует те самые состояния, на которые была разобрана вся атака:

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

Все эти состояния будут переключаться в зависимости от расстояния до шайбы и от того, кто является ее владельцем. Например, мы будем включать состояние attack, если выполняется условие team has the puck (наша команда владеет шайбой).

Как видно из изображения выше, атака делится на 4 стадии: idle, attack, stealPuck, и pursuePuck. Одно из них, состояние idle, уже было реализовано в прошлой статье. Оно является отправной точкой, с которой и начинается процесс атаки. Затем спортсмены переходят к:

  • attack, если команда уже владеет шайбой;
  • stealPuck, если соперники владеют шайбой
  • pursuePuck, если шайба никому не принадлежит и попросту скользит по льду

Давайте более подробно ознакомимся с этими состояниями.

attack описывает наступательные действия. Игрок, владеющий шайбой и именуемый лидером (leader), пытается продвинуться к вражеским воротам. Партнеры по команде тоже не стоят на месте. Они продвигаются вперед, чтобы оказать поддержку своему лидеру.

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

Ну и наконец, pursuePuck. Это состояние не относится ни к атаке, ни к защите и помогает игрокам следовать за шайбой в тех случаях, когда оно никому не принадлежит.

Обновим состояние idle

Когда мы писали функцию idle, у нас еще не было состояний, в которые мы могли перевести хоккеиста. Теперь ситуация изменилась, и, поскольку idle является отправной точкой почти для всех состояний, давайте обновим ее.

На изображении ниже вы можете наблюдать переходные состояния, в которые будут попадать игроки из idle:

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

Если команда спортсмена имеет при себе шайбу, то нужно переходить к атаке. Если же шайба у противников, то нужно переходить к состоянию stealPuck. В случае, когда шайба бесхозная и игрок находиться достаточно близко к ней, он переходит к состоянию pursuePuck. Вот и вся логика. А теперь приведем код:

			class Athlete {
    // (...)
    private function idle() :void {
        var aPuck :Puck = getPuck();
         
        stopAndlookAt(aPuck);
         
        // это простой хак, нужен для тестирования ИИ
        if (mStandStill) return;
         
        // есть ли у шайбы владелец?
        if (getPuckOwner() != null) {
            // да, есть!
            mBrain.popState();
 
            if (doesMyTeamHaveThePuck()) {
                // шайба у моей команды, время атаковать!
                mBrain.pushState(attack);
            } else {
                // шайба у соперников, надо ее отобрать
                mBrain.pushState(stealPuck);
            }
        } else if (distance(this, aPuck) < 150) {
            // шайба попросту катится по катку, надо подобрать ее
            mBrain.popState();
            mBrain.pushState(pursuePuck);
        }
    }
     
    private function attack() :void {
    }
     
    private function stealPuck() :void {
    }
     
    private function pursuePuck() :void {
    }
}
		

Как вы могли заметить, методы attack(), stealPuck() и pursuePuck() объявлены, но не реализованы. Давайте реализуем их!

Следование за шайбой — pursuePuck

Это состояние будет активно, когда шайба не будет принадлежать никому (начало игры тоже попадает под это описание, поскольку шайба без владельца будет находиться в центре поля).

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

Хоть мы и должны бежать к никому не принадлежащей шайбе, нельзя забывать об имитации реальных действий. В настоящем хоккее игроки, увидев такую шайбу, не погонятся всей командой за ней. Из стратегических соображений, погнаться за ней должен тот, кто ближе всех к ней. Все остальные игроки должны помогать ему.

Если же шайбу кто-то уже взял, то нам следует перейти либо к атаке, либо к отбору. Все это выполняется в зависимости от того, кто же умудрился захватить шайбу — игрок нашей команды или соперник.

А вот код состояния pursuePuck:

			class Athlete {
    // (...)
    private function pursuePuck() :void {
        var aPuck :Puck = getPuck();
	
        mBoid.steering = mBoid.steering + mBoid.separation();
	
        if (distance(this, aPuck) > 150) {
            // шайба слишком далеко, быть может, 
            // кто-то из моей команды ближе и он ее подберет
            mBrain.popState();
            mBrain.pushState(idle);
        } else {
            // шайба рядом, надо попытаться подобрать ее
            if (aPuck.owner == null) {
                // никто не подобрал шайбу, это наш шанс
                mBoid.steering = mBoid.steering + mBoid.seek(aPuck.position);
		
            } else {
                // кто-то уже подобрал шайбу;
                // если шайба принадлежит нашей команде, надо переходить к атаке,
                // иначе пытаемся отобрать ее у соперника
                mBrain.popState();
                mBrain.pushState(doesMyTeamHaveThePuck() ? attack : stealPuck);
            }
        }
    }
}
		

Обратите внимание на 6 строчку из приведенного выше кода. Она отвечает за то, чтобы игроки не оставались слишком близко друг к другу во время активного состояния pursuePuck, поскольку это будет выглядеть не естественно.

Ну и давайте взглянем на результат. Каждые 2 секунды шайба помещается в центр поля для того, чтобы мы могли взглянуть на способность наших игроков подбирать шайбу.

//

 

На данный момент этот блок не поддерживается, но мы не забыли о нём!Наша команда уже занята его разработкой, он будет доступен в ближайшее время.

 

 

Проведение атаки

После того, как игрок получил шайбу, он должен стремиться к воротам соперника и забить гол. Это и будет целью состояния attack.

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

Атака имеет два переходных состояния: pursuePuck и stealPuck. Игроки, будучи в состоянии attack, должны будут бежать к воротам противников. Давайте реализуем это:

			class Athlete {
    // (...)
    private function attack() :void {
        var aPuckOwner :Athlete = getPuckOwner();
     
        // есть ли у шайбы владелец?
        if (aPuckOwner != null) {
            // да, есть. давайте проверим, кто же это
            if (doesMyTeamHaveThePuck()) {
                if (amIThePuckOwner()) {
                    // шайба у моей команды, более того - она у меня
                    // пытаемся прорваться к вражеским воротам
                    mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition());
                     
                } else {
                    // шайба у моей команды, но не у меня; 
                    // надо бежать за лидером и помогать ему
                    mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid);
                    mBoid.steering = mBoid.steering + mBoid.separation();
                }
            } else {
                // шайба у противников, останавливаем атаку
                // и пытаемся отобрать шайбу
                mBrain.popState();
                mBrain.pushState(stealPuck);
            }
        } else {
            // шайба никому не принадлежит;
            // надо подобрать ее
            mBrain.popState();
            mBrain.pushState(pursuePuck);
        }
    }
}
		

Разберем приведенный код. Если у шайбы есть владелец, причем это игрок сам, то он несется к воротам противника (строка 13). Если же шайба принадлежит команде игрока, но он не является лидером, то следует бежать за нашим форвардом и помогать ему (строка 18). Обратите внимание на метод mBoid.separation() в строке 19, который мы использовали чуть ранее. На изображении ниже вы можете увидеть, как игроки помогают своему лидеру.

Обратите внимание также на то, что игроки без шайбы следуют не точно за лидером, а в точку перед ним. Это позволит избежать образования толпы, а также даст лидеру возможность совершить пас, если на его пути возникнуть какие-либо препятствия.

В случае, когда шайбой владеют противники, мы переводим игрока в состояние stealPuck. А если она никому не принадлежит, то игрок должен ее подобрать. Это делается переводом в состояние pursuePuck.

А теперь давайте посмотрим на результат:

//

 

На данный момент этот блок не поддерживается, но мы не забыли о нём!Наша команда уже занята его разработкой, он будет доступен в ближайшее время.

 

 

Улучшение поддержки атаки

Текущая реализация атаки находится на достаточно хорошем уровне исполнения. Однако недостаток все же имеется. Проявляется он в том случае, когда лидер оказывается дальше от ворот соперников, чем кто-либо другой из его команды.

Присмотритесь на последний наш результат. В случае, когда какой-нибудь игрок A находится к чужим воротам ближе, чем лидер, то игрок A начинает вести себя немного неестественно, пока не окажется позади форварда.

Этот недостаток с легкостью можно исправить путем проверки положения игрока — находится он позади лидера или опережает его:

			class Athlete {
    // (...)
    private function isAheadOfMe(theBoid :Boid) :Boolean {
        var aTargetDistance :Number = distance(getOpponentGoalPosition(), theBoid);
        var aMyDistance :Number = distance(getOpponentGoalPosition(), mBoid.position);
     
        return aTargetDistance <= aMyDistance;
    }
 
    private function attack() :void {
        var aPuckOwner :Athlete = getPuckOwner();
     
        // есть ли у шайбы владелец?
        if (aPuckOwner != null) {
            // да, есть
            if (doesMyTeamHaveThePuck()) {
                if (amIThePuckOwner()) {
                    // шайба у моей команды, более того - она у меня
                    // пытаемся прорваться к вражеским воротам
                    mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition());
                     
                } else {
                    // шайба у моей команды, но не у меня; а лидер впереди меня?
                    if (isAheadOfMe(aPuckOwner.boid)) {
                        // да, он впереди; побегу за ним,
                        // чтобы помочь ему
                        mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid);
                        mBoid.steering = mBoid.steering + mBoid.separation();
                    } else {
                        // нет, лидер позади меня; давайте добавим "разделение" (separation),
                        // чтобы предотвратить давку
                        mBoid.steering = mBoid.steering + mBoid.separation();
                    }
                }
            } else {
                // шайба у противников, останавливаем атаку
                // и пытаемся отобрать шайбу
                mBrain.popState();
                mBrain.pushState(stealPuck);
            }
        } else {
            // шайба никому не принадлежит;
            // надо подобрать ее
            mBrain.popState();
            mBrain.pushState(pursuePuck);
        }
    }
}
		

А теперь взглянем на полученный результат. Игроки ведут себя на порядок реалистичнее!

//

 

На данный момент этот блок не поддерживается, но мы не забыли о нём!Наша команда уже занята его разработкой, он будет доступен в ближайшее время.

 

 

Примечание Путем настройки измерения и сравнения расстояний между лидером и текущим игроком, можно изменить поведение команды.

Отбор шайбы

Последним состоянием атаки является stealPuck, который становится активным, когда шайба принадлежит соперникам. Цель — отобрать шайбу для проведения собственной атаки.

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

А теперь перейдем к реализации:

			class Athlete {
    // (...)
    private function stealPuck() :void {
        // есть ли у шайбы владелец?
        if (getPuckOwner() != null) {
            // да, есть
            if (doesMyTeamHaveThePuck()) {
                // шайба у моей команды
                // время перейти в атаку
                mBrain.popState();
                mBrain.pushState(attack);
            } else {
                // шайба у противников
                var aOpponentLeader :Athlete = getPuckOwner();
             
                // попробуем отобрать шайбу у врага,
                // но попытаемся предсказать его положение
                // и перехватим его там
                mBoid.steering = mBoid.steering + mBoid.pursuit(aOpponentLeader.boid);
                mBoid.steering = mBoid.steering + mBoid.separation();
            }
        } else {
            // шайба никому не принадлежит;
            // надо подобрать ее
            mBrain.popState();
            mBrain.pushState(pursuePuck);
        }
    }
}
		

Алгоритм предельно прост. Если шайба принадлежит команде игрока, то мы переходим к состоянию attack (строка 11). Если же она у команды противника, то игрок попытается перехватить ее. Однако, обратите внимание. Мы не можем передавать нашему игроку координаты противника, поскольку это приведет к тому, что наш хоккеист будет просто его преследовать. Именно поэтому игрок будет предсказывать то положение, в котором окажется противник в ближайшее время, и будет стремиться перехватить своего соперника. Все это реализовано при помощи «поведения преследования» (pursue behavior) в строке 19.

А вот и результат. Обратите внимание на то, как игроки отбирают шайбу у соперников.

//

 

На данный момент этот блок не поддерживается, но мы не забыли о нём!Наша команда уже занята его разработкой, он будет доступен в ближайшее время.

 

 

Улучшение отбора шайбы

Мы написали довольно неплохой метод отбора шайбы. Но в настоящей игре хоккеисты не поступали бы так, как они ведут себя у нас. Отобрать шайбу они пытаются всей командой, но это совсем неестественно и может привести к образованию толпы. Мы можем исправить этот недочет путем проверки расстояния от игрока до лидера соперников.

			class Athlete {
    // (...)
    private function stealPuck() :void {
        // есть ли у шайбы владелец?
        if (getPuckOwner() != null) {
            // да, есть
            if (doesMyTeamHaveThePuck()) {
                // шайба у моей команды
                // время перейти в атаку
                mBrain.popState();
                mBrain.pushState(attack);
            } else {
                // шайба у противников
                var aOpponentLeader :Athlete = getPuckOwner();
             
                // лидер соперников близок ко мне?
                if (distance(aOpponentLeader, this) < 150) {
                    // да, он рядом; надо предугадать его положение и
                    // перехватить его
                    mBoid.steering = mBoid.steering.add(mBoid.pursuit(aOpponentLeader.boid));
                    mBoid.steering = mBoid.steering.add(mBoid.separation(50));
                 
                } else {
                    // нет, он слишком далеко; в скором времени на этом 
                    // месте мы будем переходить в защиту
                    // TODO: mBrain.popState();
                    // TODO: mBrain.pushState(defend);
                }
            }
        } else {
            // шайба никому не принадлежит;
            // надо подобрать ее
            mBrain.popState();
            mBrain.pushState(pursuePuck);
        }
    }
}
		

Теперь наши игроки не будут бежать сломя голову к противнику, пытаясь отобрать у него шайбу. Побегут только те, расстояние от которых до соперника не превышает 150.

Все оставшиеся игроки останутся на своих местах, поскольку они слишком далеко. Целесообразно их перевести в состояние защиты, однако, мы пока его не реализовали. В следующем уроке мы вернемся сюда и дополним наш код.

Взгляните на полученный результат. Теперь игроки более адекватно ведут себя при отборе шайбы.

//

 

На данный момент этот блок не поддерживается, но мы не забыли о нём!Наша команда уже занята его разработкой, он будет доступен в ближайшее время.

 

 

Избежание вражеских защитников

В нашей игре не хватает еще одного трюка. В настоящей игре хоккеисты при получении шайбы не несутся сломя голову к воротам противника. Они пытаются каким-то образом маневрировать между игроками вражеской команды. Давайте реализуем это и в нашей игре.

Мы можем использовать «избежание коллизий» (collision avoidance) для того, чтобы игроки могли уклоняться от противников. Вот, как это будет выглядеть:

Для реализации уклонений нам следует добавить лишь одну строку (14 строка) в наш код:

			class Athlete {
    // (...)
    private function attack() :void {
        var aPuckOwner :Athlete = getPuckOwner();
     
        // есть ли у шайбы владелец?
        if (aPuckOwner != null) {
            // да, есть
            if (doesMyTeamHaveThePuck()) {
                if (amIThePuckOwner()) {
                    // шайба у моей команды, более того - она у меня
                    // пытаемся прорваться к вражеским воротам и уклоняемся от врагов
                    mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition());
                    mBoid.steering = mBoid.steering + mBoid.collisionAvoidance(getOpponentTeam().members);
         
                } else {
                    // шайба у моей команды, но не у меня; а лидер впереди меня?
                    if (isAheadOfMe(aPuckOwner.boid)) {
                        // да, он впереди; побегу за ним,
                        // чтобы помочь ему
                        mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid);
                        mBoid.steering = mBoid.steering + mBoid.separation();
                    } else {
                        // нет, лидер позади меня; давайте добавим "разделение" (separation),
                        // чтобы предотвратить давку
                        mBoid.steering = mBoid.steering + mBoid.separation();
                    }
                }
            } else {
                // шайба у противников, останавливаем атаку
                // и пытаемся отобрать шайбу
                mBrain.popState();
                mBrain.pushState(stealPuck);
            }
        } else {
            // шайба никому не принадлежит;
            // надо подобрать ее
            mBrain.popState();
            mBrain.pushState(pursuePuck);
        }
    }
}
		

Ниже вы сможете посмотреть на работу обновленного состояния attack. Противники обездвижены для большей наглядности.

//

 

На данный момент этот блок не поддерживается, но мы не забыли о нём!Наша команда уже занята его разработкой, он будет доступен в ближайшее время.

 

 

Вывод

В этот раз мы научили наших игроков выполнять атакующие действия: они умеют подбирать шайбу, отбирать ее у противников, а также забивать гол в чужие ворота. Как уже говорилось ранее, наша реализация атаки является лишь имитацией поведения людей. Поэтому результат будет не более, чем приближением к реальной игре.

В следующем уроке вы узнаете, как реализовать защиту. А в конце вы сможете посмотреть на игру, которая будет полностью под контролем искусственного интеллекта.

Перевод статьи «Create a Hockey Game AI Using Steering Behaviors: Attack»

Следите за новыми постами
Следите за новыми постами по любимым темам
4К открытий4К показов