作为人类,我们似乎总倾向于使用自己所熟悉的解决方法。我们总是会按照我们所知道的方式去做某些事,而不是按照做这些事的“最佳”方式。因为总是带着这种想法,所以我们很容易使用一些过时的技术,并使用那些同时代人所不理解,或者并不是那么有效的方式去执行特定功能。所以我希望通过本文以及之后的文章向广大读者们介绍更多能够在编程中带给你们帮助的解决方法。今天我要分享的便是行动列表!
行动列表是所有游戏开发者都必须清楚的简单但却强大的AI。尽管不能与巨大的AI网络相匹敌,但是它们允许相对复杂的突发行为,并且执行起来也很简单。不管你是刚刚接触AI编程还是着眼于扩展工具包的资深开发者,本文都将向你详细介绍行动列表并提供一些有效的例子帮助你更好地执行解决方法。让我们开始吧。
从前
几年前我开始开发《King Randall’s Party》,在游戏中玩家将建造城堡并为了保卫它与尝试着摧毁它的King战斗。我需要创造一个能够瞄准玩家城堡并计划如何去摧毁它以达到自己目的的机智的AI。这对我来说是个巨大的挑战,因为目标太大我很难同时顾及全面。所以就像任何其他优秀的程序员那样,我将其分解成一个比较容易管理的问题集。这时候我所面临的第一个挑战便是:我需要创造一个拥有特定行为集的单位。
King Randall’s Party(from gamedev)
而快速的网络搜索让我的精神差点崩溃。只要搜索“游戏AI”便会出现有关策划者,有限状态机,指导行为,向导等等结果。我根本不知道该从哪里开始,所以我只能做任何理性的人会做的事。对的,我询问了我们家的狗。当然了,对于我来说这并不是什么新鲜事了。不管什么时候当我遭遇技术问题时我都会去询问我们家的狗。这时候你可能会想:“Jesse,你太疯狂了吧。狗怎么可能了解计算机?!”好吧,让我们先不要管这个。也许这里包含或并未包含某种非法技术或者出现在大学审计课堂中的内容。
不管怎样我会说:“好吧Frankie,我知道对于AI我们有许多事要做,但是我并不清楚该从哪里开始。我该如何为我的游戏单位创造一个AI框架?”Frankie的表情让我觉得自己就是个愚蠢的人,尽管我的高中老师Francis先生已经告诉我不要提出任何愚蠢的问题。我非常肯定Frankie并未想到Francis先生。她反问我:“Jesse,通常你是如何开始你的一天的?”
我会把我一天需要做的所有事列下来然后根据这些任务的重要性以及所需时间对其进行排序。我这么回答Frankie然后她说道:“这便是行动列表。”她告诉我行动列表是关于你的游戏单位在特定时间内运行的任务或行为列表。这是一种有限状态系统形式,并且能够被描述为是带有一个分支的简单行为树。以下便是关于它们是如何运行的。
行动列表如何运行
首先你将写下你希望你的AI所拥有的所有行为。
action list(from gamedev)
然后你需要对它们进行先后排序。从最低到最高。
action list(from gamedev)
现在我们需要迭代该列表去检查每个行动是否能够有效执行。然后我们将检查行动的粘附性能,如果某个项目会阻碍之后的行动,我们便会退出该列表。之后我们便会清楚为什么这种阻碍如此重要了。
在我们的例子中,我们的第一个行动“攻击玩家”将只在AI靠近玩家的时候执行。让我们假设它并未靠近玩家,所以它将检查是否能在当前位置上创造一个阶梯等,直至它找到一个能够执行的行动条款,如打破门。然后它将执行破门代码。
action list(from gamedev)
“阻碍”便是在这里开始发挥作用。如果破门马上发生,它便不会阻碍到任何之后的行动,而之后的列表内容便可以继续执行。但通常情况都不会如此—-行动总是占据一个以上的帧数。所以在这种情况下破门行动项目将调用unit.Attack(door),即将单位的当前状态从等待改成破门,而在门被打破之前将恢复到true的状态。
一个简单的有限状态机
这听起来好像是可行的。Frankie提供了一个很好的建议,行动列表似乎也非常适合我的项目。但在此之前我却从未听过它们,所以我对此还是充满疑问—-我听到的大多数关于AI的内容都必须使用基于转变的有限状态机,即类似于在MUnity3D用于创造动画所使用的工具。你将定义一些状态并识别它们何时以及如何彼此转变。Frankie向我我解释了当你在创造一个基于转变的有限状态机时,你需要定义你想要拥有的所有状态,然后你也要定义这些个体状态间的所有转变。这真的会让内容很快变得复杂。如果你拥有一个“伤害”的状态,你便需要识别其它能够转变成这个状态的状态。如走路变成伤害,跳跃变成伤害,蹲伏变成伤害,攻击变成伤害等等。这是很有用的方法,但却很快便会变得复杂。如果你的AI需求非常简单的话,这可能会是一笔潜在的不必要开支。
关于基于转变的状态机的另一个难点便是调试。当你设置了一个停止点并着眼于AI当前的状态,这时候如果没有额外的调试代码,你便不可能了解AI是如何进入当前的状态,它之前的状态是什么以及怎样的转变将它带到现在的状态。
行动列表的缺陷
当我进一步深入行动列表时,我意识到它们非常适合我的执行内容,但同时我也发现它们存在一些缺陷。最大的缺陷便源自其最大的优点—-简单性。因为这是一个有序列表,所以我不可能拥有任何复杂的优先顺序结构。所以如果我希望“攻击玩家”排在“破门”前面,但却在“移向目标”后面,但同时我又希望“移向目标”出现在“破门”后面,我便很难使用行动列表做到这点,而用有限状态机的话又会非常繁琐。
总而言之,行动列表对于简单的AI系统真的很有帮助,但是如果你想创建的是较为复杂的AI,那么执行行动列表便会较困难。也就是说你可以通过某些方法去扩展行动列表概念而将它们变得更有帮助。
扩展这一概念的方法
我的一些AI单位需要完成多种任务—-它们可能需要同时移动并发动攻击。我可以创造多个行动列表(游戏邦注:一个处理移动,一个处理攻击),但这么做也存在问题,即如果有些移动类型会妨碍攻击,而有些攻击又需要单位站在原地的话该怎么办?这时候行动线路(Action Lanes)便会发挥作用。
行动线路是对于“阻碍”概念的扩展。基于行动线路,行动项目便能够识别阻碍执行的特定行动项目类型并允许其它项目顺畅地执行任务。让我们进一步展示这一执行。
行动线路只是决定行动的一种附加方法。每个行动项目都属于一条或多条线路,当阻碍性能变成true时,它将添加所属的线路,因为每个行动都拥有一条或多条线路,所以它们只是阻碍其它属于这些线路的行动。举个例子来说吧,因为单位在创造阶梯时必须保持不动且不能发动攻击,所以攻击玩家属于行动线路,移向目标属于移动线路,创造截图同时属于这两种线路。然后我们将排列这些项目的顺序,如果它们同时执行便会阻碍到后续的行动。
执行案例
现在我们已经了解了这一理论,所以我们可以尝试一次真正的执行过程。首先让我们设置行动列表和行动项目。对于行动项目我希望能够分离执行,所以让我们创造一个接口。
code(from gamedev)
以下便是BreakDoor行动项目的IActionItem执行:
code(from gamedev)
对于行动列表本身我可以使用一个简单的列表。然后我们将使用IActionItems去加载它。
code(from gamedev)
之后我们将设置一个方法并在每帧中迭代列表。需要记得我们同样也需要处理阻碍。
code(from gamedev)
如果你想要使用行动线路的话事情会变得更复杂。这时候我们将把行动线路定义为位字段然后修改IActionItem界面。
code(from gamedev)
然后我们将为这些线路修改迭代程序。如果行动项目的线路遭遇阻碍,它们便会被略过,但检查的时候仍是正常的。如果所有线路都遭到阻碍,我们便会改变这一循环。
code(from gamedev)
结论
可以看出需要理解的内容有很多。Frankie让我总结从中学到的内容,于是我经过综合思考得出了一些关键要点。
行动列表比小型基于转变的状态系统更容易创建与维护。
它们创造了一个可识别的优先系统。
存在一些扩展方法去处理扩展功能。
在编程中我们很难找到最佳解决方法。所以我们必须确保我们的工具箱中拥有各种编程工具,如此我们便可以及时选择一个能够有效解决问题的合适工具。最终证明行动列表非常适合《King Randall’s Party》。而它是否也同样适合你们的项目呢?
(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转发,如需转载请联系:游戏邦)
Action Lists: Simple, Flexible, Extendable AI
By Jesse Crafts-Finch
As humans, we like to implement solutions which are familiar to us. We get caught up doing things the way we know how to do them, rather than the “best” way to do them. It’s easy to get caught up in thinking like this and as a result we end up using outdated technologies and implement features in ways that our modern contemporaries don’t understand, or are simply less effective or efficient. My purpose with this and future papers will be to expose readers to a broad spectrum of solutions that will hopefully help them in their own coding. Today I’ll be covering Action Lists!
Action Lists are a simple yet powerful type of AI that all game developers should know about. While ultimately not scalable for large AI networks, they allow for relatively complex emergent behavior and are easy to implement. Whether you are just getting into AI programming or are an experienced industry veteran looking to expand your toolkit, this presentation will introduce you to Action Lists and provide concrete examples to help you implement your own solutions. Let’s begin the story.
Once Upon A Time…
Several years ago I was starting the development of King Randall’s Party, a game in which the player builds a castle and tries to defend it against the King who is trying to knock it down. I needed to create a reasonably smart AI that could look at the player’s castle and figure out how to circumvent or destroy it in order to reach its objective – the gold pile the player is defending. This was a big challenge, almost too big to consider all at once. So like any good programmer I broke it down into a more manageable problem set. The first challenges: I needed to get the units to have a specific set of behaviors that they would act according to.
A quick internet search caused my mind to implode, of course. Searching for “Game AI” brought up results on planners, finite state machines, steering behaviors, flocking, A*, pathfinding, etc. I had no clue where to start, so I did what any reasonable, rational person would do. I asked my dog. This isn’t anything new, of course. Whenever I have a tech problem, I ask my dog. Now I know what you’re thinking “Jesse, that’s dumb, and you’re crazy… What do dogs know about computers?” Well, let’s just gloss over that. It may or may not have involved some illegal technology and or college class auditing.
Anyway, I said “Ok Frankie – there is so much going on with AI, I really don’t have a clue where to start. How should I go about creating an AI framework for my game units?” Frankie gave me this pretty withering look that basically informed me that I was an idiot and pretty dumb, despite what my high school teacher Mr. Francis may have told me about there never being any stupid questions. I’m pretty sure Frankie wouldn’t have had nice thoughts about Mr. Francis either. “Jesse”, she asked, “how do you typically start your day?”
Well, I write everything that I need to do that day down in a list and then prioritize it according to how important the task is and how soon it needs to be done. I explained this to Frankie and she responded “And that, is an Action list.” She told me that an Action List is a list of tasks or behaviors that your game units work their way through one at a time. It is a form of finite state system, and could be described as a simple behavior tree with a single branch level. Here is how they work. According to my dog.
How Action Lists Work
First you write down all the behaviors you want your AI to have.
Then you order them according to priority. Lowest to highest.
Now we iterate over the list checking each action to see if it can execute and if it can, executing it. We then check the actions Blocking property, and if the item is Blocking further actions we exit the list. We will get into why Blocking is important later.
In our example here, our first action Attack Player will only execute if the AI is close to the player. Let’s say it is not, so it checks if it can build a ladder at this location (and should it), and so on until it finds an action item it can execute such as Break Door. It then executes the Break Door code.
Here is where Blocking comes into play. If Break Door occurs instantly, then it will not block any further actions and the rest of the list can execute. This is typically not the case – actions usually take up more than one frame. So in this case the Break Door Action Item calls unit.Attack(door), which will change the unit’s CurrentState from Waiting to BreakDoor and will return true until the door is broken.
A Simple Finite State Machine
Well ok. That sounds workable. Frankie had made a good suggestion and Action Lists seem like a great fit for my project. But I’d never heard of them before so I had my doubts – most of what I hear floating around about AI has to do with transition-based finite state machines, similar to what Unity3D uses for animations in Mechanim. You define a bunch of states, and identify when and how they transition between each other. In exchange for some belly scratches, Frankie explained to me when you make a transition based finite state machine, you need to define all the states you want to have, and then also define all of the transitions to and from each of those individual states. This can get complicated really, really fast. If you have a Hurt state, you need to identify every other state that can transition to this state, and when. Walking to hurt; jumping to hurt, crouching to hurt, attacking to hurt. This can be useful, but can also get complicated really fast. If your AI requirements are fairly simple, that’s a lot of potentially unnecessary overhead.
Another difficulty with transition-based state machines is that it is difficult to debug. If you set a break point and look at what the AI’s current state is, it is impossible without additional debug code to know how the AI got into its current state, what the previous states were and what transition was used to get to the current state.
Drawbacks of Action Lists
As I dug into action lists some more, I realized that they were perfect for my implementation, but I also realized they had some drawbacks. The biggest flaw is simply the result of its greatest strength – its simplicity. Because it is a single ordered list, I couldn’t have any sort of complex hierarchy of priorities. So if I wanted Attack Player to be a higher priority than Break Door, but lower than Move To Objective, while also having Move To Objective being lower priority than Break Door… that’s not a simple problem to solve with action lists, but trivial with finite state machines.
In summary, action lists are really useful for reasonably simple AI systems, but the more complex the AI you want to model, the more difficult it will be to implement action lists. That being said, there are a few ways you can extend the concept of Action Lists to make them more powerful.
Ways to Extend This Concept
So some of my AI units are multitaskers – they can move and attack at the same time. I could create multiple action lists – one to handle movement and one to handle attacking, but that is can be problematic – what if some types of movement preclude attacking, and some attacks require the unit to stand still? This is where Action Lanes come in.
Action Lanes are an extension to the concept of Blocking. With Action Lanes, Action Items can now identify specific types of Action Items that it blocks from executing while allowing others to execute without problem. Let’s show this in action.
An Action Lane is just an additional way to determine what action. Each Action Item belongs to one or more lanes, and when its Blocking property returns true, it will add the lanes it belongs to Each action has a lane or multiple lanes they are identified with, and they will only block other actions which belong to those lanes. As an example, Attack Player belongs in the Action Lane, Move to Goal belongs in the Movement lane, and Build Ladder belongs in both since the unit must stand still and cannot attack while building. Then we order these items, and if they execute they will block subsequent actions appropriately.
Example Implementation
Now that we’ve gone over the theory, it is useful to step through a practical implementation. First let’s setup our Action List and Action Items. For Action Items I like to decouple implementation, so let’s make an interface.
Here is an example implementation of the IActionItem for the BreakDoor Action Item.
For the Action List itself we can use a simple List. Then we load it up with the IActionItems.
After that, we setup a method that iterates over the list every frame. Remember we also have to handle blocking.
Things get a bit more complicated if you want to use action lanes. In that case we define Action Lanes as a bitfield and then modify the IActionItem interface.
Then we modify the iterator to take these lanes into account. Action Items will be skipped over if their lane is blocked, but will otherwise check as normal. If all lanes are blocked then we break out of the loop.
Conclusion
So that’s a lot to take in. While coding the other day Frankie asked me to summarize my learnings. Thinking about it, there were a few key takeaways for me.
Action lists are easier to setup and maintain then small transition-based state systems.
They model a recognizable priority system.
There are a few ways they can be extended to handle expanded functionality.
As with anything in coding, there is often no best solution. It is important to keep a large variety of coding tools in our toolbox so that we can pick the right one for the problem at hand. Action Lists turned out to be perfect for King Randall’s Party. Perhaps they will be the right solution for your project?(source:gamedev)