在读书的时候看到了这个东西,觉得很有趣,就记录下来。
为什么选择字节码
制作游戏是一项很有趣的工作,但同时也是非常复杂的,为了保证游戏的稳定和高效,使用c++是一个很好的选择。
我们会为此感到骄傲,但这也是有代价的。成为一个精通c++的程序员需要多年的专业训练,随后你又必须面对庞大的代码库。大型游戏的编译时间说短不过“喝杯咖啡”的时间,说长够你把“自己烘焙咖啡豆、磨咖啡豆、倒咖啡、打奶泡、练练拿铁的拉花”统统做一遍。——《游戏编程模式》
玩家需要新奇的体验,游戏也需要不断迭代,这会让我们不断添加新的道具、技能等等。如果每次修改都要修改c++代码并且编译,那么整个游戏创作的流程就无法顺畅的运行。并且,在游戏发布以后,我们仍然要进行更新,现在的游戏基本上都可以进行“热更新”,不去下载整个游戏而只是下载需要修改的部分,一般使用lua这样的脚本语言就可以实现“热更新”了,不过,如果你c++部分的核心逻辑需要修改,那就无能为力了,更新大包意味着你将失去一部分用户!
当然,你也可以用配置文件等方式增加游戏的多样性。不过,这种方式可以定义属性,却不能定义行为,使用字节码的方式更为灵活,功能更加强大。
“通过将行为编码成虚拟机指令,而使其具备数据的灵活性”。
字节码模式
字节码模式实际上是将一系列操作编码成字节序列,然后在虚拟机逐条执行这些指令,指令的组合可以完成很多高级的行为。这和我们使用编程语言感觉起来有点像,通过一系列简单的语法,我们可以完成很复杂的程序,这很cool,不是吗?
使用情景
字节码模式是一个很复杂的模式,并不是所有的游戏都适用。对于游戏来说,如果需要定义大量的行为,并且游戏语言是这样的才应该使用:
1.编程语言太底层了,编写起来繁琐易出错。
2.因编译时间太长或工具问题,导致迭代缓慢。
3.它的安全性太依赖编码者。你想确保定义的行为不会让程序崩溃,就得把它们从代码库转移至安全沙箱中。
如果游戏完全是脚本写的,那就不用在意这些细节了。不过在使用c++这样的语言时,还是可以根据游戏具体的内容来考虑使用的。
如何使用
想要使用字节码模式,首先我们要设计一个指令集,这个指令集可以让我们对游戏的行为进行一些基础操作。这个指令集的设计要根据游戏实际情况确定。
比如说游戏中操作的玩家有一个治疗的技能,那么我们就需要这样的操作:选取对象、修改血量,甚至可以播放声音特效等。
设计完你需要的指令以后,为了让这些指令编码成数据,我们在数组中存储一系列枚举值代表这些操作。实际上,基本操作的数量不会很多,所以枚举值取一个字节即可。最后我们将某个行为转化为一个字节序列(即字节码)。
enum Instruction
{
INST_SET_CHARACTER = 0x00, //选择角色
INST_SET_HEALTH = 0x01, //设置血量
INST_PUSH_VALUE = 0x02, //添加数据
INST_CAL_ADD = 0x03, //加法操作
INST_PLAY_SOUND = 0x04, //播放声音
}
这样,当我们得到一条指令的时候,就可以根据对应属性,执行正确的操作:
class VM {
public:
void interpret(char bytecode[], int size) {
for (int i = 0; i < size; ++i) {
char instruction = bytecode[i];
switch(instruction) {
case INST_SET_CHARACTER :
setCharacter(0);
break;
case INST_SET_HEALTH :
setHealth(100);
break;
//other case
}
}
}
};
到这里位置,我们的字节码已经初步完成。但是它还只能做一些简单的操作,只是将字节映射了对应的函数,它还不够灵活,我们要在执行指令的时候添加我们需要的数据才行,也就是要为函数添加参数。
我们想要更容易地获得数据,计算表达式并正确的传值,并且通过指令的顺序去控制,这就可以使用和CPU同样处理数据的方式——堆栈。
我们可以添加一条指令用来在堆栈中添加数据,并且在需要的时候从堆栈中获得数据。如下图所示。
字节码模式运行图这样,我们的字节码模式就可以运行起来了,通过添加对应的指令,我们可以创建一些表达式,让不同的值组合起来,比如说我们想要治疗的血量是个百分比的值而不是一个固定的值。那么我们就可以:
1.获取治疗对象
2.获取对象血量
3.获取百分比值
4.计算血量乘以百分比,得到需要增加的血量
5.执行加血指令
通过添加更多基础指令,并进行组合,我们可以实现许多多样化的操作。
其他问题
文本语言or图形界面
虽然经过上面的步骤,我们的字节码已经基本可用了,但它实在是不怎么好用,你不能指望使用它的人看着一堆指令列表来和对应的字节数字来工作,那么我们就需要为字节码提供一种可靠方便的使用形式。使用形式可以是一种文本语言,也可以做一个图形界面。
实际上,如果你的用户是策划,那么你别指望他们会喜欢使用文本语言,这种反复无常并且缺乏耐心的生物在你告诉他你创造了一个文本语言来定义游戏行为的时候,会表示:“oh,你这个东西确实很厉害,但是我看不懂,能不能把它弄得简单一点,最好有个界面能够操作!”。可怜的程序员还没有感受到完成新的模式带来的喜悦,就得继续开始拼UI了。
其实,添加一个图形界面比想象中的要好一些,因为使用文本语言,你很难保证你的用户能够正确书写语法,并且在遇到各种错误的时候能够自己修复。做一些简单的UI,来帮助用户翻译,可以保证他们很难创造非法的程序,使得创建出来的东西总是合法的,这也可以减少自己的麻烦,提高效率。
应有的指令类型
- 外部基本操作。即游戏相关的一些api。
- 内部基本操作。操作虚拟机内部的值,如添加数值、算术运算符等。
- 控制流。相当于跳转指令,可以实现循环等操作。
- 抽象化。为了重用定义内容,可以通过构造一个类似函数的形式。比如,遇到“call”指令,就将当前指令入栈,跳转到被调用字节码。遇到“return”指令,就返回,从栈中获取索引继续执行。
值的表现形式
如果你需要的虚拟机只需要支持一种类型,那么很简单,和上面一样用就好了。但是如果想要支持不同的数据类型,你就要决定如何保存这些值。
一种方法是,在数据前面再添加一个标签,这样,每次要读数据的时候,先去查看标签,再决定读取数据的长度。这种方式的好处是:有自身类型信息,可以在调用前进行类型检查。当然,缺点就是会占用更多内存。
还有一种方式是,不为数据添加额外类型标签,在使用的时候,根据需要获取对应的值,让使用的地方保证能得到正确的解析。这样做的好处是数据紧凑,速度快。缺点是不安全,如果解析出了问题,很有可能就导致游戏崩溃。
网友评论