译者:i_dovelemon
来源:http://www.gamedev.net/page/resources/_/technical/game-programming/from-user-input-to-animations-using-state-machines-r4155
日期:2015 / 8 / 23
主题:State Machines, Animation,Event processing,Synchronization,User input,animations
在游戏开发中,当用户输入信息之后,我们需要根据用户输入的信息来进行平滑的动画过渡,由于输入的不确定性以及动画的复杂性,这种平滑过渡动画的效果很难实现。任何时刻,用户都能够按下任何的按钮。你需要检查,现在的角色是否能够响应用户请求的移动,并且根据当前的状态,在正确的实际切换到一个新的动画中去。同时,你还要跟踪事情的发展,以此来响应用户的下一次输入。
总之就是说,在进行这方面的工作处理的时候,你需要处理一系列的诸如检查,动作和赋值这类的工作。这个序列,随着用户输入的增多,游戏状态的增多,以及动画的增多慢慢的就会变得越来越复杂。各种不同的组合都要在设计的时候考虑到。把他实现就已经很困难了,如果你还要在后续不断的更新,修改它,并且不能遗漏任何一个,又是一个十分棘手的问题。
但是,通过使用状态机,或许能够以一种有序可控的方式来表达这些转换。这个状态机,将只用来进行动画和动画过渡的内容。另外一个状态机,将用来处理用户的输入与角色的交互,更新角色的状态。通过将角色的状态和动画的状态分离开来,我们就能够很容易的处理他们之间的复杂交互。并且随后的变化,也将更加容易的处理。
通过将这样的状态机在纸上书写出来,就已经能够非常容易的理解了,所以对于实现来说,也将十分的容易实现,这也意味着,我们将很容易的将这里介绍的状态机嵌入到我们的游戏中去。
Audience, or what you should know before reading
这篇文章主要讲述的是什么是状态机,并且他们是怎么工作的。如果你不知道状态机是什么东西,那么你就有必要去寻找相关的读物来了解什么是状态机。这里介绍的状态机可能和你们所熟悉的状态机有一点的不同,但是知道以前的状态机的定义对于理解这里的状态机的概念也是十分有帮助的。在文章的末尾,列出来一些参考的读物,从中你可以找到相关的信息来了解什么是状态机。
这篇文章主要集中在如何使用状态机描述一些允许的行为,并且状态机是如何同步进行。虽然在文章中有相关的demo可以描述这种实现,但是状态机的整个环境却不会详细的讲解出来。诸如如何使的用户的输入和游戏状态对状态机可见,如何开始和启动动画,这样的内容都不会出现在文章中。状态机之间同步的细节也不会在文章中讲解。
State machines
一个状态机,就是一种描述行为的方式,它表达了如何在不同的行为之间进行切换。每一个行为,我们都能够通过一个状态来进行描述。由于这样的说明,十分的抽象,我们就使用你现在的状态来进行举例说明。现在,你正在阅读我的文章,那么“阅读”就是一种状态。它是你一段时间里将会保持的一种活动。现在,假设你来了一个电话。你停止了阅读,然后去接电话。那么“接电话”就是你接下来的一个状态。你其他的状态有诸如“走路”,“跑步”,“睡觉”等等。
你现在正在进行的活动有专门的描述方法。当前正在进行活动的状态我们称之为“当前状态”。它是指向你“现在正在做的事情”的状态。
拥有状态是件好事,但是到目前为止,这里只是列举了你能够进行的活动,和一个当前状态而已。那么还缺少什么东西了?在状态之间的结构,没有描述出来。你能够在“跑步”的状态下,直接进入到“睡觉”的状态,或者其他的行为中去。这样的描述并不是一个好的表述不同状态之间关联的方法。这就是我接下来要讲解的新概念“边界”。“边界”定义了你如何从一个状态切换到另外一个状态中去。你可以把边界当做一个箭头,从一个状态指向另外一个状态。边界的唯一规则就是,你只能够通过当前的状态,沿着边界到它所指向的状态中去。当转换完毕之后,接下来的新状态将是当前的状态。
通过在不同的状态之间添加或者去除边界,你就能够决定当前的状态会导致的行为有哪些。比如说,如果你没有一个直接从“奔跑”到“睡觉”的边界,你添加了从“奔跑”到“洗澡”,还有从“洗澡”到“睡觉”的边界,那么你就能够强制使得在奔跑状态下的角色,进入睡眠状态。
Defining game character behavior
同样的,你也可以使用类似的方法来控制游戏中的角色。游戏中的角色相比于现实中的角色要更加容易描述点。你可以从下面这张图中看到:
这个游戏角色只能够进行4中活动。它可以站立,奔跑,跳跃和爬行。上面的边界描述了这个角色如何在不同的状态之间进行切换。
Defining animation sequences
很明显,使用状态机能够很轻松的表达游戏角色的状态,但是状态机应该能够做出更加复杂的东西出来。如果你把播放一种动画也当做是一种活动的话,那么我们同样也能够把播放动画当做一种状态,并且通过边界在不同的动画状态之间进行切换,你可以画出下面这样的图:
对于每一个动画,你都有一个状态,而当前的状态就是正在播放的动画。边界定义了你如何从一个动画跳转到另外一个动画中去。
如果你对比游戏角色的状态和动画的状态,你会发现,其中有好多重复的地方,但并不是所有的地方都重复。角色中的Crawl状态在动画状态机中被扩展成了“Crawl_leftarm_anim”以及“Crawl_rightarm_anim”两种不同的状态。当从站立状态的时候,你将总是从Stand_anim变换到Crawl_leftarm_anim中去。并且你能够在Crawl_leftarm_anim以及Crawl_rightarm_anim之间来回的切换,慢慢的让角色爬过屏幕。游戏角色的Jump状态也被拆开了,如果你在奔跑的情况下跳跃,那么你就会进入fly_anim状态。
每一个状态机应该只关心他们自己的数据。游戏的角色状态机处理用户的输入,更新游戏角色的状态;动画状态机用来处理动画,帧率,和帧。而计算机处理如何在这两个不同的状态机之间实现同步。
Synchronizing behavior
到目前为止,我们介绍的系统还不错。我们拥有一个状态机,用来处理游戏角色的状态,同时也拥有一个状态机来处理动画播放序列的状态。
那么,现在就应该来做一些同步的工作,使得游戏角色的状态和游戏的动画状态进行同步。如果角色的状态现在正是“Standing”,而动画状态机中当前播放的状态却是“Running_anim”,这就会使得角色看上去十分的怪异。你只要在角色处于奔跑状态下的时候,才会播放奔跑动画。也就是说,这两个不同的状态机要从某种程度上进行同步操作。
最简单的同步方式就是完全的状态同步。在这种情况下,每一个角色状态都有一个独特的动画状态。当你改变角色状态的时候,你同样需要改变动画的状态。实际上,如果你真的这么做了,那么你就会发现,角色状态机和动画状态机实际上是一样的。对于这样的情况,你完全可以把他们融合成为一个状态机,没有必要分成两个不同的状态机来进行管理。
然而,在本文所介绍的例子中,这样的完全同步方式是失败的。对于Crawl状态来说,有两种不同的动画状态与之对应,而动画状态机中的Fly_anim,在角色的状态机中并没有与之对应的状态。
在我们的例子中,我们需要一个更加灵活的同步方式。动画状态机中的Crawl_leftarm_anim和Crawl_rightarm_anim直接的转换应该不会干扰到角色状态机中的Crawl状态。同样的,角色的Jumping状态也不需要考虑动画系统的状态是Fly_anim还是Jump_anim。另一方面,当你在角色的状态机中,当角色从Running变为Standing的时候,你自然也希望你的动画状态机能够播放Stand_anim。为了使这个变成可能,我们需要给每一个边界进行命名。通过辨识两个不同状态机里面的边界名是同样的时候,就能够预测这两个相同的边界名是应该同时进行工作的。
Edge synchronization
为了同步边界,所有的边界都必须有一个名字。由于一个边界表述的是一个瞬间状态的转变,所以你的边界命名最好能够带有一定的时间属性在里面,像start和touch_down之类的。进行边界同步的规则是每一个状态机都通过一个给定的名字来实现边界,并且只有当另外一个状态机中有同样的一个边界的时候,另外一个状态机才会同步的运行这一个边界。如果另外一个状态机中没有和你的状态机中要执行的边界相同的边界的时候,就不需要让另外一个状态机来执行边界。由于这样的规则是对所有的状态机施加的,那么具有同样名字边界的状态机要么一起执行这个边界,要么都不执行。
为了使描述更加的准确,下面是和上面同样的状态机描述,不够现在边界有了名字。
Example 1
我们先从简单的开始。 假设当前的状态为Crawl_leftarm_anim,它能够通过stop的边界转变到stand_anim状态,或者通过right_crawl边界跳转到Crawl_rightarm_anim状态。假设后者是我们希望的跳转方式。根据前面我们提到的关于边界的使用规则,如果当前的活动边界在其他的状态机中存在同样的边界,那么就一起执行这些边界。由于本例中的状态机不存在同样类型的状态,我们就能够直接转换crawl_rightarm_anim状态,而不需要去处理其他的状态机中的事情。
Example2
在不同的状态机中同步相同的边界,虽然处理过程有点长,但是思想是一样的。我们来考虑下游戏角色的Running状态。从Running状态,存在两个不同的边界。一个边界的名字为take_off,并且转向Jumping状态。另外一个边界的名字为stop,转向Standing状态。
假设,现在我们想要处理的是take_off边界。调用边界的规则规定了,当其他的状态机中存在相同的边界的时候,我们就需要同样的调用他们里面的边界,来进行处理。为了满足这样的要求,我们animation中的状态机的当前状态必须为Run_anim才可以。
同样的,动画系统状态机,必须愿意在这个时候调用take_off边界,而不是调用stop边界。假设,现在动画系统状态机和角色系统状态机都想要执行take_off边界。而不存在其他的状态机中,存在同样的take_off边界,那么动画系统状态机中的take_off边界和角色系统状态机中的take_off边界,将都会被调用。当调用完毕之后,角色系统状态机的当前状态变成了Jumping状态,而动画系统状态机的当前状态变为了Fly_anim状态。
Connecting to the rest of the game
到目前为止,我们讲解了状态机,当前状态,能够被同时或者单独执行的边界。仅仅这些还是不够的,我们还需要知道通过什么样的方式将这些状态机绑定到其他的代码上去。这些代码将决定你什么时候调用take_off边界。
有两个地方需要进行关联。第一个地方,是决定当前的哪些边界是可用的,这需要从所有状态机的当前状态中获取,每一个状态都保存了它能够执行那些边界的信息。第二个地方是实际进行状态转变的地方。当我们在系统中执行take_off边界的时候,动画系统进入了Fly_anim状态,我们需要让角色系统知道自己现在正处于飞行状态,进行飞行相关的操作,同时动画系统也相应的需要播放一些飞行动画。也就是说,当实际进行状态的转变的时候,我们需要进行一些动作。
Edge conditions
我们先从第一部分开始讲解。对于每一条边界,我们需要知道它当前是否能够被执行。这样的操作,能够通过给每一条边界添加条件来进行。也就是说,当我们执行一条边界的时候,我们必须要先确定这条边界的确是能够在当前情况下被调用。如果一个边界不存在什么条件,那么就表示这个边界将总是能够被执行。
比如说,当角色系统状态机里面的当前状态为RUnning的时候,我们可以为take_off边界添加一个条件测试如JumpButtonPress()。同样的,对于stop边界,我们也可以添加诸如not SpaceBarPressed()测试函数。当角色系统状态机里面的当前状态为Running的时候,玩家一直按着space键不放,SpaceBarPressed()测试将会一直通过,那么stop边界将不会得到运行。同样的,JumpButtonPressed测试也会失败。这就能够让游戏角色的状态机能够一直以Running的状态运行下去。而同样的,动画系统状态机也不能够发生状态变换,它将一直播放跑步动画。
如果现在玩家按下了jump按钮,并且还一直按着space键,那么JumpButtonPressed测试就能够通过,那么角色系统中的take_off边界就能够得到执行。而在动画系统状态机中,也存在一个同样为take_off的边界,并且它的测试也通过了,那么这个动画系统状态机将也会执行它的take_off边界,这样就导致了角色系统的当前状态转变成了Jumping状态,而动画系统状态机中的状态变成了Fly_anim状态。
由于我们希望的是一个平滑的动画过渡,那么我们就不能在用户请求转变状态的时候,就立刻的为他转变状态,我们进行时间上的调度,从而让动画系统能够有机会播放完动画。
通过一个额外的检测操作,我们就能够很容易的实现这样的工作。我们只需要在动画系统的take_off边界中添加另外一个条件,如只有当当前动画系统播放到最后一帧的时候,我们才能够奖动画系统状态机的当前状态转变到边界所指向的另外一个状态。
当玩家按下了jump按钮之后,游戏角色状态机允许执行take_off边界,但是在动画系统状态机中的take_off边界会拒绝进行执行,直到它的动画播放完毕。而这就有可能导致我们动画系统会忽略掉用户按下jump按钮的操作,只有在当用户按下jump按钮,并且动画系统当前状态的动画播放完毕,才会同步的执行两个状态机中的take_off边界。
Edge assignments
另外一个地方就是当我们从当前状态转变到另外一个状态的时候,游戏中需要作出相应的反应。当我们到达Fly_anim状态的时候,我们需要播放相应的动画,而这也需要一些代码来进行操作。
当一个边界的条件被满足之后,我们就需要调用和这个边界相绑定的代码。比如说,在动画系统状态机中,你可以添加如下的代码:startAnimation(Flying)给take_off边界。当我们快要从当前状态变为另外一个新的状态的时候,我们就可以调用边界中的这行代码。在本篇文章中,将只有边界具有执行代码的能力。
当你的状态机里面具有很多个边界,他们都指向一个相同的状态的时候,比如说Crawl_leftarm_anim,你会发现你经常需要执行相同的代码,比如说StartAnimation(LeftCrawl)。对于这种情况,你可以在状态中添加启动函数,一旦进入Crawl_left_anim状态,你就调用StartAnimation(LeftCrawl)函数。这样操作以后,不管你是从哪条边界过来到这个状态,在进入这个状态的初期都会执行一次StartAnimation(LeftCrawl)。
有时候,你也会希望在每一个状态中,运行一段代码。那么你也能够使用诸如OnEveryLoop这样的函数,来处理当前状态每一帧都需要执行的代码。
我们拿Jumping状态来进行举例。当我们的角色处在Jumping状态的时候,我们每一帧都需要更新下当前角色的高度。那么我们就需要在Jumping状态的时候,每一帧调用一个处理函数来处理角色的当前高度。
每一次循环,这个处理函数都将被调用,角色就能够实现慢慢起跳的操作。
Implementation
实现请自主浏览原文!!!