遊戲開發架構中的資料與後設資料

YJ Park發表於2015-02-28

從 2012年4月開始,一直都在開發一款平板電腦上的實時戰略類遊戲,開發平臺是當下比較流行的 Unity3D,網路傳輸部分基於 uLink。是我自己的第一個 Unity3D 專案,在架構上下了不少功夫,專案相對比較複雜,作為架構師和主要的開發者(技術團隊一共3個人,其他兩個經驗都比較少),工作量著實不小,開發過程還是相當痛苦的,相對應的收穫也很不少。

目前專案的技術部分基本上算是告一段落,下一步更多的是市場和運營的工作,我的重心也會向伺服器管理的部分轉移,計劃陸續把專案中的心得體會在這裡用文字的形式保留一下。

資料

其實廣義的說,與計算機相關的所有的資訊,包括程式碼,可執行檔案,網路資料包,一切的一切都是資料,取決於從什麼方向來看,比如說,原始碼是編譯器的輸入資料,相應的可執行檔案則是編譯器的輸出資料;從作業系統的角度來看,可執行檔案就變成了輸入資料。

後設資料

後設資料簡單的說就是關於資料的資料,聽上去可能比較抽象,其實並沒有什麼特別之處,例如關係式資料庫中資料庫的模式就是一種典型的後設資料,定義了資料表的內容格式,包含哪些列,各自的資料型別及取值的約束,索引的定義等等,表中儲存的內容是資料,模式就是資料的資料,也就是後設資料。

資料與後設資料的關係

對於軟體系統來說,後設資料往往是輸入資料的一部分,而資料則是輸出資料。後設資料會影響系統對待資料的方式,此外後設資料和資料往往是一對多的關係。

這裡的輸入輸出並不是絕對的,比較複雜的系統往往會有多重體現,例如:可能會提供後設資料的編輯工具(例如遊戲中提供了地圖編輯的功能),則對於工具來說,後設資料就成了輸出資料。

另外,大部分情況下,資料即是輸入也是輸出的,例如對文字編輯器來說,對文字檔案既會讀取,也會寫入。

架構與後設資料的關係

個人體會,架構的核心功能就是對後設資料的定義、解析,和執行,只能以一種方式執行的程式碼不能稱之為架構,必然要根據具體的後設資料(廣義上也包含構建於架構之上的邏輯程式碼)來執行不同的邏輯規則。

架構中的後設資料並不僅限於類似資料庫模式或者頁面模板這種具體的形式,也包括比較抽象的形式,例如對於實現類中特定方法的定義等等,像是 Ruby on Rails,或是 Django 這類大量基於慣例的架構就更是大量定義這種抽象型別的後設資料,有些甚至可以稱之為後設資料的後設資料。

領域專屬語言,則是這種關係達到最大程度的結果:軟體系統 = 架構 + 後設資料

遊戲開發架構中的資料與後設資料

終於進入正題,下面就以我們的遊戲中各個兵種的定製系統為例,具體解釋一下。

遊戲中玩家只負責劃線指揮位置的移動,各個單位的執行邏輯是類似 AI 的方式,會根據當前自身的狀態,玩家的命令,周邊的環境以及敵軍、友軍的具體情況產生行動。遊戲的可玩性很大程度上來自於各個兵種的設計,例如遠端攻擊的能力,近戰的攻防能力,攻擊速度;特殊兵種的獨特技能,例如忍者在運動時可以隱身,醫生可以恢復周圍友軍的生命值等等。為了達到更大的多樣性,每個單位還可以疊加特殊設定,簡單的可能是數值的加成,複雜的則可以提供額外的特殊技能。

策劃提出的要求是能在不需重新編譯的情況下調整引數,例如各兵種的移動速度,加成的數值等等,似乎要求不高,也很容易實現。然而遊戲開發中的不確定因素其實很大,往往會做大量的修改,如果用常規的方式,逐一實現功能,來來回回的修改恐怕會帶來相當大的工作量。

原型版本中實現的相對簡單,自己寫了第一個版本,基於行為樹的模型,實現了基本的攻擊邏輯,後來讓另一個程式設計師接手,加了不少功能,又改了若干次需求,後來複雜性失控了,出現很多問題,經常行為就不對了,最明顯的就是敵我雙方非常友好的轉來轉去,就是不動手。

本想湊合著先改改用著,策劃說是不行了,必須徹底修好,而且好多新技能還沒加,舊的程式碼是不可能支援了。最後還是花了近兩個月的時間,把這部分徹底重寫了一遍,基本上提供了一個小型的領域專屬語言。策劃通過 Json 的形式,利用系統支援的底層元件,可以自由定義兵種,加入新的技能、裝備,效果相當不錯。這些 Json 格式的後設資料儲存在單獨的目錄中,不需要重新編譯的情況下可以自由修改。

面向後設資料開發的優點

技術與策劃分工明確

最重要的一點就是把各工種的責任劃分的比較清晰,技術負責實現架構和各個子模組,策劃負責通過提供後設資料的方式進行具體的設計與定製。

通過簡單的培訓,和基本完整的文件,我們的策劃寫了上百個 Json 檔案,把所有的技能、裝備都實現了一遍,有些自定義的技能達到了相當複雜的程度。整個過程中我的角色都是技術支援,前期答疑、做些示例,後期主要是除錯,增加一些新的元件。雙方都很滿意,策劃可以自己實現想要的效果,我也可以集中精力在可重用的工作上。

之前看到過有些專案裡修改一個簡單的引數都必須由程式設計師負責,有些“樂於助人”的程式設計師也沒覺得這樣的方式有問題。個人認為程式設計師的工作應該更多的是提供工具,由最適合的角色使用這些工具來產生具體的資料或是後設資料。任何簡單的重複性工作都是對程式設計師時間的浪費,需要通過技術的手段解決。

推動對問題領域的充分理解

如果你是一名程式設計師的話,相信對於需求提供方的種種不靠譜行為應該都有些切身體會吧,很多時候其實他們根本就不知道,又需要做出決定才能推進專案開展,於是往往隨便想想,就埋下了種種坑。這種時候最怕的是特別天真的程式設計師,真是讓做什麼就做什麼,讓怎麼改就怎麼改,如果是原型階段,那就一切都好,否則程式碼就越來越讓人糾結了。

在專案前期,程式設計師自身對相關領域不甚瞭解,策劃者同樣不會有清晰的目標(做山寨產品自然另當別論)。需求穩定的一個解決方式是提出有效的問題,用後設資料的方式看待系統可以幫助我們找到最重要的未知因素,從而得到根本性的問題,在推動策劃回答這些問題時,共同把產品變得漸漸清晰。

穩定性、靈活性

如果不能做到比較清晰的模組化,想提供後設資料的執行引擎是不可能的。穩定性與靈活性,其實都來自於更好的模組化。

當然,只有正確的理念決不能保證具體實現的正確和優雅,開發過程中會碰到無數小的決策,如果不是非常認真的對待程式碼的組織與重用,程式碼庫劣化的速度是很可怕的。

易於管理

由於後設資料和系統本身有著清晰的界限,後設資料的管理本質上變成了資料的管理,像是在我們的系統中,對於核心戰鬥邏輯的調整,在系統提供的空間內只是若干檔案的更新而已。

面向後設資料開發的缺點

世上沒有免費的午餐,也沒有解決一切問題的銀彈,面向後設資料開發的方式也存在很多缺點,需要針對具體的場合來判斷是否值得。

前期準備工作要求比較高

開發的過程比較類似階段性的,前期出功能會很慢,還經常需要返工式的調整。

個人的經驗是對於第一個功能選擇一定會慎重,很可能需要通過幾種不同的實現方式提高自己對領域和解決方案的理解,也需要大量的對相關知識的學習。這個階段不怕慢,也不怕返工,最怕的是暴露不出問題來,大規模上實現才發現架構上的不足。

在沒有得到足夠資訊之前,都會有一種心裡沒底的感覺,這時一定要堅持,往往是一段時間過去,寫了一定量的程式碼(其中相當一部分都是被扔掉的),不知不覺就有把握了,感覺清晰的掌握了系統“應該”的實現方式。這時架構基本成型,功能的實現一下子就快起來了。

設計與開發過程比較複雜

設計一個合適的、好用的後設資料模型很不容易,合理的實現同樣頗有挑戰性,對於程式設計師的要求是相當高的,大多部分又是經驗的積累,沒有速成的捷徑。

如果團隊不具備足夠的能力,可能還是簡單直接的開發方式更加適合,當然這種情況下,最好能儘量簡化產品的技術需求,否則專案開發到了後期,很可能會出現技術搞不定的結果。

對策劃人員要求比較高

提供了工具,策劃就不是隻動嘴的工作了,必須親手來應用這些工具才行,如果系統比較複雜的話,還需要使用領域專屬語言進行二次開發,這些都對邏輯的要求比較高。

除錯

抽象層次增加後,一旦出現問題,除錯起來會格外複雜。個人經驗有下面幾點:

  • 後設資料的校驗必不可少,通過很常規的檢查就能發現後設資料格式上的大部分問題,這樣的程式碼寫起來比較無聊,不過價值很大,所以不能偷懶。

  • 除錯基礎架構要儘早準備好,我們的系統中會在日誌中產生關鍵的除錯資訊,開始並沒有很完善的工具函式來標準化除錯的格式。後期功能複雜後,很不容易使用。最終實現了這些工具,保證所有除錯資訊的統一,並提供了基於 Unity3D Editor 的過濾功能,除錯起來效率得到了數倍的提高。不過早期的程式碼就沒有完全調整了,所以說應該儘早準備好。

  • 具體的底層模組儘量設計的短小、簡單,只完成必不可少的功能。

  • 避免集中式的控制程式碼,能通過低耦合方式實現的,儘量避免直接的依賴關係。

示例

{
"abilities": [{
    "skills": [{
        "type": "repeat_trigger",
        "interval": 2.0,
        "kind": "doctor_heal"

    }],
    "define": {
        "doctor_heal_amount": 60,
        "doctor_heal_radius": 4
    },
    "triggers": [{
        "type": "custom",
        "kind": "doctor_heal"
    }],
    "targets": [{
        "type": "dynamic_sensor",
        "radius": "doctor_heal_radius",
        "side": "team"
    },{
        "type": "self"
    },{
        "type": "wounded"
    }],
        "spells": [{
            "type": "heal",
            "key": "doctor_heal",
            "visual_key": "heal_aura",
            "amount": "doctor_heal_amount"
    }]
}]
}

這是系統中一個簡單醫療技能的示例,每兩秒鐘會對周邊4個單位距離內受傷的友軍回覆最多60點的健康值。

以下幾點值得注意:

  • 通過 define 的方式,把數值定義為屬性,於是可以做進一步的加成或是修改,同一個檔案不需修改可以應用在多處
  • 重複觸發是一個模組,可以用在所有需要定時邏輯的地方
  • 醫治邏輯也是一個模組,可以由其他方式觸發

總結

這個題目比較大,要解釋清楚不太容易,很多地方個人的理解也未必就是對的,很可能還有更好的方式。

具體的在專案開發中設計與應用架構是個極為靈活的過程,沒有一定之規,必須根據專案、團隊的具體情況才能做出比較合理的選擇,我想這也是為什麼我們不僅僅把軟體開發作為一門技術,而也會作為一門藝術來看待吧。另一種看法是軟體開發更接近一門手藝,這也是我個人最有同感的。

附錄

術語

  • 資料 Data
  • 後設資料 Metadata
  • 資料庫模式 Database Schema
  • 架構 Architect
  • 領域專屬語言 DSL - Domain Specific Language

對於程式設計師來說,英文是很重要的,可以說是事實上的通用語言,恐怕在可見的未來也不會發生轉變,又不想在中文技術文章中夾入大量的英文單詞,暫定的方式是對於無法翻譯或無需翻譯的直接用英文,例如:Unity3D;可以翻譯的儘量用中文,有其他翻譯或是可能有歧義的在附錄中列出;主要的術語則也在附錄中列出對照的英文,以方便有興趣的讀者做進一步的研究。

http://cn.yjpark.org/blog/2015/02/26/data-and-metadata-in-game-development-architect/

相關文章