可扩展文字游戏引擎的基本抽象
本文是基于《空梦》引擎及CPyMO引擎的开发经验对文字游戏引擎抽象做出的总结。
本文采用如下符号:
- A → B 给定参数A返回结果B的函数
- (A, B, C) → D 给定参数A, B, C返回结果D的函数
- a: A → B 给定参数A返回结果B的函数,名称为a
- { A: a, B: b, … } 一个结构体,其中A字段为a,B字段为b,…表示还有其他字段被省略
- null 空值
一、脚本的基本抽象
在只考虑文字及演出本身的情况下,游戏脚本引擎可以看作是一个状态机。其维护了一组状态 State,State包含了除 “当前执行的位置”以外一切关于游戏的状态(如当前文本、背景图片、立绘等)。游戏脚本需要控制游戏引擎改变这些状态以进行演出,游戏脚本控制这些状态的变化。为了化简模型,这里并不会考虑任何跳转(代码只能从上往下顺序执行)和动画(命令之间不会停顿)。
游戏脚本中每一个命令都是一个 (Args, State) → State 函数,其中Args是该命令所需的参数,比如类似“更改背景 a.jpg”的命令,相当于:
更改背景: (“a.jpg”, { 背景图: null, … }) → { 背景图: “a.jpg” }
对于每个游戏场景,都有一个相同的初始状态(比如{ 背景: null, 背景音乐: null, … } )。
可以在给定初始状态和代码后得出任意一点的状态,只需要将代码在编辑器中从开始到目标状态执行一遍即可,也可以缓存某一点状态以加速后续编辑时的状态计算。得出状态后可以根据状态渲染出游戏的预览画面,从而实现可实时预览的游戏代码编辑,以及“从任意点开始的调试”功能。
二、停顿与动画
有些时候需要让代码暂时停下等待一些操作,比如执行一段动画,或者等待玩家操作,因此我们需要一个等待器,可以监测何时某件事完成:
等待器: State → Bool
假定它返回False时,处在等待状态,脚本引擎不应当执行后续代码,而返回True时等待结束,开始执行下一条命令,并且不再调用此等待器。
由此,上述游戏脚本的命令可以扩展成以下这个样子:
游戏命令: (Args, State) → (State, 等待器: State → Bool)
如果此脚本不需要等待,只需要让它永远返回True即可。
例如,我们给“更改背景”命令添加渐变动画,相当于:
更改背景: ({ 图片: “a.jpg”, 渐变动画: “1000毫秒的淡入淡出” }, { 背景图: null }) → ({ 背景图: “a.jpg” }, 等待渐变动画结束: State → Bool )
基于工程上的经验,建议所有启动动画的命令都不会产生等待,而是设置专门的等待命令等待某些动画的结束,以此来同时执行多个动画。
三、分支与跳转
为了使得“实时可视化编辑器”得以实现,必须保证被编辑的代码满足上述性质,但是满足上述性质将会使得游戏失去分支和跳转功能。
我们可以将上述满足性质的代码封装到一个“场景”中,并允许场景末尾的代码使用分支和跳转命令,这样既可以实现在场景内部对代码的实时预览,也可以在全局上实现分支和跳转的功能,同时还能简单地分析出各个场景之间的流程图。
对于场景之间跳转时状态机的情况,有两种处理方式,可以选择直接清空为统一的初始状态,或者尝试分析出其流程图后,选择一个上游场景作为初始状态。
四、图形系统
对于文字游戏的图形系统,所有的2D图形绘制操作可以抽象为一个图层 Layer,图层以画家算法从后往前绘制,将会取游戏状态State和上一个图层的绘制结果作为输入,产生一个新的输出:
图层: (State, Framebuffer) → Framebuffer
在实际工程上,为了性能考虑,可以将图层分为“后处理图层”和“普通图层”,后处理图层将会读取之前的Framebuffer并产生一个新的结果,而普通图层仅仅会对Framebuffer进行写入,尽管有一个统一的抽象,但应当使用两种不同的实现以使得各自都可以达到自己应该达到的性能。
这里有一个简单的优化方法是:两种图层使用相同的抽象类进行抽象,但是允许使用某种方式区分两种图层,每次添加或删除图层时,都需要先从前(最贴近玩家一侧)往后(最远离玩家一侧)搜索第一个后处理图层,如果有一个后处理图层,那么就建立一个离屏Framebuffer,如果有两个或更多后处理图层,就建立两个离屏Framebuffer,如果没有后处理图层则不建立。在绘制图层时,如果有后处理图层则先设置当前输出Framebuffer为一个可用离屏Framebuffer,否则设置为屏幕Framebuffer。对于普通图层,直接绘制到当前输出Framebuffer,对于上述找到的“最接近玩家”的后处理图层,则将当前输出Framebuffer设置为屏幕Framebuffer,否则设置为另一个离屏Framebuffer。
另外Framebuffer不仅仅包含颜色缓存,还可以包含速度缓存、深度缓存、模板缓存等,以实现更复杂的效果。
五、舞台系统
为了总结以上所有的内容,我们需要“舞台系统”来表示对文字游戏引擎的最终抽象。我们可以把游戏看作是一个“舞台”,而背景、立绘、对话框、文本、选项、按钮等均可看作在舞台上的演员,这些演员将会按照脚本的内容进行演出。
每个演员都拥有以下内容:
- 脚本命令集
- 自身状态State
- 一个或多个图层
- 一个更新函数(State, DeltaTime) → State
在游戏运行时,演员脚本命令集中的命令将会被传送到对应的演员上,之后演员根据自身状态做出反应。
另外,对于可视化编辑器,可能还需要具有以下内容:
- 状态计算器(从命令流快速计算出当前状态)(Command, Args, State) → State
- 状态生成器(从当前状态生成命令流)State → (Command, Args) List
在使用可视化编辑器时,如果需要跟踪状态,只需要从状态开始处将命令向演员的状态计算器派发一遍即可得到当前状态,之后直接渲染此状态下的图层即可。
当需要从任意一点开始执行游戏时,只需要将当前状态送入状态生成器后产生命令流,将其送入游戏运行时即可使游戏立刻得到编辑器中的状态。