如何系统地学习游戏中的状态机?
简介
网站上的许多初学者都是还没有上大学的学生。通常,初学者会通过阅读互联网上的教程来进行学习、从书上复制代码试着运行并尝试他们那些感兴趣的东西。
有些时候,有关基本的计算机科学理论方面的主题或者文章会被这些读者忽略或者轻视。
这篇文章将介绍一个经常容易被忽视的主题,并希望能向初学者强调其重要性。
这篇文章是基于我的开发经验里面的一系列日志和经验而完成的。
这些内容曾经只是一些零散的碎片,本篇文章会从枯燥的理论开始讲起。请持续阅读下去因为读到最后还是有一些乐趣的。
从计算机科学的观点来看下状态机
有限状态机,有时候也被称为有限状态自动机,或者简称为状态机,对于计算机科学理论来说是非常重要的。是表示有限个以及在这些状态之间的转移和动作等行为的。状态存储关于过去的信息,就是说:它反映从系统开始到现在时刻的输入变化。转移指示状态变更,并且用必须满足确使转移发生的条件来描述它。动作是在给定时刻要进行的活动的描述。
计算机科学理论里面有几种类型的虚拟机器,这其中就包括了有限状态机。这些虚拟机器里面最著名的大概就是图灵机了,它可以表示任何计算机算法的逻辑部分。
有限状态机是一种比较”弱“的虚拟机器,它只能解决几个有限类型的问题,并且很容易找到有限状态机不能解决的问题。即使有限状态机的功能很弱,我们仍然会在我们的日常生活中一直看到各种使用有限状态机的例子。有限状态机存在于各种地方,比如从电梯到交通信号灯、从自动售货机到密码锁、从信用卡数字验证到复杂的人工智能系统。
有几种类型的有限状态机游戏程序员会一直使用。这里面包括了接收器、转换器和时序控制器。接受器产生一个二元输出,说要么“是”要么“否”来回答输入是否被机器接受。所有有限状态机的状态被称为要么接受要么不接受。在所有输入都被处理了的时候,如果当前状态是接受状态,输入被接受,否则被拒绝。作为规则,输入是符号(字符),不会使用动作。使用动作基于给定输入和/或状态生成输出。它们用于控制应用。
接收器这种虚拟机器对于创建简单的语法是非常有用的。如果你能建立一个有限状态机来表示一种语法,那么这种语言就会被称为正则语言。正则语言中的单一接收器语句被称作正则表达式。有很多书专门使用正则表达式来处理用户输入。你可能已经注意到了很多用来搜索和替换的程序工具都包含了使用正则表达式进行搜索和替换的选项。
正则语言又称正规语言是满足下述相互等价的一组条件的一类:
·可以被识别
·可以被识别
·可以被只读识别
· 可以用描述
·可以用生成
·可以用生成
传感器也是经常可以在游戏工具中找到的机器。它们读一些输入文件,并生成相应的输出文件。它们可以是不同的形态,比如说将你的所有文件合并成一个大的数据文件的工具是传感器,将通用数据转换成游戏特定的数据结构来加快加载速度的工具也是传感器。时序控制器经常可以在代码和数据中找到。它们控制着事件或动作的序列。这是我会在后文提到的有限状态机的类型。
有限状态机是什么样子的?
有限状态机真正的关键组件只有两个。第一,它包含状态,这也可以被称为节点或顶点。第二,它包含转移,这也是所谓的跳转或者边缘。在有限状态机的所有状态中会有一个状态被标记为起始状态。所有状态中也可能会有一个状态被标记为退出状态。当运行的时候,有限状态机从起始状态开始运行。会有事件或者触发器或者条件来促使有限状态机跳转到下一个状态。
有许多方法来描绘有限状态机。下面是用表格方式描述的有限状态机:
这个有限状态机一共有四个状态。(有两个条目都是描述的状态2)。
下面是用图形方式描述的同一个有限状态机:
这看着非常简单,对吧。
我们并不需要强制使用数字来表示状态的名字,就如同我们并不需要强制使用i和j或者x和y来表示变量的名字一样。下面是同一个有限状态机的图形方式描述,只是名字换成更好理解更好记忆的名字而已:
有没有突然感觉这个有限状态机理论看起来不像一个枯燥的理论话题而是更像是一个游戏的话题。这种类型的简单有限状态机经常用于简单的NPC游戏逻辑中。(NPC是指非玩家角色、或非操控角色,是指中非玩家控制的角色。玩家借由他们与游戏互动。在里,NPC是由操纵,而在电脑角色扮演游戏里,则是由程序默认的剧本来决定NPC的反应,由游戏的人工智能做出控制。部分NPC有时可变成可操控角色。)
实现一个简单的有限状态机
对于一个快速实现并且没有好好设计的有限状态机来说有一点是永远不会改变的,程序员往往会对那些简单的东西进行硬编码。实现这种有限状态机最常用的方法是用一个简单的switch语句。
完整的代码(包括启动有限状态机的代码)附在文章的最后面。
在这段代码中,我们实现了上面例子说的四个不同的状态。
这看上去和之前的那个有趣的图形有点不一样,但是它确实实现了相同的有限状态机的逻辑。请记住,一个状态机只是一个概念---实际的执行细节千差万别。
一个有限状态机的接口
当构建大型程序的时候,对接口或者一个抽象基类进行编程是一个非常好的办法。这通常被称为依赖倒置原则(依赖倒置原则是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的。)。它允许我们编写可被用于许多不同的具体类的通用代码。
在接下来的几个例子中,我将使用描述状态和有限状态机的简单接口。
下面就是有限状态机接口的具体实现:
为了能够与这个有限状态机的接口一起配套使用,我们还需要一个调用这个接口的负责启动有限状态机的代码:
下面是我用于后面这些例子的负责启动有限状态机的代码:
这就是我们所需要的,我们已经得到了一个基于文本的地牢探险游戏的框架。
一个无聊的有限状态机
在上面的代码中我只是创建了一个简单的有限状态机。因为我在里面没有做任何有趣或者让人兴奋的事情,我把它称为无聊的有限状态机(BoringMachine)。要注意的地方:请记住,有限状态机只是一个概念。有各种各样的方法可以来实现它。可以是一个指向对象的集合的机器。也可以是用一个单独的变量的变化来表示状态。还可以使用一系列条件语句来实现有限状态机。甚至你还可以使用继承树来实现有限状态机。所以,我们在前面强调的都只是概念而已,具体实现的细节将由你自己做主。(不同的实现方式有不同的优缺点,有些方法简单但是扩展性差,有些方法便于调试,有些方法扩展性很好但是实现比较麻烦,需要根据自己的需求决定最适合自己的有限状态机实现)。 在这个有限状态机中,我选择使用一个单一的状态,然后让这个状态随着有限状态机的改变而改变自身。
这个有限状态机实现了上面描述的接口,但这个接口需要你明确使用跳转的名字。在这种情况下,我只是忽略状态的名字并且沿着一个固定的路线推进。请记住,这仅仅是一个启动有限状态机的概念方面的证明,它不会做任何有趣的事情。它就跟上面标着状态名为0、 1、 2和 3的有限状态机是一模一样的-虽然看着不怎么像。
有了上面这些代码,我就可以让负责启动有限状态机的程序来启动对应的具体的状态机,并且可以正常工作。
有些时候它就是最大的成就。
让我们做一个游戏
现在,我们已经证明了负责启动有限状态机的程序确实是胜任这个任务的,我会创建另一个状态机实现。这是关于写一个接口然后基于接口实现程序的另外一个好处:我可以很容易地创建各种各样的实现。
我会把这个状态机实现称为有趣的有限状态机(FunMachine)。它继承自Istate并且实现了之前说的那个简单的接口。
这个有限机表示的是一个待搜索的地图。每个状态代表一个房间。下面就是有关的代码实现:
有限状态机的大部分内容
请记住,我们可以通过多种方式来实现一个有限状态机。在这个例子中,这个状态机将指向当前的状态,并会在发生跳转的时候移动到下一个状态。这个部分讲解的源代码将包括所有的部分,除了有限状态机的构造函数。我将在下一节中将会讲解更多的细节。这个有限状态机也是实现了同一个IstateMachine接口。它还需要添加三个与这个有限状态机实现相关的值:一个状态列表、一个指向当前状态的链接、一个指向退出状态的链接(我们最终会剔除的状态)。
有两点需要注意的是它是如何得到可能的跳转的列表以及它是如何进行跳转的。我们看一下当前节点的邻居的名字-在我们这个例子中,节点的邻居的名字是地图中房间的名字。我们要确保我们只能在相邻的房间通行来进行跳转。这避免了在一开始就输入”退出移动“反而赢得了游戏。
同样的,如果你一直从开头阅读到这里这似乎应该是比较简单的。我们只用了短短几行代码就得到了一个有限状态机。
终于到了有限状态机的构造函数了
这个有限状态机仍然是通过硬编码实现的。为了生成迷宫,我们需要手动构建每个房间(或者可以说是有限状态机的状态、或者说是有限状态机的节点,也可以称为有限状态机的顶点)。我们还需要构建有限状态机的每个跳转(或者称为有限状态机的边缘)。
这就产生了一个有趣的小型地图。
如果你在可视化这个有限状态机的时候遇到了问题,画面应该看上去是这样的:
它有很多房间。它是一个文字探险游戏,有一个房间是死亡陷阱房间,并有一条通往胜利的路线。它开始看起来像一个真正的游戏了。
运行示例代码
如果你还没有通过执行上面实现的这些代码,那么现在是一个很好的时间点来做这一点。
你可以探索下这个很小的基于文字的游戏世界。它没有情节,它也没有任何道具或者除了移动以外的其他行动。但是说明了一部分游戏是如何构建的。
让它变成数据驱动
这个词出现在大量的游戏中:数据驱动。这到底是什么意思?
到目前为止,如果我想对游戏或者逻辑做出一些改变,我都需要修改源代码、重新编译和测试应用程序。对于一个小城堡地图和只有一个开发者的项目,这不是一个很难的事情。但是,当应用程序的规模增长的时候会发生什么? 让我们想象一下,我雇了一个人来设计我的游戏里面的关卡。这个人不是一个程序员。我不想让他们碰我的C#文件,我也不想教他们读和写C#。所以我应该怎么做?解决方案非常的简单:我创建一个保存起来的文件,里面包括了描述这个关卡的所有信息。
我可以让非程序员通过在修改运行时加载的数据来为我的游戏工作。数据驱动意味着关卡设计师可以修改房间、把不同的东西放在不同的位置以及做其他提高与游戏体验的修改而无需修改代码。这意味着另外一个程序员可以实现诸如绳索、瓶子和水等游戏物体而无需修改代码。这意味着,如果我们做一个图形游戏的话,艺术家可以创造新的美术资源的同时建模师可以创造新的模型,而动画师可以同时修改动画,而他们所做的这一切都不需要修改代码。
数据驱动意味着只有程序员需要修改代码。其他人都是使用数据来修改整个游戏。
比较先进的引擎会实现在游戏运行的时候重新加载数据的方法。设计师、艺术家、动画师还有建模师可以更快的迭代他们的工作,这有可能节省几个月的工作时间。
让我的地牢探险游戏是数据驱动的
这个状态的实现和上一次的实现相比只会有一点点的修改。
在上一次的实现中,我保存了状态的名字、描述和邻居节点的信息。这一次,我想补充的信息是唯一的名称键和一组标志。标志指示该节点是否是输入节点(只有一个),或者该节点是否是一个输出节点。
接下来,我添加了函数ReadXml和WriteXML。这两个函数用来保存和读取刚才提到的五个元素(唯一的名称、标志、给外界看到的名字、描述和邻居节点)到一个XML文件中。因为它基本上是免费的,所以我选择使用IXmlSerializable接口来实现它们。也许在未来的某一天,我觉得我会充满野心的扩展未来的组件通过C#的序列化协程来自动处理我的数据。
由于有限状态机将需要从XML文件中创建这些对象,我创建了第二个构造函数接受一个XmlReader参数并把这个参数传递给ReadXml函数。
最后,我添加了一些存取函数(也就是 get和set函数)来帮助有限状态机。
如果第一次就接触这么多代码可能有点吓人。但是因为我们是随着时间的推移慢慢增加这段代码的量,所以你应该每次看到的都只是一个增量变化,而且增量变化的幅度并不大。
有限状态机的变化
有限状态机的变化有点戏剧性。
现在退出节点的信息已经包含在了数据之中,所以我可以把我之前提到的mExit状态丢掉了。
为了方便起见,我把地图构造函数的代码从构造函数移出来成为一个单独的函数:GenerateDefaultMap()。它允许我们在实现我们自己的工具链的时候能够生成和保存一张地图。构造函数通过调用ImportFromXml()来代替。如果从xml加载失败的话,我们会生成默认的地图,使用ExportToXML()函数来保存一份拷贝,然后重新加载我们新生成的地图。
ExportToXML()创建了一个XML写入类,对每个状态进行遍历并使用WriteXml()函数来把每个状态写出来。ImportFromXML()函数创建了一个XML读取类,通过相应的ReadXml()函数来读取文件。
下面是修改以后的代码:
因为我们还是做了一个增量变化,所以阅读这么长的代码也没有看上去那么糟糕,还是比较容易理解的。
运行游戏和生成引导数据
现在当我运行游戏的时候,它会尝试加载和保存文件。如果它不能找到一个GameRooms.xml数据文件的话,它会去生成一个新的GameRooms.xml数据文件。然后它会像以前一样播放同样的地牢探险代码。
因为我要引导我的工具,所以我需要直接跳转到生成xml的地方:
我们可以看一下生成的程序,确认它就是我们原来的地图,只是以XML格式保存出来了。为了证明这个系统是可以正常工作的,我们可以对这章地图做一些小的修改:
这需要对入口大厅做一个小的修改(将邻居节点指向“庭院”),并创建三个新的房间。
在在文本编辑器中花了几秒钟进行复制和粘贴、一点词语上的润色,我将这些修改内容加到了保存文件中去了:
房间的部分总是有可以提高的空间
在这个简单的例子中我们可以做很多的事情。我要做的第一件事是创建对象,这些对象可以放置在房间中。这些都是简单的有限状态机。举个简单的例子来说,我可以有一个桶对象,这个桶对象有几个状态:空的状态、装水的状态以及装了水和鱼的状态。事件可以添加(同样是使用一个简单的状态机),这些事件通过查询和当你完成整个目标的时候给予奖励点数来追踪你的进展。
在游戏中这些东西都是非常有用的,但在在这种情况下它们不会添加任何超出刚才解释范围的东西。这个部分可以作为一个练习留给读者自己解决。
一个更加复杂的有限状态机
让我们进入到一个更复杂的话题里面。
每个我工作的游戏都以某种形式使用了一个人工智能系统。里面有动作对象和物体,并且动作对象会做某些时间。通常情况下,动作对象会使用这些物体。
对于这个演示程序,我创建了以下结构:
这个比赛的容器是一个公平的竞争环境。它包含一个游戏对象实例的集合。比赛场的更新周期是固定的,大概是每秒30帧,并且在每次更新的时候会把更新传递给所有实例个体。
有两种类型的游戏对象:宠物和玩具。这些游戏对象和物体一起工作来使用各种各样的活动。现在让我们对每一项的细节来具体看下。
基本的游戏物体类
一个游戏物体适合与有限状态机一起工作。既可以作为有限状态机本身又可以作为有限状态机的节点。回忆下早期讲到的,一个有限状态机就是一个概念。当概念是完整的时候,具体的实现细节并不重要。它有一个Update()函数,这意味着要运行的当前状态,并且如果必要会推进当前状态。我们将在后面一点展开这个问题。游戏物体代表着我们可以在游戏中放置的任何物体。它们有一个所有者(在这种情况下是比赛区域)。这些游戏物体有自己的位置信息。也有自己的图像表示。为了方便起见,这些游戏物体有一个ToString()函数的重写,这使得当我在属性项里面察看它们的时候更加方便舒服。通过开发,这些游戏物体还拥有了PushToward()和MaxSpeed()函数。这些函数可能会被集成到一个物理系统或碰撞系统中去,但到目前为止,这么安排对于它们来说是最合适的。
所有的游戏物体都需要实现上面这个接口。
宠物和动机
基本的宠物类是非常简单的。一个宠物是一个游戏物体(因此它自动获得了上面提到的一切内容),除此之外,它还得到了一个行为和一系列动机。
动机无非是对宠物的状态的一个封装。(请注意如何那《模拟人生3》做对话的话,那里面是有8个明显的动机 - 饥饿、社交、上卫生间、卫生、能源和乐趣)。(译注:原文就是这样,作者说有8个明显的动机,但是只列了6个具体的出来)。当创建一个宠物的时候,我们会默认这个宠物出于空闲状态,并初始化他们的动机。我们有一个默认的更新行为,来运行当前我们正在做的活动,或者如果我们没有做任何事情的话,就会创建一个新的空闲状态并开始进入这个活动状态中去。我们还实现了来推一个宠物的具体行为。
这有几个待做的事情:已经在代码中标记了,但是这都是接口内部的工作。还记得之前提过的依赖倒置原则么?对一个接口或抽象基类写相应的代码,而不是对一个具体的类进行相应的代码的编写。(依赖倒置原则是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的。)
小狗!
终于,我们要创建一个具体类了。一只小狗。请注意,我们只是从已经保存的资源中提取值,所以设计师和艺术家可以在以后对其进行修改。
是啊,虽然我们前面做了这么多的工作,但是所有的这些工作我们只得到一个小小的具体类。这其实是一个非常好的事情。这意味着,当我们想在以后扩展它为不同类型的小狗、小猫、马或者其他宠物的时候,我们只需要添加一点点代码来将新的动作对象和新的数据关联起来。
玩具
一个玩具也是一个游戏对象,因此它可以表现为一个状态机和一个状态节点。 每个玩具都有一个与之关联的默认行为。当宠物试图使用玩具时候,它们将得到这个默认的行为(又名行为树),并开始运行它。
玩具类还要负责计算游戏对象的兴趣等级。现在,这些只是每个玩具对象内部硬编码的公式。以后这些可能是一系列更复杂的交互,但现在的硬编码实现对于这个系统是足够的。
下面是玩具抽象类的代码实现:
两个玩具
现在我们将创建两个具体的玩具类。
第一个具体的玩具类是睡垫。宠物对睡垫的兴趣度只取决于宠物的能量。睡垫类会有一张图像需要绘制。宠物默认的行为是在睡垫上睡觉。
第二个实例类是一个可以踢来踢去的球。宠物对可以踢来踢去的球的兴趣度只取决于宠物的心情,尽管看上去它应该还包括一个能量组件。可以踢来踢去的球会有一张图像需要绘制。而宠物的默认行为是追这个球。
现在让我们开始关注那些驱动整个系统的活动上面去。
活动是整个系统的胶水和石油
活动将整个系统粘合在一起。活动是活动对象和物体之间的交互。如果没有活动的话,这两者不会有什么联系。
活动就像润滑油让不同的部分能够流畅的运转起来。它们会一直运转。它们会改变自身,进而也会改变它们的所属的角色,也会改变它们的工作对象。让我们举一个更加复杂的例子,一碗食物可以通过修改饥饿值来改变对象,并且通过减少在碗中食物的数量来改变目标。
这就是我们的活动基类。
一个活动会有一个对象和一个目标。我故意的把对象限定为宠物。我可以允许任何对象与任何对象之间进行交互,但这么做在实践中并没有多大意义。我们真的不希望亿碗食物与咀嚼玩具进行交互,或者一个球与一个睡垫进行交互。我们想让宠物成为社交活动这种默认活动的目标。举个简单的例子来说,宠物可以一起跳舞或者互相嗅或做任何一群宠物一起做的事情。
我们允许活动基类有一个更新事件。这个更新事件是由早前实现的宠物类调用的。我们通过每个活动的OnUpdate回调函数来传递这个更新事件。如果这个活动返回true的话我们就知道它处理完成了,而宠物就需要找一些新的事情去做。
最后,我们有一个神奇的函数FindBestActivity(),也需要出现这个基类里面。我可以单独为这个函数创建一个类,但是目前来说的,放在活动基类里面是最合适的位置。
FindBestActivity是让人工智能做一些有趣的事情的魔法函数。在这个例子当中它只有35行。我们遍历了游戏世界中所有的玩具并且看下它们到底是多少有趣。然后我们找到最好的互动并返回一个新的实例。如果我们失败了我们就返回空闲的活动。
对于像《模拟人生》这样的游戏可能会有成千上万的对象可供选择,每一个对象可能会有多个与之相关的活动。在其中找到最好的活动是一个复杂的工作。它背后的理论并没有什么不同:找到最好的活动,然后创建一个活动的实例。
现在我们知道什么是一个活动了。活动基本上只是另外一个状态机。
空闲活动
我们将从空闲这个活动开始我们的实现。
它有一个空闲时间。足够的时间过去后,我们将去寻找新的活动来做。这个新的活动将取代我们目前的空闲活动。
如果我们不能找到什么有趣的活动来做,我们可以静静的坐在那里,慢慢地降低我们的乐趣度和能源值。
因为这些都是c#实现的,我们不需要安排清理和删除自己的空闲活动的代码,这就帮我们节省了很多的开发时间。
追球活动
这实际上是两个活动。它的内部是一个微型的状态机。追逐一个球有一个组件角”跑向目标物体”,然后第二个组件是负责宠物把这个球给踢出去。这并不是一个实现起来很困难的状态机,它只有两个状态,可以直接用一个简单的if语句来表示。这是个很好的机会来再次重申下:
请记住,有限状态机只是一个概念。有各种各样的方法可以来实现它。可以是一个指向对象的集合的机器。也可以是用一个单独的变量的变化来表示状态。还可以使用一系列条件语句来实现有限状态机。甚至你还可以使用继承树来实现有限状态机。所以,我们在前面强调的都只是概念而已,具体实现的细节将由你自己做主。(不同的实现方式有不同的优缺点,有些方法简单但是扩展性差,有些方法便于调试,有些方法扩展性很好但是实现比较麻烦,需要根据自己的需求决定最适合自己的有限状态机实现)。即使是一个简单的if语句也可以实现状态机的概念。
所以每次更新的时候我们都试图跑到球对象那里。如果我们成功的跑到球对象那里的话,我们就把球踢出去一个随机的距离。我们也会把球踢出去的时候提升了一点兴趣度。
当一个活动完成的时候,它会返回结果来表明。只有当兴趣值满了的时候我们才会返回true。我们可能想要第二个退出条件:当能量过低的时候也可以退出,但是这个会放在后面一点来实现。
跑向目标物体的活动
接下来,我们将看看宠物是如何跑向一个对象的。
其实实现起来非常的简单。如果宠物和目标对象足够的接近(这是另外一个策划可以调整的值),那么代码就会让宠物直接到目标对象的跟前并且返回true。如果不是这种情况的话,就会推动宠物往目标对象那里跑并降低他们的能量以及返回false(因为我们还没有完成这个活动)。
在垫子上睡觉
就像追球状态一样,在垫子上睡觉是从跑向目标对象开始的。所以首先我们调用RunToObject。
如果它成功的话(这意味着我们最后到达垫子那里了),那么我们就开始休息。如果我们还没有到的话,会基于我们的能量状态返回true或者false。
概念验证完成了
好了,到这里为止,我们已经完成了我们的概念验证。
现在请去玩一下我们在这篇文章中实现的示例。
把多个游戏垫,多个球,和多个小狗丢在场景中。看着他们跑来跑去追球,当他们累了看他们跑去垫子上睡觉。从左边的窗格中的列表里面选择他们,可以查看细节属性。
这并没有什么了不起的地方,但是可以看到一个沙盒游戏是如何开始制作的。我把这个示例展现给我的孩子们看,然后他们立刻就开始制作了一系列新的物体添加进去。添加一个猫!添加一个跷跷板!添加一个绳球!(这是真的吗?)
重复创建三十个左右的新的对象,并给这些对象添加一些动机,然后你就可以拥有一个你自己的自我运行的模拟宠物世界了。
专注于这个实例中的一些点来创造一个迷你游戏,然后丢弃这个世界里面的一些缺点。重复这个过程直到游戏完成为止。
但我想要实现的是一个第一人称射击游戏
这个问题解决起来很容易:
把“玩具”重命名为“怪物”,把”小狗”重命名为“海军陆战队”。
把“玩具”重命名为“路点”, 把“球”重命名为“火焰喷射器”,并把“睡觉的地方”重命名为“出生点”。
最后,根据合适的场景来重命名活动。
将整个系统包装起来
所以我们了解到有限状态机是计算机科学中的自动机最弱小的一种形态。我们还了解到,有限状态机用于我们的日常生活的方方面面,并且可以为游戏做很多不可思议的事情。
我们可以使用有限状态机来基于状态运行不同的代码。
我们可以使用有限状态机来表示任意的三角形网格。这些三角形网格可以包括像是地图或者连接格之类的东西。
我们不需要对有限状态机进行硬编码。当你是从数据加载有限状态机的话,你可以让这个有限状态机同时被更多的人使用。设计师、艺术家、建模师、动画师以及其他人都可以修改游戏而不需要修改代码。
我们可以构建非常复杂的系统,比如说人工智能行为树,这是通过的一个非常简单的有限状态机的反复嵌套实现的。
我还能把有限状态机应用到什么地方?
大多数游戏会在整个游戏到处都应用有限状态机这一概念。
行业最常见的工作就是游戏逻辑开发工程师了。他们所做的工作就是创建我们在这个系列教程创建的这些东西。
举个例子来说,在《Littlest PetShop》中,我花费了几个月的时间来添加新的行为和动作。
它们的行为包括处于“位于世界之中”,那么宠物会做上面提到的那些动作的,也会玩一些小游戏,比如捉迷藏或者宠物将自己隐藏在灌木丛后面,当这些宠物们被发现的时候会跑到摄像机前和玩家一起庆祝。
在《模拟人生》中,游戏中的成千上万的物体中的每一个都需要自己的脚本。比如你需要一堆衣服,那么你会需要这么一些脚本:控制用来创建这堆衣服的行为和交互的脚本、控制扔掉这堆衣服或者把这堆衣服捡起来的交互行为的脚本、控制与洗衣机交互的脚本、控制与烘干机交互的脚本、控制与过来清理的女服务员交互的脚本,还有很多很多很多。
或者你正在开发一款有节奏的游戏,你需要给你的主角赋予行为和行动,以便让他们知道如何攻击和逃跑,或者可以决定是要攻击塔或是攻击玩家还是攻击仆从。你需要创建可以被捡起来的物品、武器、道具等等。所有这些行为都跟我们之前演示的是一模一样的。它使用一个公共的对象接口,程序员会填充里面的细节。
这些小活动和行为发展成复杂的生态系统从而让大多数游戏变得更加有意思。
所以你可以从这里触发继续探索未知的世界了。一个基于文本的地牢爬行游戏原型和一个沙箱世界的原型,在里面应用了很多的有限状态机,都只用了几个小时就开发完毕了。
像这样创建对象和行为描述了游戏工程师的日常生活。如果你在游戏行业内得到了称为一个游戏程序员的机会,你将花费好几年的时光来写如同上面的这些代码。请记住这是一个简单的状态机可以帮助避免很多讨厌的bug,并可以大大简化你的生活。
请记住,有限状态机只是一个概念。有各种各样的方法可以来实现它。可以是一个指向对象的集合的机器。也可以是用一个单独的变量的变化来表示状态。还可以使用一系列条件语句来实现有限状态机。甚至你还可以使用继承树来实现有限状态机。所以,我们在前面强调的都只是概念而已,具体实现的细节将由你自己做主。(不同的实现方式有不同的优缺点,有些方法简单但是扩展性差,有些方法便于调试,有些方法扩展性很好但是实现比较麻烦,需要根据自己的需求决定最适合自己的有限状态机实现)。
非常非常感谢你的阅读。
源代码可以在这里找到:
更新记录:
2013年3月26号 修复一个错误的地图图像。
关于作者
我从1990年代早期开始就一直以编程作为我的职业,工作内容都是和游戏产业相关的。我为相当多的游戏工作过。我的几个项目,帮了我实现了这篇文章。我曾经开发过任天堂DS上的《Littlest PetShop》,主要是负责早期原型的开发和构建游戏的行为系统,它与这篇文章提出的状态机非常像。我还花了好几年的时间在模拟人生的各种版本之中。几乎每一个我工作过的其他游戏会遵循同样的模式,只是会稍微有些变化。
【版权声明】
请见
根据授权文件,我们可以翻译这篇文章。
近期热文
经验分享丨项目实践项目孵化丨渠道发行做有梦想的游戏人
-GAME AND DREAM-