小话游戏脚本(一)
( 题记:近来在网上学习到一个新的观点(应该是来自刘未鹏的BLOG :) ):书写是为了更好的学习,这与之前脑子里传道授业解惑的观点颇为迥异,品一品又颇以为然,事物不都是两面的吗,这只是看待角度的不同罢了,所以后来想想,确实应该将自己的一些学习经历或是思想记下,不仅有益于自身的提高,说不定以后还能帮助帮助他人,如此这般好事,何乐而不为?(看来我是在渐渐脱离火星了,想想以前我可是强烈无视网上论坛的...) )
一.一点已知的零星知识
谈到游戏脚本这个论题,就我目前所知大抵同编译原理是一个概念,有所不同的可能是编译原理大多不涉及虚拟机,而游戏脚本为了灵活性和安全性的考虑,虚拟机往往是必须考虑的范畴:)
对于编译原理,我也曾经选修过这门课程,记得当时老师一直自叹该门课程的枯燥性,以致一路的上课过程都那么无聊乏味,待到考试将至,老师似乎也是担心我们的学分大计(估计是怕我们拖累他的职称评比),所以临考前发了几套模拟题下来,甚至到了后来还帮我们进一步划清了范围,唉,良苦用心,天地可鉴啊!不过对于我来说实在没有什么惊喜,该怎么考还是怎么考,所以对于这次名义上的考试没有什么太多的印象,毕竟经历这类的事情太多了些...不过对于编译原理的课程设计,我到有些体会,待到有机会下面再叙,现在我必须停止我这番情不自禁的感慨牢骚,回到我们的游戏脚本(编译原理)。
首先讲讲我所知道的书籍,曾经我在图书馆淘过这类的书籍,也花费过一些时间google,所以在此推荐三本书籍,不过在下实在笨拙,这三本书都未曾精读过,但这并不妨碍我的推销,他们就是在编译界分别拥有龙书、虎书及鲸书之称的《编译原理》、《现代编译原理》和《高级编译器设计与实现》。感觉上龙书注重编译器前端,鲸书则更着眼于编译器后端,而虎书则是一本实践性质的书籍,个人感觉应该先以虎书的讲解一步步的实践深入,中间辅以龙书的理论支持,待到熟稔之境,观略鲸书又能助以更上一层楼:)(个人看法...)
一般来讲,编译分为以下几个步骤:
.词法分析
.语法分析
.语义分析
.中间代码生成
.中间代码优化
.目标代码生成
一般来讲,以中间代码为界,之前被称为编译器的前端,之后则被称为编译器的后端,理论上来讲,除了目标代码生成 ,其他各部分都不是必须的,但是将他们一部分一部分的分解开来,自有其深远用意,首先由 词法分析 将输入的源文件中各种形式的单词提取成统一规格的属性块,称为 Token,然后 语法分析 通过这些 Token 检查他们是否符合语法规则,并同时填充符号表以及建立语法分析树,然后由语义分析 进一步检查其是否符合相应的语义规则,一切安检通过之后,则开始生成中间代码,并在之后对生成的中间代码进行优化,最终由优化后的目标代码生成可执行的目标代码。(更加详细一些的信息可参见燕良很久以前翻译过的一篇文章)
对于一些简单的语言,语义分析以及 中间代码优化 往往会有所省略,而实际上,在语法分析的同时我们其实也可以直接生成目标代码,但这样做的后果是失去灵活性,试想如果将来我们因为跨平台的目标而变更目标代码的格式,只要我们保有中间代码的生成,便可以保持编译器前端不变,否则就...
另外对于虚拟机,一般用于运行那些自定义格式的脚本代码,目标自然是做到真正的平台无关,可惜需要付出运行速度的代价。(想想Java语言已经发展的十分成熟,但较之C/C++的运行速度,其仍然难以望其项背...)一般而言,虚拟机生命周期中需要完成装载、执行以及关闭的操作,考虑进一步的细节,装载时自然需要一个脚本装载器,执行时自然少不了运行时堆栈而关闭则定然需要一个资源管理器等等,好了,关于虚拟机的论题就此打住,在谈论下去恐怕要贻笑大方了,下面的一个示例虽说没有严格的实现一个虚拟机原型,但期中也有那么一些影子,希望大家能够参考指正:)
在此我想叙述的是一个十分简单的脚本系统,其甚至都不是面向过程的,而只能称其为基于命令(对于这个议题,以前有几篇文章)。而所谓基于命令,便是脚本系统仅仅支持你调用系统固定的API(命令),而不允许你进行其他类型的任何操作(例如表达式运算),可想而知这种系统的灵活性和实用性...但本着KISS原则以及自己学习实现的需要,在此就仅对于该话题展开一些讨论,其中的大多数内容都来自于《游戏脚本高级编程》,自己仅作转述而已,但感觉仍然乐在其中:)
1.基于命令脚本的基础知识
即使对于一些复杂的游戏,游戏中的许多功能也可以通过一系列顺序的动作进行完成,例如考虑以下的一段基于命令的脚本代码:
ShowBitmap "Image/Item0.bmp"
LoadMusic "Music/Cheer.mid"
FadeMusicIn
GetItem "Gold Of Apple"
GameMessage "Oh,Yeah!You Got The Gold Of Apple!"
这段代码十分简单,所要完成的功能也非常明了,于此我们便可以看出基于命令的脚本的一大优点:形式简单;同时也暴露了其的一大缺点:功能单一,同时我们也能大抵能够看出这类脚本命令的一般形式:
Command Param0 Param1 Param2 ...
一般来讲,以上的代码格式已经能够满足基于命令脚本的需求,但同时,如果要加上如 C/C++ 那般的括号、分号之类的语法格式自然也是可以的,这仅仅是解析上的问题:)
在者,基于命令的脚本与特定的领域高度相关,就如上面所示的代码可能很适用于RPG之类的游戏,但是对于飞行模拟或者体育竞技之流,恐怕是无能为力了...
至此,你可能对于基于命令的脚本语言嗤之以鼻,也可能对其开始宠爱有加,但是总结来看,事物终归是有两面性的,对于定义那些游戏引擎将要执行的具有固定顺序的事件,基于命令的脚本的确不错,因为他快捷而又方便,但是其他更加复杂的应用,如最终Boss的AI,他可能就力不从心了,所以对于其的选取与否,仍然还是一个适用范围问题,存在着权衡。
(基于命令的脚本有一个很好的范例,那就是很早以前金点的《圣剑英雄传2》)
2.基于命令脚本的高级知识
对于一门语言,无论他多么简单,首先仍然要解决他的数据及语法问题,首先让我们来谈谈数据类型,一般来讲,对于基于命令的脚本必须支持整型 以及 字符串型 的数据,前者用以表达各类参数信息,而后者往往代表游戏中的各种文字,但实际上,只要我们做一些简单的扩展,基于命令的脚本同样可以支持 布尔型以及 浮点型 的数据,具体的做法可以参见后面部分的设计实现示例,同时我们也可以加入对于常量的支持,至于加入常量的好处,我想已经不用在此赘述了:)接着便是脚本的语法,一般来讲,基于命令的脚本语言都由一条条的命令组成,命令之间也都是顺序执行,但实际上,为了增加脚本的灵活性,我们可以加入一些简单的循环和分支,具体做法仍可参考设计实现示例:)
最后让我们谈一谈脚本文件的加载与执行,论及脚本文件的载入时,我们首先遇到的第一个问题便是预处理的问题,在基于命令的脚本中,如果我们决定支持 Include 操作,那么我们就必须对脚本进行预处理,但是这其中存在一些问题,我们留于后面讨论,在者便是是否需要对代码进行“编译”,你可能会奇怪为何会有“编译”一说,实际上按照我们的普通想法,基于命令的脚本系统的运行可能像是这副样子(忽略一些无关的细节):
首先打开脚本文件,解析指令名称,指令参数,然后根据解析后的指令以及参数执行相应的程序,接着继续解析指令,如此循环往复,直至脚本结束。
没错,这是我们想到的最普通直观的方法,但是这期间却存在一些问题,首先便是执行速度问题,众所周知,动态的解析字符串是一个非常缓慢的过程,如果我们以上述的解析操作来运行脚本,必不可少的会花费相当多的CPU时间,但实际上,如果我们将文本化的脚本文件在运行前编译成某种我们定义好的二进制格式,则将大大加快脚本文件的执行速度,并且同时我们额外获得了至少两点好处:一是我们可以更加方便的发现脚本中的错误,而不像先前边解析边执行的方式,可能引发灾难性的后果;二是我们也进一步保证了脚本的安全,相比之前可读形式的脚本文件,我们大可以在编写正确的脚本之后,仅提供对应的编译版本,而无需将脚本内容暴露在光天化日之下:)所以,既然预编译有如此的好处,那么就不要犹豫,让我们的脚本义无反顾的支持编译吧:)