- 注册时间
- 2004-12-24
- 最后登录
- 1970-1-1
|

楼主 |
发表于 2005-9-5 16:18:00
|
显示全部楼层
(2)脚本描述语言的语法关键字和函数
脚本描述语言最基本的单位是语句,它应当具备最基本语法,如表达式求值(包含各种常用的运
算),无条件转向,条件分支,循环,子函数等。变量有用户自定义数据也有游戏的全局数据;而描
述稍微复杂一些的功能可以采用全局函数,这些全局函数就象C语言的库函数或者是WINDOWS
的API一样,和各种变量一起在表达式中引用或者作为其他函数的参数。
下面是我制作的RPG游戏的一个事件脚本文件。
// 示例事件1
say(1,165,"大家好!我是张斌,这是我做的第十七个游戏。")
say(11,30,"这是一个测试!用来检验我的游戏引擎。")
say(32,300,"你好!在我这里可以点播MIDI乐曲。")
choose_flag=choose(4,"请选择乐曲:","乐曲一?乐曲二?乐曲三?乐曲四")
midi(choose_flag)
say(36,30,"小子,你来找死!")
push
s0=fight(7)
if(s0==1) goto(WIN)
msg("你打输了了!")
gameover=1
:WIN
pop
msg("你打赢了!")
end
这个事件的编号在地图装入时赋值给了一个NPC,当主角接触到这个NPC时,这个事件被触
发,于是这个文件被读入内存,开始解释执行。我们逐行解释这个文件:
第一行 前面的"//"同C++语言一样表示注释一行。
第二行 是一个函数,名称是 SAY ,有三个参数,前两个是整数1和165,第三个是字符"大家好……
"。这个函数的意义是在屏幕的纵坐标165的位置上显示人物1(头像和姓名)的语言"大家好…"
第三行 同上,在屏幕纵坐标30的位置显示人物11的话"这是一个测试……"
第四行 同上,在屏幕纵坐标300的位置显示人物32的语言"你…"
第五行 choose是一个选择函数,在屏幕上出现"请选择乐曲"的信息和4项选择,分别是"乐曲1","乐曲2","乐曲3","乐曲4",当玩家选择一个并按下回车后,
玩家选择的选项号码(0表示第一个
选项,1表示第二个,依次类推)作为这个函数的返回值赋给变量choose_flag。
第六行 midi是一个播放MIDI音乐的函数,它的唯一参数就是乐曲号,我们可以看到它的参数是变量
choose_flag,这就表示根据choose_flag中的值来选取播放的乐曲,而choose_flag中恰恰就放的是
在前一语句中我们选择的号码,因此midi函数就会播放我们前面选择的乐曲。
第七行 仍然是人物语言显示
第八句 因为要进入战斗场景,所以用push函数将当前场景的参数保存。待战斗结束后可以再用pop
函数取出,借此恢复战斗前的场景。
第九句 fight是战斗事件函数,参数表示战斗事件的号码,这里表示第7号战斗事件。战斗的结果
(0表示输1表示赢)赋值给变量s0
第十句 if语句(也可以理解为函数)对装着战斗结果的标量s0进行判断,如果s0为1(战斗胜利)
,则执行后面的goto函数,跳转到标号为WIN的语句(就是":WIN"那一行),否则继续执行下面的语
句。
第十一句(s0==0,战斗失败)msg函数表示再屏幕上显示信息"你打输了!"
第十二句 给变量gameover赋值为1,处理这个脚本事件的解释器检测到此变量为1,就终止事件然后
结束游戏。
第十三句 为作为跳转语句目标行
第十四句 pop函数弹出战斗前的场景信息,恢复场景
第十五句 msg显示信息"你打赢了!"
第十六句 end函数表示事件结束
事件结束后,脚本解释器会释放这段脚本占用的内存。
脚本中的"gameover"对应这个游戏程序中的一个全局变量"gameover",由于使用了指针,在脚本
中对它的引用就等同于对游戏程序中"gameover"的引用。同样对应于游戏程序中的其他全局变量,也
可以通过自己定义的脚本变量来引用。如地图大小"mapx"和"mapy",当前主角位置"cx","cy"等等,
这样就可以直在脚本语言中引用它们。如if(cx==5&&cy==7)判断主角是否在地图(5,7)这个位置上
if(cx==mapx-1&&cy==mapy-1) 判断主角是否在地图的角上这段脚本中"say","msg","choose",
"fight","midi"都是描述游戏中情节的函数,在游戏程序中都有相应的函数与之对应,它们有些有
返回值,有些没有。这些返回值还可以用来构成表达式,如:
midi(choose(3,"请选择乐曲:","乐曲一?乐曲二?乐曲三")+1)
这个条语句的含义就成了选择"乐曲一"的时候,实际放乐曲二,选择"乐曲二"的时候放乐曲三,
选择"乐曲三"的时候放乐曲四。
上面那段脚本中的"if","goto"可以被理解为控制语句也可以被理解成函数。所有的控制语句函
数化后,可以使脚本程序程序的格式更加统一,便于阅读。
同样对数组的引用也可以函数化,如对地图(X,Y)位置的图案类型的赋值在游戏程序中为
map[x][y]=12,是一个标准的数组元素,而在脚本程序中的引用则成了map(x,y)=12,x,y成了函数
map的两个参数。虽然形式上是函数,但实际的使用中仍然等同于变量map[x][y](因为程序内部使用
的是指针),因此可以进行赋值运算。
下面再看一段脚本文件
//示例事件2
say(12,300,"公子,你要买我的宝剑吗?")
say(1,30,"这宝剑多少钱?")
say(12,300,"30两银子!")
say(1,30,"这也太贵了!")
say(12,300,"30两也嫌贵,这剑可是削豆腐如泥哦!")
say(1,30,"让我考虑一下!")
choose_flag=choose(2,"买吗?","买了 不买")
if(choose_flag==1) goto(NoBuy)
if(haveobj(1)<30) goto(NoMoney)
msg("你花30两买下了这把破剑!")
say(12,300,"您走好!")
addobj(1,-30)
end
:NoBuy
say(12,300,"小气鬼,30两也不肯出!")
end
:NoMoney
say(12,300,"真是个穷光蛋,快滚!")
end
第一句 仍然是注释
第二句 到第七句是主角(1)和卖剑的(12)人物对话
第八句 是选择"买"还是"不买"
第九句 如果选择了不买,跳转到:NoBuy
第十句 haveobj对应游戏程序中的函数haveobj,参数是物品的种类返回值是拥有这种物品的数量(
物品1表示银子,银子的数量就是两数)这一句是判断主角拥有银子的数量如果小于30两,则跳转
到NoMoney
第十一句 显示买下剑的信息
第十二句 卖剑的招呼你走好
第十三句 addobj函数表示为主角增加物品,第一个参数为物品的种类(1为银子),第二个参数为
增加的数量(为负则是减少)
第十四句 事件结束
第十五句 不想买剑,跳转到这里
第十六句 卖剑的骂你小气鬼
第十七句 事件结束
第十八句 想卖剑但钱不够,跳转到这里
第十九句 卖剑的让你滚蛋
第二十句 事件结束
通过上面这两段脚本语言文件,我们可以清楚的了解到脚本语言的的变量对应着游戏程序中的全
局变量、函数对应着游戏程序中的函数,通过脚本语言,我们可以轻易的引用游戏中的各项值(主角
属性,物品,地图等等),引用游戏中的表述方法(人物对话,播放音乐,旁白信息等等)。如此以
来我们构建剧情不就轻而易举了吗?
然而,我们不能高兴的太早了,因为真正艰辛的,才刚刚开始!我们下一次将开始讲如何构件脚
本的解释机!
(3)解释机的编程
在任何编程语言中,表达式计算都是最重要的,它在我们的脚本描述语言中同样存在。因此在脚
本解释机中,我们最先实现的就是表达式求值。我们在写源程序的时候进行计算非常简单:
如 3*(3+2)-4
然而在脚本描述语言中,解释机所得到的并不是如此简单的算式,而是一个从文本文件中提取的
一个字符串 "3*(3+2)-4"将这个字符串转化成一个数,可不是那么简单。如果你认真学习过算法,应
该能够从容的实现它。
我们先看一个简单一点的算式 "32+41*50-2"
我们把自己看成是计算机,从字符串的左边开始扫描,当扫描到'3'时,可以知道这是一个数的
开始,将它记如入一个空闲的字符串的第一位buf[0],当我们扫描到'2',它仍然是个数字,是我们
正在记录这个数的新的一位,我们将它放入buf[1],当我们扫描到'+'时,它不是数字了,我们也就
知道第一个数读完了,在记录它的字符串的下一个位置buf[2]放入字符串结束标志'0'我们得到的这
个数放在buf中,是"32",通过一些编程系统提供的字符串转整型数的函数,我们可以将这个字符串
转化为数值,即使你用的编程系统没有这个函数,根据数字字符的ASCII码值,我们自己也可以
很容易实现:
'0',"1","2"...'9'的ASCII码值分别为 48~57,如果一个数字字符的ASCII码为n,则
它代表的数字值为n-48。如一个数char str[5]={"2341"},它的值就可以写成
(str[0]-48)*1000+(str[1]-48)*100+(str[2]-48)*10+str[3]-48于是我们可以写出下面的将字符串
转变为整数的C语言函数(其他语言也类似)
int stoi(char *str) //str是以0为结束的字符串
{
int return_value=0,i=0;
while(str{i}!=0) //字符串结束
return_value=return_value*10+str[i++]-48;
return(return_value);
}
知道了这个"32"是32,我们将它记下(装入一个变量),再继续往下扫描,直到字符'4',我们
知道我们得到了一个"+",它是一个二元运算符号,我们接着向下扫描利用上面扫描32的方法,我们
得到了另一个数41,我们现在知道了32+41,考虑一下,我们在什么情况下能将它们相加:
要么是在41后字符串结束,要么紧接着41的是一个比'+'优先级低或相等的运算符,如'-'。将它
们相加后得到73,我们就回到了刚刚得到31的那一步。
如果41后面跟着的是一个更高优先级的运算符,如本例中的'*',我们必须先进行这个乘法运算
,那只好先将31和'+'保存起来,接着向下扫描,我们又得到了50和其后的运算符'-',判断的方法和
刚才一样,因为'*'比'-'的优先级高,所以我们可以放心的先将41*50算出,得到2050。这时我们现
在所扫描到的算式就成了32+2050-2,我们再次比较运算符'+'和'-',优先级相同,我们就可以先算
32+2050了,得到2082,我们继续向后扫描,了解到在2082-2后字符串结束,我们可以继续计算最后
一步2082-2=2080,最后这个字符串表达式的结果就是2080。
现在我们再来看看括号:如 3*(3==2+2*5)-4这个式子,在读完3*之后我们读到得不是一个数,
而是一个括号,这时我们需要先算括号内的式子,所以的先将3和*保存起来,比较==和+,得先计算
加法,先将3==保存,再来比较+和*,先计算2*5得到10,因为下面一个等到算完2*5得到10,因为后
面是括号,所以要取出先前保存的数和运算符进行计算,但一直要取到前一个括号,但我们顺序存了
3*、3==、和2+,怎么知道前一个括号在那里呢?方法是在遇到前括号时也保存括号的标记,这样的
话,我们算到这一步时所保存的顺序为:3*,(,3==和2+,我们遇到一个后括号,就取出以前保存的
数进行运算,先算2+10得12,再算3==12得0,这时取出了括号(,我们这才知道这个括号内得运算完
结,现在的算式剩下了3*0-4,再比较*和-,先算3*0得0,最后得结果就是0-4得-4。
在上面的运算中,我们在遇到高优先级的运算时,需要将前面的数值和运算符保存,但我们并不
太清楚需要保存几次,如这两个算式:
1=2==3+4*5 1+2+3+4+5
它们在计算过程中需要保存的的数和运算符个数是不同的,前一个需要先算4*5结果为20,再算
3+20结果为23,再算2==23结果为0,再算1=0(在一般的语言中,象这样给常数赋值是禁止的,但在
我们脚本语言的运算中,为了保持一致性,我们允许这样的式子,但赋值被忽略),最多情况下需要
要保存三个数和运算符。而后一个式子一个也不用保存,从左到右依次运算就行了。
我们发现这些数和运算符的使用都是"先存的后用,后存的先用",这不是堆栈吗?对!我们就
用堆栈来保存它们。
堆栈的实现很多软件书中都已经讲过,心中有数的读者自然可以跳过下面这一段。
一般实现堆栈可以用链表或数组,我们犯不上用链表这么复杂的数据结构,用比较简单的数组就
可以了。对于使用C++的朋友可以用类的形式来实现
class STACK //整数堆栈
{
int *stack; //存放数据的首地址
int p; //堆栈位置指示(也可以用指针)
int total; //预先定义的堆栈大小
public:
STACK(int no); //指定堆栈大小的构造函数
~STACK(); //析构函数
int push(int n); //压栈
int pop(int *n); //出栈
};
STACK::STACK(int no)
{
total=no;
stack=new int [total];
p=0;
}
STACK::~STACK()
{
delete[] stack;
}
int STACK::push(int n) //压栈
{
if(p>total-1)
return(0);
else
stack[p++]=n;
return(1);
}
int STACK::pop(int *n) //出栈
{
if(p<1)
return(0);
else
*n=stack[--p];
return(1);
}
如果用C也是一样,使用initSTACK来声明一个堆栈,但要记着在用完之后调用freeSTACK释放内
存
typdef struct STACK
{
int *stack; //存放数据的首地址
int p; //堆栈位置指示(也可以用指针)
int total; //预先定义的堆栈大小
};
int initSTACK(struct STACK *stk,int no)
{
stk=(struct STACK *)malloc(sizeof(STACK));
stk->total=no;
stk->p=0;
stk->stack=new int [total];
//如果stack不为零表示分配成功,堆栈初始化也就成功
if(stk->stack)
return(1);
free(stk); //如果失败释放内存
return(0);
}
void freeSTACK(struct STACK *stk)
{
if(stk)
{
delete[] stk->stack;
free(stk);
}
}
int pushSTACK(struct STACK *stk,int n) //压栈
{
if(stk->p>stk->total-1)
return(0);
else
stk->stack[stk->p++]=n;
return(1);
}
int popSTACK(struct STACK *stk,int *n) //出栈
{
if(stk->p<1)
return(0);
else
*n=stk->stack[--p];
return(1);
}
可以看出这种堆栈类在声明对象时要给出堆栈的大小,对于我们的表达式求值来说,100个单
元足够了。但有人不禁会想到,上面这些都是整数堆栈,对于运符怎么存储呢?其实是一样的,我们
可以给运算符编上用整数序号来代表,这样就可以利用整数堆栈来保存了。给运算符编号的另一个好
处是可以利用运它的高位来代表运算符的优先级!如下面一个函数将字符串运算符转化成含优先级的
序号,只要比较这些序号高位值的大小就可以得出谁得优先级高了。(下面这个函数只对二元运算符
编号,没有处理一元和多元,因为它们都可以用二元运算表示。)
int convert_mark(char *str)
{
//优先级高
if(strcmp(str,"*")==0) return(240); //0xf0
if(strcmp(str,"/")==0) return(241); //0xf1
if(strcmp(str,"%")==0) return(242); //0xf2
if(strcmp(str,"+")==0) return(224); //0xe0
if(strcmp(str,"-")==0) return(225); //0xe1
if(strcmp(str,"<<")==0) return(208); //0xd0
if(strcmp(str,">>")==0) return(209); //0xd1
if(strcmp(str,"<")==0) return(192); //0xc0
if(strcmp(str,"<=")==0) return(193); //0xc1
if(strcmp(str,">")==0) return(194); //0xc2
if(strcmp(str,">=")==0) return(195); //0xc3
if(strcmp(str,"==")==0) return(176); //0xb0
if(strcmp(str,"!=")==0) return(177); //0xb1
if(strcmp(str,"&")==0) return(160); //0xa0
if(strcmp(str,"^")==0) return(144); //0x90
if(strcmp(str,"|")==0) return(128); //0x80
if(strcmp(str,"&&")==0) return(112); //0x70
if(strcmp(str,"||")==0) return(96); //0x60
if(strcmp(str,"=")==0) return(80); //0x50
if(strcmp(str,"+=")==0) return(81); //0x51
if(strcmp(str,"-=")==0) return(82); //0x52
if(strcmp(str,"*=")==0) return(83); //0x53
if(strcmp(str,"/=")==0) return(84); //0x54
if(strcmp(str,"%=")==0) return(85); //0x55
if(strcmp(str,">>=")==0) return(86); //0x56
if(strcmp(str,"<<=")==0) return(87); //0x57
if(strcmp(str,"&=")==0) return(88); //0x58
if(strcmp(str,"^=")==0) return(89); //0x59
if(strcmp(str,"|=")==0) return(90); //0x5a
//优先级低
}
在RPG得脚本描述语言中,我们基本用不上小数,因此我们在实际的二元运算中得到的将是三
个整数,其中两个是参与运算的数,另一个是运算符的序号,我们还得对此编出进行运算的函数。如
:
//运算求值 n1是第一个参加运算得数,n2是运算符号得序号
//n3是第二个参加运算的值
int quest(int n1,int n2,int n3)
{
int ret=0;
switch(n2)
{
case 240:ret=n1*n3;break; // "*" 乘法
case 241:ret=n1/n3;break; // "/" 除法
case 242:ret=n1%n3;break; // "%" 求余数
case 224:ret=n1+n3;break; // "+" 加法
case 225:ret=n1-n3;break; // "-" 减法
case 208:ret=n1<<n3;break; // "<<" 左移
case 209:ret=n1>>n3;break; // ">>" 右移
case 192:ret=n1<n3;break; // "<" 小于
case 193:ret=n1<=n3;break; // "<=" 小于等于
case 194:ret=n1>n3;break; // ">" 大于
case 195:ret=n1>=n3;break; // ">=" 大于等于
case 176:ret=n1==n3;break; // "==" 等于
case 177:ret=n1!=n3;break; // "!=" 不等于
case 160:ret=n1&n3;break; // "&" 与
case 144:ret=n1^n3;break; // "^" 异或
case 128:ret=n1|n3;break; // "|" 或
case 112:ret=n1&&n3;break; // "&&" 逻辑与
case 96:ret=n1||n3;break; // "||" 逻辑或
case 90:ret=n1|n3;break; // "|="
case 89:ret=n1^n3;break; // "^="
case 88:ret=n1&n3;break; // "&="
case 87:ret=n1<<n3;break; // "<<="
case 86:ret=n1>>n3;break; // ">>="
case 85:ret=n1%n3;break; // "%="
case 84:ret=n1/n3;break; // "/="
case 83:ret=n1*n3;break; // "*="
case 82:ret=n1-n3;break; // "-="
case 81:ret=n1+n3;break; // "+="
case 80:ret=n3;break; // "=" 赋值
case -1:ret=n3;break; // 用来表示前括号
case 0:ret=n1;break; // 空运算
}
return(ret);
}
我们可以看到,在上面得有关赋值得运算中,我们实际上并没有进行赋值,因为我们还没有任何
变量来接受赋值,下一次里我们再来讲讲将游戏中的数据作为变量进行运算和赋值,这可是最激动人
心的哦!
注意:解释机并不是独立的软件程序,它是游戏源程序的一部
分,只有这样脚本解释语言它才可能通过它引用到游戏
中的变量和函数。
为了达到引用游戏中变量和函数的目的,我们专门定制一个函数,用来将字符串转变成整数(假
如起名为val,则它的函数原型就是int val(char *str))假若输入字符串是一个数字串,我们就可
以调用前面一讲讲过的将数字字符串转变为整数的函数将它转化为数值;如果输入字符串的第一个字
符是英文字母或者下划线,我们就根据这个字串返回它所代表的游戏中的变量。
|
|