Phase 5: Editor 工具大補包 — 樹狀檢視與字典編輯器的誕生

這次做了什麼?

這次的重點從 Runtime 核心功能轉向了 Editor 工具開發,一口氣做了好幾個很實用的編輯器小工具。

為什麼突然轉向做 Editor 工具呢?其實是因為在整理文件時,發現有不少編輯器相關的模組還沒處理,而且這些工具彼此之間相對獨立,很適合分批完成。另一個原因是,這些工具都蠻有趣的 — 可以直接在 Unity 裡看到效果,比起寫抽象的資料結構來得有成就感。

這次主要做了兩大塊:

1. 樹狀結構檢視器 (DirectoryNode + DirectoryTreeDrawer)

首先是一個可以顯示檔案/資料夾樹狀結構的工具。這個工具包含兩個部分:

  • DirectoryNode — 不可變的樹狀資料結構,用來表示目錄階層
  • DirectoryTreeDrawer — 在 Editor 視窗中繪製樹狀檢視的繪製器

這個工具可以讓你把一堆檔案路徑轉換成樹狀結構,然後在 Unity 編輯器裡用 Foldout 的方式展開/收合瀏覽。每個資料夾和檔案都有對應的 icon,而且可以點擊觸發回呼事件。

2. 可序列化字典與 Inspector 繪製器 (SerializeDic + SerializeDicDrawer)

Unity 原生不支援序列化 Dictionary,所以我做了一個 SerializeDic — 一個可以在 Inspector 裡直接編輯的字典類型。

但光有資料結構還不夠,還需要一個好用的 PropertyDrawer 來顯示它。所以我做了 SerializeDicDrawer,讓你可以在 Inspector 裡用很直覺的方式新增/刪除/編輯 key-value 配對。

3. 其他小改進

除了這兩個主要功能外,還順手整理了幾個小工具:

  • LongDataParser — 長資料切分器,用更簡潔的邏輯重寫了切分演算法
  • EditorKit Icon 封裝 — 把 icon 相關功能包裝成公開 API,支援各種常見檔案類型的 icon

用了哪些技術?

Unity EditorGUI 與 IMGUI

這次用到的核心技術是 Unity 的 IMGUI (Immediate Mode GUI) 系統。雖然現在有 UI Toolkit 這種更現代化的解決方案,但對於編輯器擴充來說,IMGUI 還是最穩定、最相容的選擇。

IMGUI 的概念很特別 — 它是「立即模式」,意思是每一幀都要重新繪製所有 UI。這跟一般的「保留模式」UI 系統很不一樣。保留模式會建立 UI 物件然後保存在記憶體裡,IMGUI 則是每次都重新呼叫繪製函數。

聽起來很沒效率對吧?但其實在編輯器環境下,這種模式反而很方便,因為你不需要管理 UI 物件的生命週期,也不用處理資料同步的問題。

EditorGUILayout.Foldout 的展開/收合狀態管理

樹狀檢視最重要的就是 Foldout — 那個可以點擊展開/收合的箭頭。Unity 提供了 EditorGUILayout.Foldout 這個 API,但問題是:狀態要存在哪裡?

我的解決方案是用一個 Dictionary<DirectoryNode, bool> 來記錄每個節點的展開狀態。當使用者點擊 Foldout 時,就更新這個字典裡對應的值。這樣就算重繪 UI,展開狀態也能保持。

Unity EditorGUIUtility.IconContent 的坑

原本我想用 Unity 內建的 EditorGUIUtility.IconContent 來取得檔案 icon,但遇到一個很討厭的問題 — icon 會被自動縮放

Unity 的 icon 系統會根據 GUI 的 DPI 設定自動調整大小,結果就是 icon 變得超級小,跟文字完全對不齊。我試了半天,發現沒辦法強制指定 icon 大小。

最後的解決方案是 手動繪製 icon。我用 GUI.DrawTexture 自己畫一個 16x16 的方框,把 icon 貼上去。這樣就能完全控制大小和位置,不會被 Unity 的自動縮放干擾。

PropertyDrawer 的序列化陷阱

在做 SerializeDicDrawer 時,遇到了一個很神秘的 bug — 當我新增一個 key-value 配對時,如果這個 key 已經存在,新增的項目會憑空消失

一開始我以為是我的繪製邏輯有問題,檢查了半天才發現:原來是 Unity 的序列化系統在搞鬼

Unity 在序列化時會偵測到有重複的 key,然後就直接把重複的項目丟掉,連警告都不給你。所以當我新增一個預設的 “NewKey” 時,如果之前已經有一個 “NewKey” 了,新的那個就會被默默刪除。

解決方法是在新增時自動產生 不重複的 key。如果 “NewKey” 已存在,就改成 “NewKey1”、“NewKey2” 這樣遞增,直到找到一個沒用過的 key 為止。

不可變資料結構的設計

DirectoryNode 採用了 不可變 (Immutable) 的設計 — 所有欄位都是 readonly,建立後就不能修改。

為什麼要這樣做?因為樹狀結構如果允許修改,很容易出現意外的副作用。比如說,你在 A 地方修改了某個節點,結果在 B 地方引用到同一個節點就會受到影響。

不可變設計可以避免這個問題。如果你需要「修改」一個節點,實際上是建立一個新的節點。這樣就不會有意外的副作用,而且可以很安全地在多個地方共用同一個樹狀結構。

當然代價是會產生一些額外的記憶體分配,但對於編輯器工具來說,這點開銷完全可以接受。

遇到什麼挑戰?

挑戰 1: Icon 大小對不齊的 UI 災難

最痛苦的就是調整 icon 和文字的對齊。一開始用 Unity 內建的 GUILayout.Label 顯示 icon 和文字,結果 icon 超級小,而且怎麼調都對不齊。

我試了好幾種方法:

  1. 調整 GUIStyle — 沒用,icon 還是被縮放
  2. 用 GUILayout.Width 限制寬度 — 可以控制整體寬度,但 icon 大小還是不對
  3. 用 EditorGUILayout.BeginHorizontal 手動排版 — 好一點了,但還是有微妙的偏移

最後發現根本原因是 Unity 的 IconContent 會自動縮放 icon。唯一的解法就是 不要用 IconContent,改用 GUI.DrawTexture 手動畫

這樣做雖然麻煩一點,但好處是可以完全控制 icon 的大小和位置。我把 icon 固定為 16x16,然後手動計算位置,確保跟文字對齊。問題終於解決了!

挑戰 2: 樹狀縮排的視覺層次

樹狀結構需要用縮排來表現階層關係,但縮排太少看不出層次,縮排太多又會浪費空間。

一開始我用 EditorGUI.indentLevel 來控制縮排,這是 Unity 推薦的做法。但問題是 indentLevel 是全域變數,如果你在遞迴繪製時不小心忘記還原,就會影響到後續的 UI。

後來我改用 手動計算縮排距離。每層增加 20 像素,用 GUILayout.Space(depth * 20) 來繪製空白。這樣就不會有全域狀態的問題,而且可以精確控制每層的縮排量。

挑戰 3: 檔案類型 Icon 的覆蓋率

在實作 GetFileIcon 時,我發現要支援所有常見的檔案類型還蠻麻煩的。Unity 有上百種檔案格式,每種格式的 icon 名稱都不太一樣。

一開始我只處理了常見的 .cs.shader.png 這些,結果測試時發現 .fbx.renderTexture 都沒有對應的 icon,直接顯示預設圖示,看起來超突兀。

所以我花了點時間整理了一份 檔案類型對照表,涵蓋了:

  • 程式碼: .cs, .shader, .shadergraph, .cginc, .hlsl
  • 3D 模型: .fbx, .obj, .blend, .ma, .mb, .max
  • 音訊: .mp3, .wav, .ogg, .aif, .aiff
  • 圖片: .png, .jpg, .jpeg, .psd, .tga, .exr, .hdr
  • 動畫: .anim, .controller, .mask, .overrideController
  • 影片: .mp4, .mov, .avi, .webm
  • 字型: .ttf, .otf, .fnt
  • 特殊: .prefab, .mat, .asset, .unity, .renderTexture

這樣就算遇到比較少見的檔案類型,也能顯示正確的 icon 了。

挑戰 4: 字典的重複 Key 處理

在做 SerializeDicDrawer 時,遇到了前面提到的「重複 key 消失」問題。但更麻煩的是:怎麼讓使用者知道有重複的 key?

如果只是默默地把 key 改成 “NewKey1”、“NewKey2”,使用者可能會覺得莫名其妙:「我明明新增了一個項目,怎麼 key 變了?」

所以我加了一個 容錯機制 — 在字典類別裡檢查重複 key,如果發現重複就在 Console 印出警告訊息。這樣使用者至少會知道有問題發生,可以手動修正。

理想的做法應該是在 Inspector 裡用紅色或黃色標示重複的 key,但這需要更複雜的 UI 繪製邏輯。目前先用 Console 警告解決,以後有時間再改進。

挑戰 5: 測試視窗的路徑選擇器

一開始測試 DirectoryTreeDrawer 時,我把路徑直接寫死在程式碼裡。這樣每次要換路徑測試都得改程式碼、重新編譯,超級麻煩。

後來我發現 EditorKit 裡有一個 FolderPathField 可以用 — 這是一個可以選擇資料夾路徑的 UI 元件,點下去會跳出檔案瀏覽視窗。

用了這個元件後,測試視窗變得超方便。選完路徑會自動刷新樹狀檢視,而且會自動過濾掉 .meta 檔案,只顯示真正的專案檔案。整個測試流程變得非常流暢。

學到了什麼?

經驗 1: Unity Editor 的 Icon 系統有自己的規則

Unity 內建的 icon 系統雖然方便,但它有自己的一套縮放邏輯,不一定符合你的需求。如果你需要精確控制 UI 排版,不要怕手動繪製 — GUI.DrawTexture 雖然原始,但可以給你完全的控制權。

經驗 2: 不可變資料結構在 UI 工具中的價值

一開始我覺得不可變設計有點多此一舉,畢竟這只是個樹狀結構,改來改去應該也不會有什麼問題。但實際使用後發現,不可變設計讓整個系統變得 可預測

你不需要擔心「這個節點會不會在其他地方被改掉」,也不需要做防禦性複製。一旦建立就不會變,這種確定性讓 debug 變得容易多了。

經驗 3: PropertyDrawer 要考慮序列化的副作用

Unity 的序列化系統很聰明,但有時候太聰明反而會帶來困擾。它會自動偵測重複 key、自動清理無效引用,這些在大部分情況下是好事,但在某些特殊情況下會造成意外的行為。

寫 PropertyDrawer 時要記得:你看到的 UI 和實際的資料之間,還有一層序列化系統在中間。有時候資料不見了,不是你的繪製邏輯有問題,而是序列化系統把它過濾掉了。

經驗 4: 測試工具也要好好設計

以前寫測試視窗都很隨便,反正能用就好。但這次發現,如果測試工具設計得好,開發效率會提升很多。

像是加上路徑選擇器、自動過濾 .meta 檔案、選完自動刷新這些小細節,雖然只是測試用的功能,但可以讓你在調整 UI 時快速看到效果,不用一直改程式碼重新編譯。

好的測試工具可以加速開發迴圈,這點投資絕對值得。

目前進度

這次的提交包含了:

  • Commit 5e70808: DirectoryNode 樹狀結構與 DirectoryTreeDrawer 樹狀檢視繪製器 (5 檔案, 348 行)
  • Commit 1bf843f: LongDataParser 長資料切分器與 SerializeDic 可序列化字典 (6 檔案, 250 行)

加上之前的進度,Data 模組已經全部完成。目前整體狀態:

  • 已提交: 約 149 個 .cs 檔
  • 未提交: 約 36 個 .cs 檔

換算下來,大約完成了 80% 的檔案重寫工作 — 終點已經不遠了!

下一步

Editor 工具這塊還有一些小東西可以做,但不急著全部做完。接下來可能會:

  1. 回到 Runtime 核心功能 — 像是 Service、Event 這些還沒處理的重要模組
  2. 補完一些工具類別 — 數學、字串處理、檔案操作這些基礎工具
  3. 整合測試 — 確保重寫的模組跟舊版本相容,不會破壞現有專案

總之,編輯器工具的部分已經有了一個不錯的起點。接下來要把重心移回核心功能,確保框架的基礎穩固。

→ 繼續閱讀:[[專案/EAS Foundation/日誌/#06-UI元件大翻修-從零散工具到統一介面|#06 UI 元件大翻修]]


返回 EAS Foundation 專案主頁