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


    使用框架: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对象,而只能改改它的克隆体。

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

    情节编辑器

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

     

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

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

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

    如果没有剧情编辑器,手动编辑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,就算出了问题也是策划自己在编辑器中漏设置或者错设置数据了,不关咱们程序的事~

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

  • 相关阅读:
    Proj FuzzViz Paper Reading: Towards a JSONbased Algorithm Animation Language
    Proj FuzzViz Paper Reading: Visualization of ObjectOriented Variability Implementations as Cities
    Proj FuzzViz Paper Reading: Trace Visualization within the Software City Metaphor: A Controlled Experiment on Program Comprehension
    Proj FuzzViz Paper Reading: Understanding HighLevel Behavior with LightTraces Visualization Metaphor
    Proj FuzzViz Paper Reading: An ArchitectureTracking Approach to Evaluate a Modular and Extensible Flight Software for CubeSat Nanosatellites
    Proj FuzzViz Paper Reading: CodeCity: OnScreen or in Virtual Reality?
    ES选举机制
    ES 多重字段
    ES配置文件说明(一)
    ES段和提交点
  • 原文地址:https://www.cnblogs.com/keng333/p/3195492.html
Copyright © 2020-2023  润新知