实现一个脚本引擎

Part IX:高级主题

序言
gl_line.jpg (1418 bytes)

现在你已经玩了一下那个完成的脚本例子,也许你实现了一些新特性,或许当我们将要接触新东西时你在疑惑.

请允许我提醒您,这些好东西里的绝大部分都需要大量的工作(这些我将不再提供例子代码).我将讨论几个高级的脚本主题,给出如何实现(我的想法)的一般想法.

第一个:

A lockup-resistent VM(不会翻,晕倒....)
bl_line.jpg (1610 bytes)

前一段时间Joseph Hall给了我一个处理无限循环(infinite-looping)的脚本代码的很好的想法.他的思想是:每次调用虚拟机时给他最大数量的操作码去执行,并且如果下一帧它还没有完成时让它继续执行;这是虚拟的等价与CPU优先级多任务.这种方法使你的游戏引擎在脚本代码挂起时可以保持运行;它可以自动检测脚本是一个不变的循环并且重起VM.

现在,让我们看看我们可以怎么样扩展我们的语言:

函数
bl_line.jpg (1610 bytes)

在你的脚本语言中增加函数是非常困难的,它引入了参数和局部变量的概念.为了他们需要使用堆栈.在一个函数调用前程序把参数入栈.然后函数在同一堆栈中预留空间给它的局部变量.然后执行函数,使用预留的堆栈空间来读写值.在我们的简单的编译器中,我们仅仅从栈顶进栈和退栈,但是现在你也可以访问堆栈中间的内存地址.

你需要为函数使用两个特殊的操作码:CALL和RETURN.CALL是一个无条件的跳转,它吧指令指针保存到堆栈中.RETURN读取那个被存储的指令指针,然后跳回CALL后面的指令.

要做的一件最符合逻辑的事是让调用者(不是该函数)把参数从堆栈中移走参数;毕竟最初是调用者把他们放进来的.这也考虑到一个"输出参数(output parameters)"的简单机制:函数改变一个参数的然后调用者把这个值存入一个变量.一个函数的返回值也可以看作是一个输出参数.

函数的信息头可以存储到一个符号表中.使用他们,你可以存储它的参数和局部变量(可以每个是一个分离的符号表实体).在代码生成的过程中,你可以在符号表中存储函数的起始地址.

重载(Overloading)
函数的重载可以是一个语言中非常好的特性,但是实现它可能很棘手的.问题是如何通过提供的参数类型来正确的从可能的函数头信息中找到一个恰好匹配的函数来调用.在这种情况下,你将不得不强制某些参数到不同的类型来得到一个完全的匹配.问题是什么参数需要强制转换和把它们转换到什么类型.大多数编译器试着比较调用和可能的选择,然后选择一个需要最少强制转换的.一些编译器允许双重强制转换(例如:bool->int,然后int->unsigned),这使麻烦更复杂,我建议保持简单.

操作符可能看作是一个用不同语法调用的函数;如果用这种方法来处理你的操作符(不要真把它们作成函数(慢),而是inline函数或者宏),你可以轻松的扩展函数重载到操作符重载.


gl_line.jpg (1418 bytes)

如果你想要在你的语言中实现类,正确的决定你想要支持那些特性.支持完整的C++类,包括多继承,访问控制,动态束定,虚函数,等等是非常困难的,我建议不要在一开始就处理所有这些.一个带有单继承的简单的类系统是一个很好的起点,如果需要的话你以后可以扩展它.

类和结构体是符合数据类型:他们包含多个数据成员,并且连接到一定数量的方法或者成员函数.你可以在你的符号表中存储一个成员列表,它与其他分离的成员符号表实体相连接.这可以使你简单的找到结构中某个成员的偏移量.

继承
单继承相对的简单:当你在一个对象中查找一个成员时,检查这个成员是否在子类中;如果不是就检查它的父类.子类的存储布局很简单:首先你存储父类,然后是他的子类,其他子类,等等.这样向下的束定被隐藏:你可以处理向处理一个Animal的指针一样处理一个Cat的指针,这个的意思是你的程序可以访问更少的成员,但是指针的地址不需要改变.

多继承,当调用一个成员函数或者访问一个数据成员时,它带来了二义性问题.思考这个:两个类B和C是统一个类--类A的子类.然后建立一个类D源于类B和类C这两个类.现在,如果类A有一个公有成员函数DoSomething,当成员在一个D类型的对象中调用DoSomething时,你不能知道调用两个DoSomething中的哪个:一个是B的A部分,另一个是C的A部分..好吧,也许看图可以更清楚.

mult_inh.jpg (20710 bytes)

虚函数
虚函数是建立多态的一个方法;例如一个Animal类包含一个虚函数MakeSound(),一个子类Cat和Dog都各自用不同的方法实现一个这个函数(我想让你考虑如何正确的实现他们).于是当你调用一个Animal对象的MakeSound函数时,你不知道(并且不需要知道)是那种动物在发出声音.

虚函数函数使用一个所谓的vtable来实现.当父类声明一个函数为virtual时,它在那个类中增加了vtalbe.每个子类现在取得他们自己版本的vtable,这样,不同的函数调用基于那个对象实际的类型,尽管在调用者看来这些table之间并没有区别.

动态束定
动态束定可以很便利:例如,在UnrealScript中你不仅仅可以向下束定一个对象(把它束定到它的父类型),而且可以向上束定(束定一个对象到它的子类),如果这个对象的确是子类的对象.这意味着你需要一个方法来决定一个Parent类型的对象实际上是向下束定的一个Child1对象(在这种情况它可以被向上束定),或者是一个Child2对象(在这种情况它不可以被向上束定).在最新的C++编译器中你可以使用dynamic_cast<...>操作符.怎么觉得这个呢?每个对象都将必须有一个独一无二的号码,也许是一个类的表和他们的父类的索引.使用这个号码,你可以断定它到底是那种对象.

类型变量(Type variables)
类型变量允许类型的变量.这允许你动态建立一个变量类型的对象.举个例子,你有一个游戏,一个敌人走了进来,两个同样的敌人走了出去.你可能会看到一个包含所有可能的敌人的巨大的switch语句,但是这不是很好扩展.所以你可以存储敌人的类型,告诉游戏使用这个类型建立一个怪物.这是一些假想的语言代码:

TypeVar<Enemy> enemytype; // A type variable
enemytype = typeof (monster); // Get the monster's type
Enemy *newmonster = new enemytype; // Create a new monster of the same type

你可以传递类型变量到一个函数;这将使得他们很有可塑性,你可以使用同一个函数来建立和处理很多不同类型的对象.

为了类型变量,你需要扩充类和他们的父类的表来包含每个类型的大小;否则你将没法动态建立他们.

Game-specific language constructs
bl_line.jpg (1610 bytes)

UnrealScript(据我所知)是第一个提出了两个在游戏中非常有用的特性的语言:状态和隐藏代码.

状态
UnrealScript中的类可以有几种状态;一个对象总是在一个确定的状态.基于对象处在那个状态,为这个对象执行不同的函数.所以如果这个对象是一个敌人并且它处在Angry的状态,Angry版本的SeePlayer函数将被执行,这个敌人将可是攻击玩家.如果这个敌人处在一个Frightened的状态,另一个SeePlayer函数(使用同样的参数类型)将被调用,使得敌人逃跑.

状态并不是非常难加入,尽管它的确需要一些工作;状态是一个额外的类成员(不可见),并且每当调用特定的状态函数时恰当的函数版本将被执行.这可以使用一个使用状态号码为索引的跳转表来轻松实现.

状态可以有它们自己的函数外的代码,在UnrealScript中是状态代码.这可以方便的与下一个构思相结合:隐藏的函数.

隐藏的函数(latent code)
隐藏的函数相当的难实现,但是非常的酷:一个隐藏的函数花费一些游戏时间来执行;换句话说,这个过程可以起动一个函数等待或者激活那个等待或者激活一个人物,当这个动画完成后代码继续执行.这是一个AI脚本很好的特性.

隐藏代码带来的另一个问题是本质上它与其他代码并行执行.偶尔隐藏代码被执行,然后它又被停止.所以我们必须记住隐藏代码的指令指针.并且当对象改变它的状态时,你将也需要执行其他的隐藏代码.

我们可以看到UnrealScript唯一提供隐藏代码的原因是为了调用状态代码,而不是普通函数:假设隐藏函数可以在任何地方被调用,每个对象本质上可以有很多的并行执行的"线程"..这可能需要大量的记录并且将变慢.而且也将产生同步问题:一个对象的线程将把一个成员变量设为某个特定的值,然后一个其他的线程变为活动后再次修改它...如果你想允许它将需要实现一个完整的多线程系统.

That's it for now..
gl_line.jpg (1418 bytes)

我希望这可以激发你的想象力.有许多特性你的脚本语言可以实现;如果你想完成它你将限制自己为某一个.

这可能是这个系列教程的最后以部分.我乐于写它.如果你觉得在一些地方还不够,让我知道,也许我将写一个额外的部分.当然,如果你有其他的一些问题我也乐于听你说.

Good luck, and keep on scripting! ;-)

Quote!
gl_line.jpg (1418 bytes)

"He stared at it for some time as things began slowly to reassemble themselves in his mind. He wondered what he should do, but he only wondered it idly. Around him people were beginning to rush and shout a lot, but it was suddenly very clear to him that there was nothing to be done, not now or ever. Through the new strangeness of noise and light he could just make out the shape of Ford Prefect sitting back and laughing wildly.

A tremendous feeling of peace came over him. He knew that at last, for once and for ever, it was now all, finally, over."

HHG 5:25



返回目录
diamond Garden制作 2000年1月