今日更新總結:Unity 2D 粒子模擬系統全面優化

郭俊鑫郭俊鑫
2 min read

一、模組化重構

  1. 拆分核心腳本

    • ParticleManager.cs:物件池化管理粒子與標籤

    • GravitationalSystem.cs:萬有引力、合併/分裂邏輯

    • EnergyTracker.cs:動能統計與能量柱圖顯示

    • SimulationUIBindings.cs:延遲啟動與 UI 控制

    • LogExporter.cs:粒子狀態與碰撞事件日誌輸出

    • SimulationController.cs:流程調度、暫停/重置/倍率控制

  2. 延遲啟動

     // SimulationController.cs
     private bool hasStarted = false;
    
     public void StartSimulation()
     {
         if (hasStarted) return;
         hasStarted = true;
         // 初始化模組…
         GenerateParticlesFromInput();
         StartCoroutine(LogLoop());
     }
    
     private void Update()
     {
         if (!hasStarted || isPaused) return;
         // 模擬邏輯…
     }
    

二、效能優化五部曲

1. 物理計算節流

// SimulationController.cs
private int physicsFrameCounter = 0;
public int physicsFrameThreshold = 2; // 每兩幀執行一次

private void FixedUpdate()
{
    if (!hasStarted || isPaused) return;
    if (++physicsFrameCounter % physicsFrameThreshold == 0)
        gravitationalSystem.ApplyForces();
}

2. 全面物件池化

// ParticleManager.cs (節錄)
private Queue<GameObject> particlePool = new();
private Queue<TMP_Text>    labelPool    = new();

private void Awake()
{
    for (int i = 0; i < initialParticlePoolSize; i++)
    {
        var p = Instantiate(particlePrefab, transform);
        p.SetActive(false);
        particlePool.Enqueue(p);
    }
    // 標籤池同理…
}

public void CreateParticle(Vector2 pos, Vector2 vel, float mass)
{
    var p   = particlePool.Count>0 ? particlePool.Dequeue() : Instantiate(particlePrefab, transform);
    p.SetActive(true);
    // 設定 Rigidbody2D、顏色、大小…
    var lbl = labelPool.Count>0 ? labelPool.Dequeue() : Instantiate(labelPrefab, worldCanvas.transform).GetComponent<TMP_Text>();
    lbl.gameObject.SetActive(true);
    // 註冊到 activeParticles / activeLabels…
}

3. GC 分配優化

// LogExporter.cs (節錄)
private StringBuilder sbScreen = new();
private StringBuilder sbFile   = new();

public void LogParticleStates()
{
    sbScreen.Clear();
    sbFile.Clear();
    float t = Time.time;
    foreach (var p in particleManager.GetActiveParticles())
    {
        // AppendLine 到 sbScreen / sbFile…
    }
    logOutputText.text = sbScreen.ToString();
    File.AppendAllText(logFilePath, sbFile.ToString());
    // collisionLog…
}

4. 尾部交換移除法

// ParticleManager.cs (節錄)
public void RemoveParticle(int idx)
{
    int last = activeParticles.Count - 1;
    if (idx < 0 || idx > last) return;
    var p   = activeParticles[idx];
    var lbl = activeLabels   [idx];
    // 回收到池…
    if (idx != last)
    {
        activeParticles[idx] = activeParticles[last];
        activeLabels  [idx] = activeLabels  [last];
    }
    activeParticles.RemoveAt(last);
    activeLabels  .RemoveAt(last);
}

5. Coroutine 定時更新

// EnergyTracker.cs
private IEnumerator EnergyLoop()
{
    while (true)
    {
        yield return new WaitForSeconds(energyLogInterval);
        LogTotalKineticEnergy();
    }
}

private void Start()
{
    StartCoroutine(EnergyLoop());
}

三、UI 顯示控制

  • 開始前:只顯示「粒子數量輸入框」「速度輸入框」「開始按鈕」。

  • 按下開始:隱藏初始輸入欄,顯示完整控制面板(暫停、重置、速度滑桿、能量圖、日誌等)。

  • 重置:清除所有粒子與標籤,重新顯示開始按鈕。

// SimulationUIBindings.cs (節錄)
public GameObject[] uiHideOnStart;
public GameObject[] uiShowAfterStart;

startButton.onClick.AddListener(()=>
{
    controller.StartSimulation();
    foreach(var go in uiHideOnStart) go.SetActive(false);
    foreach(var go in uiShowAfterStart) go.SetActive(true);
    startButton.gameObject.SetActive(false);
});


0
Subscribe to my newsletter

Read articles from 郭俊鑫 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

郭俊鑫
郭俊鑫