做个RPG游戏主人翁 第四篇 命运由我掌握——学习游戏脚本系统
软件世界
通过盘古的一番唇舌和阿志的一番努力,阿志终于可以在冒险世界里闯荡了。这时的冒险世界还是那样美好,高山、绿水、蓝天、白云,这一切都让阿志忍不住想赞美几句。可阿志看了半天只会睁大眼张大口站着,却说不出话来。等到阿志走出冒险世界,立刻向盘古开始抱怨。盘古笑呵呵地说道:“别急,让我慢慢告诉你在冒险世界中说话的秘密。”
冒险之旅又和各位朋友见面了!随着上一篇的讲解,阿志已经能够在冒险世界中走动了,是不是觉得我们离成功越来越近了呢?这一期要更进一步,阿志将要学会在冒险世界中说话了。但如何才能让人物开口呢?这就要学习使用脚本这个武器,它就是让人物开口的关键。从场景的虚无到创建、从人物的呆立到走动、从英雄的闭口不言到滔滔不绝……一路的精彩因你们而存在。
一、点哪就走哪──人物走动用DirectInput
还记得上一篇的内容吗?阿志已经开始在冒险世界中闯荡了。既然要控制人物的走动,当然就要学会控制键盘鼠标。可拿什么控制你们呢?武器就是DirectInput。
DirectInput也是DirectX的一个组成部分,只不过DirectDraw主要掌管二维图形的绘制,DirectInput则主要负责输入和输出的控制。所以,要想控制键盘鼠标的话,就请学习一下DirectInput吧。
1.磨刀不误砍柴功──初始化DirectInput
在介绍如何使用DirectInput前,照例我们要学习如何初始化DirectInput。所谓磨刀不误砍柴功,将DirectInput的初始化做好之后,就能方便地调用了。
首先要把dinput8.lib和dxguid.lib加入到工程中,当然少不了要敲入“#include <dinput.h> ”。然后定义DirectInput8对象和DirectInput设备:
LPDIRECTINPUT8 lpDI; //DirectInput8对象
LPDIRECTINPUTDEVICE8 m_pKeyboard; //键盘设备
LPDIRECTINPUTDEVICE8 m_pMouse; //鼠标设备
完整的初始化代码的下载地址:http://www.cpcw.com/31/game41.rar。
2.试着了解你 ──读取键盘鼠标数据
键盘数据三部曲:
(1)要在游戏中使用键盘,就是要知道键盘哪些或某个键被按下了。所以我们首先需要一个保存键盘数据的地方:
char buffer[256]; //键盘数据
(2)然后用以下方法得到键盘数据:
m_pKeyboard->GetDeviceState(sizeof(buffer),(LPVOID)&buffer);
(3)最后我们就可以用以下方法来测试某个键是否按下并执行相应的操作:
if(buffer[键值]&0x80)
{
//按下键后所执行的操作
}
鼠标数据三部曲:
(1)首先需要保存鼠标状态的变量:
DIMOUSESTATE mouse_state;
(2)然后获取鼠标状态:
m_pMouse->GetDeviceState(sizeof(DIMOUSESTATE),(LPVOID)&mouse_state);
(3)最后用以下方法测试鼠标某个键是否被按下并执行相应的操作:
//键值为0表示左键,1表示右键,2表示中键
if(mouse_state.rgbButtons[键值]&0x80)
{
//鼠标按下后所要执行的操作
}
二、连接你自己的故事──游戏场景切换的秘密
阿志在冒险世界中第一次想说话,就是因为想走却走不出同一个场景。所以盘古觉得在给阿志讲解说话的秘密前,还是先来解决一下这个小问题,免得心急的阿志来怪他。
我们在讲解创建场景的时候已经绘制好了场景的出口了,所以当主角走到场景的出口时就应该切换到另一个场景。要实现场景的切换,首先当然要用地图编辑器编辑好两个以上的地图,然后设置好每个场景的出口。
要实现场景切换其实很简单,程序必须一直都检测主角是否“踩”到出口了,如果是的话就要进行场景切换了。
首先要把地图结构清空,再把当前地图的名字(不是MapStruct.mapname,场景类CScene有一个变量保存了当前的地图名字)改为目的地图的名字(即ExitStruct.m_chMap),然后把主角的坐标改到目的场景后的坐标,方向也要改。最后把游戏状态改为GAMESTATE_RUN_INIT,这样就会再次进行初始化场景,而载入的就是新的地图文件,进而就实现了场景切换。程序流程图如图1所示。
场景切换的程序下载地址:http://www.cpcw.com/31/game42.rar。
小提示:场景切换的时候为了好看可以自己画一张Loading图像,在读取新地图的时候就先调出Loading图像,等把场景的初始化工作完成后才调出场景。大家如有兴趣可以自己做一个试试。
三、打好腹稿再讲话──运用脚本系统
场景切换也讲述完了,阿志兴奋地等着听盘古关于说话秘密的讲解了。可盘古却带来了一个小人──精灵阿比。阿志想想也对,这一定是冒险世界里的对话对象了。于是也不等盘古说话,阿志就将这小精灵带入了冒险世界,准备和他交流交流。可进去就傻眼了,他和阿比还是不能对话。
这时传来盘古的声音:“不好意思,我还没给精灵阿比添加脚本,所以他还不会说话。”
“脚本?那是什么东西啊?”
“不用急,我一边为他添加脚本,一边告诉你。”
1.游戏世界的剧本──认识脚本系统
还记得你玩过的RPG游戏那有趣而感人的剧情吗?还记得游戏中那经典的对白吗?这些都是游戏的剧本所设定好的,这里所指的游戏剧本就好像电影剧本一样,它主导着整个游戏,是RPG游戏的灵魂。
那在游戏中如何表现剧情呢?在程序中直接添加对白吗?可以,但方法太笨了。像现在RPG游戏这样的庞大剧情要用这种方法来实现,工作量大得惊人。当要改某段剧情的时候就要修改一大堆的代码,麻烦而且容易发生错误。所以大家就选择了一个聪明的方法──给游戏写剧本。
这里的游戏剧本就是脚本。游戏剧本不是直接用我们的语言写出来,而是用我们自定义的脚本语言写出来的。例如:
TALK(“阿志” , “妖怪!我勇士阿志奉女王之命来搞定你们,准备受死吧!”);
TALK(“妖怪A” , “嘻嘻!就凭你?”);
TALK(“阿志” , “哼!等会看你还笑不笑得出!”);
FIGHT(“妖怪A”);
RETURN();
以上就是一段自定义的游戏脚本,TALK是一个谈话命令,FIGHT是一个战斗命令。以上描述的就是阿志与妖怪A的谈话,然后就开始打斗了。这就是一个简单的脚本,怎么样?是不是真的有点像电影剧本?更方便的是这些脚本可以用Windows的记事本程序来编写。
2.自己就是大导演──自定义脚本格式
虽然游戏脚本就好像电影剧本,但写起来却不是那样自由。也必须像我们编程一样,要按一定的格式来编写。好在我们可以做自己冒险之旅的导演,设定脚本语言的格式。现在就以上面的游戏脚本为例,其每一句的脚本格式如下:
命令(参数1,参数2,……,参数N);
这看起来更像是一个函数的调用。每一句脚本由命令、括号、参数等组成,最后以一个分号来结束一句脚本。为了统一,脚本中的命令一律采用大写字母。括号中的参数以逗号分隔开,像TALK命令那样的参数要以双引号括起来,用双引号括起来的字符串一律用全角格式编写。每一段脚本最后以“RETURN();”结束,就好像函数那样。
3.其实我是一个演员──分离出每句脚本
演员的台词有很多,但只在适当的地方说适当的台词。游戏的脚本也是这样,我们写了很多,但每次调用都只是一部分,所以要让游戏角色成为一个合格的演员,就首先要学会分离出每句的脚本。
为此我们首先要为游戏添加一个游戏脚本类CScript。为游戏脚本类CScript添加一个运行脚本函数RunScript()。而游戏中的脚本是以文本格式保存的,那我们如何读取需要的脚本文件呢?
ifstream is(“role.scr”);
“role.scr”是一个脚本文件,is把这个脚本文件读取了,然后我们就可以把它分成一句句来执行了。要在整个脚本文件中读取一句脚本就要用getline()函数了。还需要一个用来存放一句脚本的字符串对象:
#include<string>
using namespace std;
string m_buffer; //用来保存一句脚本的字符串对象
不理解getline()函数的用法?没关系,看下面的讲解。
getline()函数的功能是从输入流(如 is )中读取多个字符,允许在getline函数中指定输入终止符,默认的输入终止符为换行符“\n”。我们知道,我们的脚本每句都是以分号结束的,所以可以用以下方法读取出一句脚本:
getline(is,m_buffer,';'); //读取一句脚本到字符串对象m_buffer中
在读取完一句脚本后,输入终止符是会在已读取的内容中删除了,所以分号不会保存在字符串对象m_buffer中。
我们在文本文件输入脚本时,每输入一句就按下回车再输入第二句脚本,这个回车就是一个换行符,它同脚本文本一样也保存了起来。因为有些脚本语句是没有以分号结束的(如选择结构,后面有详细讲解),所以我们就以换行符为终止符:
getline(is,m_buffer,'\n');
换行符是默认的输入终止符,可以省略。读取一个脚本文件时就是不停地读取每一句脚本,把脚本交给分析脚本函数处理,直到遇到“RETURN()”或文件结束,读取每句脚本的流程图如图2所示。
4.你在说什么──分析每句脚本
演员的台词说的什么,观众就听见什么。可游戏脚本就不同了,前面读出的每句脚本放到游戏程序中,程序并不理解说的是什么意思。所以我们要让程序拥有能解析我们输入的脚本语言的能力。如何做到呢?请看下面。
为游戏脚本类CScript添加一个分析脚本函数AnalyzeScript()。也就是图2中的“分析脚本”部分。它的作用是分析每一句脚本所执行的是什么脚本命令,分析出它的参数,最后调用该脚本命令的执行函数。
定义一些字符串对象用来存放脚本命令和参数:
string m_Command; //用于存放脚本命令
string m_Parameter[10]; //用于存放脚本参数(最多可以有十个参数)
int m_nParameterCount; //参数个数
string类为提取字符串的某些子字符串提供了一些非常好用的函数:
(1) find()
它的作用是:给出一个子字符串,返回与该子字符串第一个字符匹配的的索引位置,如果找不到则返回string :: npos。
(2) substr()
它的作用是取一个子字符串,从pos位置开始的n个字符,然后把这个子字符串以一个string对象返回。
find()函数和substr()函数的综合用法如下:
string str=“ TALK(“阿志” , “哼!等会看你还笑不笑得出!”);” ;
int pos=str.find(‘(‘);
m_Command=substr(0, pos); //m_Command得到的就是“TALK”。
分析脚本函数AnalyzeScript()的流程图如图3所示。
5.是打还是和──TALK命令和FIGHT命令的实现
谈话的实现:
先来看一句TALK命令的脚本:
TALK(“阿志” , “妖怪!我勇士阿志奉盘古之命来搞定你们,准备受死吧!”);
第一个参数“阿志”是说话人的名字,我们也可以利用这个名字把说话人的头像调出来,头像都是以文件名“名字+face.bmp”保存的。第二个参数是说话的内容。现在我们就可以按自己的喜好做一个漂亮的说话栏了。按下鼠标左键就结束一句TALK命令。图4中的主角正在跟精灵阿比谈话。
战斗的实现:
FIGHT命令如下:
FIGHT(“妖怪A”);
相信学习了以上和脚本知识,大家也知道如何实现更简单的FIGHT命令了。FIGHT命令只有一个参数,就是战斗的对象。
6.分支剧情的建立──实现选择结构
讲了这么多,阿志已经很满意了,他决定先到冒险世界里去试试看。
到了冒险世界里,阿志高兴地对精灵阿比说:“你好,精灵阿比。”然后阿志准备问问任务,于是又张开了口:“你好,精灵阿比”。怎么回事,还是一样的对白!阿志气得涨红了脸,一下就跳出了冒险世界,准备找盘古理论理论。可盘古早就有了准备:“是你太心急了,我又没说已经讲完了所有的关于说话的秘密。”
我们先来看以下对话:
//对话1
阿志:“盘古叫我来找你,说你可以帮助我消灭妖怪。”
精灵阿比:“我知道前面有妖怪,你有胆就去吧,不要在这里啰嗦了,我可不想去冒险。”
阿志:“……”
当我们用鼠标点一下精灵阿比时就应该出现以上的对话,但再点一下如果也是出现以上的对话,那就不合情理了。假设我们第二次跟精灵阿比谈话时会出现以下对话:
//对话2
精灵阿比:“你还在这里干什么,不敢去吗?唉,其实我也知道,你的本领……”
阿志:“谁说我不敢去?我是大家公认的勇士啊。我现在就自己去!”
这就要用到选择结构了,第一次出现的是对话1,第二次出现的是对话2。如下:
IF(TALKNUM==2)
{ //对话2 }
IF(TALKNUM==1)
{
//对话1
TALKNUM=2;
}
实现脚本的选择结构在游戏中是少不了的,就好像C++中少不了选择结构一样。上面的只是选择结构的一个小小的应用。游戏中的多分支剧情也是用选择结构实现的。看了上面的讲解,你一定会觉得实现选择非常简单。别忙,想过没有上面的if 怎么实现呢?请看:
实现IF语句
首先用一个变量来标志IF中的条件是真还是假。
bool m_bIsTrue;
看以下的IF语句:
IF(TALKNUM(1))
{
//脚本语句
}
以上的IF语句的条件是“变量TALKNUM等于1”,我们先判断这个条件是否成立,如果成立就执行IF语句里面的脚本语句,这些语句以一对大括号括起来,跟C++一样。
本篇的完整程序下载地址:http://www.cpcw.com/31/game43.rar。
等盘古讲了那么久,阿志终于可以和精灵阿比顺利地交流了。
精灵阿比:“听盘古说你要去打妖怪,真的吗?”
阿志:“当然!这是勇士应该做的事!我立刻就出发!”
精灵阿比:“等等,你现在什么东西都没有怎么打?我给你一些武器和道具吧。”
阿志:“太好了,这正是我想要的东西。你给了吗?我怎么没看见?”
精灵阿比:“呵呵,下一次我再给你。”
交流:电脑报论坛支持(bbs.cpcw.com)。在疑难问题解答区有置顶的帖子供大家交流。




