讓角色半透明:樹形結構(三)

上一篇文章,我們已經在技術上完成了淡入淡出角色。但實際應用中,發現一些問題。

「懸空」的盒子

女孩拿起盒子,相機拉進,盒子「懸空」了。這是因為,我們將角色、物體都當作獨立物體對待,他們無法形成統一的整體。為了解決這個問題,必須提供一種方法,在運行時可以將它們組合起來,作為整體對待。

為什麼使用樹

如果將女孩看作主體,拿在手上的盒子對她而言是從屬關係。將女孩、盒子看作樹的節點,那麼女孩拿起盒子時,就相當於把盒子節點連接到女孩節點下。類似的,在其他遊戲中角色會披麻戴孝——哦不,帶上很多裝備和武器,就是將各種裝備、武器節點連接到角色節點下。這種組織方式十分靈活。

我們以《MGSV》為例來思考這件事。

Snake 帶著武器、富爾頓氣球駕駛著 D-Walker

一開始 Snake 啥也沒帶,他自己是孤立節點(也就是樹根),他負責自己的淡入淡出處理(整棵樹);

後來他呼叫支援,裝備上了武器和富爾頓氣球,武器和富爾頓氣球要和 Snake 看起來是一體的,因此以 Snake 為父節點,Snake 依然是樹根,現在他負責整棵樹的淡入淡出處理;

再後來 Snake 跳上一台繳獲的 D-Walker 開始駕駛,現在他們要看起來是一體的,因此 Snake 以 D-Walker 為父節點,D-Walker 成了樹根,由它負責整棵樹的淡入淡出處理;

最後 Snake 離開將要爆炸的 D-Walker,Snake 又變成了樹根。

從這個過程來看,一個樹根的所有子節點組成一個整體,樹根應該以相機到所有子物體中最短的距離來處理淡入淡出,也只有樹根才需要執行這個邏輯。

使用 Foreach 遍歷樹

我們需要的樹具備連接和斷開父節點、遍歷子樹等功能。翻開年久失修的代碼,遍歷子樹的實現用的是遞歸,遍歷處理通過傳遞 Action 實現。

public void TraverseChildren(Action> action) { action(this); if (_firstChild != null) { var node = _firstChild; do { node.TraverseChildren(action); node = node._next; } while (node != _firstChild); } }

這種方法難以處理元素之間的關聯,比如找到最短距離。雖然通過閉包可以做到,但遊戲每幀更新會產生垃圾。要是能像 List 之類可以 foreach 就好了。在 C# 中,實現 IEnumerable 就可以 foreach,因此……我們其實不需要實現這個介面(如果你了解 IEnumerable、IEnumerator 接下來可能知道我在說什麼,否則會一頭霧水)。

鴨子類型:不關心這貨是不是鴨子,只關心它會不會呱呱叫。意思是,我們只需要寫個叫 GetEnumerator 的方法,而不用實現 IEnumerable 介面就可以實現 foreach 了!這是 C# 編譯器提供的強大特性,可以用來避免不必要的裝箱。下面我們就要用到。

GetEnumerator 需要返回一個 IEnumerator,這個對象在每次進入 foreach 的時候都會臨時創建一個。對於頻繁遍歷的需求,如果這個對象是引用類型,會產生大量垃圾。但是介面一定會是引用類型呀?所以同理,我們也不實現 IEnumerator 介面,利用鴨子類型的特性,我們在自己的 struct 類型 Enumerator 中直接添加 Current、MoveNext 即可。

先看一個簡單例子,實現遍歷所有父級節點:

public struct ParentsEnumerable { TreeNode _node; internal ParentsEnumerable(TreeNode node) { _node = node; } public ParentsEnumerator GetEnumerator() { return new ParentsEnumerator(_node); } } public struct ParentsEnumerator { TreeNode _node; TreeNode _current; internal ParentsEnumerator(TreeNode node) { _node = node; _current = null; } public T Current { get { return _current.value; } } public bool MoveNext() { if (_current == null) { _current = _node; return true; } _current = _current._parent; return _current != null; } } public ParentsEnumerable parents { get { return new ParentsEnumerable(this); } }

然後就可以這樣使用:

foreach (var parent in node.parents) { ... }

類似的,更複雜的遍歷所有子節點的 ChildrenEnumerator 是這樣實現的:

public struct ChildrenEnumerator { TreeNode _node; TreeNode _current; int _state; internal ChildrenEnumerator(TreeNode node) { _node = node; _current = null; _state = 0; } public T Current { get { return _current.value; } } public bool MoveNext() { switch (_state) { // root case 0: _state = 1; _current = _node; return true; // first child case 1: if (_current._firstChild != null) { _current = _current._firstChild; return true; } if (_current != _node) { _state = 2; return MoveNext(); } return false; // right brother case 2: if (_current.next != null) { _state = 1; _current = _current._next; return true; } if (_current._parent != _node) { _current = _current._parent; return MoveNext(); } return false; } return false; } }

結語

至此,我們完成了整個讓角色靠近相機淡入淡出的系統。實現中還有很多細節,比如多相機處理、編輯器支持、物體連接和斷開保持平滑過渡等,這些細節沒有什麼高深的原理,卻影響著最終產品的品質。

(遊戲:原生體 Protoform)


推薦閱讀:

unity3d在android使用sqlite
行為樹對於遊戲的意義
針對中小遊戲的Unity3D插件推薦
Dice (EA) 工作室遊戲開發技術概覽

TAG:遊戲編程 | 遊戲開發 |