ScriptableObject 作為 Dependency Injection 的工具

在 Unity 中,使用 Singleton Manager 是一種方便的設計模式,但它也有一些潛在的缺點和風險。如下

  1. 由於 Singleton 通常是全域可訪問的,這使得任何腳本都可以直接與其交互,導致程式碼之間的強耦合,使其難以測試以及維護
  2. 由於 Singleton 通常是靜態的並且在整個應用程式中持續存在,在單元測試時很難模擬或替換。
  3. 隨著功能增多, Singleton Manager 的代碼會變得臃腫,難以維護或擴展,可能導致其責任過多(違反單一職責原則,SRP)。
  4. 通常會使用 DontDestroyOnLoad 保持 Singleton 在場景切換中持久化,但這可能導致重複生成 Singleton (如果新場景中也有同樣的 Manager)導致出現錯誤或是多餘的資源佔用。
  5. 隱藏的依賴性,由於 Singleton 是全域性的,腳本中直接調用 Singleton 的方法可能隱藏了實際的依賴性,導致在代碼重構或移植時,可能很難理清某些功能到底依賴於哪些 Singleton

我們可以使用 ScriptableObject 作為 Dependency Injection 的工具,用來減少 Singleton Manager 的使用。
首先建立一個 Abstract Class

1
2
3
4
5
6
public abstract class BaseDataService : ScriptableObject
{
public abstract void SaveLevelData(LevelData levelData);

public abstract LevelData LoadLevelData(int level);
}

之後根據需要的實作去繼承這個 BaseDataService 類,例如你想要實作將存擋儲存到本機上的檔案中時,可以建立一個 FileGameDataService ,然後在該類實作這些方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[CreateAssetMenu(fileName = "FileGameDataService", menuName = "ScriptableObject/Data/GameDataService")]
public class FileGameDataService : BaseDataService
{
public override void SaveLevelData(LevelData levelData)
{
string json = JsonUtility.ToJson(levelData);
File.WriteAllText(Application.persistentDataPath + $"/level_{levelData.level}_{levelData.levelType}_{levelData.levelDifficulty}_data.json", json);
}

public override LevelData LoadLevelData(int levelIndex, LevelType levelType, LevelDifficulty levelDifficulty)
{
string path = Application.persistentDataPath + $"/level_{levelIndex}_{levelType}_{levelDifficulty}_data.json";
if (File.Exists(path))
{
string json = File.ReadAllText(path);
return JsonUtility.FromJson<LevelData>(json);
}
return null;
}
}

使用 Unity 編輯器建立一個 ScriptableObject。

在需要使用讀檔存檔方法的 MonoBehaviour 類中,加入 [SerializeField] private BaseDataService dataService 參考

1
2
3
4
5
6
7
8
9
public class GameManager : MonoBehaviour
{
[SerializeField] private BaseDataService dataService;

public void SaveGame(LevelData levelData)
{
dataService.SaveLevelData(levelData);
}
}

最後,把這個 FileGameDataService 設定上去即可。

Reference:

緩動函數 (Easing function)

緩動函數 (Easing function) 用於控制物件的速度變化。它們決定物件如何從開始到結束的進行方式,比如是否線性移動,或逐漸加速、減速,甚至彈跳等效果。
讓物件的移動看起來更生動。

  • 提升動畫的自然感:讓動畫看起來更流暢,避免機械化的線性運動。

  • 增強視覺效果:通過不同的速度變化,創造出多樣的動畫風格。

  • 改善用戶體驗:讓交互更生動,提升用戶對動畫的感知。

  • 緩入 (Ease In):物件從靜止逐漸加速 (從 0 開始加速)。

    • 適合物體從靜止開始移動的動畫。
  • 緩出 (Ease Out):物件從高速逐漸減速直到靜止 (減速到 0)。

    • 適合物體移動到目標位置後停止的動畫。
  • 緩入緩出 (Ease InOut):先加速後減速的平滑運動 (前半段從 0 開始加速,後半段減速到 0)。

    • 適合彈跳、滑動等動畫。
  • 線性運動 (Linear) : 等速運動,從開始到結束的速度完全一致。

    • 適合進度條。

緩動函數的圖示

  • 線性(Linear):一條直線。

  • 緩入(Ease-In):曲線從平緩到陡峭。

  • 緩出(Ease-Out):曲線從陡峭到平緩。

  • 緩入緩出(Ease-In-Out):曲線呈 S 形。

  • 彈性(Elastic):曲線有多次波動。

  • 反彈(Bounce):曲線有多次階梯狀下降。

  • Quad : 二次方曲線。

    • 適合簡單的加速或減速動畫,如按鈕點擊效果。
  • Cubic : 三次方曲線。

    • 適合需要更強烈速度變化的動畫,如頁面切換。
  • Quart : 四次方曲線。

    • 適合需要快速加速或減速的動畫,如彈出窗口。
  • Quint : 五次方曲線。

    • 適合極端的速度變化效果,如快速閃現的動畫。
  • Since : 平滑的正弦波曲線。

    • 適合需要柔和過渡的動畫,如淡入淡出效果。
  • Expo : 指數運動,起始或結束速度變化極快,幾乎瞬間到達或停止。

    • 適合需要極快速度變化的動畫,如閃電效果。
  • Circ : 模擬圓形曲線運動,速度變化平滑且自然。

    • 適合模擬圓形軌跡的動畫,如旋轉效果。
  • Bounce : 模擬物體彈跳的效果。

    • 適合模擬球體落地或彈跳的動畫,如按鈕點擊後的反饋。
  • Elastic : 模擬彈簧運動的效果,帶有回彈。

    • 適合模擬彈簧或彈性物體的動畫,如彈性按鈕。
  • Flash : 快速來回閃動的效果。

    • 適合需要快速閃動的動畫,如警告提示。

---
Quad
Cubic
Quart
Quint
Sine
Exponential
Circ
Elastic
Back
Bounce
- 集合在一張圖中 -

Reference:

Unity 資料(Data) 與 序列化(Serialization)

資料結構

在 Unity 中儲存資料時要選擇適合的資料結構來儲存,以下是 C# 常用的資料結構。

  1. 陣列 (Array):陣列適用於大小固定、元素類型一致的集合。例如,儲存固定數量的關卡分數或完成狀態。
    1
    2
    3
    int[] levelScores = new int[10]; // 用於存儲 10 個關卡的分數
    levelScores[0] = 100; // 第一關的分數
    levelScores[1] = 200; // 第二關的分數
  2. 列表 (List):列表是動態的,能在執行時調整大小,適合在需要新增或移除元素的情境。例如,動態管理角色的物品欄。
    1
    2
    3
    List<string> inventoryItems = new List<string>(); // 用於儲存物品清單
    inventoryItems.Add("Item1"); // 添加物品
    inventoryItems.Add("Item2"); // 添加物品
  3. 字典 (Dictionary):字典是鍵(Key)與值(Value)的儲存方式,適合需要快速查找的情況。例如,排行榜系統或配置表。
    1
    2
    3
    Dictionary<string, int> leaderboard = new Dictionary<string, int>(); // 玩家名稱對應分數
    leaderboard["Player1"] = 1000; // Player1 的分數
    leaderboard["Player2"] = 1500; // Player2 的分數
  4. 自定義資料結構:當內建資料結構無法滿足需求時,可設計自定義結構以應對特定場景。例如,設計角色屬性與技能系統。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class Character
    {
    public string Name;
    public int Health;
    public int Mana;
    public List<string> Abilities;

    public Character(string name, int health, int mana)
    {
    Name = name;
    Health = health;
    Mana = mana;
    Abilities = new List<string>();
    }

    public void AddAbility(string ability)
    {
    Abilities.Add(ability);
    }
    }

資料結構對效能的影響

  1. 優化記憶體使用
    選用適當的資料結構能減少不必要的記憶體浪費。例如:若需要儲存固定數量的元素,可使用 陣列 (Array) 而非列表 (List)。陣列因固定大小而佔用較少的記憶體空間。
  2. 提升存取與檢索速度
    不同的資料結構提供不同的存取效率,選擇合適的結構能顯著提升遊戲回應速度。例如:字典 (Dictionary) 提供常數時間的查找,非常適合需要快速檢索的場景,例如排行榜的玩家數據。
  3. 高效的資料操作
    適合的資料結構能簡化資料新增、刪除等操作,讓遊戲流程更順暢。例如:列表 (List) 適合用於需要頻繁增刪元素的場景,例如動態的物品欄。
  4. 降低處理負擔
    選用適當的資料結構可減少額外處理,進一步提升遊戲效能。例如:若需要按順序遍歷元素,使用 列表 (List) 而非字典 (Dictionary) 可避免多餘的鍵值查找。

資料序列化 (Serialization)

序列化是將資料轉換為可儲存或傳輸的格式(如 JSON 或 XML)。在 Unity 中,以下是常見的序列化方法

  1. 使用 JSON 序列化,適合用於需要與網路服務或跨平台數據交換的情境。而 Unity 內建的 JsonUtility 類別,適合簡單的序列化。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    using UnityEngine;

    [System.Serializable]
    public class GameData
    {
    public int playerLevel;
    public int playerScore;
    }

    public class SerializationExample : MonoBehaviour
    {
    private void Start()
    {
    // 建立資料
    GameData data = new GameData { playerLevel = 10, playerScore = 500 };

    // 序列化
    string json = JsonUtility.ToJson(data);
    Debug.Log("Serialized JSON: " + json);

    // 反序列化
    GameData loadedData = JsonUtility.FromJson<GameData>(json);
    Debug.Log("Loaded Level: " + loadedData.playerLevel);
    Debug.Log("Loaded Score: " + loadedData.playerScore);
    }
    }
  2. 使用 XML 序列化,XML (eXtensible Markup Language) 是一種結構化的數據格式,常用於儲存遊戲設定(configuration settings)或是與舊系統的兼容。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    using System.IO;
    using System.Xml.Serialization;
    using UnityEngine;

    [System.Serializable]
    public class GameData
    {
    public int playerLevel;
    public int playerScore;
    }

    public class XmlSerializationExample : MonoBehaviour
    {
    private void Start()
    {
    // 建立資料
    GameData data = new GameData { playerLevel = 10, playerScore = 500 };

    // 序列化
    XmlSerializer serializer = new XmlSerializer(typeof(GameData));
    StringWriter writer = new StringWriter();
    serializer.Serialize(writer, data);
    string xml = writer.ToString();
    Debug.Log("Serialized XML: " + xml);

    // 反序列化
    StringReader reader = new StringReader(xml);
    GameData loadedData = (GameData)serializer.Deserialize(reader);
    Debug.Log("Loaded Level: " + loadedData.playerLevel);
    Debug.Log("Loaded Score: " + loadedData.playerScore);
    }
    }
  3. 二進位序列化(Binary serialization)
    二進位序列化是一種將物件壓縮成緊湊的二進位格式的方式,適合用於需要高效儲存或讀寫數據的情境,例如保存遊戲進度或本地化配置文件。相比於 JSON 或 XML 等文本格式,二進位格式在讀取和寫入速度上具有顯著優勢,同時生成的數據也更小。
    BinaryFormatter 在 ASP.NET 應用程式中已淘汰並禁止
  4. ScriptableObject
    ScriptableObject 是 Unity 中用來存儲資料的資源(assets)類型,提供一種直觀的方式來管理設定、參數和可重複使用的組件。它們在 Unity 編輯器中以序列化的格式儲存數據。
    1. 定義 ScriptableObject
      1
      2
      3
      4
      5
      6
      7
      8
      using UnityEngine;
      [CreateAssetMenu(fileName = "NewSettings", menuName = "Game Settings")]
      public class GameSettings : ScriptableObject
      {
      public int playerHealth;
      public int enemyCount;
      public float playerSpeed;
      }
    2. ScriptableObject 的序列化與反序列化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      using UnityEngine;
      using System.IO;

      public class SettingsManager : MonoBehaviour
      {
      public GameSettings gameSettings; // 引用 ScriptableObject

      // 保存遊戲設定到檔案
      public void SaveSettings()
      {
      string jsonSettings = JsonUtility.ToJson(gameSettings); // 序列化為 JSON
      File.WriteAllText(Application.persistentDataPath + "/settings.json", jsonSettings); // 寫入檔案
      Debug.Log("Settings saved to " + Application.persistentDataPath + "/settings.json");
      }

      // 從檔案加載遊戲設定
      public void LoadSettings()
      {
      string path = Application.persistentDataPath + "/settings.json";
      if (File.Exists(path)) // 檢查檔案是否存在
      {
      string jsonSettings = File.ReadAllText(path); // 讀取檔案
      JsonUtility.FromJsonOverwrite(jsonSettings, gameSettings); // 將 JSON 加載到現有的 ScriptableObject
      Debug.Log("Settings loaded from " + path);
      }
      else
      {
      Debug.LogWarning("Settings file not found at " + path);
      }
      }
      }

PlayerPrefs

PlayerPrefs 是 Unity 提供的內建解決方案,用於以鍵(Key)值(Value)對的形式存儲小型資料,如玩家偏好設置、基本遊戲進度或遊戲內設置資料。它易於使用且跨平臺支持,適合管理輕量級資料。

  • PlayerPrefs 僅支持基礎類型(如 int、float 和 string),
  • 如果需要存儲更複雜的資料結構,則需要進行序列化。將資料轉換為 JSON 或 XML 格式,然後以字符串形式存儲到 PlayerPrefs 中。
  • PlayerPrefs 的資料以明文形式存儲,容易受到未授權的訪問。
  • 何時不使用 PlayerPrefs,儘管 PlayerPrefs 簡單易用,但在以下情況下可能並不適合:
    • 大型資料:對於複雜或大規模資料,就不適合使用,可改使用資料庫(如 SQLite)。
    • 敏感數據:對於高度敏感的信息,應考慮使用加密文件或平台特定的安全存儲方案。
    • 網絡同步:PlayerPrefs 不適合在設備間同步資料,應使用雲存儲或服務器端資料管理。

Unity UGUI performance

Canvas

在 Unity UI 中,修改單一元素可能會觸發整個 Canvas 的重新整理。這種重新評估與網格生成的過程會對效能造成嚴重影響,特別是在複雜的 UI 設計中。主要原因如下:

  1. 網格生成成本高:Unity 的 UI 系統會將元素分組為繪製批次(Draw Calls),但每次小變動都需要重新生成批次,導致高資源消耗。
  2. 過度使用單一 Canvas:將大量 UI 元素集中於一個 Canvas 內,當有微小更新時,會引發顯著的效能尖峰。

解決方式:將 UI 分割到多個 Canvas 是減少效能問題的有效方法。

  • 將單一個 Canvas 分割為 巢狀 Canvas(Nested Canvases),這麼做的好處有
    • 子 Canvas 與父 Canvas 和兄弟 Canvas 互相隔離,具有獨立的幾何圖形和批次處理功能。
    • 有助於組織層次結構化的 UI。
    • 單一 Canvas 的變動不會影響其他 Canvas,縮小了網格重新生成的範圍。
  • 分割 Canvas 的最佳實踐
    • 依刷新頻率分組:
      • 靜態元素:將不常變動的 UI 元素(如背景圖片、標題)放在單獨的 Canvas 上。
      • 動態元素:將頻繁更新的 UI 元素(如血量條、分數、計時器)分配到不同的 Canvas。
    • 保持一致性:
      • 確保 Canvas 內的元素共享相同的 Z 值、材質和貼圖,以提高批次處理的效率。
    • 避免過度巢狀化:
      • 雖然巢狀 Canvas 功能強大,但過度巢狀化會增加維護難度。應針對邏輯分組策略性使用。
    • 範例結構:
      • 主 Canvas:作為 UI 的總容器。
        • HUD Canvas:顯示血量、分數和小地圖(頻繁更新)。
        • 暫停菜單 Canvas:包含按鈕和靜態菜單元素(很少更新)。
        • 背包 Canvas:用於展示背包內容(僅在打開時更新)。
      • 這樣的結構下,當玩家血量在遊戲中變動時,僅 HUD Canvas 會更新,暫停菜單 Canvas背包 Canvas 不受影響。這樣的分割方式有效避免了不必要的效能開銷。

Graphic Raycasters

Graphic Raycaster 是 Unity UI 系統的一部分,負責將玩家的觸控或點擊行為轉換為遊戲可理解的事件。它的主要作用是:

  • 事件檢測:確認玩家觸碰的屏幕區域是否對應到某個 UI 元素。
  • 信息傳遞:將觸控位置與設定為可互動的 UI 元素匹配,並將事件傳遞給正確的遊戲部分。
  • 運作方式:Graphic Raycaster 僅關注 UI 圖形元素(例如按鈕、圖片)。檢查所有設置為可響應觸控的 UI 部件,並確認玩家的觸碰是否在其範圍內。

Graphic Raycaster 產生的問題:

  • 效能消耗高:
    • 每次觸控時,Graphic Raycaster 都需要遍歷屏幕上的所有可交互 UI 元素。
    • 過多的 Graphic Raycaster 或不必要的檢測可能導致資源浪費,尤其是在大型 UI 設計中。
  • 非交互元素:
    • 並非所有 UI 元素都需要響應觸控事件,例如裝飾性的圖片或靜態文本。對這些元素進行射線檢測會降低效能。

解決方式:

  • 移除不必要的 Graphic Raycasters
    • 確保僅在需要檢測觸控事件的 UI Canvas 或元素上添加 Graphic Raycaster。
    • 非交互式 Canvas(例如背景圖像)則移除 Graphic Raycaster,避免不必要的檢測。
  • 關閉非交互元素的 Raycast Target
    • 對於不需要觸控的 UI 元素(如純裝飾性圖片),關閉其 Raycast Target 設定。
      • 在 Image 組件中,取消勾選 Raycast Target。
      • 這樣可以防止該元素被包含在射線檢測的範圍內,減少計算負擔。
  • 使用阻擋遮罩(Blocking Mask)
    • 當 Canvas 的 Render Mode 設置為 Worldspace Camera 或 Screen Space Camera 時,可以使用 Blocking Mask:
      • 該遮罩決定 Raycaster 是否使用 2D 或 3D 物理來檢測阻擋物。
      • 如果物理檢測對 UI 無直接影響,避免啟用該功能以節省資源。
  • 分割 Canvas
    • 將靜態和動態 UI 元素分離至不同的 Canvas。
    • 靜態 Canvas 不需要頻繁更新,也不需要 Graphic Raycaster。

例子:
假設遊戲的 UI 包含以下部分

  • 主菜單:大部分按鈕需要觸控響應,因此保留 Graphic Raycaster,並確保按鈕的 Raycast Target 為啟用狀態。
  • 背景圖片:純裝飾性元素,關閉 Raycast Target 並移除其 Canvas 的 Graphic Raycaster。
  • 玩家 HUD(血量條、得分顯示):分配到單獨的 Canvas,僅需要檢測少數互動(例如按鈕點擊)。

管理 UI 對象池

問題:在常見的 UI 對象池使用方式中,開發者通常先更改對象的父節點(parent),再禁用對象(disable)。然而,這樣的操作會導致:

  • 多次改變層級結構:反復更新對象的層級關係(hierarchy),使得整體性能受到影響。
  • 不必要的開銷:每次改變父節點或激活/禁用對象時,Unity 會標記整個層級結構為“髒”,從而增加開銷。

解決方案:優化對象激活與重設順序

  • 為提高效率,建議按照以下順序管理 UI 對象池:
    • 禁用對象再更改父節點:
      • 禁用對象(SetActive(false))後再將其重新分配到對象池的父節點。
      • 這樣可以確保原始層級結構僅被標記一次為“髒”,避免重複更新。
  • 從對象池提取時的順序:
    • 先更改父節點:將對象移動到新父節點中。
    • 更新對象數據:重設對象的數據(如 UI 文本或圖片)。
    • 最後激活對象:使用 SetActive(true) 啟用對象。
  • 這樣的流程可將每個對象的層級結構變化降至最少,從而減少不必要的性能開銷。

隱藏 Canvas 的方法

在開發過程中,可能需要暫時隱藏某些 UI 元素或整個 Canvas。常見的方式有

  • 使用 SetActive(false) 禁用整個 GameObject
    • 可能會導致 Canvas 層級結構中的回調函數執行(如 OnDisable 和 OnEnable),增加不必要的性能開銷。
  • 移動 Canvas 的位置,讓使用者看不見
    • 並不能停止 GPU 的繪製操作,仍會影響性能。
  • 改變透明度
    • 並不能停止 GPU 的繪製操作,仍會影響性能。

解決方案:停用 Canvas 組件,透過停用 Canvas 可以

  1. 停止繪製操作,當停用 Canvas 組件會立即停止向 GPU 發送繪製請求,使畫布變得不可見。
  2. 並保留頂點緩衝區,Canvas 的頂點資料(meshes 和 vertices)會被保留,因此在重新啟用時無需重建(rebulid),僅需恢復繪製操作。
  3. 避免不必要的回調,停用 Canvas 組件不會觸發整個層級結構的 OnDisable/OnEnable 回調,從而減少性能損耗。

UI 元素動畫

當在 UI 元素上應用 Animator 時,即使動畫的值保持不變,也會在每一幀對 UI 元素產生影響。這會導致

  • 不必要的性能消耗,特別是在多個靜態 UI 元素上。

解決方案:針對靜態或偶爾改變的 UI 動畫需求,避免使用 Animator。可以透過以下方式實現高效的 UI 動畫。使用程式碼或 Tweening 系統進行動畫處理

  • 手寫動畫程式碼:針對簡單的動畫需求,直接使用 C# 程式碼逐步更改 UI 屬性(如位置、透明度等)。
  • Tweening 系統:使用輕量級的 Tweening 庫來簡化動畫實現。
  • Tweening 系統通過插值逐步更改屬性,對於臨時或事件驅動的動畫需求非常高效。
  • 使用專業的 Tweening 資產(如 DoTween)可以快速實現高效動畫。

開發 UI 建議

  1. 為最常用的 UI 元素建立 Prefabs
    • 對於經常使用的 UI 元素(如標題文字),可以將其創建為 Prefab,並將所需的組件附加到這些 Prefab 上。這樣一來,當你需要修改某些元素時,所有使用該 Prefab 的地方都會自動更新,讓你更輕鬆地管理與修改 UI 元素。
    • 例子:假設你有多個地方需要顯示標題文字,將標題文字做成 Prefab,並在遊戲中重複使用。未來如果需要調整字體或大小,只需更改 Prefab,就能同步更新所有相關元素。
  2. 使用 Sprite Atlas
    • 在 Unity 中使用 Sprite Atlas 來將多個 Sprite 紋理打包到一個單一的紋理資源中。這對於優化遊戲效能非常有幫助,因為它減少了渲染過程中的 draw calls,從而提升遊戲性能,特別是在移動設備上。
    • Sprite Atlas 的優點:
      • Draw call 優化:使用 Sprite Atlas,可以減少 draw calls,讓 Unity 在渲染多個 Sprite 時只用一個 draw call,大大提高性能。
      • 紋理分組:Sprite Atlas 讓你能夠將多個 Sprite 或紋理打包在一起,便於管理和組織資源。
      • 自動打包:Unity 會自動進行紋理打包,將個別的 Sprite 儘可能地安排在 Atlas 中,減少空白區域的浪費,並優化紋理使用。
      • Mipmapping 支援:Sprite Atlas 支援 Mipmapping,這能夠提高在遠距離查看紋理時的渲染質量。
      • 平台適配:可以為不同的裝置平台或螢幕解析度創建不同的 Sprite Atlas 變體,保證在各種設備上的最佳效能。
      • Unity 編輯器整合:你可以直接在 Unity 編輯器中創建與管理 Sprite Atlas,方便遊戲開發者進行資源的視覺化與調整。
  3. 使用透明圖片疊加進行設計對齊
    • 將設計圖層疊加一個稍微透明的圖片,這樣可以幫助你更準確地對齊並組織 UI 視圖,使其符合最終設計。

MVVM

在 Unity 中實作 MVVM

1
2
3
4
5
6
// Model
public class GameData
{
public int level;
public int starts;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ViewModel
public class GameViewModel : MonoBehaviour
{
private GameData gameData;
public int level => gameData.level;
public int starts => gameData.starts;
private void Start()
{
gameData = new GameData();
}
public void UpdateGameData(int level, int score)
{
gameData.level = level;
gameData.starts = score;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// View
public class GameView : MonoBehaviour
{
[SerializeField] private GameViewModel gameViewModel;
private void Start()
{
gameViewModel.UpdateGameData(1, 100);
}
private void Update()
{
Debug.Log("Level: " + gameViewModel.level);
Debug.Log("Starts: " + gameViewModel.starts);
}
}

TextMeshPro 顯示中文

使用 TextMeshPro 時,因為 Unity 預設的字型資源(如 LiberationSans SDF)不包含中文字元,所以中文會顯示為空白或方框。
我們需要自己下載支援中文的字體檔案,一些免費的可以在網路上找到,像是 Google 的 Noto Sans Traditional Chinese

生成靜態字體

  • 將下載的字體檔案(如 .ttf 或 .otf)拖入 Unity 的專案中。
  • 使用 Window -> TextMeshPro -> Font Asset Creator,選擇字型檔案後,可以生成 .asset 檔案。
  • 這種方式需要手動指定要包含的字元範圍(如常用中文字、標點符號等),因此使用上較麻煩。

動態字體:如果不想手動管理字元集,可以使用 Dynamic Font Asset

  • 選擇 Assets -> Create -> TextMeshPro -> Font Asset
    • 快捷鍵:Ctrl/Cmd + Shift + F12
  • Unity 將自動創建一個動態字體資產,該資產會在運行時根據需要生成缺失的字元,避免因字元缺失而顯示為空白。

Reference:

How to use Cursor AI in Unity

在 Mac 的 Unity 中如何使用 Cursor AI ?
以下記載如何在 Mac 的 Unity 中設定 Cursor AI

  1. 在 Unity 編輯器中,找到 Windows -> Package Manager

  2. 點擊 + 展開選單,並找到 Add package from git URL 並選擇,在輸入 https://github.com/boxqkrtm/com.unity.ide.cursor.git , 以安裝 Cursor Editor

  3. 接著到 https://www.cursor.com/ 註冊帳號並下載 Cusor Editor 安裝。

  4. 然後到 Preferences -> External Tools -> External Script Editor ,選擇安裝的 Cursor

  5. 至此,便可以在編寫程式碼的時候,使用 Cursor AI 了。

  • 按下 + L 可以開啟 AI 互動,在這邊你可以向AI闡述你的問題,讓它生成相關的程式碼

  • 按下 + K 在這邊你可以向AI闡述你的需求,讓它在選中的段落為你產生程式碼。

Unity 2D Liquid

以下將介紹如何使用 Shader , Texture , 以及 Camera 來在 2D 中建立液體(Liquid)效果。

建立互動物件

  1. 首先建立液體:建立一個 Circle Sprite 並添加 CircleCollider2D 與 Rigidbody2D ,並命名為 Liquid 。
  2. 建立一個 Empty Game Object 命名為 Liquids , 並在其中將剛剛建立的 Liquid 複製多份。
  3. 建立牆體:建立一個 Square Sprite 並加入 BoxCollider2D ,並命名為 Wall 。
  4. 建立一個 Empty Game Object 命名為 Walls , 並在其中將剛剛建立的 Wall 複製多份,排列為一個可以承裝物體的凹型。
  5. 建立一個 Liquids Layer
  6. 建立一個 LiquidCamera 並更改以下屬性
    • Clear Flags : Solid Color
    • Background : 黑色
    • Culling Mask : 設為剛剛建立的 Liquids Layer
    • Projection : 更改為 Orthographic
  7. 建立一個 Textures 資料夾,在其中建立一個 Render Texture 並命名為 LiquidTexture
  8. 將剛剛建立的 LiquidTexture 設定到 LiquidCamera 的 Target Texture
  9. 安裝 Shader Graph
  10. 建立一個 Shaders 資料夾,在其中建立一個 Unlit Shader Graph 並命名為 LiquidShaderGraph
  11. 使用滑鼠左鍵點擊剛剛建立的 LiquidShaderGraph 打開 Shader Graph Editor
  12. 滑鼠在 + 按鈕上面點擊一下展開選單,找到 Texture 2D 並點擊建立,並命名為 MainTex
  13. 點擊剛剛建立的 MainTex 在右側的 Graph Inspector 中找到 Default 並將 LiquidTexture 設定到這個 Default
  14. 使用滑鼠左鍵按住 MainTex 並拖拉到中央,如下圖
  15. 在空白處按下滑鼠右鍵找到 Create Node 並按下,在 Create Node 選單中找到 Sample Texture 2D 並按下建立一個 Sample Texture 2D Node
  16. 使用滑鼠左鍵將 MainTex 的接點拖拉到剛剛建立的 Sample Texture 2D Node
  17. 建立一個 Float 並命名為 Tickness
  18. 建立一個 Step Node ,將剛剛建立的 Thickness 連接到 Edge ,將 Sample Texture 2D 的 Alpha (A) 連接到 In
  19. 建立一個 Color 並命名為 Color , Default 設為藍色
  20. 建立一個 Multiply Node 並將 Step Node 的 Out 連接到 A ; 將剛剛建立的 Color 連接到 B
  21. 找到 Fragment Node ,將它的 Suface Type 設為 Transparent ,將 Step Node 的 Out 連接到 Fragment 的 Alpha ; 將 Multiply 的 Out 連接到 Base Color
  22. 最後按下 Save Asset 把它儲存
  23. 建立一個 Materials 資料夾,並在其中建立 Material 並命名為 LiquidMaterial
  24. 將剛剛建立的 LiquidMaterial 的 Shader 設為之前建立的 LiquidShaderGraph 並設定它的顏色為藍色
  25. 在 LiquidCamera 之下建立一個 Quad ,將它的 Z Position 設為 10 ,它的 Materials 設為 LiquidMaterial
  26. 最後觀看結果

Reference:

粒子系統的 Color by Speed

此模組可以根據粒子的速度(每秒的距離單位)來改變顏色

屬性 說明
Color 根據速度範圍定義的粒子顏色漸變。
Speed Range 此屬性用來映射顏色漸變(速度在範圍外的將映射到漸變的端點)。

燃燒或發光的粒子(如火花)在快速通過空氣時通常會燃燒得更亮(例如,當火花暴露在更多氧氣中時),但隨著速度減慢會稍微變暗。為了模擬這一點,你可以使用按速度變化顏色功能(Color By Speed),將漸變色設置為速度範圍的高端為白色,低端為紅色(在火花的例子中,更快的粒子會顯示為白色,而較慢的粒子則顯示為紅色)。

Color over Lifetime

此模組指定粒子在生命週期中要如何要如何顯示它的顏色與透明度。

屬性 說明
Color 粒子在其生命週期內的顏色漸變。漸變條最左邊的點表示粒子生命的開始,最右邊的點表示粒子生命的結束。

粒子會隨著時間變化顏色,例如,白熱的火花在穿過空氣時會冷卻,而魔法咒語可能會爆發出彩虹般的顏色。同樣重要的是透明度的變化。粒子在其生命週期結束時通常會燃燒殆盡、逐漸消失或散佈(例如,熱火花、煙火和煙霧粒子),而一個簡單的逐漸減少的漸變可以產生這種效果。

當同時使用起始顏色屬性時,此模組會將兩種顏色相乘,得到最終的粒子顏色。

粒子系統的 Lifetime by Emitter Speed

Lifetime by Emitter Speed 模組根據發射器的速度來控制每個粒子的初始壽命。它會將粒子的初始壽命乘以一個取決於發射物體速度的值。對於大多數粒子系統來說,這個值是來自遊戲物體的速度,但對於子發射器(sub-emitter)來說,速度來自於生成該子發射器粒子的父粒子。

屬性 說明
Multiplier 粒子的初始壽命會乘上一個倍數。這個模組根據你設置的曲線模式以不同方式使用這個倍數。曲線模式包括:
Constant :使用固定倍數值。這種模式會忽略 Speed Range 屬性。
Curve : 根據發射器的速度,將其映射到介於 0 和 1 之間的值,然後使用正規化為這個曲線取樣。
Random Between Two Constants :為每個粒子設置一個介於兩個值之間的隨機倍數。這種模式會忽略 Speed Range 屬性。
Random Between Two Curves :根據發射器的速度,將其映射到介於 0 和 1 之間的值,然後使用正規化為這些曲線取樣。對於每個粒子,模組會將倍數設置為這兩個樣本之間的隨機值。
Speed Range 粒子系統將發射器速度映射到 Multiplier(倍數)曲線上的最小值和最大值。如果發射器的速度等於第一個值,那麼倍數就是曲線開始時的值。如果發射器的速度等於第二個值,那麼倍數就是曲線末端的值。這個屬性僅在 倍數(Multiplier)的曲線模式設置為曲線 CurveRandom Between Two Curves 時才有關聯。

Reference: