作者:Ferdinand Joseph Fernandez
我參加“7天速成RTS”(7dRTS)遊戲製作活動完成的《Strat Souls》是一款簡單的多人迷你戰鬥RTS遊戲。
如何製作像即時策略遊戲那麼複雜的東西?特別是當隻有你一個程序員負責所有東西,而且要在7天內完成?
我的做法是使用標準軟件工程原則。

Strat Souls
這個思路就是把類從低級細節分成高級概念。
我通常用一些術語如封裝、抽象、鬆耦合等解釋這個過程。但我還會加上例子。
以下是一個單位的一組類,你會看到有不少。
最低級類這些類直接與Unity係統(遊戲的引擎)相關。一個類隻做一件事。
通常來說,除了這些不會有其他職業直接調用Unity的內部結構(但也有例外)。
UnitMovement類:負責移動和旋轉單位。這是唯一直接控製單位的剛體的類。還處理碰撞和避開障礙。
UnitAnimation類:控製Mecanim(通過動畫類)的唯一的類。Mecanim是Unity的動畫係統。
UnitNetwork類:處理NetworkView和Network的唯一的類,即為Unity發送和接收網絡數據。對於單位,這是具有RPC(遠程過程調用)和網絡狀態同步的唯一的類。
Health類:管理單位的命值,非常基礎的代碼。
……
這些類是溝通Unity的係統和我的代碼的橋梁。那意味著我的代碼或多或少不是直接訪問Unity類庫(除了前麵提到的那些)。

strat souls
基本的問題域級類注:這些名稱是我自己編的。
在最低級之上的是基本的問題域級。它利用最低級的類(準確地說,是通過它們提供的公共功能)來製作更高層次的便利功能。就問題域而言,在其之上的層次繼續分類。
但“問題域”到底是什麼?因為我的遊戲是一款RTS,所以我的功能是按照TurnToTargetUnit()構建的,而不是RotateToVector()。
我現在是按照RTS遊戲思的,不是基礎的3D係統。那是因為我的問題域是即時策略。所以我的代碼是根據“即時策略”的概念寫的。它提供的功能允許調用者不需要知道底層3D世界和物理係統,或至少不是那麼需要。
這一級隻包含一個類,就是Unit,它作為各種最低級類的外觀類。沒有繼承性。Unit隻有已存在的UnitMovement, UnitAnimation, UnitNetwork等的變量。
Unit類有公共功能如:
MoveToPosition或MoveToUnit:使用UnitMovement移動,使用UnitAnimation播放移動動畫。
DoMeleeAttack:播放近戰動畫(通過UnitAnimation),開啟近戰碰撞。當處於多人模式時,它還保證這個活動通過網絡重複播放(通過UnitNetwork)。
IsNearPosition或IsNearUnit:使用UnitMovement檢查某個單位是在附近位置。
IsFacingUnit:使用UnitMovement檢查某個單位是否麵對另一個單位。
IsNearAndFacingUnit:組合這兩個單位的便利功能。
IsDead/IsAlive:使用Health類來檢查某單位是生或活。
GetAllEnemiesNearMe:返回在某個範圍內的敵人單位數列,並按接近程度分類。
……
如你所見,Unit隻是順從最低級類。Unit就像樂隊中的指揮,他並不演奏任何樂器,但協調其他演奏樂器的人。他告訴他們該做什麼,什麼時候做。

strat souls
中級類在基本的問題域級上添加基本的行為。
當你右擊某物時,中級現在直接對應單位的活動。因為我希望不同類型的單位做不同的事,所以我把這每一個類都分開了。
所以單體可以做不同類型的Action。我創建一個抽象的基類Action,Unit類有兩個Action變量,當你左擊地麵時,它使用一個變量;當你右擊敵人時,它使用另一個變量。
Action有不同的亞類:
ActionMove:隻會移動到被要求的目的地(使用Unit.MoveToPosition)。
注意,隻需要一次功能調用,Unit類就既能處理移動物理,也能負責播放移動動畫。
ActionMelee:單位移動到距離目標敵人足夠近的地方(調用Unit.MoveToUnit),一旦單位到達目的地(遊戲邦注:繼續查看Unit.IsNearAndFacingUnit的返回值),就會保持攻擊狀態(調用Unit.DoMeleeAttack)直到目標敵人活亡(查看Unit.IsDead的返回值)。
另外,注意調用所有那些功能會保證動畫正確播放、通過網絡的移動和攻擊保持同步,我們不用擔心不能達到同步,因為最低級能處理同步問題。在這一級,我們隻關心單位的行為,而不是低級細節如設置速度或協調兩段動畫。
還有其他東西如ActionRangedAttack或ActionDashAttack(在我的遊戲中,這是Bone Wheel使用的),也許還有ActionBuffAlly,等等。
所以現在我通過組合Unit類提供的公共功能得到更高級的行為。我的Unit類代碼組合不同的低級工具最終做出實用的東西。
另外,我可以給單位賦上不同的Action。這意味著我可以通過交換近戰單位使用的Action,把它轉變成遠程單位。
注:那些Action類如何知道要求的目的地或目標是什麼?這是由我的Order singleton類負責的。它隻要知道鼠標光標所在的3D X、Y、Z位置,或鼠標控製的單位,它就會與UnitSelector singleton交流,把命令告訴所有當前選中的單位(當檢測到右擊事件時)。但當命令發布時,各個單位到底做什麼,取決於它使用的Action亞類。
最高級類添加玩家通常期望的行為。在這短短的7天裏,我其實沒有時間執行這個。以下是一些想法:
侵略狀態跑到距離最近的敵人麵前攻擊它,直到對方活亡。然後再跑到另一個最接近的敵人旁邊,再攻擊它至活。這整個過程一直循環到它找不到任何敵人。
聽起來很複雜,但實際上,我隻是讓單位對它看到的任何敵人使用ActionMelee,直到它在它的範圍內再也找不到敵人。
防守狀態打擊任何攻擊它的敵人,但如果敵人撤退或離開它的攻擊範圍,它便不會再追趕。我還要為此做一個Unit.OnBeingAttacked(Unit attacker)。
警戒狀態(隻適用於遠程單位)盡可能與敵人保持能夠進攻敵人的最遠距離。這意味著當它距離目標太近時,它將往後移動;當它距離目標太遠時,它將往前移動。

strat souls
反思也許我應該把中級和最高級合並成一個級。我可能本應試寫出代碼的,這樣我就可以使用我的行為樹插件來執行這種高級行為了,但當然,我隻有7天時間,時間用花了,所以把這些行為硬編碼成那些Action類還是有好處的。
事實上我還少了一些比較基本的東西,如遠程攻擊、單位隊形、尋徑、特殊技能(如魔法)等。這些取決於我希望《Strat Souls》達到的設計程度吧。
我還沒給遊戲添加建築,我想我可能得再次修改代碼。
我還應該做一個分開的UnitAttack類作為最低級類的一部分。它可以處理近戰碰撞和啟動投射動作。不過,我沒有時間執行合適的運程攻擊,所以我隻能做到那樣了。
各個級都為上一級提供便利的功能。
這麼做有兩個好處:
最小化修改的影響:修改一個級的代碼時不必大改其他級的代碼(有時候甚至完全不必修改),最小化重寫工作、人為錯誤和漏洞。因為遊戲的開發本來就是一個重複製作的過程(你要不斷改進原型),所以反複修改代碼是不可避免的。
例如,如果你打算切換到CharacterController而不是Rigidbody,會怎麼樣呢?你知道必須修改的唯一一個類是UnitMovement類;它是唯一一個處理物理係統的類。所以你不必擔心你的其他類是否需要修改。如果你想切換到Photon networking library,又怎麼樣呢?你隻要修改UnitNetwork類。
這就使得我可以很輕易地轉換出遊戲的不同的亞係統。如果你必須要,你可以把你的代碼移植到2D遊戲引擎,並且隻需要大改低級類。
管理複雜度:像這種結構更易於思考,因為你通常一次隻需要處理一個級。當你寫代碼時,比如說巡邏行為,你不必擔心剛體、速度或碰撞之類的東西。你隻要使用你在Unit類中做的便利功能就行了。
如果你確實需要製作新的便利功能,那麼在你修改低級層時,你同樣不必擔心(太多)高級行為。
總有時候你必須反複修改所有級的代碼,特別是當你的係統還不穩定的時候(當你仍然在改進原型時)。但一旦你確定你希望單位如何表現,那麼你通常隻要一次處理一個級。為大腦減輕了不少負擔啊。
給單位的類是不是太多了?
當然要這麼多。單位是遊戲的關鍵部分,所以根據單位做出這麼龐大的係統是應該的。
但這樣會降低遊戲速度吧?
通常來說,不會。這個概念本身就不應該影響你的遊戲的幀速率,但如果你確實要在緊湊的循環中執行非常複雜的計算,那麼當然會有一些延遲。
至於其他的事,在你指點其他東西前先看你的代碼表現吧。記住,你的代碼是按級分開的。當你的發現阻塞,你可以優化有問題的部分,並不會太影響其他級的代碼。
聽起來還是很複雜。
你之後會了解為什麼這麼做是徝得的。
就像我一樣。
無論如何,我解釋的代碼隻是讓遊戲係統的結構清楚的方法之一。也有可能,你的遊戲更簡單,所以你的執行方法也更簡單。
但無論你的執行方法是什麼,底層概念應該總是一樣的:鬆耦合&模塊化(注:也就是可以輕易地從一個亞係統切換到另一個)、抽象(通過使用便利功能隱藏低級細節)和封閉(不允許你的高級類直接幹擾低級類的變量)。
聲明:我其實沒有接受過軟件工程的正規教育,我還在學習中。所有知識都是通過網絡、朋友討論和看書學來的。