1. 程式人生 > >瞭解RPG遊戲中劇情播放器的製作原理及流程

瞭解RPG遊戲中劇情播放器的製作原理及流程

http://www.iamsevent.com/post/44.html

本章原始碼下載:http://www.iamsevent.com/zb_users/UPLOAD/dramaPlayer/MyDramaSystem.rar(其中包含劇情編輯器及劇情測試應用。對於劇情編輯器,要看原始碼的話直接在FB中匯入專案資料夾,要直接執行的話執行.air程式安裝包,要釋出.air,可以使用我放在編輯器目錄下的.p3檔案,釋出密碼是123456)

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,就算出了問題也是策劃自己在編輯器中漏設定或者錯設定資料了,不關咱們程式的事~

好了,那麼各位,咱們下回見吧~有問題記得留言給我哈!