万物起源

当你开了一个新档,第一次进入游戏的时候,会有一个生成世界的步骤。这个过程可以称之为世界的初始化。系统首先绘制世界地图,将各种地皮串联起来。然后在每一个地皮上放置一些资源和奇遇。世界地图是随机生成的,但也是有一定的规律,可以进行设置的。掌握了编程的方法后,你就可以绘制出自己想要的世界地图了。不过本期的重点并不在于此,后续会有一期专门讲解地图。这里想说的是各种资源,比如花、草、蜜蜂、蝴蝶、猪人等等。他们都有一个统一的称谓:Prefab,翻译成中文叫作预制物。除了各种资源之外,各种特效、建筑物,那些可以与玩家发生互动的或者不能互动的,看得见的或者看不见的东西,都可以称为Prefab。另外,我们还可以把Prefab理解为生物学里的染色体,它的基因记录了一类物体的初始化方法(应该如何生成,包含外观、各项属性以及行为方式等等)。系统会根据染色体上的基因来将Prefab生成实体,投放到游戏系统里。每种东西都有自己的Prefab,有着唯一的名字,有一条控制台命令用于生成Prefab:c_spawn("{prefab}", amount),这里面的{prefab}就是就是指Prefab的名字,比如spider,pigman等等。有一些物种之间很相近,比如各类蜘蛛,他们会共用一部分初始化的内容,但也会有各自的差异。除了使用游戏提供的Prefab,我们也可以根据需要,自己定义新的Prefab。这需要给Prefab起一个名字,提供一个初始化方法,最后再调用系统的函数进行注册,让系统知道有了一个新的Prefab。

游戏世界里丰富多彩的各种东西,本质上都是由各种Prefab生成的实体,它们之间的核心区别,就在于染色体上的基因,也就是初始化方法。Prefab是多种多样的,它们的初始化方法也各不相同,但也有一些共性。接下来就讲讲,Prefab的初始化都有哪些内容。如果有兴趣的同学,也可以直接阅读源码文件夹下的Prefabs文件夹。

灵魂

首先第一步,添加灵魂,也就是创建一个实体,这是实现与游戏世界交互的第一步。在这一步里面,系统会赋予实体一个唯一的ID,这是与其他实体区别的标识,即使属于同一个Prefab,也拥有各自不同的实体。除了赋予实体ID之外,系统还会分配相应的内存,记录这个实体的各种信息,比如创建时间,存档信息等等。在大多数Prefab的初始化方法中,第一行代码都是local inst = CreateEntity(),这里的CreateEntity就是系统提供的一个创建实体的方法,调用它会获得一个由游戏引擎创建的实体,同时还会记录实体id,创建时间等基本信息。这个实体,我们用inst来指代。注意,这个inst实际上也是个容器,用于存储各种数据,在CreateEntity方法中,真正返回的实体是被存在inst.entity下。在这一步创建的实体,只有一个灵魂和一些创建时记录的信息,既看不到,也做不了任何事情。

血肉

第二步,添加骨骼和血肉。实际上也就是为实体添加各种组件,并设置每个组件的初始化参数。组件可以分为两类,一类是底层游戏引擎提供的组件,比如位置组件Transform,动画组件AnimState等等,在联机版中为了实现机器之间的数据交换,还有网络组件Network,这些组件由游戏引擎提供,通过调用inst.entity.AddXxx来获得,数量不多,但都是与游戏引擎强相关的核心组件(比如位置、动画、光照、网络)。它们没有lua语言定义的源码,无法看到具体的功能,只能通过名字和参数去理解,然后模仿游戏里已有的lua代码的用法来使用。另一类是通过lua语言定义的组件,这些组件首先需要被声明为Component类,并给出一个唯一的名字,然后在Prefab的初始化方法中执行inst:AddComponent("{component}"),这里的{component}就是组件的名字。

添加组件,是Prefab初始化方法中代码占篇幅最大的内容,常常可以占到60%以上。可以说,组件是染色体上的主要基因。组件种类以及初始化参数的不同,造就了这个世界丰富多彩的种群。比如人物都会有饱食和精神组件,但怪物没有。动物都有血量的组件,植物却没有。即使有着同样的组件,不同的Prefab也有不同的初始化参数。比如人物都可以吃食物,但机器人WX78还可以吃齿轮。游戏Mod中数量最多的当属人物Mod,每个人物都有自己独特的形象,这是因为他们通过inst.AnimState.SetBuild('{build_name}')读取了相应的设置。而一些比较有特色的人物,作者往往自己编写一个专属组件。比如我就为Samansha编写了组件Photosynthesis,用来实现Samansha的光合作用。组件是游戏机制的核心。各种功能不同的组件,为我们带来丰富多彩的游戏体验。比如Transform与位置相关,像是虫洞瞬移之类的动作,都需要借助TransformAnimState与外形和动画相关,特定条件下改变人物外形,不同地动作下播放不同的动画,都需要借助AnimState;而游戏最基本的三围——饱食度、精神值、血量分别对应着hunger, sanity, health三个组件。只要能了解这些清楚各种组件的作用,就能极大地提升对游戏核心机制的理解。

动起来

第三步,添加状态图,也就是SG,全称是StateGraph,在业界更常用的称呼是有限状态机(finite-state machine,FSM)。这一步是可选的,对于一些比较简单的Prafab,比如特效,在生成之后,只播放了一遍动画之后就自动被移除了,很容易描述,就不需要设置SG。但对于玩家这样,或者一些有复杂行为的BOSS,能做出的行动非常多,每个行动下都会有不一样的表述。比如玩家,静止不动时,偶尔会打个哈欠;在地图上走动时,会动起手脚,踩到某些地板还会发出响声,自己的位置也会移动;战斗时,会挥舞武器进行攻击。这里的状态-State,指的是像「静止」、「走动」、「攻击」这样一个整体的概括。前面说到,游戏的核心处理逻辑是在组件里写的,比如攻击怪物会扣多少血,一秒内可以移动多远,都是通过对应组件来完成的。这里的State承担的主要功能是,写清楚这个状态下应该播放什么动画和音效,触发相应组件的计算逻辑,在这个状态下的逐帧变化,以及如何转化为其他State等等。通常来说,有多套动画的生物,都会有自己的SG,并且哪怕是同一个State,不同的生物也有不同的表现,比如各种怪物都会有基础的「攻击」State,但每种怪物的攻击动画都是不一样的。攻击的「前摇」、「后摇」在不同怪物上的差异,主要就是通过SG来实现的。可以说,SG决定了如何、何时触发伤害计算逻辑,而组件则决定了具体伤害的数值以及造成伤害后的效果。细心的玩家肯定会发现,人物拿不同的武器进行攻击与空手攻击之间的攻击频率是有所差别的,这是因为「攻击」这个状态是有一个冷却时间的,在冷却时间内无法再次攻击。每一帧的时间为1/30秒,各类武器的冷却时间如下:鞭子17帧、书19帧、露西斧11帧、一般武器、提灯、海狸撕咬为13帧、空手为25帧。可以明显看出,空手的冷却时间很长,几乎是两倍于一般武器,所以能很明显地感受到攻击频率的不同。大多数人物都是用同一套SG,如果想要改变人物的攻击频率的话,就需要修改SG中相应的「攻击」状态的定义。

对State的定义,可以决定在一个State里动画、音效、组件触发的各种情况,乃至于在一个State结束后应该转向哪一个State,但不能完全覆盖State之间的转换。比如玩家进入到了「攻击」的State,刚刚抬手就被怪物的攻击打断,则会进入「硬直」的State,还有玩家在抬手攻击时,发现自己血量很低,马上吃了补血的食物,从而进入「进食」的State。这些类型的State转换都是通过触发某种事件(比如被攻击)或者动作(比如进食)来实现的。在代码的层面上,通过事件触发State转换,称之为EventHandler,通过动作触发State转换,称之为ActionHandler。事件触发State的机制,主要是为了方便各种组件进行State管理,比如战斗组件,只要在受到攻击伤害时,发送一个「被攻击受伤」事件,就能自动触发自身进入「硬直状态」。而动作触发State的机制,主要是为了玩家操作考虑。玩家与物品发生交互的过程中必然会产生动作,这个动作直接推动SG进入相应的State,播放对应的动画和音效。

研究SG,对于Mod制作者的意义在于,能够很方便地管理一些复杂生物的行为。将一个行为的各项代码组合到一个State里,在程序设计上会显得更为清晰,也容易维护,还可以实现逐帧操作,决定何时触发动画、音效的播放和组件逻辑的计算。而对于一般的玩家来说,则可以更深入地了解生物的行为细节,比如怪物的攻击频率和前摇后摇,硬直时间等等,从而更好地进行战斗。

下面的图是一个典型的状态图,简单直观容易理解

状态图

AI决策

第四步,添加AI。在玩家操作的角色之外,所有的生物都有自己的一套行为逻辑,也就是我们常说的AI。实现AI的方式有很多,如果判定标准和行为都比较简单,用上面说到的SG来实现就可以。但如果行为比较复杂,就需要其他的方式来实现了。在这个游戏中,复杂的AI是用「行为树」实现的,实质上就是一个树形结构图,每个节点代表一个行为或者条件判断逻辑,分支代表着不同条件对应的下一步行动。在游戏的源码中,与AI相关的代码有两部分,存储在两个文件夹下,分别是brainsbehavious,前者代表着一个生物完整的行为树定义,后者则是具体到某个行为节点的形式逻辑。比如brain会决定兔人发现玩家持有肉类时,发起「攻击」行为。而behavious则会详细描述如何发起攻击,比如说要不要追击玩家,追多远会返回等等。

AI的理解和编写,还是相对复杂的,后续会专门写一篇来解读。研究清楚AI,对于Mod制作者来说,可以实现一些很智能的操作,比如让宠物自己干活。对普通玩家来说,则是能清晰地了解生物的行动模式,更好地去与各种生物进行互动。与SG的区别在于,SG主要强调状态的变化细节(比如在哪一帧触发伤害),而AI强调的是行为的决策(比如什么条件下会发起攻击)。

下面的图是一个简单的行为树,从根节点起,从左到右依次遍历子节点,直到执行成功(不满足条件的节点就是执行不成功,比如周围没有食物,则这个节点执行不成功,不会往下,而是接着访问下一个兄弟节点-是否有主人)

行为树

世界地图

除此之外,Prefab的初始化内容还包括添加TagAddTag和注册事件监听回调ListenForEvent,但这些内容不是必须的,通常是为了特定目的而设置,没有太多共性,在下一期专门介绍Prefab的内容中才会详细解释。

综上总结,Prefab 可以说是世界构成的基本元素,可以理解为成类似染色体的模板,让系统能快速生成一批同种类的东西,比如一群蜘蛛或者猪人或者一片森林。Prefab 以组件实现各种游戏逻辑,通过SG来控制自身动画、音效的播放、通过行为树来实现AI。

了解了 Prefab 之后,我们就可以来谈谈更大的内容了,那就是世界地图的生成方式。

在世界地图的生成过程中,Prefab是最小的单位,比如树木、干草、灌木、怪物、动物、石头等等,系统以某种方式将这些Prefab分散到地图上。在 Prefab 之上,是 Static Layout,中文为静态布局,可以理解为几个Prefab以固定的静态布局展现出来的东西。比如猪王就是一个典型的 Static Layout,在猪王周围总是有方尖碑,而且位置方向都是固定的。如果静态布局再加上能以某种方式触发的事件,就成了Set Piece,也就是我们常常说到的彩蛋。再往上一层,被称为Room。Room是最小的随机生成单位。一个Room,可以理解为一块小区域,它会有统一的地皮,还可以设置随机或固定数量的Prefab、Static Layout。同样是森林地皮,有的区域树很多,有的却很少,某些还会有海象巢穴,这是因为它们属于不同的Room。在Room之上,是Task,一个Task内可以包含若干个Room,也可以理解为是一个大区块。比如一大片草原,就是一个草原的Task,会有很多个不同的Room,某些Room有很多牛,而另一些则有很多兔子。再往上,是Task Set,实质上就是一整个世界,会囊括很多个Task。不同的Task Set有不同的地图设计和生成机制,比如一般游戏模式下进入游戏的世界被称为forest,而敲碎地洞进去的世界,被称为cave,它们各自代表着一个Task Set,很明显两者的地图是大不相同的。

那么世界是如何生成的呢?首先,根据玩家的设置,获取对应的Task Set,如果没有进行修改,默认会取用名为forest的Task Set。然后根据Task Set的设置,生成若干个Task,并以某种机制把这些Task串联起来。然后在每个Task上生成对应的Room,最后在每个Room上放置适当数量的Prefab和Static Layout,世界的生成就完成了。

对于Mod制作者来说,如果你希望设计独特的地图,或者添加某些特定的区域,就需要研究世界地图是怎样生成的,比如我的Samansha人物Mod,就添加一个区域来生成特有的鹿。对一般玩家来说,了解世界的生成机制,就有利于快速找到一些有用的资源,比如海象屋,沼泽地芦苇丛等等。

交互界面

到这里,世界的构成就已经讲完了,但我还想要再补充游戏的交互界面的知识。进入游戏后的各种按钮,状态指示器等等,都被称为Widget,中文叫小部件。它就是很小的一个元素,为了特定的需求服务,比如显示饱食度,或者浏览一个箱子的内部储存等等。而一些要占据整个屏幕的东西,比如选人画面、小地图画面等,就被称为Screen。

对于Mod制作者来说,想要实现一些特殊的UI操作,就必须了解Widget和Screen。而对于一般玩家来说,了解这些,主要是弄清楚各种按钮的交互作用。

好了,本期的介绍就到这里,下一期会详细讲解Prefab,除了更细致地讲解Prefab的生成之外,还会介绍各种典型的Prefab,欢迎关注。

results matching ""

    No results matching ""