使用框架: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 &gt;= _timeLine.endTime )

{

dispatchEvent(new Event("complete"));

stop();

return;

}

var playingKeyFrames:Vector.&lt;KeyFrame&gt; = getCurrentFrames();

for each(var keyFrame:KeyFrame in playingKeyFrames)

{

playKeyFrame( keyFrame );

}

}

/** 检查当前将播放的关键帧,检查前请确保_timeLineCopy列表已经根据其元素的startTime属性排过序 */

private function getCurrentFrames():Vector.&lt;KeyFrame&gt;

{

var result:Vector.&lt;KeyFrame&gt; = new Vector.&lt;KeyFrame&gt;();

var keyFrames:Vector.&lt;KeyFrame&gt; = _timeLineCopy.keyframes;

if( keyFrames.length &gt; 0 )

{

var keyFrame:KeyFrame;

while(keyFrames.length &gt; 0 &amp;&amp; keyFrames[0].startTime &lt;= _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对象,而只能改改它的克隆体。

二:实现各类型的帧播放的具体业务逻辑。这一步我表示没什么好说的,如果你要播放的是类型为对白的帧,那么你需要自己编写一个对话框组件;如果你需要播放类型为黑屏的帧,你需要一个黑屏的组件……当然,你还需要创建用来显示人物的组件,这些都是需要花时间来做的事情,此处不再一一赘述。

情节编辑器

情节编辑器也是剧情系统的一个非常重要的组成部分,有了情节编辑器能让工作流更加地流畅,编辑剧情的事情交给策划,而我们程序则在完成剧情系统后不用再关心任何的事情了,可谓是一劳永逸。为了让界面更加整洁且易于策划使用,我设计的情节编辑器包含三块区域:时间轴区域,地图区域及属性区域,如下图所示:【AS3 Coder】任务八:没剧情还玩毛RPG-LMLPHP

在地图区域,用户可以看到其设置的剧情播放背景图,且找到指定坐标所在的位置,在设置人物出现位置、移动目的地时提供参考;

在时间轴区域,用户可以了解到剧情的一个大纲,点击某帧还可以编辑帧属性;

在属性区域,用户可以设置时间轴、剧情背景图等信息。

如果没有剧情编辑器,手动编辑XML文件将会让策划痛苦不堪,且出错率高,工作量大。考虑到编写一个剧情编辑器对大多数道友来说难度很大,我这边将会提供一个我写的编辑器的源码供各位参考(包含在顶部的源码压缩包中),如果你想直接用我的编辑器,可以直接双击压缩包中的.air文件安装编辑器程序,装完后就可以直接使用了。如果要投入到项目开发中使用,那么你是必须修改编辑器源码了,因为我的人物、聊天框等组件在列位的项目中肯定不能通用的。

剧情的触发

在《神仙道》中,剧情触发条件有两个:1.进入地图时;2.完成任务时。比如你接了一个打老板(BOSS)的任务,那么当你进入老板所在地图时会触发一段剧情,基本上就是说一些挑衅之类的话,然后就开打,打完之后该任务完成,再度触发一段剧情,这段剧情基本上就是聊一些“怎……怎么可能?我居然会败在一个小毛孩手里!”“战胜你的不是我,是正义!”之类的P话,我TMD看这类型的剧情都直接跳过的,要是我来设计剧情的话,作为一个站在2B之顶点的男人,绝对不会设计出这么2的剧情,而会出更2的剧情,哇哈哈哈!贫道的座右铭是:没有最2,只有更2!

那么为了能够触发剧情,我们需要一个剧情汇总文件,它的格式如下:

在根目录下将会包含多个drama标签,每个标签表示一个任务所关联的一或两个剧本,该标签的taskID属性就表示任务ID。在drama标签下存在一个before标签(代表进入地图时触发)和一个after标签(代表完成任务时触发)或者两者只存在其一。before标签下存在一个triggerMap子标签,它表示将在进入哪个地图时触发剧情,url子标签则表示剧本文件的名字;after标签下的triggerMap子标签往往不会有值,就算有值也没有意义,因为它只有在完成taskID对应任务时才会触发。为了生成剧情汇总文件,你需要在你的剧情编辑器中增加相应的功能。当然,你也可以手动编辑生成,那样的话比较麻烦且出错率高。下图给出的时我的编辑器中的剧本汇总功能:

【AS3 Coder】任务八:没剧情还玩毛RPG-LMLPHP

汇总时会加载被勾选的全部剧本文件,然后根据这些剧本文件中的map标签的taskID及triggerMap属性来生成汇总文件XML中的内容(若triggerMap的值非空,则会被作为一个before标签,否则作为after标签)。在编辑器中的属性区域有放给用户设置触发条件的输入组件:

【AS3 Coder】任务八:没剧情还玩毛RPG-LMLPHP

这里,为了降低出错率及便于策划辨认,我的“触发任务”的输入组件选择了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,就算出了问题也是策划自己在编辑器中漏设置或者错设置数据了,不关咱们程序的事~

好了,那么各位,咱们下回见吧~有问题记得留言给我哈!

05-11 17:21