概述
这篇文章会阐述Don'tStarve代码的整体框架,帮助Mod新手们快速了解Don'tStarve Mod能实现哪些功能,需要做哪些基本的配置,以利于有目的有针对性地进行学习。
首先从整体上来了解一下Don'tStarve MOD的概念。
能干什么
你能够添加自己制作的内容(物品,人物,生物等等),新的屏幕界面/操作(Widget,Screen),也能够修改游戏原有的内容,甚至连其它MOD的内容都能修改(只要它们在游戏中被启动并接入了游戏系统)。Don'tStarve是个高度开放的游戏,除了底层引擎和部分十分消耗资源的操作外,大部分的游戏内容都是用Lua语言编写的。它们被公开地集中存放在游戏根目录/data/scripts下。这就意味着,只要你懂得如何寻找lua文件,并能够读懂源代码,你就能自己实现、或者修改游戏里的已有内容。也就是说饥荒的MOD上限是非常高的,随着你的能力增长,你就能够大幅度地修改Don'tStarve,甚至重造一个全新的世界。
怎么实现
这是通过游戏提供的一系列API实现的。下面的基本原理一节会提到API是什么。
如何学习
学习MOD制作知识,最终目的就是为了制作并发布我们自己的MOD。与其漫无目的进行学习,不如用项目驱动学习,更有目的性也更清楚自己到底掌握了什么知识。
所以,我们以0基础为起点,完成并发布一个自己想要的MOD为终点,来说明MOD制作知识的学习流程。
这里所指的游戏逻辑是丰富而复杂的,短时间内全部学习完是不现实也是不必要的。我们要学习的是与自己想要实现的功能相关的游戏逻辑,知道如何进行修改或者添加,然后再掌握相应的MOD API来进行修改。从想法萌发到完全实现并发布,中间的距离有多远?你可以根据这个流程图来大致估计一下。在这里面,知识量最大,也最为重要的部分,就是游戏逻辑。如果你熟悉游戏,并且充满想象力,那么就很容易将你想要实现的功能与游戏里的某些功能联系起来。进而可以通过阅读相关的源码来获得更多的细节信息。
预备知识
Don'tStarve MOD是用Lua语言编写的,很显然,在正式开始写MOD之前,你需要了解一些基本的Lua语法,否则,看不懂代码,写就无从谈起。但编写入门级的Don'tStarve Mod,只需要了解编程语言通用的基础知识就足够了(数据类型,变量,循环,流程控制,函数,运算符,数组,表)。菜鸟驿站的教程能够帮助你短时间内快速入门。在这篇文章中我并不打算讲解代码的基本含义。能够自行阅读源码,是一个打算做Mod编程的人所必需掌握的能力。源码的阅读能力取决于你的编程基础,以及你积累的Don'tStarve API知识的多少。
基本原理
运行
游戏利用Lua语言的特性,为每一个启动了的MOD提供一个独立的运行环境,不仅独立于其它MOD,也独立于游戏本身的运行环境,这意味着,游戏系统以及不同的MOD下的同名的(MOD语义下的)全局变量,不会对彼此形成干扰。游戏的制作者们为MOD制作提供了大量的API,利用这些API,MOD制作者们可以修改游戏本身的内容,获取其它MOD的信息甚至修改它们。也可以向游戏添加各种新的内容。
API
接触编程不久的人可能会疑惑什么是API。在Don'tStarve MOD的语境下,API主要是指官方提供的一系列预定义了访问规则的公共函数。我们只要了解这些函数的访问规则(传入什么参数,获得什么样的效果,返回值是什么),就可以很轻松地使用它,而不必关心这个函数时如何被实现的。
Don'tStarve MOD中的API分为两种类型。一种类型是写在modutil.lua文件下的API,这种API必须在mod提供的运行环境下运行,不妨称之为MOD API。它们提供了一系列接入游戏环境的接口。但本身并不具备修改功能。你只是通过这个接口,获取了游戏系统的某些内容。对这些内容的修改,会由MOD API保存下来。除了修改,你还可以增加或删除内容,但本质上并无区别:增加或删除内容,在一个更大的层次上也可以看作对更大范围的内容的修改。
第二种类型则是游戏系统的API,一般是指component或widget,以及一些公有的工具函数。这类API是游戏系统逻辑的组成部分。你不仅可以使用它们,甚至还可以通过MOD API来修改它们。饥荒的游戏逻辑并不完美,直到现在,仍然有很多的Bug。而你如果熟悉游戏系统逻辑,就可以通过MOD API来修补这些Bug。在熟悉游戏逻辑的情况下,你甚至可以通过MOD API提供的接口来重构游戏系统逻辑。
结构
了解游戏的代码组织结构,才能更有效率地学习游戏逻辑。
Don'tStarve 这个游戏世界,是由一个一个prefab类的对象组成的。我们大部分的修改,都是在prefab的基础上进行的。Prefab本身只具备一些基本的功能,要想实现更多的功能,需要给Prefab添加各种不同的component。简单地说,prefab描述了一个东西是什么,而component则描述了这个东西能干什么。一个prefab搭载多个不同的component,就足以完成Don'tStarve 里最基本的东西了。
我们可以从这个基本的prefab-components的构成结构引出一种非常实用的MOD编写技术——迁移法。这种方式是每个有志于制作出高级Don'tStarve MOD的人都必须掌握的技术,这是了解游戏逻辑的基础。
迁移法:当我们想要在某个物品上添加一项功能而不知道该怎么实现的时候,可以思考一下这个功能是否在游戏中已经存在或者有类似的存在。如果有的话,再看一下是哪个物品拥有这项功能,然后阅读这个物品的源码,看看它由哪些component构成,找出想要实现的功能对应的component,再仿照相关代码来写,就能得到想要的结果了。
在这个最基本的东西上,如果一个Prefab的动画、声音很多,为了方便管理不同状态下的不同动画、声音表现,就需要添加StateGraph。如果是一个需要表现出一定智能的生物,就需要添加Brain。另外,如果要表达某些特征,就需要使用Tag系统。还有事件监听,环境检测等等系统,都需要依附于Prefab来进行。而人物的操作很多时候需要借助交互面板,比如说物品栏,装备栏,制作栏等等,这就涉及到了widget了,如果widget更大一些,比如占到了全屏幕或者半个屏幕,而且玩家在使用这个交互的时候处于长时间闲置的状态,那就可以转为screen。
下面来展开详细讲讲各个部分。
Component(组件)
为什么要先从component(组件)开始讲呢?因为这更符合我们的游戏认知。对于游戏,我们在描述一个物品的时候,通常都是在描述它的属性以及和属性密切相关的功能。我们要做一个物品或者修改一个物品,也主要是从功能入手。在Don'tStarve的系统中,这被抽象成了component,不同的属性用不同的component来实现。
一个物品可能有很多种属性,就可以用不同的component来描述。比如说人物,拥有三项基本属性-饥饿,精神和健康,这在Don't Starve里被分别抽象成三个组件hunger,sanity和health。
Component在本质上只是一个类,它储存一些变量,提供一些方法对这些变量进行操作。官方制作者们还提供了一些约定的函数供我们重写使用,如OnSave函数,可以用于在游戏退出时保存组件中的变量,OnLoad函数,可以读取OnSave保存的结果,OnUpdate函数,则可以在执行了StartUpdatingComponent后持续被执行,从而实现高频更新。
总而言之,component可以理解为物体的某一个功能或某一类功能。
Prefab(预设物)
正如现实生活中要描述一个功能必须要借助一个物体,在游戏中,组件也必须要依附于一个物体才能发挥功能。在游戏里,这样的物体被抽象成一个类,Prefab。Prefab是源自Unity3D的术语,翻译为预设物。Prefab的出现,使得我们免去了大量编写类的烦恼。我们不再需要为每一把武器,每一个工具单独写一个类,只需要把他们统统归为Prefab,然后在各自的构造函数中设置不同的组件来让它们拥有不同的功能就行了。比如说武器拥有weapon组件,表明了自己是一个武器,能战斗,为人物提供额外的攻击力;再比如说斧头拥有tool组件,表明自己是一个工具,能砍树。
弄明白Prefab和Component已经足够做出简单的MOD了。下面的内容较深,需要有一定的MOD经验才能明白,初入门的读者可以先按简单教程做出一些简单的东西之后再来深入研究。
StateGraph(状态图)
这个概念并不是那么好理解,因为它源自于计算机理论中的Graph(图)。
让我们从wilson开始谈起。wilson在进入游戏,经过开场动画之后,会站着一动不动。实际上这时候他的动画控制器(AnimState)在播放名为"idle"的动画。过了几秒之后,根据周围环境以及自身状态的不同,会播放以下动画之一:发抖动画(环境气温较低),擦汗动画(气温较高),捂肚子动画(饥饿度较低),捂头动画(精神较低),踢石头动画(不满足前面的特殊条件)。如果你让wilson去检查某件物品,他就会说话,
播放人物的声音,以及说话的动画。
我们很容易在上面这段场景中,将人物分为三种不同的状态,第一种是刚进入游戏时的,一动不动的闲置状态。第二种是闲置(idle)状态下不进行操作,几秒之后自动进入的趣味闲置(funny_idle)状态,第三种是检查物品时进入的说话状态(talk)。
游戏制作者把状态抽象成了一个类-State,这个类定义在stategraph.lua文件下。可以设置多项属性,让你能够描述一个状态的全过程,并且,可以借助EventHandler来完成状态之间的连结。一个个State,就构成了StateGraph的基础,在图论里,State可以被视为Node(结点)。idle,是游戏开始时人物所处的状态,也就是初始状态结点,每一次进入StateGraph,都是从idle开始的。官方设定的StateGraph,大多数状态结点在执行完一整套流程后,也会回归idle结点。有了结点,自然还需要有连接结点的方式,这在图论里叫做Edge(边)。所为连接,就是定义如何由一个结点跳转到另一个结点。这是通过StateGraph提供的名为GoToState的函数实现的,你可以使用这个函数来跳转到任意一个state上。下一个问题是,要在什么时候进行跳转。这一般可以通过ActionHandler或者EventHandler来设置。ActionHandler通过action来触发处理函数,比如说你吃东西,触发了进食动作,就会跳转到进食状态。EventHandler通过事件来触发处理函数。最常用的就是"animover"或"animqueueover",前者表示当前动画结束,后者表示当前动画序列结束。
总的来说,StateGraph就是一个典型的Graph,结点是State,边是GoToState。要了解更多更详细的信息,可以参考stategraph.lua文件下定义的StateGraph类,实例可以在stategraphs文件夹下随意找一个SGxxx打开,比较适合学习的是SGwilson。
Brain(AI)
这个概念比StateGraph更难理解。它的主要作用是为生物设置AI。Brain是以行为树(BehaviourTree)的方式实现的,不了解什么是行为树的话,你就很难去阅读Brain的代码,更别说去写自己的AI了。行为树是很复杂的,我在这方面积累的经验也远远不够,没有办法通过三言两语讲明白,以后有时间再写一篇文章专门进行讲解。
Widget和Screen
这两个概念的分界并不是十分明显,连官方都会把他们混淆在一起(某些Screen子类放在了widgets文件夹下),所以我就一起说了。这两个概念都是用于游戏界面编写的,接触过图形界面编程的读者,对这一方面应该比较容易理解。一般来说,小部件比如一个按钮,一个对话框,可以当作是一个widget,多个widget也可以组合成一个大的widget。而如果占用的屏幕范围比较大,甚至引起游戏暂停(如切换显示地图),就可以归为Screen了。由于官方没有给出任何规范,因此你完全可以随意地进行使用。widgets文件夹下有各种widget类,他们都有共同的祖先类-Widget,screens文件夹下的各种screen类则是继承自Screen类。游戏里已经提供了足够丰富的基本类,比如Button,Text等。你可以在这些基本类上进行继承扩展,也可以使用游戏原本就提供的扩展类,甚至可以使用它们提供的模板类(在widgets/templates.lua下)。
游戏界面的编写也是值得再开一篇文章讲的,这里不再赘述。