如果你還不熟悉Unity物件池的基本概念,建議先閱讀我之前的文章「Unity物件池完全指南:讓你的捕魚機遊戲不再卡頓」https://jack9327.pixnet.net/blog/post/185522161

那裡有詳細的入門教學。今天我們要探討更進階的Unity效能優化技術:如何用多物件池系統管理複雜的魚群生成!

🐟 魚群生成的挑戰:比子彈更複雜的世界

還記得我們之前討論子彈物件池時,覺得那已經很厲害了嗎?但魚群系統其實是一個更有趣也更複雜的挑戰。想像一下,你的捕魚機遊戲中可能有小丑魚、鯊魚、章魚、海龜等十幾種不同的魚類,每種魚都有不同的大小、游泳速度、血量和獎勵值。

很多魚種.png

傳統的魚群生成系統可能會這樣運作:每隔幾秒鐘,隨機選擇一個位置和一種魚類,然後用Instantiate創建一群魚,讓它們游過螢幕。聽起來沒問題,但當你的遊戲有很多玩家,魚群密度很高時,問題就來了。

讓我們先看看原本的魚群生成程式碼:

void MakeFishes()
{
    int genPosIndex = Random.Range(0, genPositions.Length);     // 隨機位置
    int fishPreIndex = Random.Range(0, fishPrefabs.Length);    // 隨機魚種
    int maxNum = fishPrefabs[fishPreIndex].GetComponent<FishAttr>().maxNum;
    int num = Random.Range((maxNum / 2) + 1, maxNum);
    
    if (moveType == 0)  // 直線游泳
    {
        StartCoroutine(GenStraightFish(genPosIndex, fishPreIndex, num, speed, angOffset));
    }
    else               // 轉彎游泳
    {
        StartCoroutine(GenTrunFish(genPosIndex, fishPreIndex, num, speed, angSpeed));
    }
}

IEnumerator GenStraightFish(int genPosIndex, int fishPreIndex, int num, int speed, int angOffset)
{
    for (int i = 0; i < num; i++)
    {
        GameObject fish = Instantiate(fishPrefabs[fishPreIndex]);  // 每次都創建新魚!
        fish.transform.SetParent(fishHolder, false);
        fish.transform.localPosition = genPositions[genPosIndex].localPosition;
        // 設定各種屬性...
        yield return new WaitForSeconds(fishGenWaitTime);
    }
}

這個系統的問題和子彈一樣:每條魚都是全新創建的,用完就銷毀。在一個熱鬧的捕魚機遊戲中,可能每分鐘要創建幾百條魚,這對性能是很大的負擔。

🎯 設計魚群物件池:多樣性的管理藝術

設計魚群物件池比子彈複雜許多,因為我們要處理多種不同類型的魚。這裡有個重要的設計決策:我們要為每種魚建立獨立的池子,還是建立一個統一的魚池管理系統?

經過仔細考慮,我認為最好的方案是建立一個魚池管理器,內部包含多個專門的魚種池子。這樣設計的好處是既保持了每種魚的獨立性,又有統一的管理介面。

想像一下,這就像是一個專業的水族館後台,有專門的小丑魚繁殖池、鯊魚養殖池、章魚培育池等,每個池子都有專業的管理員,而總管理員負責協調所有池子的運作。

物件池示意圖.png

public class FishPoolManager : MonoBehaviour
{
    [System.Serializable]
    public class FishPool
    {
        public FishType fishType;
        public GameObject fishPrefab;
        public int poolSize = 20;
        public Queue<GameObject> availableFish = new Queue<GameObject>();
        public List<GameObject> activeFish = new List<GameObject>();
    }
    
    public FishPool[] fishPools;  // 各種魚類的池子
    private Dictionary<FishType, FishPool> poolDictionary = new Dictionary<FishType, FishPool>();
    
    void Start()
    {
        InitializeAllPools();
    }
    
    void InitializeAllPools()
    {
        foreach (var pool in fishPools)
        {
            poolDictionary[pool.fishType] = pool;
            
            // 為每種魚預先創建池子
            for (int i = 0; i < pool.poolSize; i++)
            {
                GameObject fish = Instantiate(pool.fishPrefab);
                fish.SetActive(false);
                pool.availableFish.Enqueue(fish);
            }
        }
    }
}

這個設計讓我們可以很靈活地管理不同魚種。比如說,如果小丑魚出現頻率很高,我們可以把小丑魚池設大一點;如果Boss級的巨型章魚很少出現,章魚池就可以小一點,這樣既節省記憶體又保證效率。

🌊 智慧的魚群調度系統

有了各種魚池後,我們需要一個聰明的調度系統來決定何時從哪個池子取魚。這個系統不只要考慮隨機性,還要考慮遊戲平衡、玩家體驗和池子的使用狀況。

public GameObject SpawnFish(FishType fishType, Vector3 position, Quaternion rotation)
{
    if (!poolDictionary.ContainsKey(fishType))
    {
        Debug.LogWarning($"沒有找到 {fishType} 的魚池!");
        return null;
    }
    
    var pool = poolDictionary[fishType];
    GameObject fish;
    
    if (pool.availableFish.Count > 0)
    {
        // 從池子裡取魚
        fish = pool.availableFish.Dequeue();
        fish.SetActive(true);
    }
    else
    {
        // 池子空了,臨時創建(並記錄這個情況)
        Debug.LogWarning($"{fishType} 池子已空,臨時創建新魚");
        fish = Instantiate(pool.fishPrefab);
    }
    
    // 設定魚的基本屬性
    fish.transform.position = position;
    fish.transform.rotation = rotation;
    fish.GetComponent<FishAttr>().ResetFish();  // 重置魚的狀態
    
    pool.activeFish.Add(fish);
    return fish;
}

public void ReturnFish(GameObject fish)
{
    FishType fishType = fish.GetComponent<FishAttr>().fishType;
    var pool = poolDictionary[fishType];
    
    fish.SetActive(false);
    pool.activeFish.Remove(fish);
    pool.availableFish.Enqueue(fish);
}

現在我們來改造原本的魚群生成系統,讓它使用物件池:

IEnumerator GenStraightFishWithPool(int genPosIndex, FishType fishType, int num, int speed, int angOffset)
{
    for (int i = 0; i < num; i++)
    {
        // 不再使用Instantiate,改用物件池
        Vector3 spawnPos = genPositions[genPosIndex].localPosition;
        Quaternion spawnRot = genPositions[genPosIndex].localRotation * Quaternion.Euler(0, 0, angOffset);
        
        GameObject fish = FishPoolManager.Instance.SpawnFish(fishType, spawnPos, spawnRot);
        
        if (fish != null)
        {
            fish.transform.SetParent(fishHolder, false);
            fish.GetComponent<SpriteRenderer>().sortingOrder += i;
            
            // 設定移動腳本
            var moveScript = fish.GetComponent<Ef_AutoMove>();
            if (moveScript == null)
                moveScript = fish.AddComponent<Ef_AutoMove>();
            moveScript.speed = speed;
        }
        
        yield return new WaitForSeconds(fishGenWaitTime);
    }
}

🧠 魚類重置的藝術:讓老魚變新魚

魚類的重置比子彈複雜很多,因為魚有更多的狀態需要管理。一條被回收的魚可能之前受過傷、有特殊的動畫狀態,或是掛載了額外的腳本組件。我們需要確保每條從池子裡取出的魚都像全新的一樣。

public class FishAttr : MonoBehaviour
{
    public FishType fishType;
    public int maxHealth = 100;
    public int currentHealth;
    public float swimSpeed = 2f;
    public int rewardValue = 50;
    
    void Start()
    {
        currentHealth = maxHealth;
    }
    
    public void ResetFish()
    {
        // 重置血量
        currentHealth = maxHealth;
        
        // 重置動畫狀態
        var animator = GetComponent<Animator>();
        if (animator != null)
        {
            animator.Play("Swim", 0, 0f);  // 回到游泳動畫
        }
        
        // 清理可能掛載的額外腳本
        var autoRotate = GetComponent<Ef_AutoRotate>();
        if (autoRotate != null)
        {
            Destroy(autoRotate);
        }
        
        // 重置渲染順序
        GetComponent<SpriteRenderer>().sortingOrder = 0;
        
        // 重置任何特效狀態
        ResetVisualEffects();
    }
    
    void OnTriggerEnter2D(Collider2D other)
    {
        // 當魚被擊中或游出邊界時,回收到池子
        if (other.CompareTag("Bullet") || other.CompareTag("Border"))
        {
            FishPoolManager.Instance.ReturnFish(this.gameObject);
        }
    }
    
    private void ResetVisualEffects()
    {
        // 重置所有視覺特效組件
        var particleSystems = GetComponentsInChildren<ParticleSystem>();
        foreach (var ps in particleSystems)
        {
            ps.Stop();
            ps.Clear();
        }
        
        // 重置材質屬性
        var renderer = GetComponent<SpriteRenderer>();
        if (renderer != null)
        {
            renderer.color = Color.white;
            renderer.material = defaultMaterial;
        }
    }
}

這個重置系統確保每條魚都能以最佳狀態重新投入戰鬥。就像是給魚做了一次全身健康檢查和美容護理,讓它們重新煥發活力。

📊 智慧池子管理:動態調整的秘密

一個真正聰明的魚池管理系統會根據遊戲進行的情況動態調整池子大小。比如說,如果發現某種魚的池子經常用完,系統就會自動擴展那個池子;如果某個池子很少使用,就可以考慮縮小它。

public class PoolAnalytics : MonoBehaviour
{
    private Dictionary<FishType, int> poolMissCount = new Dictionary<FishType, int>();
    private Dictionary<FishType, float> lastUsageTime = new Dictionary<FishType, float>();
    
    public void RecordPoolMiss(FishType fishType)
    {
        if (!poolMissCount.ContainsKey(fishType))
            poolMissCount[fishType] = 0;
            
        poolMissCount[fishType]++;
        
        // 如果某種魚池經常不夠用,考慮擴展
        if (poolMissCount[fishType] > 10)
        {
            Debug.Log($"考慮擴展 {fishType} 的池子大小");
            ExpandPool(fishType);
            poolMissCount[fishType] = 0;  // 重置計數
        }
    }
    
    void ExpandPool(FishType fishType)
    {
        var pool = FishPoolManager.Instance.GetPool(fishType);
        int expandSize = Mathf.Max(5, pool.poolSize / 4);  // 至少擴展5個,或原大小的25%
        
        for (int i = 0; i < expandSize; i++)
        {
            GameObject fish = Instantiate(pool.fishPrefab);
            fish.SetActive(false);
            pool.availableFish.Enqueue(fish);
        }
        
        pool.poolSize += expandSize;
        Debug.Log($"{fishType} 池子已擴展 {expandSize} 個位置");
    }
    
    public void MonitorPoolUsage()
    {
        // 定期檢查池子使用情況
        foreach (var kvp in poolDictionary)
        {
            var fishType = kvp.Key;
            var pool = kvp.Value;
            
            float usageRate = (float)pool.activeFish.Count / pool.poolSize;
            
            if (usageRate > 0.9f)
            {
                Debug.LogWarning($"{fishType} 池子使用率過高:{usageRate:P}");
            }
            else if (usageRate < 0.1f && pool.poolSize > 10)
            {
                Debug.Log($"{fishType} 池子可能過大,考慮縮小");
            }
        }
    }
}

🚀 效能提升的驚人數據

實裝魚群物件池後,效能提升真的很顯著。在我的測試中,原本的系統在高密度魚群場景下,每分鐘會創建約400條魚,垃圾回收觸發30-40次,記憶體使用量波動很大,FPS會從60掉到45左右。

使用物件池後,同樣的場景只在初始化時創建了總共200條各種類型的魚,之後完全沒有創建和銷毀動作,垃圾回收幾乎沒有觸發,記憶體使用量穩定,FPS穩定維持在60。更重要的是,遊戲可以支援更高密度的魚群而不會卡頓。

🎯 給魚池新手的實用建議

在實作魚群物件池時,有幾個要點特別重要。首先是池子大小的估算,一般建議根據螢幕上同時可能出現的同類型魚數量來決定,再乘以1.5到2倍作為緩衝。比如小丑魚可能同時有20條在螢幕上,那池子準備30-40條比較安全。

其次是重置的徹底性,魚類比子彈複雜,要確保血量、動畫、腳本組件都正確重置。特別要注意那些動態添加的組件,如果不清理乾淨,魚可能會有奇怪的行為。

最後是監控和調優,建議添加一些統計功能來追蹤各個池子的使用情況,這樣可以及時發現問題並調整池子大小。

🎮 魚類類型定義

為了讓系統更完整,我們需要定義魚類的類型:

public enum FishType
{
    SmallFish,      // 小魚
    MediumFish,     // 中型魚
    LargeFish,      // 大型魚
    BossFish,       // Boss魚
    SpecialFish,    // 特殊魚類
    GoldenFish,     // 黃金魚
    SharkFish       // 鯊魚
}

🔧 完整的池子管理介面

最後,我們需要一些輔助方法來完善整個系統:

public partial class FishPoolManager : MonoBehaviour
{
    private static FishPoolManager _instance;
    public static FishPoolManager Instance => _instance;
    
    void Awake()
    {
        if (_instance == null)
        {
            _instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    public FishPool GetPool(FishType fishType)
    {
        return poolDictionary.ContainsKey(fishType) ? poolDictionary[fishType] : null;
    }
    
    public int GetActiveCount(FishType fishType)
    {
        var pool = GetPool(fishType);
        return pool?.activeFish.Count ?? 0;
    }
    
    public int GetAvailableCount(FishType fishType)
    {
        var pool = GetPool(fishType);
        return pool?.availableFish.Count ?? 0;
    }
    
    public void ReturnAllFish()
    {
        foreach (var pool in fishPools)
        {
            var activeFishCopy = new List<GameObject>(pool.activeFish);
            foreach (var fish in activeFishCopy)
            {
                ReturnFish(fish);
            }
        }
    }
    
    void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus)
        {
            // 遊戲暫停時回收所有活躍的魚
            ReturnAllFish();
        }
    }
}

魚群物件池是一個比子彈物件池更進階的應用,但掌握了這個技術,你就能處理各種複雜的遊戲物件管理需求。記住,好的物件池設計不只是為了效能,更是為了讓整個遊戲系統更加穩定和可預測。現在就動手改造你的魚群系統吧,讓那些游來游去的魚兒也能享受物件池帶來的高效管理!

創作者介紹
創作者 傑克淺談遊戲邏輯 的頭像
傑克的遊戲宇宙

傑克淺談遊戲邏輯

傑克的遊戲宇宙 發表在 痞客邦 留言(0) 人氣( 6 )