Site icon 8bit.media

使用 AssetPostprocessor 和 Blender 在 Breachers 中進行快速設計迭代

Spread the love

https://blog.unity.com/games/rapid-design-iteration-in-breachers-using-assetpostprocessor-and-blender

Triangle Factory 是一家發展迅速的比利時遊戲公司,該公司使用 Unity 開發高質量的多人 VR 遊戲,例如Hyper Dash及其最新遊戲Breachers。Triangle Factory 利用 Cinemachine、Unity Profiler、 Game Server Hosting、Matchmaker、Voice Chat (Vivox)Friends等工具為玩家創造身臨其境的體驗。

在這篇博客中,Jel Sadones、首席關卡設計/技術美術和首席開發人員 Pieter Vantorre 向我們介紹了他們從 Blender 到 Unity 的流程,以及他們如何將他們的 VR 戰術 FPS 遊戲 Breachers 變為現實

十多年來,Unity 一直是我們的首選引擎和開發環境,多年來我們經歷了許多環境建模和設計工作流程。這包括使用像 ProBuilder 這樣的引擎內建模工具(我們仍然使用它並喜歡它來進行快速原型製作)以及從其他建模包中創建的預製件中組裝場景。不過,對於我們當前的項目,我們採用了一個工作流,在該工作流中我們在 Blender 中建模和組織關卡,並依靠 Unity 的AssetPostprocessor將它們集成到我們的 Unity 項目中。

在本文中,我們將與您分享我們如何完成這個工作流程,以及它如何支持我們遊戲所需的快速設計迭代。

找到正確的工作流程

2021 年,我們發布了第一個大型 VR 遊戲Hyper Dash,這是一款快節奏的 5v5 競技場射擊遊戲。當我們在 2019 年開始開發遊戲時,我們有一個基本的從 Blender 到 Unity 的工作流程,這對許多人來說可能看起來很熟悉:我們只是在 Blender 中建模幾何體,將我們的資產導出為 FBX 文件,然後手動將它們集成到 Unity 中。手動集成涉及幾個步驟:

  • 在場景中設置動態對象,例如武器拾取器、重生門、捕獲點
  • 放置碰撞器以防止玩家在某些區域行走或傳送
  • 設置隱形指南以允許機器人正常運行
  • ETC。

超級短跑 (2021)

此過程適用於較小的項目,但隨著項目的擴展和發展,很快就會變得麻煩。當我們開始計劃下一款遊戲的開發時,我們知道我們需要大幅改進工作流程。

使用原型識別痛點

Breachers是一款競爭激烈的射擊遊戲,具有復雜的關卡佈局、更微妙的遊戲機制、更多的技術系統,以及針對最新一代獨立 VR 硬件的更高水平的圖形潤色。就複雜性而言,它比Hyper Dash更進一步,我們很快就感受到了它對我們工作流程的影響。

在原型設計階段,我們仍然嚴重依賴動態對象的預製件,例如窗戶路障。這些是我們放置在窗框內的物體,用於阻擋內部和外部之間的視線,以防止團隊在比賽熱身階段看到對方。

在測試我們的原型時,我們不斷地在窗口周圍移動以改進遊戲玩法,這意味著在 Blender 中更改幾何形狀並重新導出到 Unity,然後手動移動路障對像以匹配我們的更改。很多小時都花在了 Unity 的場景視圖上,手動檢查和修復這些東西。儘管如此,我們進行了不止一次遊戲測試,我們只是在遊戲過程中才注意到有些東西被忽略了。

製作藏身處關卡的原型

藏身處的最終版本

顯然,這個工作流程不會讓我們能夠在我們進行遊戲測試時快速迭代我們的地圖設計,無論是在內部還是作為我們開放 alpha 的一部分,我們計劃免費提供一張地圖以從社區獲得反饋。我們期待所有這些反饋,但根本不期待將其應用於我們的地圖所涉及的手動工作。

基於預製件的設計工作流程的另一個潛在缺點是性能。我們主要針對我們遊戲的移動、獨立 VR 耳機。我們想盡可能地推動視覺效果,因此我們需要從我們的工作流程中榨取最後一點性能。

從預製件組裝關卡的效率可能低於在建模程序中創建防水網格。如果將兩個模塊化牆塊拼接在一起,它們之間總會有一個未合併的幾何循環。使用預製件,也很容易在場景中放置大量不可見的幾何體(因為它位於物體的底部,或靠牆放置),但仍會佔用寶貴的光照貼圖空間。在整個關卡中,這些小的低效率會累積起來導致性能浪費和視覺效果下降。

原型期間沙盒級別的可破壞窗口

我們要提及的預製件的最後一個問題是,通過在 Blender 中對源模型應用看似無害的更改(例如重命名對象),很容易破壞事物。隨著遊戲或關卡的發展,您通常希望重新組織您的資產並賦予它們改進的或更一致的名稱。但是在 Blender 中重命名對象並重新導出它很容易(並且沒有警告)破壞 Unity 中對對象所做的覆蓋和添加,從而導致回歸。

在這個簡化的示例中,我們有一個通風格柵預製件,並希望從中散發出煙霧。將網格導入 Unity 後,我們的美術師將煙霧粒子系統添加為子對象,並向預製件添加表麵類型組件以將其標記為金屬對象。

在這裡你可以看到如果我們在 Blender 中重命名我們的網格會發生什麼:

使用更新後的名稱重新導入網格時,Unity 無法再通過名稱找到舊網格,因此它會從模型預製件中刪除該對象。這個被移除對象的子對像被移動到預製件的根目錄並且現有腳本被移除,再次導致我們寧願避免的手動清理工作。

為生產流水線設定明確的目標

隨著Breachers的原型設計階段結束,我們準備在 2022 年初進入全面生產模式,我們的藝術和開發團隊坐下來研究我們可以做些什麼來解決這些問題。我們為理想的資產管道定義了明確的目標,該目標將支持Breachers所需的快速靈活的迭代:

  • 關卡幾何體的所有創建和修改都應該在 Blender 中進行。
  • 所見即所得:設計師在 Blender 中創建的內容應與 Unity 中的結果盡可能接近。
  • 當 Blender 中有更新時,將更改導入 Unity 應該會自動發生,不需要任何手動操作。

讓 Blender 愛上 Unity

如上所述,我們的主要目標是在 Blender 中對遊戲進行準確的可視化——不僅要正確反映最終結果在 Unity 中的外觀,還要正確反映遊戲機制的設置方式。Breachers中的遊戲玩法不僅取決於關卡佈局,還取決於動態對象(如可破壞的牆壁)和不可見元素(如音量和碰撞器)。我們希望所有這些信息在設計階段都可見,並準確地傳遞到 Unity。

我們在 Blender 中的摩天大樓關卡的一部分(僅限靜態關卡幾何和道具)

添加了動態對象的相同場景

導入 Unity 的相同場景,在光照烘焙之前

Unity 中的最終場景

自定義屬性和 Unity 的 AssetPostprocessor

自定義屬性對我們的工作流程至關重要,我們將這些屬性分配給 Blender 中的對象。然後這些通過 FBX 格式在 Unity 中進行,因此我們可以讀取它們並在我們的資產導入 Unity 時運行自定義邏輯。

在 Blender 中分配給對象的自定義屬性示例

這給了我們很大的靈活性和穩定性。這些屬性與整個管道中的對象保持連接,因此我們可以根據需要在我們的關卡中重新組織和重命名事物,而不必擔心事物中斷或不同步。

Unity 有一個名為 AssetPostprocessor 的強大類,它允許在導入資產時對其進行修改。這就是我們在導入時用來解析這些自定義屬性並對其進行操作的方法。

用例

預製鏈接

我們有一個名為 PrefabLink 的自定義屬性,它告訴 Unity 從 Blender 導入的對象應該替換為 Unity 項目中已有的預製件,同時保留導入模型的轉換。這使我們能夠將這些動態對象放置在 Blender 中,同時在將它們導入 Unity 後保留預製件的優勢。上面 Blender 場景中的窗口路障就是一個很好的例子。

表面類型

表面清晰度在Breachers中極為重要。在金屬樓梯上行走的聲音與在混凝土地板上行走的聲音不同。子彈穿透木頭與穿透鋼鐵有很大不同。並且每種表麵類型都有其自身的影響效果。在 Unity 中檢查每個道具並將其標記為正確的表麵類型將非常耗時,因此我們也在 Blender 的設計階段通過在我們的幾何碰撞器上設置自定義屬性來解決這個問題。

靜態標誌

另一個重要的優化設置是 Unity 的靜態標誌。正確設置這些會對可見性剔除、光照烘焙和批處理等事情產生深遠的影響。使用 Blender 中的自定義屬性,我們可以在關卡的任何部分設置這些屬性,包括可重複使用的道具,並將這些信息傳遞到關卡中的 Unity 中。

對撞機

最後,我們想分享一下我們如何設置對撞機。Unity 有一個簡單但有效的系統,當您使用 _LOD0、_LOD1 等後綴模型資產名稱時,它會自動檢測模型的細節級別變體。我們受此啟發並為碰撞器創建了一個類似的系統:通過簡單地使用幾何體名稱中的 _BoxCollider 或 _NoCollision,我們將 Blender 中的網格替換為 Unity 中的碰撞器。

觀察 Blender 中的 _BoxCollider 和 _NoCollision 名

標記為 _BoxCollider 的對像在 Unity 中被轉換為實際的 BoxCollide

代碼示例

作為一個具體示例,這是我們的 LevelSetupPostprocessor 的一個片段,它讀取自定義屬性並為每個導入的對象分配正確的靜態標誌:

public class LevelSetupPostprocessor : AssetPostprocessor
{
    // Dictionary of each object that is using a custom property.
	private readonly Dictionary<string, (string[], object[])> _userPropertyMap = new ();

    // List of all the custom properties we support
	private static readonly string[] SupportedPropNames = new []
	{
		"Surface",
		"Layer",
		"PrefabLink",
		"Collision",
		"StaticFlags",
		"LightmapScale",
		"LightMeshPreset"
	};

    // Unity Event from AssetPostprocessor
    // Called for each object in the model
	private void OnPostprocessGameObjectWithUserProperties(GameObject go, string[] propNames, object[] values)
	{
        // Check if the custom properties contain any that we are interested in and add them to the dictionary.
		if (SupportedPropNames.Select(x => x.ToLowerInvariant()).Intersect(propNames.Select(x => x.ToLowerInvariant())).Any())
		{
			_userPropertyMap.Add(go.name, (propNames, values));
		}
	}

	// Unity Event from AssetPostprocessor
	private void OnPostprocessModel(GameObject model)
	{
		// For each of the discovered custom properties,
		// find the corresponding gameobject in the Model Prefab Variant
		// and apply the appropriate logic
		for(int i = _userPropertyMap.Count -1; i >= 0; i--)
		{
			var kvp = _userPropertyMap.ElementAt(i);
			GameObject go = FindGameObjectInHierarchy(model, kvp.Key); // searches the model's children by name
			string[] propNames = kvp.Value.Item1;
			object[] values = kvp.Value.Item2;
			for(int j = 0; j < propNames.Length; j++)
			{
				object value = values[j];
				switch (propNames[j])
				{
					case "staticflags":
						HandleStaticFlags(go, value);
						break;
					// ...
				}
			}
		}
	}

    // Applies StaticFlags on the object based on custom properties from Blender
	private void HandleStaticFlags(GameObject go, object value)
	{
		string[] staticFlags = value.ToString().Split(',');
		StaticEditorFlags activeFlags = 0;
		for(int i = 0; i < staticFlags.Length; ++i)
		{
			string flag = staticFlags[i].ToLower().Trim();
			switch (flag)
			{
				case "batching static":
					activeFlags |= StaticEditorFlags.BatchingStatic;
					break;
				// ...
				default:
					LogWarning($"Unknown static flag {flag} detected when importing {go.name}", go);
					break;
			}
		}
		GameObjectUtility.SetStaticEditorFlags(go, activeFlags);
	}
}

自定義 Blender 以更好地與 Unity 配合使用

為了讓這一切順利進行,我們也必須在 Blender 方面做一些工作。

自定義屬性有點隱藏在 Blender 的 UI 中,並且需要藝術家每次都手動輸入自定義屬性,這不是很好的用戶體驗。依賴手動文本輸入也很容易出錯,首先會抵消在 Blender 中進行設置的大部分優勢。從基於預製件的工作流程轉移到 Blender 也讓我們錯過了預製件的一些優勢,比如擁有一個很好的對像庫來瀏覽和挑選。幸運的是,Blender 與 Unity 一樣,非常靈活且易於擴展。

攪拌機資產庫

Prefab 組織問題的答案出現在帶有資產庫的 Blender 3.2 中。這個系統有點像 Unity 中的 Prefab 系統:它允許您在單獨的文件中創建資產,然後將它們導入到您的 Blender 場景中,而資產文件中的更改會自動反映在 Blender 場景中。此外,它確保任何自定義屬性或碰撞器都正確應用於 Blender 中此資產的每個實例。

我們在 Blender 中的道具庫的一部分
所有使用 PrefabLink 自定義屬性的動態對像類型

自定義攪拌機插件

對於 Blender,我們編寫了一個內部插件來幫助在更清晰的用戶界面中設置自定義屬性。這簡化了自定義屬性的設置,只需選擇相關的 Blender 對象並點擊一個按鈕,而不是手動輸入每個屬性。

Bundle Exporter 插件是一個開源插件,我們用它來一鍵導出所有 FBX 文件。我們對其進行了修改,使其也可以使用自定義屬性,並更新了 UI 以更快地導出以滿足我們的特定需求。

結論

為Breachers設置我們的關卡設計工作流程最初花費了大量時間,但我們相信這是該項目的正確選擇。而且,這很有趣!

由於我們從最初的封鎖到 alpha 測試以及最終發布前的幾個月都在構建遊戲,因此我們的關卡迭代快速而輕鬆。我們已經能夠為我們的設計師和藝術家消除開銷和繁重的工作,同時也將他們以前需要開發人員的責任轉移給他們。

我們對 Unity 和 Blender 如此順利地相互集成的能力印象深刻,我們堅信這種集成對於讓Breachers成為一款我們很高興並自豪地與世界分享的遊戲 至關重要。

感謝閱讀,祝遊戲愉快!

Triangle Factory 的破壞者現已上市在此處查看 Made with Unity 開發人員的更多博客。

Exit mobile version