DSL在項目中的應用:用DSL高效組織遊戲情節
最近工作當中,任務部分的代碼寫的耦合度非常高並且也不好擴展,由我接手重構之後我自己編寫了一個簡單的DSL用於幫助策劃配置情節,在這裡簡單記錄一下吧。
由於開發時間非常緊,重構時間整個任務以及NPC對話系統只有短短三天而已,實際將DSL完成並且接入也僅僅花了一天的時間而已,也不需要自己過分編寫語法解析之類的輪子。
當然如果希望精益求精的話當然可以自己開發一套完整的語言,但是夠用就行了,不是嗎~
為什麼要劇情腳本
相對於遊戲邏輯,劇情邏輯更像是傳媒學生在剪輯片子,邏輯更加線性,而不像其他邏輯呈現樹狀結構。
同樣的,我們試著思考一下,如果用節點編輯器(類似於Behavior Designer)編寫劇情,會是多麼感人,完全是一條線!
這個時候劇情腳本的優勢就凸顯了,完全線性,邏輯清晰,看劇本就像是讀一整個故事一樣,由劇作人員接手編寫,沒有複雜的邏輯,策劃能夠將心思全部放在故事上面,比如策劃提出了某些需求,程序只需要提供相應的介面就完全可以滿足(類似於節點編輯器中的節點)
這種做法事實上在gal引擎中非常常見,日本的知名gal引擎KRKR(中文譯名吉里吉里,Fate/stay night使用的引擎)中就使用了兩種不同的腳本語言來進行編寫,一種為線性語言(KAG3),用於描述故事,而另一種則是用於編寫引擎擴展(TJS),實現複雜功能,這就同我們當前項目中所採用的Lua+DSL如出一轍,不僅有極強的擴展性,對於策劃而言也更為友好(況且實現成本也不高)。
劇情腳本介紹
基本語法
事實上這個DSL的語法非常簡單:
函數名@參數1|參數2...
例如:
log@輸出信息
也加入了一些簡單的語法來減少配置量
例如「~」可以用來替代上一次使用的函數
例如:
第二句調用的函數與第一句相同。
或者也可以通過「*」來表示上一次使用的對應位置的參數。
與上一句的參數完全相同
宏與表達式
當然,因為策劃需要獲取我們的變數名,所以我們就需要宏的機制。
<<宏名稱>>可以代表被替換的宏,例如角色名、角色等級等等。
而且宏是支持表達式的。(事實上這只是簡單地將表達式轉換為了Lua語句而已,此處需要用到簡單的詞法解析,我直接用正則做了)
類似於 < <PlayerLv > 30 and PlayerLv < 60>>
上面的語句就會返回True或者false
而如果是以LUA<<Lua代碼>>出現的則是直接運行Lua代碼並且以結果替換內容。
例如:LUA<<1+1>>
返回2,在Lua中可以做的都可以直接嵌入腳本。
後面提到的If、Switch、Condition函數都是基於表達式來做到分支的。
跳轉
另外一個比較重要的概念就是Tag,我們可以在劇本中插入Tag,策劃可以根據自己的需要來跳轉劇本。
例如:tag@testtag
則是定義了一個tag。
當策劃希望進行跳轉的時候就會直接使用:
gototag@testtag
各類的分支都是基於這個來做的。
分支
目前使用了三種方式來進行邏輯分支
- If語句
如果表達式為true,前往tag1,否則前往tag2
if@<<條件表達式>>|tag1|tag2
- switch語句
求值,如果與某個值匹配則跳轉相應的tag,否則繼續執行
switch@<<條件表達式>>|值1,值2,ect…|tag1,tag2,ect…
- condition語句
多路條件判斷
對多個條件進行判斷,如果滿足某一個則跳轉,否則繼續執行
condition@<<條件表達式>>|tag1|<<條件表達式2>>|tag2
腳本的基石:行為隊列
說實話,要實現這個DSL非常簡單,也就是逐行解析,每一行代表的就是一個命令。當策劃對功能感到不夠用的時候就添加新的命令,就像KRKR中包含了包含音頻、視頻、對話等各種介面,我們根據自己遊戲的需要自行實現就可以了。
這裡需要講講的行為隊列。
行為隊列其實就是隊列,只不過裡面放著的是行為罷了。隊列最前端執行完成之後自動執行下一個行為,這就讓我們能夠做各類非同步的行為了。
行為隊列的用法
行為隊列使用類似於下面的代碼:
ActionQueue queue = new ActionQueue();queue.AddAction((callback)=>{ //任意方法也可以是非同步的 callback();}).AddAction((callback)=>{ //任意方法 callback();});
我們可以看到,我們傳入的行為必須調用callback,這個callback就意味著下一個行為出隊並且執行。
行為隊列非常有用,如果沒有行為對列的話我們可能會掉入callback hell——無盡的回調
DoSomething(()=>{ DoNext(()=>{ DoMoreNext(()=>{ //ect... }) })});
而在行為隊列中則可以做到線性思維:
ActionQueue queue = new AcitonQueue();queue.AddAction(DoSomething) .AddAction(DoNext) .Action(DoMoreNext);
隊列則會乖乖地為你依次執行。
行為隊列的陷阱:中斷處理
這個時候我們或許已經躍躍欲試想用行為隊列做很多事情,例如非同步載入場景時可以線性列出我們需要載入的東西,或者是劇情所需要的逐個行為。
但是這個時候我們必須引起警惕的是中斷處理。
例如:我們編寫了一個代碼
ActionQueue queue = new AcitonQueue()queue.AddAction((callback)=>{ //監聽完成事件 //當收到完成事件時調用callback,並且清理事件});
與普通的隊列一樣,我們的隊列中存在著Clear函數。
當我們調用Clear時我們以為我們做完了所有事情,因為隊列也停下來了,我們什麼都不用管了吧?
實際上,大家思路縝密的話就會發現我們需要卸載事件監聽,否則在事件觸發的時候則會嘗試對隊列出隊,造成意想不到的錯誤。(即使沒有被調用內存也泄漏了,事件沒有被釋放)
所以正確的方式應該是這樣
ActionQueue queue = new AcitonQueue();queue.AddAction((callback)=>{ //監聽完成事件 //當收到完成事件時調用callback,並且清理事件}, ()=>{ //卸載事件});
當中斷的時候調用當前行為的卸載方法。
更進一步的話我們可以將行為包裝成一個對象:
class SingleAction : IDisposable{ Action<Action> act;//行為 Action dispose;//卸載方法 void Dispose(){ dispose(); }}
當然,這裡只是簡單寫了一個樣例,如何封裝一百個人有一百種做法。
在我們的腳本系統當中,實際上就是進一步封裝的行為而已,裡面包含了上下文、參數等等,以幫助腳本系統更好地工作。
總結
就像眾多的可視化編輯器一樣,雖然腳本並非可視化編輯器,但是我認為其效率更高並且更加直觀,就像編劇編寫劇本,後期剪輯視頻一樣,策劃可以更輕易地編寫劇情、任務流程等等,並且實現成本不高,有效解放了程序的生產力,在這裡只是與大家做一個簡單的分享,說不定早就有公司在這樣做了吧!
我的資歷尚淺,如果有更好地任務、劇情解決方案的話還請大家多多分享,學習交流。
推薦閱讀:
※[專題思考] 為什麼我們總愛討論技術與業務之間的那些是是非非?
※PM06|乾貨:PMP基礎知識培訓課件PPT(上)
※技術經理這種做法對嗎?
※線性&沙盒遊戲與項目管理
※不以用戶價值為目的的方案都是耍流氓