使用框架:AS3
任务描述:了解RPG游戏中剧情播放器的制作原理及流程
难度系数:3(了解原理,能根据XML文件播放剧情) / 5(会制作剧情编辑器)
本章源码下载:http://www.iamsevent.com/zb_users/UPLOAD/dramaPlayer/MyDramaSystem.rar(其中包含剧情编辑器及剧情测试应用。对于剧情编辑器,要看源码的话直接在FB中导入项目文件夹,要直接运行的话运行.air程序安装包,要发布.air,可以使用我放在编辑器目录下的.p3文件,发布密码是123456)
结果演示:http://www.iamsevent.com/zb_users/UPLOAD/dramaPlayer/MyDramaPlayer.html
Hi,列位道友,我们又见面了,2D横版RPG游戏已经火了好一阵子了,这类型的游戏在其代表作DNF(地下城与勇士)、神仙道、龙将、海贼王OL等的带领下着实赚了不少钱,这也引领了许多小公司纷纷效仿,我们公司也不例外。在这个项目中,我的工作之一就是实现剧情系统。作为一个RPG游戏,最重要的自然就是任务和剧情,没有剧情还玩毛RPG啊对不对?当然,刚开始的时候不是很有头绪,于是就研究了一下神仙道的剧本文件,这些文件都是以XML形式存在的,当要播放一段剧情的时候就会加载对应的剧本文件。现在,让我们看一个剧本文件的内容。
剧本文件
<?xml version="1.0" encoding="utf-8"?>
<xianxiaDrama>
<map mapUrl="304197.jpg" taskID="" triggerMap=""/>
<timeline endTime="5000">
<frame type="appear" name="user" sign="" x="200" y="200" startTime="0" roleType="user"/>
<frame type="moveAvatar" name="user" startTime="1000" x="400" y="200" speed="100"/>
<frame type="say" name="user" msg="<![CDATA[<FONT FACE="Arial" COLOR="#000000" >你好</FONT>]]>" direction="-1" startTime="100" endTime="500"/>
<frame type="appear" name="man" sign="1003" x="500" y="200" startTime="0" roleType="enemy"/> <frame type="dir" name="man" direction="-1" startTime="0"/>
<frame type="say" name="man" msg="<![CDATA[<FONT FACE="Arial" COLOR="#FFFFFF" >你好</FONT>]]>" direction="1" startTime="600" endTime="1000"/>
<frame type="say" name="user" msg="<![CDATA[<FONT COLOR="#FFFFFF" >我叫大SB,你呢?</FONT>]]>" direction="-1" startTime="2000" endTime="2300"/>
<frame type="say" name="man" msg="<![CDATA[<FONT COLOR="#FFFFFF" >我叫小2货</FONT>]]>" direction="1" startTime="2400" endTime="2600"/>
<frame type="moveAvatar" name="user" startTime="3500" x="3000" y="300" speed="200"/>
</timeline>
</xianxiaDrama>
相信聪明的各位从这XML中应该已经能获取一些启发,那么接下来让贫道为各位详细分析一下吧。
一个剧情应该是具备一个时间轴(timeline)的,什么时间发生什么事情都记录在这条时间轴上面。在时间轴上记录每一件要发生的事情的对象被称为关键帧或帧(frame)。
timeline标签在一个剧本文件中必须存在也仅能存在一个,它所具备的属性如下:
●endTime:时间轴结束时间,也代表剧情播放的总时间
frame标签是timeline标签的子标签,它所具备的属性如下:
●type:帧类别,代表将发生的事件。可选值可根据情况自定,一般会存在的选项有:say(对白)、dir(调整某个人物的朝向)、appear(人物出现)、moveAvatar(移动人物)等等
●startTime:帧发生时间
●name:角色名,代表该事件所关联的人物。该值必须设置为已经出现的人物,若该人物尚未出现(在该帧发生前不存在type为appear且name等于该帧name值的帧),则执行该帧不会产生任何效果。若该值为user,则表示帧发生对象为玩家,在剧情播放器中会被替换成玩家的具体名称
●msg:该属性默认作为type为say的帧的对白内容,但也可以另作他用。该属性中记录的内容由于可能包含文本格式,所以需要使用CDATA标记来将我的htmlText包裹起来以避免XML解析出错
●direction:在type为dir的帧中指示人物将要调整到的转向,1为朝右,-1为朝左
●endTime:帧结束时间。该属性一般只会出现在type为say的帧中,用以指示聊天文字出现的快慢。对于同一段话,endTime - startTime的值越大,文字出现的速度越慢。
●sign、roleType:在type为appear的帧中指示出现的人物所用外观资源名称及角色类型,角色类型不同,其名字颜色也不同
其余属性均根据需要出现,此处不再列举
剧情播放器
有了剧本文件,接下来需要做的,就是加载剧本文件然后播放了,为此,我们需要一个剧情播放器。制作剧情播放器的过程分两步:
一:创建时间检查器。我们需要使用一个Timer对象来作为时间轴播放指针,随着时间的流逝,播放指针会一直往后走,若是走到的位置处存在帧则播放之。为了不漏掉每一帧的检查,我们可以让指针的的移动间隔小一些,我此处设置的是100毫秒,也就是说,每100毫秒会检查一次时间轴,看看是否有新的一帧会被播放了。下面给出实现了该思想的代码:
public class DramaPlayer extends Sprite
{
/** 情节计时器步长 */
public static const DRAMA_TIMER_DELAY:Number = 100;
private var _timeLine:TimeLine;
private var _timeLineCopy:TimeLine;
/** 情节行进计时器 */
private var _dramaTimer:Timer = new Timer(DRAMA_TIMER_DELAY);
private var _timePassed:Number = 0;//已经过时间
private var _isPlaying:Boolean = false;
public function DramaPlayer()
{
super();
}
public function start():void
{
_dramaTimer.addEventListener(TimerEvent.TIMER, onTimer);
_dramaTimer.start();
checkTimeLine();
_isPlaying = true;
}
public function stop():void
{
_dramaTimer.removeEventListener(TimerEvent.TIMER, onTimer);
_dramaTimer.stop();
_isPlaying = false;
}
public function reset():void
{
_timeLineCopy = _timeLine.clone();
_timeLineCopy.sortKeyFrames();
_timePassed = 0;
stop();
}
private function onTimer( e:TimerEvent ):void
{
_timePassed += DRAMA_TIMER_DELAY;
checkTimeLine();
}
/** 检查当前时间的时间轴,若有某一关键帧在该时间开始,则播放之 */
private function checkTimeLine():void
{
if( _timePassed >= _timeLine.endTime )
{
dispatchEvent(new Event("complete"));
stop();
return;
}
var playingKeyFrames:Vector.<KeyFrame> = getCurrentFrames();
for each(var keyFrame:KeyFrame in playingKeyFrames)
{
playKeyFrame( keyFrame );
}
}
/** 检查当前将播放的关键帧,检查前请确保_timeLineCopy列表已经根据其元素的startTime属性排过序 */
private function getCurrentFrames():Vector.<KeyFrame>
{
var result:Vector.<KeyFrame> = new Vector.<KeyFrame>();
var keyFrames:Vector.<KeyFrame> = _timeLineCopy.keyframes;
if( keyFrames.length > 0 )
{
var keyFrame:KeyFrame;
while(keyFrames.length > 0 && keyFrames[0].startTime <= _timePassed)
{
result.push( keyFrames.shift() );//将符合条件的关键帧从时间轴列表中剔除
}
}
return result;
}
/** 播放关键帧 */
private function playKeyFrame( keyFrame:KeyFrame ):void
{
var role:RoleView;
switch( keyFrame.type )
{
case DramaEventType.ACTION:
……
break;
case DramaEventType.MOVE_AVATAR:
……
break;
case DramaEventType.ROLE_APPEAR:
……
break;
case DramaEventType.SAY:
……
break;
case DramaEventType.TURN_DIRECTION:
……
break;
default:
trace("Wrong keyFrame type!");
}
}
//------------------------------------------------------------------get / set functions------------------------------------------------------//
/** 播放的情节时间轴 */
public function get timeLine():TimeLine
{
return _timeLine;
}
public function set timeLine(value:TimeLine):void
{
_timeLine = value.clone();//使用副本而非本体
reset();
}
/** 是否正在播放 */
public function get isPlaying():Boolean
{
return _isPlaying;
}
}
相信列位对这段代码理解起来不会有太大难度,唯一值得注意的是,在使用时间轴对象(TimeLine)的时候,每次播放前需要创建一份副本,因为我在每播放完一帧时会把这帧的数据对象(Keyframe)从timeline.keyframes这个数组中取出来,这样做会破坏数组的结构,因此,为了保持被播放时间轴数据的完整性,我不能直接改原始timeline对象,而只能改改它的克隆体。
二:实现各类型的帧播放的具体业务逻辑。这一步我表示没什么好说的,如果你要播放的是类型为对白的帧,那么你需要自己编写一个对话框组件;如果你需要播放类型为黑屏的帧,你需要一个黑屏的组件……当然,你还需要创建用来显示人物的组件,这些都是需要花时间来做的事情,此处不再一一赘述。
情节编辑器
情节编辑器也是剧情系统的一个非常重要的组成部分,有了情节编辑器能让工作流更加地流畅,编辑剧情的事情交给策划,而我们程序则在完成剧情系统后不用再关心任何的事情了,可谓是一劳永逸。为了让界面更加整洁且易于策划使用,我设计的情节编辑器包含三块区域:时间轴区域,地图区域及属性区域,如下图所示:
在地图区域,用户可以看到其设置的剧情播放背景图,且找到指定坐标所在的位置,在设置人物出现位置、移动目的地时提供参考;
在时间轴区域,用户可以了解到剧情的一个大纲,点击某帧还可以编辑帧属性;
在属性区域,用户可以设置时间轴、剧情背景图等信息。
如果没有剧情编辑器,手动编辑XML文件将会让策划痛苦不堪,且出错率高,工作量大。考虑到编写一个剧情编辑器对大多数道友来说难度很大,我这边将会提供一个我写的编辑器的源码供各位参考(包含在顶部的源码压缩包中),如果你想直接用我的编辑器,可以直接双击压缩包中的.air文件安装编辑器程序,装完后就可以直接使用了。如果要投入到项目开发中使用,那么你是必须修改编辑器源码了,因为我的人物、聊天框等组件在列位的项目中肯定不能通用的。
剧情的触发
在《神仙道》中,剧情触发条件有两个:1.进入地图时;2.完成任务时。比如你接了一个打老板(BOSS)的任务,那么当你进入老板所在地图时会触发一段剧情,基本上就是说一些挑衅之类的话,然后就开打,打完之后该任务完成,再度触发一段剧情,这段剧情基本上就是聊一些“怎……怎么可能?我居然会败在一个小毛孩手里!”“战胜你的不是我,是正义!”之类的P话,我TMD看这类型的剧情都直接跳过的,要是我来设计剧情的话,作为一个站在2B之顶点的男人,绝对不会设计出这么2的剧情,而会出更2的剧情,哇哈哈哈!贫道的座右铭是:没有最2,只有更2!
那么为了能够触发剧情,我们需要一个剧情汇总文件,它的格式如下:
在根目录下将会包含多个drama标签,每个标签表示一个任务所关联的一或两个剧本,该标签的taskID属性就表示任务ID。在drama标签下存在一个before标签(代表进入地图时触发)和一个after标签(代表完成任务时触发)或者两者只存在其一。before标签下存在一个triggerMap子标签,它表示将在进入哪个地图时触发剧情,url子标签则表示剧本文件的名字;after标签下的triggerMap子标签往往不会有值,就算有值也没有意义,因为它只有在完成taskID对应任务时才会触发。为了生成剧情汇总文件,你需要在你的剧情编辑器中增加相应的功能。当然,你也可以手动编辑生成,那样的话比较麻烦且出错率高。下图给出的时我的编辑器中的剧本汇总功能:
汇总时会加载被勾选的全部剧本文件,然后根据这些剧本文件中的map标签的taskID及triggerMap属性来生成汇总文件XML中的内容(若triggerMap的值非空,则会被作为一个before标签,否则作为after标签)。在编辑器中的属性区域有放给用户设置触发条件的输入组件:
这里,为了降低出错率及便于策划辨认,我的“触发任务”的输入组件选择了ComboBox而非Textinput,只提供几个有限的选项给策划让他们选,而不是让他们手动填写。这些可选任务的选项来自于一张任务配置表,该配置表格式类似于:
<?xml version="1.0" encoding="utf-8"?>
<root>
<quest>
<id>2001</id>
<name>任务一</name>
</quest>
<quest>
<id>2002</id>
<name>任务二</name>
</quest>
<quest>
<id>2003</id>
<name>任务三</name>
</quest>
<quest>
<id>2004</id>
<name>任务四</name>
</quest>
<quest>
<id>2005</id>
<name>任务五</name>
</quest>
</root>
该任务配置表可以直接拿你游戏项目中所用的任务配置表过来用,就不需要再另外配一份了,这样保证了统一性和通用性,更加确保了不会出现“配置的剧情触发任务在游戏中不存在”的错误。
有了剧本汇总文件之后,你需要在你的项目中一开始就加载汇总文件,之后,当你进入某张地图时需要检查一次是否需要播放剧情,在完成任务时再检查一次。检查的依据就是当前已接任务列表以及剧本汇总文件。
结束语
对于剧情系统的原理,基本上就这么多好说的了,列位道友需要结合我提供的源码及我在文章中的介绍的思路来学习,最好自己再练习一二,试着触发一下剧情就更好了。贫道在此介绍的剧情系统是贫道在项目中实战应用着的,所以经得起考验,只要实现了这套系统,之后基本上不需要维护和操心了,它完全能正常运作无BUG,就算出了问题也是策划自己在编辑器中漏设置或者错设置数据了,不关咱们程序的事~
好了,那么各位,咱们下回见吧~有问题记得留言给我哈!