知识屋:更实用的电脑技术知识网站
所在位置:首页 > 综合

游戏编程模式--字节码

发表时间:2022-03-25来源:网络

写在前面  

  ”通过将行为编码成虚拟机指令,而使其具备数据的灵活性。”

动机

  制作游戏很有趣,但制作游戏却不易,特别是现在的游戏。现代游戏随着硬件技术的发展,游戏内容和玩法变得越来越丰富多样,在以前可能代码就几千行,但现代游戏的代码往往能达到几十万甚至几百万行。这么大的代码量,如果我们选择了c++这样的重量级语言(对于性能的追求,往往会让我们选择c或c++),编译的时间就不得不考虑了。游戏一个独有的苛刻的要求:有趣。玩家需要既新奇又具有平衡性的体验。这就需要持续迭代,但假如我们每一次修改都要工程师改底层的代码,然后等待漫长的重新编译,那么整个游戏的创作流程就被毁了。比如我们当前流行的moba类游戏,对于每个英雄的技能效果,设计都需要反复的修改才能在整体上达到一个平衡,如果不能提供一种修改后快速反馈结果的方式,相信没有那个设计师能坚持下去。

  很明显,我们的编程语言不适合解决这个问题,我们需要另一种方式把这些经常需要修改的部分转移到安全沙箱中,同时让它们易于加载且在物理上于游戏的可执行文件分离。其实这些特性就优点像“数据”了,我们在运行的时候把它加载到内存中,然后按某种方式执行。也就是说我们使用“数据”来定义行为,然后执行这些数据。那有什么方法能让我们实现这种功能呢?其中一个是解释器模式,一个就是我们本节要讲的字节码模式。

  首先我们简单来了解一下解释器模式,这能对我们接下来要将的字节码模式有个更好的理解。

解释器模式

  解释器模式Gof早已著作成书,在这里,我们只做简单的了解。首先我们使用解释器来实现一个数学计算器,面对表达式:

  (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:public Expression { public: AdditionExpression(Expression* left,Expression* right):left_(left),right_(right) {} virtual double evaluate() { //Evaluate the operands. double left = left_->evaluate(); double right = right_->evaluate(); //add them. return left+right; } private: Expression* left_; Expression* right_; };

  只需要简单的几个类,就能够表达任何复杂的算术表达式了。我们要做的只是创建几个对象,然后把它们关联起来。

  解释器模式虽然很简单,很漂亮,但是也是有些问题:

  从磁盘加载它需要进行实例化并串联成堆的小对象;   这些对象和它们之间的指针占用了大量内存;   从每个指针遍历子表达式都会消耗大量的数据缓存,而虚函数调用也会对指令缓存造成很大的压力。

  简单来说,就是:慢。大部分广泛使用的编程语言没有基于解释器模式也正因于此。它太慢了,且占用了大量的内存。

虚拟机器码

  对比一下计算机的机器码,它不会生产小的对象,也不会有各种对象间的关联,它的特点是:

  高密度。它是坚实连续的二进制数据块。不会再内存中跳跃访问;   线性。指令被打包在一起顺序执行,不会再内存中跳跃访问(当然,除非你确实写了控制流);   底层。每个单独的指令仅仅完成一个小动作,各种有趣的行为都是这些小动作的组合;   迅速。以上几点让机器码疾行如风。

  当然,我们不会直接使用机器码来构建游戏。但我们确实希望能获得机器码的效率,这个时候我们就想到了搞一个折中的方案——虚拟字节码。即我们自己定义机器码,然后实现一个执行这些机器码的模拟器。机器码和模拟器完全受游戏本身的安全管理,这样就达到了灵活由高效的目标。我们把这个小型的模拟器称为虚拟机(VM),自定义的机器码称为字节码。看着挺吓人的,但如果你的功能清单不是太复杂的话,这个方案将非常可行。

字节码模式

  指令集定义了一套可以执行的底层操作。一戏列的指令被编码称为字节序列。虚拟机逐条执行指令栈上这些指令。通过组合指令,即可完成很多高级的行为。

使用情境

  字节码模式是最复杂的一个模式,它不是简单能放进你的游戏里的,当你的游戏中需要定义大量的行为,而且实现游戏的语言出现下列情况时才应该使用。

  编程语言太底层了,编写起来繁琐易错;   因编译是按太长或工具问题,导致迭代慢;   它的安全性太依赖编码者。你想确保定义的行为不会让程序崩溃,就得把它们从代码库转移到安全沙箱中。

  当然,这个列表符合大多数游戏的情况,谁不想提高迭代速度?谁不想让程序更安全?但那是有代价的,字节码比本地码要慢,所以它不适合对性能有极高要求的核心部分。

使用须知

  建立一个自己的语言是一件非常有吸引力的事,但再游戏中我们需要克制,除非你确实想做个成熟的语言。否则,就需要对你的字节码所能表达的事情范围做一个限制,不能无限扩张。当你使用字节码的时候,你需要注意:

  1)你需要一个界面

  字节码性能很高,但你没法让用户直接编写二进制码。我们将行为从代码中移出来,就是想再更高级的层面上表述它。所以你需要为用户提供一个界面,让用户在更高的层次上编辑,然后使用一个工具自动生成字节码,这个工具我们称之为编译器。所以,如果你没有足够的资源实现一个编译器,那么字节码不适合你。

  2)你会想念调试器的

  成熟的编程语言,都会有一条完整的工具链,比如:调试器、静态分析器、反编译工具等。这些工具能极大的提高我们编程的效率,但当你使用自己的字节码时,这些工具就没有了(或者你也可以同时把它们都做了),那这个时候,如果程序出现bug,那你就只能慢慢猜是那个环节出错了,或者单步进入虚拟机的代码调试,这是非常低效的。

示例

  现在我们已经对字节码有一个大概的了解了,现在我们来做一个简单的示例。

  假设我们的游戏中有各种法术,大部分的法术都会改变角色身上的某个状态,比如生命值,我们就从定义这些行为的API开始:

  void setHealth(int wizard,int amount);   void setWisdom(int wizard,int amount);   void setAgility(int wizard, int amount);

  当然,为了游戏的有趣性,我们还会假如一些其它的特效和音效,

  void playSound(int soundId);   void spawnParticles(int particleType);

  现在我们已经有了这些API了,现在让我们把它们编程一系列的指令并加入到我们的指令集中,一个指令对应一个枚举(因为我们的指令不多,枚举值长度取一个字节足矣,这就是所谓的字节码):

enum Instruction { INST_SET_HEALTH =0X00, INST_SET_WISDOM =0X01, INST_SET_AGILITY =0X02, INST_PLAY_SOUND =0X03, INST_SPAWN_PARTICLES =0X04 };

  现在我们已经有了指令了,但还不够,我们还需要引入指令操作的参数,这里我们使用一个堆栈来存储所需的参数,堆栈也是实现表达式求值的通用做法。我们把栈放在虚拟机中,同时实现入栈,出栈的操作(假设数值只有整形):

class VM { public: VM():stackSize_(0){} void interpret(char bytecode[],int size) { for(int i=0;i
收藏

热门推荐

  • 人气文章
  • 最新文章
  • 下载排行榜
  • 热门排行榜