手把手教你写剧本引擎(二)——呼吁剧本
副标题#e#
这次要实现的是一个形式最简朴的剧本。这种剧本仅有呼吁、标号及跳转组成,看起来就跟汇编一样,不外好是较量好读的。固然这种剧本语言的语法很是简朴,可是最根基的要素照旧要有的。
作为一个剧本引擎,为了可以在各类百般的符合的宿主措施中利用,剧本自己最好不要涉及到详细的规模。虽然,假如这个剧本被建设的目标仅仅是为了某个规模的话,那就无所谓了。因此,一个剧本引擎需要一个查抄和运行代码的机制、运行时情况的维护以及一个成果足够利用的插件系统。一个完整的剧本引擎至少需要如下部件:
1、代码数据布局。代码的数据布局用来存放颠末阐明的剧本代码。事实上表明型的剧本引擎,也就是边执行边阐明代码字符串的剧本引擎是较量难做,并且效率也不高的。剧本代码颠末事先阐明,可以查抄一出一些在运行之前就可以或许查抄的错误。并且我们把剧本的代码从头处理惩罚成一个数据布局之后,执行也变得越发容易节制。
2、运行时情况。运行时情况用于存放剧本在运行的进程中发生的数据,譬如仓库、变量和状态信息等。对付一个已知的代码,差异的运行时情况代表差异的剧本执行流程。为了让剧本可以同时(但不必然是并发)执行,将运行时情况独立出来也就显得须要了。
3、语法阐明器。语法阐明器用于将代码转换成等价的代码数据布局,并在发明代码堕落的时候输出符合的错误信息。
4、插件。插件是剧本与外部情况交互的途径之一。有了插件系统,我们可觉得剧本引擎添加特另外、跟剧本引擎无关的成果,譬如文件操纵、屏幕输入输出等。假如须要的话,插件系统可以将剧本引擎与规模信息相互断绝,系统将变得越发容易利用。
5、虚拟机。虚拟机用于执行代码并返回相应的功效。我们在利用剧本引擎时直接跟虚拟机举办交互,虚拟机则协调上述4个部件的彼此协作。
在知道了这些之后,我们就可以开始开拓一个基于呼吁的剧本引擎了。为了越发具体以及明晰地报告开拓进程以及道理,在这里将结构一门简朴的基于呼吁的语言。一门语言至少照旧要有分支和轮回的。可是为了简化,我们将分支和轮回解析成判定与跳转。语言可以自由添加标号,标号将作为跳转的方针而呈现。这门语言利用如下语法:
<值>:值可以是整数、小数、字符串或名字。
<名>:名可以是变量名可能标号等,利用字母与下划线开始,后接不定命量的字母、下划线与数字。
<名>::名字后接冒号代表一个标号。这个标号代表着一个指令的位置,用于指定跳转方针。
goto <名>:goto用于直接跳转到一个位置继承执行。
set <名> <值>:set用于将一个值赋值给一个指命名字的变量。这个变量不存在则建设。
opcode <名> <值> <值>:opcode可以是add、minus、mul、div、idiv或mod。这6个呼吁将两个值举办加、减、乘、除、整除及求余,并将功效赋值给一个指命名字的变量。这个变量不存在则建设。
if <值>[ opcode <值>] goto <名>:if用于判定一个条件并在条件满意被满意的时候跳转到指定的处所。条件可以是一个值,这个值必需是整数,而且在这个值不为0的时候条件被满意。条件也可以是一个较量,这个时候opcode可以是is、is_not、less_than、greater_than、less_equal或greater_equal,别离在第一个值便是、不便是、小于、大于、小于或便是、大于或便是第二个值的时候满意条件。
exit:竣事执行
<名> <值>*:假如呼吁名称不是上面的5种的个中一种的话,那么这个呼吁将被通报给插件举办执行。这个时候,呼吁可以有任意的参数。
在这种语法下,我们可以假设宿主措施给了我们write、writeln和read呼吁用于输入输出,并获得一个判定输入的数字是否质数的措施:
write "请输入一个数字:"
read Number
if Number less_then 2 goto FAIL
if Number is 2 goto SUCCESS
set Divisor 2
LOOP_BEGIN:
if Number is Divisor goto SUCCESS
mod Remainder Number Divisor
if Remainder is 0 goto FAIL
add Divisor Divisor 1
goto LOOP_BEGIN
SUCCESS:
writeln Number "是质数。"
exit
FAIL:
writeln Number "不是质数。"
#p#副标题#e#
这个措施首先判定输入是不是小于便是2,假如不是的话则利用一种简朴的要领来判定输入是不是质数。假设输入的数字为n,那么在n>2的时候,假如2到n-1中的任何一个数字可以或许整除n的话,那么n就不是质数了。下图是这个剧本的运行功效:
此刻开始实现它。
#p#分页标题#e#
在真正开始读剧本之前,我们需要一个在内存中表达呼吁的要领。呼吁有两种,一种是跳转标号,另一种是普通的呼吁。于是我们能够给出一个数据布局。跳转标号表用于查询一个名字所指定的呼吁的位置,而一个呼吁就由一个名字和一个参数列表组成。参数列表中的参数不只有内容,尚有范例。主要用于区分字符串和名字:
enum LexerType
{
ltString,
ltName
};
class LexerToken
{
public:
LexerType Type;
wstring Token;
};
class Command
{
public:
wstring Name;
vector<LexerToken> Parameters;
};
至于呼吁与标号的暗示要领例用如下代码:
vector<Command> FCommands;
map<wstring , size_t> FLabels;
好了,此刻让我们看看一行代码应该如何阐明。由于剧本支持字符串,所以我们不能简朴地利用空格来支解。假如我们碰着了“ writeln Number "是质数。"”,那么我们期望的功效是这一行代码被拆分成三个部门,别离是writeln、Number和"是质数。"。于是我们可以写一个函数,一次取出一个部门。那么我们只要一直取道换行符可能字符串竣事,就能得到一行的所有部门了。
剧本代码由整数、小数、字符串、名字以及冒号构成。于是我们可以写许多雷同的代码,然而名目都是int GetXXX(wchar_t*& Input);。这个函数查抄Input是否由XXX开始,返回值代表XXX用掉了几多个字符,然后把Input参数往后推那么多个字符返回给你。举个例子:
wchar_t* Input=L”123vczh”;
int Chars=GetInt(Input);
这个时候Chars=3,并且Input已经往后推了三个字符,指向了”vczh”。
于是颠末尽力,我们就拥有了一些函数:GetInt、GetReal、GetName、GetString、GetColon、GetSpace和GetLineBreak。我们如何利用呢?首先,我们在每一次得到一个部门之前,我们都要挪用GetSpace以过滤所有空格。然后就按如下顺序挪用上面的5个函数:
GetColon
GetString
GetName
GetReal
GetInt
事实上只要GetInt在GetReal之下就好了。因为假如123.456被GetInt先吃掉了3个字符之后,剩下的就无法表明白。
假如全都失败(函数返回0,代表什么都没查抄到)了,那么我们可以GetLineBreak。假如再次失败,那么证明这个输入的剧本就有问题了。那么报错吧。在示例代码的Lexer.h/Lexer.cpp中有一个很是雷同的词法阐明器用于将一行代码分段。
让我们把“ writeln Number "是质数。"”分行吧。
首先挪用GetSpace,字符串指向了“writeln Number "是质数。””,然后依次挪用5个函数一直到GetName乐成。GetName返回7,拿到了writeln,字符串指向了“” Number "是质数。””。
然后挪用GetSpace,接着仍然到了GetName乐成。GetName返回6,字符串指向了“"是质数。””。
接着挪用GetSpace,挪用到GetString的时候就乐成了。GetString返回6(留意我们用的是wchar_t),字符串指向了“”。
后头所有的挪用都失败了。我们意识到字符串已经用完了,于是对这一行代码的阐明就到此为止了。
到了这里,我们把所有的行都支解成一堆对象了。于是下面可以在采纳一个步调。我们首先分辨出哪一些是标号,哪一些是呼吁,然后填入上面的代码中提到的vector<Command>和map<wstring , size_t>中。假如我们碰着了一个标号,那么就将标号名和呼吁表当前存在的呼吁的数量插手标号表,其余的都放进呼吁表。于是我们在goto的时候,就可以从标号表中查到呼吁在呼吁表中的位置,从而乐成跳转了。
对付上面那段查抄是否质数的代码,最终的阐明功效如下:
标号表:
LOOP_BEGIN: 05
SUCCESS: 10
FAIL:12
呼吁表:
00 write "请输入一个数字:"
01 read Number
02 if Number less_then 2 goto FAIL
03 if Number is 2 goto SUCCESS
04 set Divisor 2
05 if Number is Divisor goto SUCCESS
06 mod Remainder Number Divisor
07 if Remainder is 0 goto FAIL
08 add Divisor Divisor 1
09 goto LOOP_BEGIN
10 writeln Number "是质数。"
11 exit
12 writeln Number "不是质数。"
#p#分页标题#e#
呼吁表内里有13个项,每一个项都被分成了呼吁名和参数表两个部门。执行的时候可以通过呼吁名来做相应的事情。让我们来手工执行一下这个代码。
执行00,执行01,我们输入“5”。
02条件失败,03条件失败,04配置变量Divisor为2。
05条件失败,06配置Remainder=5%2=1,07条件失败,08 Divisor酿成3,09跳转到05(LOOP_BEGIN)。
05条件失败,06配置Remainder=5%3=2,07条件失败,08 Divisor酿成4,09跳转。
05条件失败,06配置Remainder=5%4=1,07条件失败,08 Divisor酿成5,09跳转。
05条件乐成,跳转到10(SUCCESS)。
10输出“是质数。”,11退出措施。
于是此刻剩下了最后一个问题。write、writeln和read原本是不存在于剧本引擎的。可是剧本引擎不具有输入输出的要领也是不可的,所以我们需要实现一个插件系统。这个插件系统可以让我们在剧本引擎的外部添加呼吁。也就是说,我们结构了一个剧本引擎,然后在外部建设一个插件,包括write、writeln和read,然后毗连他们。最后做一些手段让剧本引擎在执行到外部呼吁的时候将节制权转移给插件。
在这里,我们可以利用责任链模式。剧本引擎在碰着一个不认识的呼吁的时候,就会见第一个链接到剧本引擎的插件。这个时候插件可以返回三种功效:乐成、失败可能弃权。返回乐成代表呼吁被乐成执行,剧本引擎继承往下走。返回失败代表指令被执行了,可是执行堕落,这个时候剧本引擎返回错误信息并遏制执行。返回弃权代表这个插件不受理这个呼吁,剧本引擎将这个呼吁通报给下一个插件。假如所有的插件都弃权的话,那么剧本引擎将返回“无效呼吁”并遏制执行。
所以插件只需要有一个函数就行了。这个函数返回执行功效(乐成、失败或弃权),参数为当前的呼吁以及运行时情况(生存变量的处所)。剧本引擎利用一个vector去记录所有链接的插件的指针,这样的话剧本引擎在碰着不能表明的呼吁的时候就可以依次会见插件了。下面是插件的示例代码:
class Plugin
{
public:
virtual PluginStatus Execute(const Command& aCommand , Environment& aEnvironment , wstring& ErrorMessage)=0;
};
vector<Plugin*> FPlugins;
呼吁剧本的对象就讲到这里了。接下来的一些文章将报告如那里理惩罚高级语言,而且开拓一门新的语言出来。这门语言将只支持bool、int、double、string、数组和函数。
代码布局如下:
Lexer.h/Lexer.cpp:词法阐明器
ScriptCommand.h/ScriptCommand.cpp:剧本引擎
Main.cpp:主措施
这个措施(SE_02.exe)读取一个文本文件(SE_02.txt)并执行,可以在debug文件夹下看到编译功效。