游戏编程模式之字节码模式
通过将行为编码成虚拟机指令,而使其具备数据的灵活性。
(摘自《游戏编程模式》)
游戏是一个庞大的工程,因此,在开发的过程中我们会选用高稳定性和高效率的重型语言,例如C++。C++的每一次代码更新都需要编译,然而庞大的代码量使得编译时间变得很长。因此,我们需要想办法将可自定义性高、变动可能性高的部分从代码主干中剥离出来(而游戏代码中恰好有很多部分符合这个要求),这样的代码极大的减少每一次更改后需要重新编译的次数。这就是字节码模式最根本的目的。
至于其实现原理,很简单,将可变动内容(数值、执行指令、简单逻辑)从游戏核心代码转移到独立的文件中,游戏主干需要实现从这些文件中读取、判断、执行的功能即可。
解释器模式
GoF的解释器模式其实就是实现字节码模式的一种途径。下面我们将插叙,简单介绍一下GoF的解释器模式。程序读取到一个字符串,并将字符串转化为语法树,解释器需要根据语法树准确的实现执行。那么如何执行语法树呢?以一个简单的运算 (1+2)×(3-4) 为例,解析后的抽象语法树如下图所示:
"(1+2)×(3-4)"这一字符串解析的元素将存储在一个前序遍历的树中。从元素来看,将被分为值表达式和运算符表达式两种。下面则是示例代码:
//表达式基类
class Expression
{
public:
virtual ~Expression(){}
virtual double evaluate()=0;
}
//数值表达式
class NumberExpression : public Expression
{
public:
NumberExpression(double _value) : value(_value){}
virtual double evaluate(){return value;}
private:
double value;
}
//加法表达式
class AdditionExpression : Expression
{
public:
AdditionExpression(Expression* _left,Expression* _right) : left(_left),right(_right){}
virtual double evaluate()
{
double _left=left->evaluate();
double _right=right->evaluate();
return _left+_right;
}
private:
Expression* left;
Expression* right;
}
我们来分析以下解释器模式的缺点:
- 每一个表达式都意味着一个实例,除此之外,对于运算符表达式来说,还要维护两个数值表达式的指针。这样的编写方式占用了很大的内存来处理一个简答的表达式运算。
- 基于虚函数实现,维护虚函数表对于这一个简单的运算来说也是大材小用。
- 表达式语法树的遍历也消耗大量的数据缓存。
总的来说,就是用了复杂的方式来实现了一个不起眼的小功能,性价比低太低。
字节码模式
我们都知道,编译式语言需要在运行前将代码编译成机器码,机器码的优点如下。这些特点使得机器码执行效率极高。
-
高密度。字节码是连续的二进制数据块,不会浪费任何一个字节。
-
线性执行程度高。除了控制流跳转,其他的指令都是顺序执行的。
-
底层。其执行指令是不可分割的,最简单的一个执行单元。
然而,我们将指令、数据分割开的部分不可能用人工去编写机器码。因此,我们可以自己定义虚拟的机器码,并自行完成执行步骤。事实上,这就是自建一个简单的虚拟机(开发游戏引擎的脚本系统就是运用了字节模式)。
使用字节码模式编写游戏中独立于核心代码的部分是一个大工程。因此,这里不可能给出一个比较完整的运用示例,不过我们可以见微知著,从一个最简单的案例来体会其字节码模式。
示例
-
游戏核心部分:
//示例类 class Character { public: void SetAttack(unsigned int type); void SetDefend(unsigned int type); void SetWalk(); void AddHealth(double addition); } enum Ops { OPS_SET_ATTACK =0x00, OPS_SET_DEFEND =0x01, OPS_SET_WALK =0x02, OPS_ADD_HEALTH =0x03 } //自建虚拟机 class VM { public: vm() : stackSize(0) {} void interpret(char bytecode[],int size) { for(int i=0;i<size;i++) { char instruction=bytecode[i]; switch(instruction) { case Ops.OPS_SET_ATTACK: int type=(int)pop(); SetAttack(type); break; case Ops.OPS_SET_DEFEND: int type=(int)pop(); SetDefend(type); break; case Ops.OPS_SET_WALK: SetWalk(); break; case Ops.OPS_ADD_HEALTH: double val=(double)pop(); AddHealth(val); break; } } } void push(int value) { assert(stackSize<MAX_STACK); stack[stackSize++]=value; } void pop() { assert(stackSize>0); return stack[--stackSize]; } private: static const int MAX_STACK=128; int stackSize; int stack[MAX_STACK]; }