通用游戏设计
总的来说,我们可以将游戏拆解为核心状态机与动画控制两部分,其中核心状态机负责游戏的逻辑控制,动画控制负责游戏的显示控制。
FPGA 上的游戏开发最大的困难即在于如何实现复杂的动画效果,例如,切换背景、人物移动等等。由于硬件处理显示等功能不够灵活,许多使用 Unity 等游戏引擎可以轻松完成的功能,在这里会变得异常复杂。
为了降低大家的设计难度,更轻松地设计出有趣的游戏,我们在这里介绍一套比较完整通用的 FPGA 游戏设计模式,供大家参考。
Tips
本文档中的游戏设计模式,由助教受 Unity 启发自主设计,主要针对动画控制,覆盖了一般游戏开发所需要的大部分动画功能,部分内容可能仍有所缺漏,仅供参考,大家可以根据需要自行修改调整。
我们会在介绍中穿插与 Unity 的对应,加深对该设计模式的理解。没有接触过 Unity 的同学可以忽略,不会对理解造成影响。
3.3.1 核心设计思想
我们通过画布与对象等设计来分离后端的动画控制与前端的硬件显示;并利用 coordinate 和 sign 等对象性质,有层次地,从存储有图片信息的 ROM 中选择合适的信息,写到画布上,实现多功能的动画控制。
具体来说,有以下几个核心的设计思想:
3.3.1.1 画布渲染
在前面的小节里,我们已经设计了 vga 的显示模块,在这里,我们将其作为一个独立工作的黑盒子使用,各像素点的 rgb 信息都直接读取自一个 BRAM(可由 IP 核生成),我们称之为「画布」。在游戏进行的每一「帧」,都对画布进行「刷新」,即对整个 BRAM 进行一次刷写操作。帧率可使用参数自主设定。(注意,这里的帧率是指画布的实际更新频率,与 vga 显示模块的刷新频率独立)
本质
这一设计本质上分离了实际的 vga 显示模块 (DST、DDP 等) 与动画控制,使我们在设计过程中只需要考虑对画布的修改,而不需要与复杂的 vga 信号直接打交道。
对应到 Unity,即,Scene(画布)与 camera(摄像机):vga 显示模块作为摄像机对画布进行实际的显示。你甚至可以基于此扩展摄像机的跟踪与抖动等更复杂的效果。
3.3.1.2 面向对象
将各个需要显示的动画单元,如背景、任务、UI 等,拆分为若干对象。每个对象有自己的坐标、图像、大小、显示优先级等属性,由状态机控制跳动、打击等动画播放。图像资源存储于 ROM 中,画布在每一帧依据各对象的属性自动更新;控制模块只需要传递控制信号给对象,对对象的属性进行修改,就可以实现对动画的控制。
本质
这一设计本质上分离了画布与各个游戏对象,使得可以分别对各个对象的属性进行控制。
对应到 Unity,即画布与 gameobject(游戏对象)。
3.3.2 具体功能实现
3.3.2.1 动画切换
前面提到,对象的图像信息可以通过读取 ROM 来实现。因此,为了实现动画切换,我们可以使用一些标志位来指示画布读取不同的 ROM,从而读取到不同的图像信息,完成动画的切换。
用一个 ROM 存储多个动画
在 FPGA 中,ROM 由成块的存储单元组成,如果每个图片都用单独的 ROM 存储,可能会造成资源的浪费。你可以考虑将一个对象的不同动画的图像信息存储在同一个 ROM 中,用从不同位置开始读取的方式区分各个图片。
此外,在画布刷写时对每个对象都进行图像的选择,会导致代码冗杂,延迟也会相应增大。你可以考虑进一步分层,为每个对象维护一个 BRAM,将要显示的图像信息存储在 BRAM 中,画布刷写时只需要读取对应对象的 BRAM 中的信息即可,而无需考虑对象的实际状态。
3.3.2.2 对象移动
对象的移动可以通过修改对象的坐标来实现,由于图片资源以方形形式存储,我们为每个对象维护上下左右边界,共四个坐标。
当画布进行刷写每个像素时,会参照对象的坐标信息,只可能将坐标范围包含该像素点的对象的 rgb 写入画布。
具体的移动方式及速度,由对象的状态机,通过修改坐标进行控制。
3.3.2.3 图层显示
实际的游戏中,不同的对象可能会有不同的显示优先级,例如,背景应该在最底层,人物应该在中间层,UI 应该在最上层。为了实现这一功能,我们可以为每个对象设置一个显示优先级,在画布刷写像素时,使用 if-else 语句,按照优先级从高到低,写入最先符合显示条件的对象的 rgb 信息。
动态图层显示
该方法只能实现静态的图层显示,如果需要实现动态的图层,一种可供参考的方案是:
- 进一步分层,将画布拆解为各个图层,每个图层的画布独立刷写,画布在每一帧直接从各图层的画布读取信息。
- 为每个对象维护一个可修改的图层属性,在每个图层渲染的过程中,对各对象判断其是否在该图层内,如果在,则将其 rgb 信息写入该图层的画布中。
3.3.2.4 透明显示
由于 FPGA 的存储空间较小,可支持的像素较少等原因,我们只能采用方形的图像资源,但显然许多对象都不是方形的,这自然地要求我们实现透明显示。此外,实际的游戏中,不同的对象可能会有不同的透明度。
因此,我们可以在用 ROM 存储图像信息时,为每个像素点增加一个 alpha 属性,用来表示该像素点的透明度。在画布刷写时,当且仅当该像素点在某对象的坐标范围内,且该对象在此坐标的 alpha 为 1,才对画布刷写对应的 rgb,否则继续考虑更低图层的对象。
alpha 属性的生成
vga 并不实际拥有 alpha 通道,所以不能实现任意的透明度,只能用一位 bit 表示是否透明。
alpha 属性的生成可以考虑修改之前提供的 python 脚本实现,当实际图片的 alpha 值小于某个值时,即认为该像素是透明的,在写入 coe 时设置 alpha 为 0。
3.3.2.5 碰撞反馈
碰撞反馈是大部分游戏都需要的功能,它为各对象之间的交互提供了可能。
我们这里的设计中,将碰撞反馈交给了画布来检测。这是因为,画布在刷写像素时能够自然地汇总各个对象的状态及坐标信息。当两个有碰撞可能的对象同时在某像素点上有效时,我们就可以认为发生了碰撞,并反馈碰撞信息给控制模块。
3.3.2.6 对象的动态生成
有些游戏可能需要在游戏进行过程中动态地生成一些对象,如敌人、子弹等。FPGA 要求一切资源都要在编译时确定,因此,我们不能真正地实现动态的创建,只能提前在代码中维护所有可能出现的对象及其有效位,然后在游戏进行过程中,根据需要,修改对象的有效位,当该位为 1 时,才将该对象的信息写入画布。
3.3.2.7 代码示例
下面是以上功能的部分代码示例,供大家参考。希望能加深大家对以上内容的理解。
代码示例
game.v | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
注意,以上仅为示例,并没有关注时序等细节。各位在设计时,请自行根据需要修改,尤其要注意各个信号的时序关系。