状态驱动的游戏智能体设计

State-Driven Game Agent Design
 
 
状态驱动的游戏智能体设计
Mat Buckland
赖勇浩 (http://blog.csdn.net/lanphaday)
Note: the text for this tutorial comprises part of the second chapter of the book Programming Game AI by Example. Its appearance online is a cunningly disguised attempt to tempt you into purchasing said book. I have no shame. Buy the book, you’ll enjoy it. J
  题注:本文取自《 Programming Game AI by Example 》一书的第二章,它在网上刊出的原因是想让你购买这本书。尽管这看起来有点狡猾,但我并不感到羞愧,因为如果你购买了这本书,你会发现它值得你购买。
原文地址: http://www.ai-junkie.com/architecture/state_driven/tut_state1.html
Finite state machines, or FSMs as they are usually referred to, have for many years been the AI coder’s instrument of choice to imbue a game agent with the illusion of intelligence. You will find FSMs of one kind or another in just about every game to hit the shelves since the early days of video games, and despite the increasing popularity of more esoteric agent architectures, they are going to be around for a long time to come. Here are just some of the reasons why:
就像经常听到的,有限状态机(简写为 FSM )早就被 AI 程序员用来实现游戏智能体以体现智能感。你会发现在 FSM 几乎是所有视频游戏的基础构架,不管正在出现和流行的越来越深奥的智能体架构, FSM 在长久的未来仍然有武之地。这里是一些为什么 FSM 如此强劲的原因:
  • They are quick and simple to code.There are many ways of programming a finite state machine and almost all of them are reasonably simple to implement. You’ll see several alternatives described in this article together with the pros and cons of using them.
  • 可以快速简单地编写代码。实现有限状态机有多种途径,而且都可以简单实现。在本文你就能看到多种描述和赞成或者反对使用它们的理由。
  • They are easy to debug.Because a game agent’s behavior is broken down into easily manageable chunks, if an agent starts acting strangely, it can be debugged by adding tracer code to each state. In this way, the AI programmer can easily follow the sequence of events that precedes the buggy behavior and take action accordingly.
  • 易于调试。因为一个游戏智能体的行为由一个易于管理的代码段来实现,如果一个智能出现奇怪的行为,可以通过为每一个状态增加Tracer来调试。这样能够容易地跟踪事件序列,就可以针对之前的怪异行为修改代码了。
  • They have little computational overhead.Finite state machines use hardly any precious processor time because they essentially follow hard- coded rules. There is no real “thinking” involved beyond the if-this-then-that sort of thought process.
  • 需要付出一点计算代价。有限状态机几乎不使用宝贵的处理器时间,因为他们本质上跟硬编码是一样的。在那种“如果这样就那样”的思考处理中根本不存在真正的“思考”。
  • They are intuitive.It’s human nature to think about things as being in one state or another and we often refer to ourselves as being in such and such a state. How many times have you “got yourself into a state” or found yourself in “the right state of mind”? Humans don’t really work like finite state machines of course, but sometimes we find it useful to think of our behavior in this way. Similarly, it is fairly easy to break down a game agent’s behavior into a number of states and to create the rules required for manipulating them. For the same reason, finite state machines also make it easy for you to discuss the design of your AI with non-programmers (with game producers and level designers for example), providing improved communication and exchange of ideas.
  • 符合直觉。类天生就以当前处以这种或者哪种状态来思考事情,所以我们常常听到我们自己处于什么状态的说法。多少次你“让你自己进入状态”或者发现你自己处于“正确的精神状态”?尽管人类并非真的像有限状态机那样工作,但通常我们发现这样有利于我们思考我们的行为。同样地,这易于通过一系列的状态和创造操作规则来实现一个游戏智能体的行为。基于同样的原因,有限状态机能让你和非程序员(如游戏策划和关卡设计师等)更好地进行关于你的AI设计的讨论,改进交流和交换观点。
  • They are flexible.A game agent’s finite state machine can easily be adjusted and tweaked by the programmer to provide the behavior required by the game designer. It’s also a simple matter to expand the scope of an agent’s behavior by adding new states and rules. In addition, as your AI skills grow you’ll find that finite state machines provide a solid backbone with which you can combine other techniques such as fuzzy logic or neural networks.
  • 可伸缩性。游戏智能体的有限状态机易于调整,能够很容易让程序员实现游戏设计师需要的行为,也易于通过增加新的状态和规则来扩展智能体的行为。此外,随着你AI技术的增进,你将发现有限状态机提供坚实的基础,让你能够把模糊逻辑和神经网络之类的技术组合到游戏中。
 
What Exactly Is a Finite State Machine?
有限状态机定义
Historically, a finite state machine is a rigidly formalized device used by mathematicians to solve problems. The most famous finite state machine is probably Alan Turing’s hypothetical device: the Turing machine, which he wrote about in his 1936 paper, “On Computable Numbers.” This was a machine presaging modern-day programmable computers that could perform any logical operation by reading, writing, and erasing symbols on an infinitely long strip of tape. Fortunately, as AI programmers, we can forgo the formal mathematical definition of a finite state machine; a descriptive one will suffice:
  从历史观点上来说,有限状态机是一种严格的公式化的被数学家用以解决难题的一种策略。最著名的有限状态机可能是阿兰·图灵在 1936 年发表的论文《 On Computable Numbers 》上写下的的猜想——图灵机。这是现代计算机的雏形,能够通过在无限长的磁带上进行读、写和擦除符号来实现所有逻辑操作。幸运的是,作为 AI 程序员,我们能够对公式化的数学定义不加理会,如下描述已经足够:
A finite state machine is a device, or a model of a device, which has a finite number of states it can be in at any given time and can operate on input to either make transitions from one state to another or to cause an output or action to take place. A finite state machine can only be in one state at any moment in time.
有限状态机是一种策略或者一种策略模型,它由有限的一系列状态构成,在任一给定时刻,可以通过输入操作作出从一种状态到另一种状态的转换或者产生输出或者发生动作。有限状态机在任一时刻都只能够处于一种状态中。
The idea behind a finite state machine, therefore, is to decompose an object’s behavior into easily manageable “chunks” or states. The light switch on your wall, for example, is a very simple finite state machine. It has two states: on and off. Transitions between states are made by the input of your finger. By flicking the switch up it makes the transition from off to on, and by flicking the switch down it makes the transition from on to off. There is no output or action associated with the off state (unless you consider the bulb being off as an action), but when it is in the on state electricity is allowed to flow through the switch and light up your room via the filament in a lightbulb. See Figure 2.1.
因此,有限状态机背后的思想就是把一个对象的行为分解为易于管理的“块”或者状态。例如墙上的电灯开关,就是一种非常简单的有限状态机。它有两个状态:开与关。两个状态通过你指头产生的输入来切换。把开关扳上,它就从关的状态转换到开的状态,把开关扳下,它就从开的状态转换到关的状态。在关的状态没有任何输出或者动作(除非你装灯泡被关掉视为一种动作),但当在开的状态下时电流通过开关并且通过灯泡里的灯丝照亮房间。如图 2.1
 
Figure 2.1. A light switch is a finite state machine. (Note that the switches are reversed in Europe and many other parts of the world.)
2.1 开关是一种有限状态机。(注意:这种开关在欧洲和其它很多国家仍有存在。)
Of course, the behavior of a game agent is usually much more complex than a lightbulb (thank goodness!). Here are some examples of how finite state machines have been used in games.
当然,游戏智能体的行为往往比灯泡要复杂得多。下文是一些在游戏中使用有限状态机的的例子。
本文由恋花蝶最初发表于 http://blog.csdn.net/lanphaday,欢迎转载,但必须保持全文完整,也必须包含本声明。
译者并示取得中文版的翻译授权,翻译本文只是出于研究和学习目的。任何人不得在未经同意的情况下将英文版和中文版用于商业行为,转载本文产生的法律和道德责任由转载者承担,与译者无关。
·         The ghosts’ behavior in Pac-Man is implemented as a finite state machine. There is one Evade state, which is the same for all ghosts, and then each ghost has its own Chase state, the actions of which are implemented differently for each ghost. The input of the player eating one of the power pills is the condition for the transition from Chase to Evade. The input of a timer running down is the condition for the transition from Evade to Chase.
·         Pac-Mac 里的精灵的行为用有限状态机实现。所有的精灵都有一种 Evade (逃避)状态,它们的实现都是一样的;但每一个精灵都一个 Chase (追踪)状态,它的实现各不相同。
·         Quake-style bots are implemented as finite state machines. They have states such as FindArmor , FindHealth , SeekCover , and RunAway . Even the weapons in Quake implement their own mini finite state machines. For example, a rocket may implement states such as Move , TouchObject , and Die .
·         Quake 系列的机器人以有限状态机实现。它们 FindArmor (找装备)、 FindHealth (找补血)、 SeekCover (找掩护)和 RunAway (逃跑)等多种状态。甚至 Quake 里实现的武器都带有小型有限状态机,例如一个火箭炮实现的状态就有 Move (移动)、 TouchObject (触到物体)和 Die (死亡)等几种状态。
·         Players in sports simulations such as the soccer game FIFA2002 are implemented as state machines. They have states such as Strike , Dribble , ChaseBall , and MarkPlayer . In addition, the teams themselves are often implemented as FSMs and can have states such as KickOff , Defend , or WalkOutOnField .
·         FIFA2002 之类的运动模拟游戏里的运动员是用状态机实现的,它们有 Strike (踢出)、 Dribble (带球)、 ChaseBall (逐球)和 MarkPlayer (盯人)等状态。此外,整个球队通常也是用 FSM 实现的,有 KickOff (发球)、 Defend (防守)和 WalkOutOnField (不知道怎么翻译,请足球达人告知一下)。
·         The NPCs (non-player characters) in RTSs (real-time strategy games) such as Warcraft make use of finite state machines. They have states such as MoveToPosition , Patrol , and FollowPath .
·         RTS (实时策略游戏)(例如 Warcraft )中的 NPC (非玩家角色)也利用有限状态机。它们的状态有 MoveToPosition (移动到某地)、 Patrol (巡逻)和 FollowPath (跟随)等。
 
Implementing a Finite State Machine
有限状态机实现
  There are a number of ways of implementing finite state machines. A naive approach is to use a series of if-then statements or the slightly tidier mechanism of a switch statement. Using a switch with an enumerated type to represent the states looks something like this:
实现有限状态机有许多方式。一个直观的做法就是使用一系列的 if-then 语句或者稍显整洁的 switch 语句。使用 switch 的实现看起来就像这里的代码:
enum StateType{state_RunAway, state_Patrol, state_Attack};
void Agent::UpdateState(StateType CurrentState)
{
  switch(CurrentState)
  {
  case state_RunAway:
     EvadeEnemy();
     if (Safe())
     {
       ChangeState(state_Patrol);
     }
     break;
  case state_Patrol:
    FollowPatrolPath();
    if (Threatened())
    {
      if (StrongerThanEnemy())
      {
        ChangeState(state_Attack);
      }
      else
      {
        ChangeState(state_RunAway);
      }
    }
    break;
  case state_Attack:
    if (WeakerThanEnemy())
    {
       ChangeState(state_RunAway);
    }
    else
    {
      BashEnemyOverHead();
    }
    break;
  }//end switch
}
Although at first glance this approach seems reasonable, when applied practically to anything more complicated than the simplest of game objects, the switch/if-then solution becomes a monster lurking in the shadows waiting to pounce. As more states and conditions are added, this sort of structure ends up looking like spaghetti very quickly, making the program flow difficult to understand and creating a debugging nightmare. In addition, it’s inflexible and difficult to extend beyond the scope of its original design, should that be desirable… and as we all know, it most often is. Unless you are designing a state machine to implement very simple behavior (or you are a genius), you will almost certainly find yourself first tweaking the agent to cope with unplanned-for circumstances before honing the behavior to get the results you thought you were going to get when you first planned out the state machine!
尽管咋一看这个方案还可以,但只要将其应用到比最简单的游戏对象稍为复杂的实际情况下,这个 switch/if-then 方案就变成了蛰伏在阴影下的怪物——随时都可能突袭你一下。随着大量的状态和条件的增加,那种结构很快就会变得像意大利面条一样,使用程序难以理解,并成为调试梦魇。此外,它不可伸缩并且难以在最初的设计范围之外进行扩展,然而我们都知道,它极为常见。除非你用状态机实现非常简单的行为(或者你是一个天才),否则当你第一次策划状态机的时候,在你“磨合”取得的结果和你希望取得的结果之前,你几乎肯定会发现你的智能体无法应用未考虑到的环境。
Additionally, as an AI coder, you will often require that a state perform a specific action (or actions) when it’s initially entered or when the state is exited. For example, when an agent enters the state RunAway you may want it to wave its arms in the air and scream “Arghhhhhhh!” When it finally escapes and changes state to Patrol , you may want it to emit a sigh, wipe its forehead, and say “Phew!” These are actions that only occur when the RunAway state is entered or exited and not during the usual update step. Consequently, this additional functionality must ideally be built into your state machine architecture. To do this within the framework of a switch or if-then architecture would be accompanied by lots of teeth grinding and waves of nausea, and produce very ugly code indeed.
此外,作为一名 AI 程序员,你经常需要在某一状态实现一种特殊行为(或者一系列行为),比如在进入或者离开某一状态的时候。例如当一个智能体进行 RunAway (逃跑)状态时你希望它把武器抛向空中并尖叫一声“ Arghhhhhh (啊)!”当它成功逃脱并转换到 Patrol (巡逻)状态,你可能想让它喘口气、擦擦额头然后说一声“ Phew (呸)!”这些行为都只在进入和离开 RunAway (逃跑)状态时才会发生,而不是整个普通的 update (更新)阶段都会出现。因此这些额外的功能必须被完美地集成到你的状态机架构里。在 switch 或者 if-then 架构里实现这些将让人难以忍受,产生的代码将非常丑陋,不忍卒读。
本文由恋花蝶最初发表于 http://blog.csdn.net/lanphaday,欢迎转载,但必须保持全文完整,也必须包含本声明。
译者并示取得中文版的翻译授权,翻译本文只是出于研究和学习目的。任何人不得在未经同意的情况下将英文版和中文版用于商业行为,转载本文产生的法律和道德责任由转载者承担,与译者无关。
State Transition Tables
状态转换表
A better mechanism for organizing states and affecting state transitions is a state transition table . This is just what it says it is: a table of conditions and the states those conditions lead to. Table 2.1 shows an example of the mapping for the states and conditions shown in the previous example.
一个能够更好地组织和进行状态转换的机制是状态转换表。顾名思义这就是一个包含条件和条件导致的状态的表。表 2.1 是前文代码的状态和条件影射表:
Table 2.1. A simple state transition table
2.1 简单状态转换表
Current State
Condition
State Transition
Runaway
Safe
Patrol
Attack
WeakerThanEnemy
RunAway
Patrol
Threatened AND StrongerThanEnemy
Attack
Patrol
Threatened AND WeakerThanEnemy
RunAway
This table can be queried by an agent at regular intervals, enabling it to make any necessary state transitions based on the stimulus it receives from the game environment. Each state can be modeled as a separate object or function existing external to the agent, providing a clean and flexible architecture. One that is much less prone to spaghettification than the if-then/switch approach discussed in the previous section.
智能体隔一定时间查询这个表格,以使得它能够基于从游戏环境接收到的消息来进行必须的状态转换。每一个状态都能够实现为彼此分离的与智能体不耦合的对象或函数,以提供清晰和可伸缩的架构。这一设计不再那么容易像前文讨论的 if-then/switch 架构那样容易成为意大利面条。
Someone once told me a vivid and silly visualization can help people to understand an abstract concept. Let’s see if it works…
曾有人告诉我一个明晰而无聊的可视物能帮助人们理解抽象的理论,让我们来看看当它工作时……
Imagine a robot kitten. It’s shiny yet cute, has wire for whiskers and a slot in its stomach where cartridges — analogous to its states — can be plugged in. Each of these cartridges is programmed with logic, enabling the kitten to perform a specific set of actions. Each set of actions encodes a different behavior; for example, play_with_string , eat_fish , or poo_on_carpet . Without a cartridge stuffed inside its belly the kitten is an inanimate metallic sculpture, only able to sit there and look cute… in a Metal Mickey kind of way.
想像存在一个机器猫,它闪闪发亮但是非常可爱,有着金属丝制作的胡须并且在胃部有一个插槽——可以依照状态插入可插入模组。每一个可插入模组都是一段逻辑程序,使得小猫能够实现一系列指定的动作。每一系列动作都编码为不同的行为,如 play_with_string eat_fish poo_on_carpet 。如果没有在小猫的胃部插上可插入模组,它就是一件死物——坐在那里,看起来蛮可受。
The kitten is very dexterous and has the ability to autonomously exchange its cartridge for another if instructed to do so. By providing the rules that dictate when a cartridge should be switched, it’s possible to string together sequences of cartridge insertions permitting the creation of all sorts of interesting and complicated behavior. These rules are programmed onto a tiny chip situated inside the kitten’s head, which is analogous to the state transition table we discussed earlier. The chip communicates with the kitten’s internal functions to retrieve the information necessary to process the rules (such as how hungry Kitty is or how playful it’s feeling). As a result, the state transition chip can be programmed with rules like:
IF Kitty_Hungry AND NOT Kitty_Playful SWITCH_CARTRIDGE eat_fish
All the rules in the table are tested each time step and instructions are sent to Kitty to switch cartridges accordingly.
这个小猫非常灵巧,并且能够根据指令自动地更换可插入模组。通过给定什么时候该更换可插入模组的规则,它能利用连贯地插入一系列的可插入模组创造所有有趣而复杂的行为。这些与前文讨论的状态转换表相类似的规则被编成程序写入到一个极小的芯片,放置在小猫的头部。芯片与小猫的内部功能通信,以获得处理规则时需要的信息(如 Kitty 有多饿和它感觉到的好玩度是多少)。状态转换芯片可以用如下的方式编写规则:
IF Kitty_Hungry AND NOT Kitty_Playful SWITCH_CARTRIDGE eat_fish
在每一个时间片都检测表中的所有的规则,从而给 Kitty 发送指令以切换弹可插入模组。
This type of architecture is very flexible, making it easy to expand the kitten’s repertoire by adding new cartridges. Each time a new cartridge is added, the owner is only required to take a screwdriver to the kitten’s head in order to remove and reprogram the state transition rule chip. It is not necessary to interfere with any other internal circuitry.
这种架构有良好的伸缩性,通过增加新的可插入模组就可以轻易扩展小猫的指令表。每一次增加新的可插入模组,只需要用起子打开小猫的头壳,重编程状态转换规则芯片即可,不需要与其它内部电路打交道。
Embedded Rules
规则内嵌
An alternative approach is to embed the rules for the state transitions within the states themselves . Applying this concept to Robo-Kitty, the state transition chip can be dispensed with and the rules moved directly into the cartridges. For instance, the cartridge for play_with_string can monitor the kitty’s level of hunger and instruct it to switch cartridges for the eat_fish cartridge when it senses hunger rising. In turn the eat_fish cartridge can monitor the kitten’s bowel and instruct it to switch to the poo_on_carpet cartridge when it senses poo levels are running dangerously high.
另一可选的方法是把状态转换规则内嵌到状态本身当中。在对机器猫应用这个概念后,可以去除状态转换芯片,直接将规则内置到可插入模组里。例如 play_with_string 可插入模组能够监视小猫的饥饿度并适时地命令它切换到 eat_fish 可插入模组。同样地, eat_fish 可插入模组能够监视小猫是否已经吃饱,并在感觉到迫切的排泄感时命令它切换到 poo_on_carpet 可插入模组。
Although each cartridge may be aware of the existence of any of the other cartridges, each is a self-contained unit and not reliant on any external logic to decide whether or not it should allow itself to be swapped for an alternative. As a consequence, it’s a straightforward matter to add states or even to swap the whole set of cartridges for a completely new set (maybe ones that make little Kitty behave like a raptor). There’s no need to take a screwdriver to the kitten’s head, only to a few of the cartridges themselves.
尽管每一个可插入模组需要知道其它模组的存在,但它们都是自包含的,无论是否要其它模组来替换自己,都并不需要任何外部逻辑来帮助决策。从而可以推论出其中的简明关系,甚至可以用全新的模组集合替换已有的集合(可能这会使用小猫的行为像鸟类一样)。使用这个方案不再需要用起子打开小猫的头部,只要改变可插入模组即可。
Let’s take a look at how this approach is implemented within the context of a video game. Just like Kitty’s cartridges, states are encapsulated as objects and contain the logic required to facilitate state transitions. In addition, all state objects share a common interface: a pure virtual class named State . Here’s a version that provides a simple interface:
现在来看看在视频游戏中如何实现这一方案。像刚才讨论的小猫的可插入模组,可以封装到一个对象里,其中包含辅助状态转换的的逻辑。另外,所有的状态共享一个通用接口:一个命名为 State 的纯虚类。这里有个接口简单的实现:
Class State
{
public:
 
  virtual void Execute (Troll* troll) = 0;
};
  Now imagine a Troll class that has member variables for attributes such as health, anger, stamina, etc., and an interface allowing a client to query and adjust those values. A Troll can be given the functionality of a finite state machine by adding a pointer to an instance of a derived object of the State class, and a method permitting a client to change the instance the pointer is pointing to.
现在想象 Troll 类有一系列的数值成员变量: health anger stamina 等,当然也有相应的接口以查询和设置这些变量值。 Troll 通过增加一个成员指针变量(指向 State 类派生类的实例)来增加有限状态机的功能,还提供一个改变指针指向的实例的方法。
class Troll
{
  /* ATTRIBUTES OMITTED */
 
  State* m_pCurrentState;
 
public:
 
  /* INTERFACE TO ATTRIBUTES OMITTED */
 
  void Update()
  {
    m_pCurrentState->Execute(this);
  }
 
  void ChangeState(const State* pNewState)
  {
    delete m_pCurrentState;
    m_pCurrentState = pNewState;
  }
};
  When the Update method of a Troll is called, it in turn calls the Execute method of the current state type with the this pointer. The current state may then use the Troll interface to query its owner, to adjust its owner’s attributes, or to effect a state transition. In other words, how a Troll behaves when updated can be made completely dependent on the logic in its current state. This is best illustrated with an example, so let’s create a couple of states to enable a troll to run away from enemies when it feels threatened and to sleep when it feels safe.
当调用 Troll Update 方法时,它以 this 指针为参数调用当前状态类型的 Excecute 方法。当前状态可能使用 Troll 的接口查询它的拥有者,设置拥有者的属性或者产生一个状态转换。换句话说, Troll 能依赖当前状态的逻辑作出完整行为。这里有最好的例子表达这一观点,让我们来完成两个状态实现以使得 Troll 能够在危险时逃跑,或者在安全时睡觉。
//----------------------------------State_Runaway
class State_RunAway : public State
{
public:
 
  void Execute(Troll* troll)
  {
    if (troll->isSafe())
    {
      troll->ChangeState(new State_Sleep());
    }
    else
    {
      troll->MoveAwayFromEnemy();
    }
  }
};
 
//----------------------------------State_Sleep
class State_Sleep : public State
{
public:
 
  void Execute(Troll* troll)
  {
    if (troll->isThreatened())
    {
      troll->ChangeState(new State_RunAway())
    }
 
    else
    {
      troll->Snore();
    }
  }
};
  As you can see, when updated, a troll will behave differently depending on which of the states m_pCurrentState points to. Both states are encapsulated as objects and both provide the rules effecting state transition. All very neat and tidy.
如你所见,当调用 Update 时, Troll 的行为依赖 m_pCurrentState 指向的状态不同而有所不同。两者状态都封装到对象里,并且都提供了产生状态转换的规则。所有这一切都灵巧而整洁。
This architecture is known as the state design pattern and provides an elegant way of implementing state-driven behavior. Although this is a departure from the mathematical formalization of an FSM, it is intuitive, simple to code, and easily extensible. It also makes it extremely easy to add enter and exit actions to each state; all you have to do is create Enter and Exit methods and adjust the agent’s ChangeState method accordingly. You’ll see the code that does exactly this very shortly.
这一架构即是有名的状态设计模式,它提供了雅致的状态驱动行为实现。尽管这有违 FSM 的数学形式,但它符合直觉、易于编码并且容易扩展。它同样也能够极其容易地增加进入和离开状态时的动作;你需要做只是实现 Enter Exit 方法并相应地改变 ChangeState 方法。你将可以看到完成这些所产生的代码真的非常短小。
The West World Project
WestWorld 项目
As a practical example of how to create agents that utilize finite state machines, we are going to look at a game environment where agents inhabit an Old West-style gold mining town named West World. Initially there will only be one inhabitant — a gold miner named Miner Bob — but later his wife will also make an appearance. You will have to imagine the tumbleweeds, creakin’ mine props, and desert dust blowin’ in your eyes because West World is implemented as a simple text-based console application. Any state changes or output from state actions will be sent as text to the console window. I’m using this plaintext-only approach as it demonstrates clearly the mechanism of a finite state machine without adding the code clutter of a more complex environment.
作为一个如何利用有限状态机的创造智能体的实例,我们创建名为 WestWorld 的旧西部风格的淘金镇的游戏,并研究其中的智能体实现。一开始只存在一个名为 Miner Bob 的淘金者,随后他的妻子也出现。你可以想像风滚草、叽叽作响的淘金用具和沙漠的风把沙吹进你的眼睛,因为 WestWorld 只是一个简单的基于文本的控制台程序。所有的状态改变和状态动作产生的输出都作为文本传送到控制台窗口。我使用纯文本的原因是为了清晰地示范有限状态机的机制,不想增加代码以免搞得太过于复杂。
There are four locations in West World: a goldmine , a bank where Bob can deposit any nuggets he finds, a saloon in which he can quench his thirst, and home-sweet-home where he can sleep the fatigue of the day away. Exactly where he goes, and what he does when he gets there, is determined by Bob’s current state. He will change states depending on variables like thirst, fatigue, and how much gold he has found hacking away down in the gold mine.
WestWorld 有四个场景:一个金矿、一个储藏库( Bob 把找到的金块存放在这里)、一个酒吧(喝水吃饭)和一个家(睡觉)。确切来讲就是他去哪里、做什么和什么去,都由 Bob 当前的状态决定。他根据饥渴度、疲惫度和从金旷获得的金块数量来改变状态。
Before we delve into the source code, check out the following sample output from the WestWorld1 executable.
在我们研究代码之前,我们先来看看 WestWorld1 可执行文件产生的输出:
Miner  Bob:  Pickin'  up  a  nugget
Miner  Bob:  Pickin'  up  a  nugget
Miner  Bob:  Ah'm  leavin'  the  gold  mine  with  mah  pockets  full  o'  sweet  gold
Miner  Bob:  Goin'  to  the  bank.  Yes  siree
Miner  Bob:  Depositin’  gold.  Total  savings  now:  3
Miner  Bob:  Leavin'  the  bank
Miner  Bob:  Walkin'  to  the  gold  mine
Miner  Bob:  Pickin'  up  a  nugget
Miner  Bob:  Ah'm  leavin'  the  gold  mine  with  mah  pockets  full  o'  sweet  gold
Miner  Bob:  Boy,  ah  sure  is  thusty!  Walkin'  to  the  saloon
Miner  Bob:  That's  mighty  fine  sippin  liquor
Miner  Bob:  Leavin'  the  saloon,  feelin'  good
Miner  Bob:  Walkin'  to  the  gold  mine
Miner  Bob:  Pickin'  up  a  nugget
Miner  Bob:  Pickin'  up  a  nugget
Miner  Bob:  Ah'm  leavin'  the  gold  mine  with  mah  pockets  full  o'  sweet  gold
Miner  Bob:  Goin'  to  the  bank.  Yes  siree
Miner  Bob:  Depositin'  gold.  Total  savings  now:  4
Miner  Bob:  Leavin'  the  bank
Miner  Bob:  Walkin'  to  the  gold  mine
Miner  Bob:  Pickin'  up  a  nugget
Miner  Bob:  Pickin'  up  a  nugget
Miner  Bob:  Ah'm  leavin'  the  gold  mine  with  mah  pockets  full  o'  sweet  gold
Miner  Bob:  Boy,  ah  sure  is  thusty!  Walkin'  to  the  saloon
Miner  Bob:  That's  mighty  fine  sippin'  liquor
Miner  Bob:  Leavin'  the  saloon,  feelin'  good
Miner  Bob:  Walkin'  to  the  gold  mine
Miner  Bob:  Pickin'  up  a  nugget
Miner  Bob:  Ah'm  leavin'  the  gold  mine  with  mah  pockets  full  o'  sweet  gold
Miner  Bob:  Goin'  to  the  bank.  Yes  siree
Miner  Bob:  Depositin'  gold.  Total  savings  now:  5
Miner  Bob:  Woohoo!  Rich  enough  for  now.  Back  home  to  mah  li'l  lady
Miner  Bob:  Leavin'  the  bank
Miner  Bob:  Walkin'  home
Miner  Bob:  ZZZZ...
Miner  Bob:  ZZZZ...
Miner  Bob:  ZZZZ...
Miner  Bob:  ZZZZ...
Miner  Bob:  What  a  God-darn  fantastic  nap!  Time  to  find  more  gold
In the output from the program, each time you see Miner Bob change location he is changing state. All the other events are the actions that take place within the states. We’ll examine each of Miner Bob’s potential states in just a moment, but for now, let me explain a little about the code structure of the demo.
 (You can download the accompanying project files here (24k))
从程序的输出来看, Miner Bob 每一次改变他所处的场景时,他都会改变状态。所有的其它事件都发生在状态里的动作。我们将会检测 Miner Bob 在每一时刻的每一个潜在状态,但现在让我来对 demo 的代码结构稍作解释。
( 你可以从 这里 下载项目代码 (24k))
The BaseGameEntity Class
BaseGameEntity
All inhabitants of West World are derived from the base class BaseGameEntity . This is a simple class with a private member for storing an ID number. It also specifies a pure virtual member function, Update , which must be implemented by all subclasses. Update is a function that gets called every update step and will be used by subclasses to update their state machine along with any other data that must be updated each time step.
WeskWorld 游戏里的所有物体都从 BaseGameEntity 类派生。这是只有一个私有成员(用以保存 ID )的简单类,此外就只有一个纯虚函数 Update 了,它必须在子类中实现。 Update 函数将在更新步骤里调用,用以给子类在每一个时间片依据其它数据更新他们的状态机里必须被更新的其它数据。
The BaseGameEntity class declaration looks like this:
BaseGameEntity 的声明如下:
class BaseGameEntity
{
private:
 
  //every entity has a unique identifying number
  int          m_ID;
 
  //this is the next valid ID. Each time a BaseGameEntity is instantiated
  //this value is updated
  static int  m_iNextValidID;
 
  //this is called within the constructor to make sure the ID is set
  //correctly. It verifies that the value passed to the method is greater
  //or equal to the next valid ID, before setting the ID and incrementing
  //the next valid ID
  void SetID(int val);
 
public:
 
  BaseGameEntity(int id)
  {
    SetID(id);
  }
 
  virtual ~BaseGameEntity(){}
 
  //all entities must implement an update function
  virtual void  Update()=0;
 
  int           ID()const{return m_ID;} 
};
For reasons that will become obvious later [in the book] , it’s very important for each entity in your game to have a unique identifier. Therefore, on instantiation, the ID passed to the constructor is tested in the SetID method to make sure it’s unique. If it is not, the program will exit with an assertion failure. In the example given, the entities will use an enumerated value as their unique identifier. These can be found in the file EntityNames.h as ent_Miner_Bob and ent_Elsa .
为游戏里的每一个实体设置一个唯一的 ID 是非常重要的,在本书后面的章节将为你讲述为什么非常重要。因此,在实例化的时候把 ID 通过构造函数传递,并通过 SetID 函数来测试它是否唯一,如果不唯一,程序将会退出,产生一个断言失败错误。在本文的例子中,将把一个枚举值作为唯一的 ID ,在 EntityNames.h 文件里可以找到 ent_Miner_Bob ent_Elsa 等枚举值。
The Miner Class
Miner
The Miner class is derived from the BaseGameEntity class and contains data members representing the various attributes a Miner possesses, such as its health, its level of fatigue, its position, and so forth. Like the troll example shown earlier, a Miner owns a pointer to an instance of a State class in addition to a method for changing what State that pointer points to.
Miner 类从 BaseGameEntity 类派生,它包括健康、疲惫程度和位置等数据成员。像前文描述过的 Troll 例子, Miner 也有一个指向 State 类实例的指针,当然也少不了用以改变 State 指针所指向的实例的方法。
Class Miner : public BaseGameEntity
{
private:
 
  //a pointer to an instance of a State
  State*                m_pCurrentState;
 
  // the place where the miner is currently situated
  location_type         m_Location;
 
  //how many nuggets the miner has in his pockets
  int                   m_iGoldCarried;
 
  //how much money the miner has deposited in the bank
  int                   m_iMoneyInBank;
 
  //the higher the value, the thirstier the miner
  int                   m_iThirst;
 
  //the higher the value, the more tired the miner
  int                   m_iFatigue;
 
public:
 
  Miner(int ID);
 
  //this must be implemented
  void Update();
 
  //this method changes the current state to the new state
  void ChangeState(State* pNewState);
 
  /* bulk of interface omitted */
};
  The Miner::Update method is straightforward; it simply increments the m_iThirst value before calling the Execute method of the current state. It looks like this:
Miner::Update 方法直接明了:它在调用当前状态的 Execute 方法之前简单地增加 m_iThirst 。它的实现如下:
本文由恋花蝶最初发表于 http://blog.csdn.net/lanphaday,欢迎转载,但必须保持全文完整,也必须包含本声明。
译者并示取得中文版的翻译授权,翻译本文只是出于研究和学习目的。任何人不得在未经同意的情况下将英文版和中文版用于商业行为,转载本文产生的法律和道德责任由转载者承担,与译者无关。
void Miner::Update()
{
  m_iThirst += 1;
 
  if (m_pCurrentState)
  {
    m_pCurrentState->Execute(this);
  }
}
Now that you’ve seen how the Miner class operates, let’s take a look at each of the states a miner can find itself in.
现在你知道 Miner 类的操作了,让我们来看看它的每一个状态是怎么样的。
The Miner States
Miner 的状态
The gold miner will be able to enter one of four states. Here are the names of those states followed by a description of the actions and state transitions that occur within those states:
淘金者 Bob 能够进入这四个状态之一。下文是这些状态的名字(结合了动作的描述),状态转换发生在状态内部。
  • EnterMineAndDigForNugget: If the miner is not located at the gold mine, he changes location. If already at the gold mine, he digs for nuggets of gold. When his pockets are full, Bob changes state to VisitBankAndDepositGold, and if while digging he finds himself thirsty, he will stop and change state to QuenchThirst.
  • EnterMinAndDigForNuggetBob不在金矿的时候,他移动到金矿。如果已经在金矿,他会持续掘金。直到他的袋子装满金矿石,Bob将会转换到VisitBankAndDepositGold状态。但如果在掘金的时候觉得饥渴,他就会停下来,把状态转换到QuenchThirst
  • VisitBankAndDepositGold: In this state the miner will walk to the bank and deposit any nuggets he is carrying. If he then considers himself wealthy enough, he will change state to GoHomeAnd- SleepTilRested. Otherwise he will change state to EnterMine- AndDigForNugget.
  • VisitBankAndDepositGold处于这个状态时淘金者会走到储藏库并把带来的金矿石保存起来。如果他觉得自己足够富有,他就转换到GoHomeAndSleepTilRested状态,否则就转换到EnterMineAndDigForNugget
  • GoHomeAndSleepTilRested: In this state the miner will return to his shack and sleep until his fatigue level drops below an acceptable level. He will then change state to EnterMineAndDigForNugget.
  • GoHomeAndSleepTilRested:处于此状态的淘金者会返回到他的房子里睡觉,直到疲惫程序下降到可接受的情况,这时转换到EnterMineAndDigForNugget
  • QuenchThirst: If at any time the miner feels thirsty (diggin’ for gold is thusty work, don’t ya know), he changes to this state and visits the saloon in order to buy a whiskey. When his thirst is quenched, he changes state to EnterMineAndDigForNugget.
  • QuenchThirst任何时候当淘金者感到饥渴,他就改变他的状态去商店买威士忌,解渴后转换到EnterMineAndDigForNugget
Sometimes it’s hard to follow the flow of the state logic from reading a text description like this, so it’s often helpful to pick up pen and paper and draw a state transition diagram for your game agents. Figure 2.2 shows the state transition diagram for the gold miner. The bubbles represent the individual states and the lines between them the available transitions.
通过阅读来理解状态逻辑流是相当困难的,所以最后为你的游戏智能体画一张状态转换图。图 2.2 是淘金者的状态转换图,圆角矩形是独立的状态,它们之间的连线是允许的转换。
A diagram like this is better on the eyes and can make it much easier to spot any errors in the logic flow. 
一个这样的图示有助于我们理解,也更容易找出逻辑流中的错误。
 
Figure 2.2. Miner Bob’s state transition diagram
2.2         淘金者 Bob 的状态转换图
  The State Design Pattern Revisited
重温状态设计模式
You saw a brief description of this design pattern earlier, but it won’t hurt to recap. Each of a game agent’s states is implemented as a unique class and each agent holds a pointer to an instance of its current state. An agent also implements a ChangeState member function that can be called to facilitate the switching of states whenever a state transition is required. The logic for determining any state transitions is contained within each State class. All state classes are derived from an abstract base class, thereby defining a common interface. So far so good. You know this much already.
之前已经对这个模式作了简单介绍,但不够深入。每一个游戏智能体的状态机都作为唯一的类来实现,智能体拥有一个指向当前状态实例的指针。智能体需要实现 ChangeState 成员函数以实现状态切换。决定状态转换的逻辑包含在每一个 State 派生类的内部。所有的状态类都从一个抽象类派生,以获得统一接口。现在,你已经知道足够多关于状态设计模式的知识了。
Earlier it was mentioned that it’s usually favorable for each state to have associated Enter and Exit actions. This permits the programmer to write logic that is only executed once at state entry or exit and increases the flexibility of an FSM a great deal. With these features in mind, let’s take a look at an enhanced State base class.
之前也提及过通常每一个状态都有相应的 Enter Exit 动作,这将使得程序员能够编写仅在进入或者离开状态只执行一次的逻辑以增强 FSM 的可伸缩性。为了实现这一点,让我们来看看改进后的 State 基类。
class State
{
public:
 
  virtual ~State(){}
 
  //this will execute when the state is entered
  virtual void Enter(Miner*)=0;
 
  //this is called by the miner’s update function each update-step
  virtual void Execute(Miner*)=0;
 
  //this will execute when the state is exited
  virtual void Exit(Miner*)=0;
}
  These additional methods are only called when a Miner changes state. When a state transition occurs, the Miner::ChangeState method first calls the Exit method of the current state, then it assigns the new state to the current state, and finishes by calling the Enter method of the new state (which is now the current state). I think code is clearer than words in this instance, so here’s the listing for the ChangeState method:
这两个方法仅在 Miner 改变状态的时候调用,当发生一个状态转换, Miner::ChangeState 方法首先调用当前状态的 Exit 方法,然后它为当前状态指派一个新的状态,最后调用新状态的 Enter 方法。我认为代码比言语更清晰,所有这里列出 ChangeState 方法的代码:
void Miner::ChangeState(State* pNewState)
{
  //make sure both states are valid before attempting to
  //call their methods
  assert (m_pCurrentState && pNewState);
 
  //call the exit method of the existing state
  m_pCurrentState->Exit(this);
 
  //change state to the new state
  m_pCurrentState = pNewState;
 
  //call the entry method of the new state
  m_pCurrentState->Enter(this);
}
  Notice how a Miner passes the this pointer to each state, enabling the state to use the Miner interface to access any relevant data.
注意 Miner this 指针传递到每一个状态,使得状态能够使用 Miner 的接口获取相关数据。
TIP: The state design pattern is also useful for structuring the main components of your game flow. For example, you could have a menu state, a save state, a paused state, an options state, a run state, etc.
提示:状态设计模式对于游戏主流程的组织也是非常有用的,例如,你可能有菜单状态、保存状态、暂停状态、设置状态和运行状态等。
Each of the four possible states a Miner may access are derived from the State class, giving us these concrete classes: EnterMineAndDigForNugget , VisitBankAndDepositGold , GoHomeAndSleepTilRested , and QuenchThirst . The Miner::m_pCurrentState pointer is able to point to any of these states. When the Update method of Miner is called, it in turn calls the Execute method of the currently active state with the this pointer as a parameter. These class relationships may be easier to understand if you examine the simplified UML class diagram shown in Figure 2.3. (Click here for an introduction to UML class diagrams)
Miner 可能处于四个状态之一,它们都从 State 类派生而来,具体是: EnterMineAndDigForNugget VisitBankAndDepositGold GoHomeAndSleepTilRested QuenchThirst Miner::m_pCurrrentState 可能指向其中的任何一个。当 Miner Update 方法被调用,它就以 this 指针为参数调用当前活动状态的 Execute 方法。如果你能看图 2.3 UML 图,应该很容易理解这些类之间的关系。( 这里 有对 UML 图的介绍)
本文由恋花蝶最初发表于 http://blog.csdn.net/lanphaday,欢迎转载,但必须保持全文完整,也必须包含本声明。
译者并示取得中文版的翻译授权,翻译本文只是出于研究和学习目的。任何人不得在未经同意的情况下将英文版和中文版用于商业行为,转载本文产生的法律和道德责任由转载者承担,与译者无关。
Each concrete state is implemented as a singleton object. This is to ensure that there is only one instance of each state, which agents share (those of you unsure of what a singleton is, please read this ). Using singletons makes the design more efficient because they remove the need to allocate and deallocate memory every time a state change is made. This is particularly important if you have many agents sharing a complex FSM and/or you are developing for a machine with limited resources.
每一个状态都以单件对象的形式实现,这是为了确保只有一个状态的实例,所有的智能体共享这一实例(想了解什么是单件,可以阅读这个 文档 )。使用单件使得这一设计更加高效,因为避免了在每一次状态转换的时候申请和释放内存。这在你有很多智能体共享复杂的 FSM 的时候变得极其重要,特别是你在资源受限的机器上进行开发的话。
 
Figure 2.3. UML class diagram for Miner Bob’s state machine implementation
2.3         Miner Bob 的状态机实现的 UML 类图
 
NOTE     I prefer to use singletons for the states for the reasons I’ve already given, but there is one drawback. Because they are shared between clients, singleton states are unable to make use of their own local, agent-specific data. For instance, if an agent uses a state that when entered should move it to an arbitrary position, the position cannot be stored in the state itself (because the position may be different for each agent that is using the state). Instead, it would have to be stored somewhere externally and be accessed by the state via the agent’s interface. This is not really a problem if your states are accessing only one or two pieces of data, but if you find that the states you have designed are repeatedly accessing lots of external data, it’s probably worth considering disposing of the singleton design and writing a few lines of code to manage the allocation and deallocation of state memory.
注意:我乐于使用单件的原因在上文已经给出,但这也有一个缺陷。因为他们由客户共享,单件状态不能使用他们自有的,特定智能体的数据。例如,当某一处于某状态的智能体移动到某一位置时,他不能把这一位置存储在状态内(因为这个状态可能与其它正处于这一状态的智能体不同)。它只能把它存储在其它地方,然后由状态机通过智能体的接口来存取。如果你的状态只有一两个数据要存取,那这也不是什么大问题,但如果你在很多外部数据,那可能就值得考虑放弃单件设计,而转而写一代码来管理状态内存的申请与释放了。
 
Okay, let’s see how everything fits together by examining the complete code for one of the miner states.
好了,现在让我们来看看如何把所有的东西都融合在一起完成一个淘金者的状态。
The EnterMineAndDigForNuggetState
EnterMineAndDigForNugget状态
In this state the miner should change location to be at the gold mine. Once at the gold mine he should dig for gold until his pockets are full, when he should change state to VisitBankAndDepositNugget .If the miner gets thirsty while digging he should change state to QuenchThirst .
淘金者在这个状态会改变所在地,去到 金矿场,到矿场后就开始掘金,直到装满口袋,这时改变状态到 VisitBankanDepositNugget 。如果掘金中途感到口渴,淘金者就转换到 QuenchThirst 状态。
 
Because concrete states simply implement the interface defined in the virtual base class State , their declarations are very straightforward:
因为具类只是简单地实现虚基类 State 定义的接口,它们的声明非常简明:
class EnterMineAndDigForNugget : public State
{
private:
 
  EnterMineAndDigForNugget(){}
 
  /* copy ctor and assignment op omitted */
 
public:
 
  //this is a singleton
  static EnterMineAndDigForNugget* Instance();
 
  virtual void Enter(Miner* pMiner);
 
  virtual void Execute(Miner* pMiner);
 
  virtual void Exit(Miner* pMiner);
};
  As you can see, it’s just a formality. Let’s take a look at each of the methods in turn.
如你所见,这只是一个模式,让我们来看看其它方法。
EnterMineAndDigForNugget::Enter
EnterMineAndDigForNugget::Enter
The code for the Enter method of EnterMineAndDigForNugget is as follows:
下面是 EnterMineAndDigForNugget Enter 方法:
void EnterMineAndDigForNugget::Enter(Miner* pMiner)
{
  //if the miner is not already located at the goldmine, he must
  //change location to the gold mine
  if (pMiner->Location() != goldmine)
  {
    cout << "/n" << GetNameOfEntity(pMiner->ID()) << ": "
         << "Walkin' to the goldmine";
 
    pMiner->ChangeLocation(goldmine);
  }
}
This method is called when a miner first enters the EnterMineAndDigForNugget state. It ensures that the gold miner is located at the gold mine. An agent stores its location as an enumerated type and the ChangeLocation method changes this value to switch locations.
当淘金者第一次进入 EnterMineAndDigForNugget 状态时调用这个方法,这确保淘金者位于金矿场。智能体以枚举量的形式保存当前位置, ChangeLocation 方法用以改变位置值。
EnterMineAndDigForNugget::Execute
EnterMineAndDigForNugget::Execute
The Execute method is a little more complicated and contains logic that can change a miner’s state. (Don’t forget that Execute is the method called each update step from Miner::Update .)
Execute 有点复杂,它包含了改变淘金者状态的逻辑。(不要忘记 Miner::Update 在每一个更新帧都会调用 Execute 方法。)
void EnterMineAndDigForNugget::Execute(Miner* pMiner)
  //the miner digs for gold until he is carrying in excess of MaxNuggets.
  //If he gets thirsty during his digging he stops work and
  //changes state to go to the saloon for a beer.
  pMiner->AddToGoldCarried(1);
 
  //digging is hard work
  pMiner->IncreaseFatigue();
 
  cout << "/n" << GetNameOfEntity(pMiner->ID()) << ": "
       << "Pickin' up a nugget";
 
  //if enough gold mined, go and put it in the bank
  if (pMiner->PocketsFull())
  {
    pMiner->ChangeState(VisitBankAndDepositGold::Instance());
  }
 
  //if thirsty go and get a beer
  if (pMiner->Thirsty())
  {
    pMiner->ChangeState(QuenchThirst::Instance());
  }
}
  Note here how the Miner::ChangeState method is called using QuenchThirst ’s or VisitBankAndDepositGold ’s Instance member, which provides a pointer to the unique instance of that class.
值得注意的是 Miner::ChangeState 方法调用了 QuenchThirst VisitBankAndDepositGold Instance 成员函数,以获得指向该类唯一实例的指针。
  EnterMineAndDigForNugget::Exit
 EnterMineAndDigForNugget::Exit
The Exit method of EnterMineAndDigForNugget outputs a message telling us that the gold miner is leaving the mine.
EnterMineAndDigForNugget Exit 方法只是简单地输出一条消息告诉我们淘金者离开了金矿。
void EnterMineAndDigForNugget::Exit(Miner* pMiner)
{
  cout << "/n" << GetNameOfEntity(pMiner->ID()) << ": "
       << "Ah'm leavin' the goldmine with mah pockets full o' sweet gold";
}
I hope an examination of the preceding three methods helps clear up any confusion you may have been experiencing and that you can now see how each state is able to modify the behavior of an agent or effect a transition into another state. You may find it useful at this stage to load up the WestWorld1 project into your IDE and scan the code. In particular, check out all the states in MinerOwnedStates.cpp and examine the Miner class to familiarize yourself with its member variables. Above all else, make sure you understand how the state design pattern works before you read any further. If you are a little unsure, please take the time to go over the previous text until you feel comfortable with the concept.
我希望前述的三个方法能帮助你理清头绪,现在你应该已经理解了每一个状态怎么改变智能体的行为,又如何从一个状态到另一个状态转换。你用 IDE 打开 WestWorld1 项目并浏览一遍代码应该有助于理解,可以抽取出 MinerOwnedStates.cpp 里的所有状态并检阅 Miner 类实现,让你熟悉它的成员变量。最重要的是,确定你理解了状态设计模式是如何工作的,然后再作进一步阅读。如果你有一些不了解,请重温上文直到你觉得已经完全理解了相关理论。
You have seen how the use of the state design pattern provides a very flexible mechanism for state-driven agents. It’s extremely easy to add additional states as and when required. Indeed, should you so wish, you can switch an agent’s entire state architecture for an alternative one. This can be useful if you have a very complicated design that would be better organized as a collection of several separate smaller state machines. For example, the state machine for a first-person shooter (FPS) like Unreal 2 tends to be large and complex. When designing the AI for a game of this sort you may find it preferable to think in terms of several smaller state machines representing functionality like “defend the flag” or “explore map,” which can be switched in and out when appropriate. The state design pattern makes this easy to do.
正如你所见,状态设计模式为状态驱动的智能体提供了具有非常好的伸缩性的机制,当需要的时候,你可以极其容易地增加新的状态。在你有非常复杂的设计,而且能更好地组织一系列的分散的小状态机的时候,甚至可以替换智能体的整个状态架构。例如,像 Unreal2 这样的第一人称射击游戏( FPS )有着巨大而复杂的状态机,当设计这种游戏的 AI 的时候,你将发现它能完美地应用于基于团队而设计的多个小状态机(对应的功能可能是“保旗”或者“探险”),使得能够在需要的时候进行切换。正在状态设计模式使它易于实现。
Making the State Base Class Reusable
编写可重用的 State 基类
  As the design stands, it’s necessary to create a separate State base class for each character type to derive its states from. Instead, let’s make it reusable by turning it into a class template.
作为立足之本,有必要构造一个独立的 State 基类,以供每一个角色类类型获得自身的状态。我们可以通过类模板来使得它可重用:
template
class State
{
public:
 
  virtual void Enter(entity_type*)=0;
 
  virtual void Execute(entity_type*)=0;
 
  virtual void Exit(entity_type*)=0;
 
  virtual ~State(){}
};
 
The declaration for a concrete state — using the EnterMineAndDigForNugget miner state as an example — now looks like this:
下面是 Miner 类的 EnterMineAndDigForNugget 状态:
class EnterMineAndDigForNugget : public State
{
 
public:
 
  /* OMITTED */
};
This, as you will see shortly, makes life easier in the long run.
如你所见,它短小精悍。
Global States and State Blips
全局状态和状态闪动(诚心求更好的译法)
  More often than not, when designing finite state machines you will end up with code that is duplicated in every state. For example, in the popular game The Sims by Maxis, a Sim may feel the urge of nature come upon it and have to visit the bathroom to relieve itself. This urge may occur in any state the Sim may be in and at any time. Given the current design, to bestow the gold miner with this type of behavior, duplicate conditional logic would have to be added to every one of his states, or alternatively, placed into the Miner::Update function. While the latter solution is accept- able, it’s better to create a global state that is called every time the FSM is updated. That way, all the logic for the FSM is contained within the states and not in the agent class that owns the FSM.
通常当设计有限状态机的时候,你最后都会在所有状态中出现重复代码。例如,在 Maxis 开发的流行游戏《 The Sims (第二人生)》中, Sim 可以感受到内急等生理需要,必须去洗手间解决。无论 Sim 在哪里、在什么时间,内急都可能发生。根据当前的设计,给淘金者加上这样一种行为,重复的条件逻辑就可能增加到每一个状态,或者放到 Miner::Update 函数里。下面介绍一个可接受的解决方案,它增加了一个全局状态——供 FSM 更新的时候调用。这样, FSM 的所有的逻辑都包含在状态内,而不在智能体类的 FSM 里。
To implement a global state, an additional member variable is required:
实现全局状态,需要增加一个成员变量:
//notice  how  now  that  State  is  a  class  template  we  have  to  declare  the  entity  type
State*  m_pGlobalState;
  In addition to global behavior, occasionally it will be convenient for an agent to enter a state with the condition that when the state is exited, the agent returns to its previous state. I call this behavior a state blip . For example, just as in The Sims, you may insist that your agent can visit the bathroom at any time, yet make sure it always returns to its prior state. To give an FSM this type of functionality it must keep a record of the previous state so the state blip can revert to it. This is easy to do as all that is required is another member variable and some additional logic in the Miner::ChangeState method.
有时智能体从一个状态进入另一个状态,当它退出这个状态时需要回到它的前一个状态,我将之称为状态闪动。例如就像《 The Sims 》中你可能必须让你的智能体能够在任何时间进入洗手间,之后再回到之前的状态。要实现这样的功能,就必须记录前一个状态,以便在状态闪动时返回。这可以容易地通过增加成员变量和对 Miner::ChangeState 方法增加一些额外逻辑来实现。
[ 译注:状态闪动这一概念的确比较难以理解。所以我画了下面这一张图来帮助理解 ]
状态驱动的游戏智能体设计_第1张图片
译注图 1 状态闪动示意图
By now though, to implement these additions, the Miner class has acquired two extra member variables and one additional method. It has ended up looking something like this (extraneous detail omitted):
到现在,为了完成这些额外功能, Miner 类已经增加了两个成员变量和一个方法。它最后看起来就像这样(忽略无关元素):
class Miner : public BaseGameEntity
{
private:
 
  State*   m_pCurrentState;
  State*   m_pPreviousState;
  State*   m_pGlobalState;
  ...
 
public:
 
  void ChangeState(State* pNewState);
  void RevertToPreviousState();
  ...
};
  Hmm, looks like it’s time to tidy up a little.
  嗯,的确需要整理一下。
Creating a State Machine Class
创建一个状态机类
  The design can be made a lot cleaner by encapsulating all the state related data and methods into a state machine class. This way an agent can own an instance of a state machine and delegate the management of current states, global states, and previous states to it.
把所有的状态有关的数据和方法封装到一个状态机类里有利于精简设计。这使智能体能够拥有一个状态机实例并委派它管理当前状态、全局状态和前一个状态。
With this in mind take a look at the following StateMachine class template.
现在来看看 StateMachine 模板类。
template
class StateMachine
{
private:
 
  //a pointer to the agent that owns this instance
  entity_type*          m_pOwner;
 
  State*   m_pCurrentState;
 
  //a record of the last state the agent was in
  State*   m_pPreviousState;
 
  //this state logic is called every time the FSM is updated
  State*   m_pGlobalState;
 
public:
 
  StateMachine(entity_type* owner):m_pOwner(owner),
                                   m_pCurrentState(NULL),
                                   m_pPreviousState(NULL),
                                   m_pGlobalState(NULL)
  {}
 
  //use these methods to initialize the FSM
  void SetCurrentState(State* s){m_pCurrentState = s;}
  void SetGlobalState(State* s) {m_pGlobalState = s;}
  void SetPreviousState(State* s){m_pPreviousState = s;}
 
  //call this to update the FSM
  void  Update()const
  {
    //if a global state exists, call its execute method
    if (m_pGlobalState)   m_pGlobalState->Execute(m_pOwner);
 
    //same for the current state
    if (m_pCurrentState) m_pCurrentState->Execute(m_pOwner);
  }
 
  //change to a new state
  void  ChangeState(State* pNewState)
  {
    assert(pNewState &&
           ": trying to change to a null state");
 
    //keep a record of the previous state
    m_pPreviousState = m_pCurrentState;
 
    //call the exit method of the existing state
    m_pCurrentState->Exit(m_pOwner);
 
    //change state to the new state
    m_pCurrentState = pNewState;
 
    //call the entry method of the new state
    m_pCurrentState->Enter(m_pOwner);
  }
 
  //change state back to the previous state
  void  RevertToPreviousState()
  {
    ChangeState(m_pPreviousState);
  }
 
  //accessors
  State*  CurrentState()  const{return m_pCurrentState;}
  State*  GlobalState()   const{return m_pGlobalState;}
  State*  PreviousState() const{return m_pPreviousState;}
 
  //returns true if the current state’s type is equal to the type of the
  //class passed as a parameter.
  bool  isInState(const State& st)const;
};
  Now all an agent has to do is to own an instance of a StateMachine and implement a method to update the state machine to get full FSM functionality.
现在所有的智能体都能够拥有一个 StateMachine 实例,需要做的就是实现一个方法来更新状态机以获得完整的 FSM 功能。
本文由恋花蝶最初发表于 http://blog.csdn.net/lanphaday,欢迎转载,但必须保持全文完整,也必须包含本声明。
译者并示取得中文版的翻译授权,翻译本文只是出于研究和学习目的。任何人不得在未经同意的情况下将英文版和中文版用于商业行为,转载本文产生的法律和道德责任由转载者承担,与译者无关。
The improved Miner class now looks like this:
新实现的 Miner 类看起来就是这样的:
class Miner : public BaseGameEntity
{
private:
 
  //an instance of the state machine class
  StateMachine*  m_pStateMachine;
 
  /* EXTRANEOUS DETAIL OMITTED */
 
public:
 
  Miner(int id):m_Location(shack),
                m_iGoldCarried(0),
                m_iMoneyInBank(0),
                m_iThirst(0),
                m_iFatigue(0),
                BaseGameEntity(id)
                              
  {
    //set up state machine
    m_pStateMachine = new StateMachine(this);
   
    m_pStateMachine->SetCurrentState(GoHomeAndSleepTilRested::Instance());
    m_pStateMachine->SetGlobalState(MinerGlobalState::Instance());
  }
 
  ~Miner(){delete m_pStateMachine;}
 
  void Update()
  {
    ++m_iThirst;
    m_pStateMachine->Update();
  }
 
  StateMachine* GetFSM()const{return m_pStateMachine;}
 
  /* EXTRANEOUS DETAIL OMITTED */
};
  Notice how the current and global states must be set explicitly when a StateMachine is instantiated. The class hierarchy is now like that shown in Figure 2.4.
注意 StateMachine 实例化后如何正确设计当前和全局状态。图 2.4 是现在的类层次结构图。
状态驱动的游戏智能体设计_第2张图片
Figure 2.4. The updated design
Introducing Elsa
介绍 Elsa
  To demonstrate these improvements, I’ve created another project: WestWorldWithWoman. In this project, West World has gained another inhabitant, Elsa, the gold miner’s wife. Elsa doesn’t do much; she’s mainly preoccupied with cleaning the shack and emptying her bladder (she drinks way too much cawfee). The state transition diagram for Elsa is shown in Figure 2.5.
为了验证这些改进,我创建了一个新的项目—— WestWorldWithWoman 。在这个项目里, WestWorld 多了一个人物—— Elsa ,她是淘金者 Bob 的妻子。 Elsa 做的事不多,主要是打扫房子和上洗手间(她喝了太多咖啡)。图 2.5 Elsa 的状态转换图。
状态驱动的游戏智能体设计_第3张图片
Figure 2.5. Elsa’s state transition diagram. The global state is not shown in the figure because its logic is effectively implemented in any state and never changed.
When you boot up the project into your IDE, notice how the VisitBathroom state is implemented as a blip state (i.e., it always reverts back to the previous state). Also note that a global state has been defined, WifesGlobalState , which contains the logic required for Elsa’s bathroom visits. This logic is contained in a global state because Elsa may feel the call of nature during any state and at any time.
当你把这个项目导入到你的 IDE 的时候,注意 VisitBathroom 状态是如何以状态闪动的形式实现的(也就是它如何返回到前一个状态),同样值得注意的是定义了一个全局状态—— WifesGlobalState ,它包含了 Elsa 上洗手间所需的逻辑。在全局状态中包含这一逻辑是因为 Elsa 可能在任何时候都会感到内急,这是天性,哈哈。
Here is a sample of the output from WestWorldWithWoman.
这里是 WestWorldWithWoman 项目的输出示例。
Miner  Bob:  Pickin'  up  a  nugget
Miner  Bob:  Ah'm  leavin'  the  gold  mine  with  mah  pockets  full  o'  sweet  gold
Miner  Bob:  Goin'  to  the  bank.  Yes  siree
Elsa:  Walkin'  to  the  can.  Need  to  powda  mah  pretty  li'l  nose
Elsa:  Ahhhhhh!  Sweet  relief!
  Elsa:  Leavin'  the  john
Miner  Bob:  Depositin'  gold.  Total  savings  now:  4
Miner  Bob:  Leavin'  the  bank
Miner  Bob:  Walkin'  to  the  gold  mine
Elsa:  Walkin'  to  the  can.  Need  to  powda  mah  pretty  li'l  nose
Elsa:  Ahhhhhh!  Sweet  relief!
Elsa:  Leavin'  the  john
Miner  Bob:  Pickin'  up  a  nugget
Elsa:  Moppin'  the  floor
Miner  Bob:  Pickin'  up  a  nugget
Miner  Bob:  Ah'm  leavin'  the  gold  mine  with  mah  pockets  full  o'  sweet  gold
Miner  Bob:  Boy,  ah  sure  is  thusty!  Walkin'  to  the  saloon
Elsa:  Moppin'  the  floor
Miner  Bob:  That's  mighty  fine  sippin'  liquor
Miner  Bob:  Leavin'  the  saloon,  feelin'  good
Miner  Bob:  Walkin'  to  the  gold  mine
Elsa:  Makin'  the  bed
Miner  Bob:  Pickin'  up  a  nugget
Miner  Bob:  Ah'm  leavin'  the  gold  mine  with  mah  pockets  full  o'  sweet  gold
Miner  Bob:  Goin'  to  the  bank.  Yes  siree
Elsa:  Walkin'  to  the  can.  Need  to  powda  mah  pretty  li'l  nose
Elsa:  Ahhhhhh!  Sweet  relief!
Elsa:  Leavin'  the  john
Miner  Bob:  Depositin'  gold.  Total  savings  now:  5
Miner  Bob:  Woohoo!  Rich  enough  for  now.  Back  home  to  mah  li'l  lady
Miner  Bob:  Leavin'  the  bank
Miner  Bob:  Walkin'  home
Elsa:  Walkin'  to  the  can.  Need  to  powda  mah  pretty  li'l  nose
Elsa:  Ahhhhhh!  Sweet  relief!
Elsa:  Leavin'  the  john
Miner  Bob:  ZZZZ...
 
Well, that's it folks. The complexity of the behavior you can create with finite state machines is only limited by your imagination. You don’t have to restrict your game agents to just one finite state machine either. Sometimes it may be a good idea to use two FSMs working in parallel: one to control a character’s movement and one to control the weapon selection, aiming, and firing, for example. It’s even possible to have a state itself contain a state machine. This is known as a hierarchical state machine. For instance, your game agent may have the states Explore , Combat , and Patrol . In turn, the Combat state may own a state machine that manages the states required for combat such as Dodge , ChaseEnemy , and Shoot .
夸张点说,这相当了不起啊。你能够用有限状态机创造非常复杂的行为,到底有多复杂仅受限于你的相像力。你无需限制你的游戏智能体只能有一个有限状态机,有时候你可以使用两个 FSM 来并行工作:一个控制角色的移动,而另一个控制武器选择、瞄准和开火。你甚至可以创造包含状态机的状态机,即分级状态机。例如你的游戏智能体可能有 Explore (探测)、 Combat (战斗)和 Patrol (逻辑)等状态,而 Combat (战斗)状态又可以拥有一个状态机来管理 Dodge (躲避)、 ChaseEnemy (追逃)和 Shoot (射击)等战斗时需要的状态。
 
本文由恋花蝶最初发表于 http://blog.csdn.net/lanphaday,欢迎转载,但必须保持全文完整,也必须包含本声明。
译者并示取得中文版的翻译授权,翻译本文只是出于研究和学习目的。任何人不得在未经同意的情况下将英文版和中文版用于商业行为,转载本文产生的法律和道德责任由转载者承担,与译者无关。
 

你可能感兴趣的:(其他分类)