代码改变世界

游戏编程模式之字节码模式

2021-10-25 16:34  ZhuSenlin  阅读(99)  评论(0编辑  收藏  举报

通过将行为编码成虚拟机指令,而使其具备数据的灵活性。
(摘自《游戏编程模式》)

  游戏是一个庞大的工程,因此,在开发的过程中我们会选用高稳定性和高效率的重型语言,例如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];
            
    }