查看: 2749|回复: 1
收起左侧

[已翻译] 使用群体行为创建一个冰球游戏的AI(二):发起攻击

[复制链接]

[已翻译] 使用群体行为创建一个冰球游戏的AI(二):发起攻击[复制链接]

初末 发表于 2017-12-25 14:51:22 [显示全部楼层] |只看大图 回帖奖励 |倒序浏览 |阅读模式 回复:  1 浏览:  2749
翻译:张华栋(wcby)      审校:陈敬凤(nunu)
这篇文章是《使用群体行为创建一个冰球游戏的AI》的第二部分,这个系列共4篇文章,将用详细的步骤来描述如何构建一个冰球游戏的AI部分。
在这篇文章里,我们将继续讨论如何使用群体行为和基于堆栈的有限状态机来给一个冰球游戏创建AI。这个系列的这篇文章主要讲述如何赋予游戏实体以AI,让他们能够协作来发起攻击,其中包括拦截和运着冰球向对方球门发起攻击。

关于攻击AI的一些想法
在一个合作类的体育游戏里面协调沟通以及发起攻击是一个非常复杂的任务。在现实世界中,如果人们在进行冰球或者曲棍球比赛,他们会基于很多变量来做决策。
这些决策包括计算将要发生的事情以及对将要发生事情的理解。如果是一个人类,他可以说出为什么一个对手会基于其他队友的动作而发生移动,比如“他现在移动到了一个策略更佳的位置”。但是这些事情让计算机来理解并不容易。
因此,如果我们试图对人工智能编码来让它能够遵循所有人类的行为差别和感知,将会导致非常庞杂并且可怕的代码。此外,人工智能运行得到的结果可能不准确或者难以修改。
所以我们的攻击AI试图模仿的是一群人比赛时候做出的决策结果,而不是模仿人们的感知本身。这种方法只能给出一个近似的结果并不完美,但是人工智能的代码更加容易理解和方便调试。产出的结果在几个用例上面还是能得到很好的结果的。
用状态来组织攻击AI
我们将把攻击过程分解为一些小的片段,每一个片段会执行一个特定的行动。这些片段都是基于堆栈的有限状态机里面的状态。正如之前解释过的那样(http://gamedevelopment.tutsplus. ... undation--cms-20971),每一个状态会产生一个群体力,这将导致运动员根据群体力做相应的行为。
这些状态的组织编排以及用来状态变换的条件一起定义了攻击AI。下图给出了整个过程完整的有限状态机:
DSC0000.png
用一个基于堆栈的有限状态机来表示攻击全过程。
如图里解释的那样,决定各个状态切换的条件完全基于运动员到冰球的距离以及运动员对冰球的所有权。比如,“团队是否拥有冰球所有权“或者“冰球离我太远“。
整个攻击过程将由4个状态组成:空闲、攻击、偷球和追球。空闲状态已经在前面的教程中实现过了,并且它是整个攻击过程的起点。在空闲状态下,如果运动员所属的团队拥有冰球的控制权,这个运动员可以切换到攻击状态,如果是对手那边拥有冰球的控制权,这个运动员将切换到偷球状态,如果没有团队拥有对冰球的控制权并且冰球又离这个运动员比较近,那么这个运动员就会切换到追球状态。
攻击状态代表着攻击性的移动。在这个状态下,运球的运动员(我们称他为带头人)将视图攻击对方的球门。他的同伴将会跟随他的轨迹,试图对他进行支援。
偷球状态代表着介于进攻和防守之间的移动状态。在这个状态下,运动员将专注于追逐运球的对手。他们的目标是恢复对团队对冰球的控制,为团队再次发起进攻做准备。
最后一个状态是追球状态,这个状态和进攻或者防守无关。它只是在冰球处于非受控状态的时候负责引导运动员的行为。在这个状态下,冰球在比赛场上自由移动(比如,某个运动员挥杆击打了球以后冰球所处的状态),而运动员在试图得到冰球的控制权。
更新空闲状态
之前实现的空闲状态并没有跳转。因为这个状态是整个人工智能的起点,让我们对它进行更新并让它能够跳转到其他状态。
空闲状态有三个跳转:
DSC0001.png
用来描述整个攻击过程的状态机里面的空闲状态和它的跳转。
如果运动员所属的团队拥有了对冰球的控制权,那么空闲状态应该从状态机的大脑中弹出,然后攻击状态应该被压入状态机的堆栈中(这样,执行的就是攻击状态了)。同样的道理,如果是运动员对手的团队拥有了对冰球的控制权,那么空闲状态应该从状态机的大脑中弹出,然后偷球状态应该被压入状态机的堆栈中(这样,执行的就是偷球状态了)。剩下一个跳转发生的条件是没有哪个团队拥有对冰球的控制权而且冰球又靠近运动员,在这种情况下,追球状态应该被压入状态机的堆栈中(这样,执行的就是追球状态了)。
带有更新方法的空闲版本如下(其他状态会在稍后实现):
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em]22

[size=1em]23

[size=1em]24

[size=1em]25

[size=1em]26

[size=1em]27

[size=1em]28

[size=1em]29

[size=1em]30

[size=1em]31

[size=1em]32

[size=1em]33

[size=1em]34

[size=1em]35

[size=1em]36

[size=1em]37

[size=1em]38

[size=1em][size=1em]class Athlete {
[size=1em]    // (...)
[size=1em]    private function idle() :void {
[size=1em]        var aPuck :Puck = getPuck();
[size=1em]         
[size=1em]        stopAndlookAt(aPuck);
[size=1em]         
[size=1em]        // This is a hack to help test the AI.
[size=1em]        if (mStandStill) return;
[size=1em]         
[size=1em]        // Does the puck has an owner?
[size=1em]        if (getPuckOwner() != null) {
[size=1em]            // Yeah, it has.
[size=1em]            mBrain.popState();
[size=1em]  
[size=1em]            if (doesMyTeamHaveThePuck()) {
[size=1em]                // My team just got the puck, it's attack time!
[size=1em]                mBrain.pushState(attack);
[size=1em]            } else {
[size=1em]                // The opponent team got the puck, let's try to steal it.
[size=1em]                mBrain.pushState(stealPuck);
[size=1em]            }
[size=1em]        } else if (distance(this, aPuck) < 150) {
[size=1em]            // The puck has no owner and it is nearby. Let's pursue it.
[size=1em]            mBrain.popState();
[size=1em]            mBrain.pushState(pursuePuck);
[size=1em]        }
[size=1em]    }
[size=1em]      
[size=1em]    private function attack() :void {
[size=1em]    }
[size=1em]      
[size=1em]    private function stealPuck() :void {
[size=1em]    }
[size=1em]      
[size=1em]    private function pursuePuck() :void {
[size=1em]    }
[size=1em]}



接下来让我们处理其他状态的实现。
追球状态
既然运动员现在已经获得了一些对环境的感知能力并且能够从空闲状态切换到其他状态,接下来让我们集中精力解决当冰球不受任何团队控制的时候运动员如何追球的问题。
当比赛开始以后,运动员会立刻切换到追球状态,因为冰球就放在比赛场的中间并且不受任何团队的控制。追球状态也有三个跳转:
DSC0002.png
用来描述整个攻击过程的状态机里面的追球状态和它的跳转。
第一个跳转的条件是冰球离运动员太远,并且追球状态会试图模拟真实比赛中发生的关于追逐冰球的行为。出于战略原因,通常是由最靠近冰球的运动员去试图控制它,而其他人则原地等待或提供帮助。
当冰球距离运动员很远的时候,如果不把运动员的状态切换到空闲状态,就会发现每个由人工智能控制的运动员都会同时处于追球状态,即使有些运动员离球很远,这明显非常的不真实。通过检测冰球和运动员之间的距离,如果发现太远,追球状态就会把自己从有限状态机的大脑中弹出来,并且把空闲状态压入状态机的堆栈,这意味着这名运动员会放弃追球而停下来。
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em][size=1em]class Athlete {
[size=1em]    // (...)
[size=1em]  
[size=1em]    private function pursuePuck() :void {
[size=1em]        var aPuck :Puck = getPuck();
[size=1em]      
[size=1em]        if (distance(this, aPuck) > 150) {
[size=1em]            // Puck is too far away from our current position, so let's give up
[size=1em]            // pursuing the puck and hope someone will be closer to get the puck
[size=1em]            // for us.
[size=1em]            mBrain.popState();
[size=1em]            mBrain.pushState(idle);
[size=1em]        } else {
[size=1em]            // The puck is close, let's try to grab it.
[size=1em]        }
[size=1em]    }
[size=1em]      
[size=1em]    // (...)
[size=1em]}




当冰球离运动员很近的时候,运动员必须去追逐冰球,这可以通过追逐行为来很容易的实现。把冰球的位置作为追逐行为的目标点,运动员会优雅地追逐冰球并且会根据冰球的移动来不断调整自己的轨迹:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em]22

[size=1em]23

[size=1em]24

[size=1em]25

[size=1em]26

[size=1em]27

[size=1em]28

[size=1em]29

[size=1em]30

[size=1em]31

[size=1em]32

[size=1em]33

[size=1em]34

[size=1em]35

[size=1em]36

[size=1em]37

[size=1em]38

[size=1em]39

[size=1em]40

[size=1em]41

[size=1em]42

[size=1em]43

[size=1em]44

[size=1em]45

[size=1em]46

[size=1em]47

[size=1em]48

[size=1em]49

[size=1em]50

[size=1em]51

[size=1em]52

[size=1em]53

[size=1em]54

[size=1em]55

[size=1em]56

[size=1em]57

[size=1em][size=1em]class Athlete {

[size=1em]    // (...)

[size=1em]    private function pursuePuck() :void {

[size=1em]        var aPuck :Puck = getPuck();

[size=1em]               

[size=1em]        mBoid.steering = mBoid.steering + mBoid.separation();

[size=1em]               

[size=1em]        if (distance(this, aPuck) > 150) {

[size=1em]            // Puck is too far away from our current position, so let's give up

[size=1em]            // pursuing the puck and hope someone will be closer to get the puck

[size=1em]            // for us.

[size=1em]            mBrain.popState();

[size=1em]            mBrain.pushState(idle);

[size=1em]        } else {

[size=1em]            // The puck is close, let's try to grab it.

[size=1em]            if (aPuck.owner == null) {

[size=1em]                // Nobody has the puck, it's our chance to seek and get it!

[size=1em]                mBoid.steering = mBoid.steering + mBoid.seek(aPuck.position);

[size=1em]                              

[size=1em]            } else {

[size=1em]                // Someone just got the puck. If the new puck owner belongs to my team,

[size=1em]                // we should switch to 'attack', otherwise I should switch to 'stealPuck'

[size=1em]                // and try to get the puck back.

[size=1em]                mBrain.popState();

[size=1em]                mBrain.pushState(doesMyTeamHaveThePuck() ? attack : stealPuck);

[size=1em]            }

[size=1em]        }

[size=1em]    }

[size=1em]}




追球状态剩下的2个跳转是“团队拥有对冰球的控制权“和”对手拥有对冰球的控制权“,这涉及到在运动员追逐冰球的过程中冰球被某一方团队控制的问题。如果某一方控制了冰球,那么运动员必须立刻弹出追球状态然后把新的状态压入状态机的堆栈中。
具体压入哪一个状态取决于冰球的控制权在那个团队手里。如果对todoesMyTeamHaveThePuck()的调用返回为真,那么意味着队友控制了冰球,所以这名运动员应该压入状态机的状态是攻击状态,说明现在是时候停止追逐冰球并且应该向对方球门发起攻击了。如果是对手控制了冰球,那么这名运动员应该压入状态机的状态是偷球状态,这将使得团队争取恢复对冰球的控制。
这里还有一个对表现效果的一个小提升,就是运动员不应该在追球状态的时候彼此离得太近,因为拥挤的追逐冰球的行为非常的不自然。在这个状态的群体力里面添加一个分离力(上面代码的第6行)来确保运动员之间保持着一个最小距离,这样他们就不会离得太近。
结果就是团队现在可以追逐冰球了。这个demo只是为了测试,所以每隔几秒,冰球就会被重置到比赛场的中间位置,以确保运动员可以持续移动:
运球发起攻击
在得到冰球的控制权以后,运动员和他的团队应该向对手的球门移动并试图得分,这就是攻击状态的目的:
DSC0003.png
用来描述整个攻击过程的状态机里面的攻击状态和它的跳转。
攻击状态只有2个跳转:“对手获得了对冰球的控制权“以及”没有团队有对冰球的控制权“。因为设计这个状态的目的仅仅是为了让运动员向对手球门移动并发起攻击,如果冰球已经不在自己团队的控制下,保持攻击状态就没有什么意义了。
关于向对手的球门移动并发起攻击:运球的运动员(带头人)和帮助他的队友的行为应该有所不同。运球的运动员必须要到达对手的球门,而他的队友应该在沿途对他进行帮助。
这可以通过检查运行此段代码的运动员是否控制着冰球来实现:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em]22

[size=1em]23

[size=1em]24

[size=1em]25

[size=1em]26

[size=1em]27

[size=1em]28

[size=1em]29

[size=1em]30

[size=1em]31

[size=1em]32

[size=1em]33

[size=1em]34

[size=1em][size=1em]class Athlete {
[size=1em]    // (...)
[size=1em]    private function attack() :void {
[size=1em]        var aPuckOwner :Athlete = getPuckOwner();
[size=1em]      
[size=1em]        // Does the puck have an owner?
[size=1em]        if (aPuckOwner != null) {
[size=1em]            // Yeah, it has. Let's find out if the owner belongs to the opponents team.
[size=1em]            if (doesMyTeamHaveThePuck()) {
[size=1em]                if (amIThePuckOwner()) {
[size=1em]                    // My team has the puck and I am the one who has it! Let's move
[size=1em]                    // towards the opponent's goal.
[size=1em]                    mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition());
[size=1em]                     
[size=1em]                } else {
[size=1em]                    // My team has the puck, but a teammate has it. Let's just follow him
[size=1em]                    // to give some support during the attack.
[size=1em]                    mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid);
[size=1em]                    mBoid.steering = mBoid.steering + mBoid.separation();
[size=1em]                }
[size=1em]            } else {
[size=1em]                // The opponent has the puck! Stop the attack
[size=1em]                // and try to steal it.
[size=1em]                mBrain.popState();
[size=1em]                mBrain.pushState(stealPuck);
[size=1em]            }
[size=1em]        } else {
[size=1em]            // Puck has no owner, so there is no point to keep
[size=1em]            // attacking. It's time to re-organize and start pursuing the puck.
[size=1em]            mBrain.popState();
[size=1em]            mBrain.pushState(pursuePuck);
[size=1em]        }
[size=1em]    }
[size=1em]}




如果对amIThePuckOwner()的调用返回为真(在代码的第10行),那么代表着运行这段代码的运动员控制着冰球。在这种情况下,他只会以对方球门的位置作为追逐目标。这与追球状态下用于追逐冰球的逻辑几乎完全一样。
如果对amIThePuckOwner()的调用返回为假,那么代表着运行这段代码的运动员没有控制着冰球,所以他必须要帮助运球的运动员。但是帮助运球的运动员是一个非常复杂的任务,所以我们需要简化这个事情。运动员将只追逐位于带球运动员之前的位置。
DSC0004.gif
队友帮助运球的运动员。
这样当运球的运动员移动的时候,他将被他的队友围绕保护,因为他的队友追逐的目标点位于他的前方。这样当运球的运动员遇到麻烦的时候可以选择把冰球传给自己的队友。在一个真实的比赛中,周围的队友应该和运球的运动员保持一定的距离以防止阻碍他运球。
协助模式可以通过对跟随领导者行为(上面代码的第18行)进行一些细微的调整来实现。唯一的区别是跟随领导者行为里面其他人的追逐目标是一个落后于领导者的位置,而现在要改成一个稍微领先于领导者的位置。
运动员对控球运动员的协助也要相互之间保持一个最小距离。这可以通过添加一个分离力(上面代码的第19行)来实现。
这样修改的结果就是团队可以向对方的球门集体移动,同时又不会拥挤,在移动的过程中还会有协助攻击的移动:
提高攻击支持的效果
攻击状态的目前实现在很多情况下工作的非常良好,但是它有一个缺点。当某个运动员获取对冰球的控制权以后,他就会变成整个团队的领导者并且立刻被他的队友所跟随。
如果领导者在捕获对冰球的控制权的时候正在往自己方球门移动会发生什么?请仔细看下上面的演示,注意下队友开始追随领导者时候的不自然。
当领导者捕获对冰球的控制权以后,追逐行为需要花一些时间来修正领导者的轨迹以便让他有效的向对方球门进军。即便领导者处于一个调整状态,他的队友也会以他前方的某个位置作为追逐行为的目标点,这意味着他们将向自己的球门移动(或者是领导者注视的其他地方)。
当领导者调整完毕以后,他将准备向对方球门移动,他的队友将需要调整才能跟随领导者。这样就会出现领导者在没有队友保护的情况自己往对方球门移动的情况,因为他的队友还在调整他们的轨迹没有办法跟上领导者。
可以在团队恢复对冰球控制权的时候,判断队友是否在领导者前方来修正这个问题。在这里,队友在领导者前方意味着队友比领导者更靠近对方球门:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em]22

[size=1em]23

[size=1em]24

[size=1em]25

[size=1em]26

[size=1em]27

[size=1em]28

[size=1em]29

[size=1em]30

[size=1em]31

[size=1em]32

[size=1em]33

[size=1em]34

[size=1em]35

[size=1em]36

[size=1em]37

[size=1em]38

[size=1em]39

[size=1em]40

[size=1em]41

[size=1em]42

[size=1em]43

[size=1em]44

[size=1em]45

[size=1em]46

[size=1em]47

[size=1em]48

[size=1em]49

[size=1em][size=1em]class Athlete {
[size=1em]    // (...)
[size=1em]    private function isAheadOfMe(theBoid :Boid) :Boolean {
[size=1em]        var aTargetDistance :Number = distance(getOpponentGoalPosition(), theBoid);
[size=1em]        var aMyDistance :Number = distance(getOpponentGoalPosition(), mBoid.position);
[size=1em]      
[size=1em]        return aTargetDistance <= aMyDistance;
[size=1em]    }
[size=1em]  
[size=1em]    private function attack() :void {
[size=1em]        var aPuckOwner :Athlete = getPuckOwner();
[size=1em]      
[size=1em]        // Does the puck have an owner?
[size=1em]        if (aPuckOwner != null) {
[size=1em]            // Yeah, it has. Let's find out if the owner belongs to the opponents team.
[size=1em]            if (doesMyTeamHaveThePuck()) {
[size=1em]                if (amIThePuckOwner()) {
[size=1em]                    // My team has the puck and I am the one who has it! Let's move
[size=1em]                    // towards the opponent's goal.
[size=1em]                    mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition());
[size=1em]                     
[size=1em]                } else {
[size=1em]                    // My team has the puck, but a teammate has it. Is he ahead of me?
[size=1em]                    if (isAheadOfMe(aPuckOwner.boid)) {
[size=1em]                        // Yeah, he is ahead of me. Let's just follow him to give some support
[size=1em]                        // during the attack.
[size=1em]                        mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid);
[size=1em]                        mBoid.steering = mBoid.steering + mBoid.separation();
[size=1em]                    } else {
[size=1em]                        // Nope, the teammate with the puck is behind me. In that case
[size=1em]                        // let's hold our current position with some separation from the
[size=1em]                        // other, so we prevent crowding.
[size=1em]                        mBoid.steering = mBoid.steering + mBoid.separation();
[size=1em]                    }
[size=1em]                }
[size=1em]            } else {
[size=1em]                // The opponent has the puck! Stop the attack
[size=1em]                // and try to steal it.
[size=1em]                mBrain.popState();
[size=1em]                mBrain.pushState(stealPuck);
[size=1em]            }
[size=1em]        } else {
[size=1em]            // Puck has no owner, so there is no point to keep
[size=1em]            // attacking. It's time to re-organize and start pursuing the puck.
[size=1em]            mBrain.popState();
[size=1em]            mBrain.pushState(pursuePuck);
[size=1em]        }
[size=1em]    }
[size=1em]}




如果领导者(也就是本方的控球运动员)在运行这段代码的运动员的前面,那么运动员应该按照之前的方式(上面代码的第27和28行)来跟随领导者。如果领导者在运行这段代码的运动员的后面,那么运动员应该保持自己的当前位置,与其他运动员保持一个最小距离(上面代码的第33行).
这样的结果比最初攻击状态的实现让人更信服一点:
小提示:通过调整theisAheadOfMe()方法里面的距离计算和比较方法,可以对运动员停留在当前位置上的做法进行修改。
偷球状态
攻击过程中最后一个状态是偷球状态,当对方团队获得对冰球的控制权后,这个状态就会被激活。偷球状态的主要目的是从运球的对手手中偷得对冰球的控制权,这样本方团队才能再次发起对对方球门的攻击:
DSC0005.png 用来描述整个攻击过程的状态机里面的偷球状态和它的跳转。
因为这个状态的思路是从对方手中偷得对冰球的控制权,所以如果冰球的控制权回到本方团队手中或者冰球不受任意一方团队控制(也就是自由移动)的时候,偷球状态会把它自己从状态机中弹出然后压入合适的状态来处理新情况:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em]22

[size=1em]23

[size=1em]24

[size=1em]25

[size=1em]26

[size=1em]27

[size=1em]28

[size=1em]29

[size=1em]30

[size=1em][size=1em]class Athlete {
[size=1em]    // (...)
[size=1em]    private function stealPuck() :void {
[size=1em]        // Does the puck have any owner?
[size=1em]        if (getPuckOwner() != null) {
[size=1em]            // Yeah, it has, but who has it?
[size=1em]            if (doesMyTeamHaveThePuck()) {
[size=1em]                // My team has the puck, so it's time to stop trying to steal
[size=1em]                // the puck and start attacking.
[size=1em]                mBrain.popState();
[size=1em]                mBrain.pushState(attack);
[size=1em]            } else {
[size=1em]                // An opponent has the puck.
[size=1em]                var aOpponentLeader :Athlete = getPuckOwner();
[size=1em]              
[size=1em]                // Let's pursue him while mantaining a certain separation from
[size=1em]                // the others to avoid that everybody will ocuppy the same
[size=1em]                // position in the pursuit.
[size=1em]                mBoid.steering = mBoid.steering + mBoid.pursuit(aOpponentLeader.boid);
[size=1em]                mBoid.steering = mBoid.steering + mBoid.separation();
[size=1em]            }
[size=1em]        } else {
[size=1em]            // The puck has no owner, it is probably running freely in the rink.
[size=1em]            // There is no point to keep trying to steal it, so let's finish the 'stealPuck' state
[size=1em]            // and switch to 'pursuePuck'.
[size=1em]            mBrain.popState();
[size=1em]            mBrain.pushState(pursuePuck);
[size=1em]        }
[size=1em]    }
[size=1em]}




如果冰球现在是受对方团队的控制,运动员必须去追逐对面控球的运动员并试图夺回对冰球的控制权。要能对对面控球的运动员进行追逐,运动员必须对对面控球的运动员的稍后的位置有一个判断,所以他可以根据这个判断来提前卡位。这与直接以对面控球的运动员当前的位置进行追逐会有一点不同。
幸运的是,这可以很容易通过追逐行为(在上面代码的第19行)来实现。通过在偷球状态添加一个追逐力,运动员将试图预判对面控球运动员的位置并提前卡位而不是简单的跟随:
避免拥挤的偷球移动
目前实现的偷球状态工作良好,但是如果是在真实的比赛中,将只有一到两名运动员会去接近对方控球运动员试图偷球。团队中剩下的运动员将留在周围的区域以便提供帮助,这就不会出现一个特别拥挤的偷球移动模式。
这个可以通过在对对方控球运动员进行追逐之前添加一个距离检测(在下面的第17行)来修正:
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em]22

[size=1em]23

[size=1em]24

[size=1em]25

[size=1em]26

[size=1em]27

[size=1em]28

[size=1em]29

[size=1em]30

[size=1em]31

[size=1em]32

[size=1em]33

[size=1em]34

[size=1em]35

[size=1em]36

[size=1em]37

[size=1em]38

[size=1em]39

[size=1em]40

[size=1em][size=1em]class Athlete {
[size=1em]    // (...)
[size=1em]    private function stealPuck() :void {
[size=1em]        // Does the puck have any owner?
[size=1em]        if (getPuckOwner() != null) {
[size=1em]            // Yeah, it has, but who has it?
[size=1em]            if (doesMyTeamHaveThePuck()) {
[size=1em]                // My team has the puck, so it's time to stop trying to steal
[size=1em]                // the puck and start attacking.
[size=1em]                mBrain.popState();
[size=1em]                mBrain.pushState(attack);
[size=1em]            } else {
[size=1em]                // An opponent has the puck.
[size=1em]                var aOpponentLeader :Athlete = getPuckOwner();
[size=1em]              
[size=1em]                // Is the opponent with the puck close to me?
[size=1em]                if (distance(aOpponentLeader, this) < 150) {
[size=1em]                    // Yeah, he is close! Let's pursue him while mantaining a certain
[size=1em]                    // separation from the others to avoid that everybody will ocuppy the same
[size=1em]                    // position in the pursuit.
[size=1em]                    mBoid.steering = mBoid.steering.add(mBoid.pursuit(aOpponentLeader.boid));
[size=1em]                    mBoid.steering = mBoid.steering.add(mBoid.separation(50));
[size=1em]                  
[size=1em]                } else {
[size=1em]                    // No, he is too far away. In the future, we will switch
[size=1em]                    // to 'defend' and hope someone closer to the puck can
[size=1em]                    // steal it for us.
[size=1em]                    // TODO: mBrain.popState();
[size=1em]                    // TODO: mBrain.pushState(defend);
[size=1em]                }
[size=1em]            }
[size=1em]        } else {
[size=1em]            // The puck has no owner, it is probably running freely in the rink.
[size=1em]            // There is no point to keep trying to steal it, so let's finish the 'stealPuck' state
[size=1em]            // and switch to 'pursuePuck'.
[size=1em]            mBrain.popState();
[size=1em]            mBrain.pushState(pursuePuck);
[size=1em]        }
[size=1em]    }
[size=1em]}




这样运动员就不会再盲目的追逐对方控球运动员,而是会先检测下他与对方控球运动员之间的距离,判断距离是否少于某一个值,比如说150。如果确实少于这个值,那么追逐才会发生,但是如果距离超过了这个值,这意味着运动员和对方控球运动员之间的距离太远了,就不会去追逐。
如果确实超过了那个特定值,试图去偷球就变得毫无意义了,因为距离太远了而且很有可能队友已经在试图做同样的事情了。最好的选择是把偷球状态从有限状态机中弹出来,压入防守状态(防守状态会在下一个教程中进行解释)。现在,如果对方控球运动员离他太远的话,运动员会保留在他当前的位置上。
结果就是一个更让人信服也更合理的一个偷球运动模式(没有拥挤的行为):
在发起攻击的时候避开对手
如果运动员想要有效的发起攻击,这是他们要学习的最后一个技巧。目前他们在攻击对手的球门时并没有考虑对手所处的位置。一个对手必须视为对进攻能否顺利进行的一个威胁,所以应该在发起进攻的时候尽量躲避。
),运动员可以在移动的时候避开他们的对手。
DSC0006.gif
用来躲开对手的碰撞躲避行为。
对手将被视为圆形的障碍物。由于群体行为的动态特性,会在游戏循环的每一帧进行更新,躲避模式可以非常优雅平滑的工作,用来避开移动的障碍物(译注:在这个例子里,要避开的是对手,所以是移动的障碍物)。为了让运动员能够避开对手(也包括障碍物),需要在攻击状态中添加一行代码(下面代码的第14行):
[size=1em]
[size=1em]1

[size=1em]2

[size=1em]3

[size=1em]4

[size=1em]5

[size=1em]6

[size=1em]7

[size=1em]8

[size=1em]9

[size=1em]10

[size=1em]11

[size=1em]12

[size=1em]13

[size=1em]14

[size=1em]15

[size=1em]16

[size=1em]17

[size=1em]18

[size=1em]19

[size=1em]20

[size=1em]21

[size=1em]22

[size=1em]23

[size=1em]24

[size=1em]25

[size=1em]26

[size=1em]27

[size=1em]28

[size=1em]29

[size=1em]30

[size=1em]31

[size=1em]32

[size=1em]33

[size=1em]34

[size=1em]35

[size=1em]36

[size=1em]37

[size=1em]38

[size=1em]39

[size=1em]40

[size=1em]41

[size=1em]42

[size=1em]43

[size=1em][size=1em]class Athlete {
[size=1em]    // (...)
[size=1em]    private function attack() :void {
[size=1em]        var aPuckOwner :Athlete = getPuckOwner();
[size=1em]      
[size=1em]        // Does the puck have an owner?
[size=1em]        if (aPuckOwner != null) {
[size=1em]            // Yeah, it has. Let's find out if the owner belongs to the opponents team.
[size=1em]            if (doesMyTeamHaveThePuck()) {
[size=1em]                if (amIThePuckOwner()) {
[size=1em]                    // My team has the puck and I am the one who has it! Let's move
[size=1em]                    // towards the opponent's goal, avoding any opponents along the way.
[size=1em]                    mBoid.steering = mBoid.steering + mBoid.seek(getOpponentGoalPosition());
[size=1em]                    mBoid.steering = mBoid.steering + mBoid.collisionAvoidance(getOpponentTeam().members);
[size=1em]         
[size=1em]                } else {
[size=1em]                    // My team has the puck, but a teammate has it. Is he ahead of me?
[size=1em]                    if (isAheadOfMe(aPuckOwner.boid)) {
[size=1em]                        // Yeah, he is ahead of me. Let's just follow him to give some support
[size=1em]                        // during the attack.
[size=1em]                        mBoid.steering = mBoid.steering + mBoid.followLeader(aPuckOwner.boid);
[size=1em]                        mBoid.steering = mBoid.steering + mBoid.separation();
[size=1em]                    } else {
[size=1em]                        // Nope, the teammate with the puck is behind me. In that case
[size=1em]                        // let's hold our current position with some separation from the
[size=1em]                        // other, so we prevent crowding.
[size=1em]                        mBoid.steering = mBoid.steering + mBoid.separation();
[size=1em]                    }
[size=1em]                }
[size=1em]            } else {
[size=1em]                // The opponent has the puck! Stop the attack
[size=1em]                // and try to steal it.
[size=1em]                mBrain.popState();
[size=1em]                mBrain.pushState(stealPuck);
[size=1em]            }
[size=1em]        } else {
[size=1em]            // Puck has no owner, so there is no point to keep
[size=1em]            // attacking. It's time to re-organize and start pursuing the puck.
[size=1em]            mBrain.popState();
[size=1em]            mBrain.pushState(pursuePuck);
[size=1em]        }
[size=1em]    }
[size=1em]}




这一行代码将给运动员添加一个碰撞躲避力,这个力将和其他已经存在的力一起起作用。因此,运动员可以在接近对手球门的同时还能有效地避开障碍物。
下面是一个运动员处于攻击状态的演示。对手不能移动从而突出移动躲避行为:
总结
这个教程部分解释了如何实现攻击模式,运动员使用这个模式来偷球并运球攻击对手的球门。通过群体行为和基于堆栈的有限状态机的混合使用,运动员现在可以执行一些复杂的移动模式,比如追随自己方的运球运动员或者堵截带球的对手。
如同之前讨论的那样,攻击AI的实现目标是模仿人们在比赛中的行为,所以产生的结果是对人们在真实比赛中行为的一个近似。通过对组成攻击AI的各个状态进行独立的调整,可能会产生一个更好的模拟,或者让攻击AI更符合你的需求。
在下篇文章里面,你将学到如何让运动员进行防守。这样人工智能部分将会功能完备,既能攻击又能防守,从而可以进行一场100%由人工智能控制的比赛,2个人工智能可以互相比拼。
参考文献:

+1
2742°C
1
  • anmie7723
过: 他们
因分享而快乐,学习以自强!
anmie7723 发表于 2017-12-26 08:29:28 显示全部楼层
路过看看 感谢分享
因分享而快乐,学习以自强!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

VR/AR版块|Unity3d|Unreal4|新手报道|小黑屋|站点地图|沪ICP备14023207号-9|【泰斗社区】-专注互联网游戏和应用的开发者平台 ( 沪ICP备14023207号-9 )|网站地图

© 2001-2013 Comsenz Inc.  Powered by Discuz! X3.4

1
QQ