【5/8 更新】2d 粒子模擬系統:ui 分區、標籤管理、邊界限制與力向量可視化

郭俊鑫郭俊鑫
3 min read
  1. 畫面 UI 三區塊化

  2. 標籤動態顯示與排序

  3. 畫面邊界位置強制約束

  4. 萬有引力向量可視化

以下分別介紹每個部分的需求、實作與程式碼重點。


一、畫面 UI 三區塊化

需求

  • 左側輸入區(僅在按下「開始」前顯示)

  • 中間世界區(永遠用來渲染粒子本體與標籤)

  • 右側控制區(開始後顯示所有操作按鈕/滑桿/圖表,不覆蓋世界區)

實作

  • 在 Canvas 下建立三個 Panel:LeftPanelWorldPanelRightPanel

  • LeftPanel:固定寬 300px,掛 VerticalLayoutGroup + ContentSizeFitter,放置「粒子數量」和「速度上限」輸入,以及「開始」按鈕。

  • WorldPanel:錨點全螢幕 Stretch,掛載 CanvasGroup,由腳本控制顯示。

  • RightPanel:固定寬 300px,同樣加上垂直排版,用於暫停/重置/開啟資料夾按鈕、滑桿與圖表。

// UILayoutManager.cs — 自動將左右兩側 Panel 固定 300px
using UnityEngine;

public class UILayoutManager : MonoBehaviour
{
    public RectTransform leftPanel, rightPanel;

    void Awake()
    {
        // 左側 300px
        leftPanel.anchorMin = new Vector2(0,0);
        leftPanel.anchorMax = new Vector2(0,1);
        leftPanel.pivot     = new Vector2(0,0.5f);
        leftPanel.sizeDelta = new Vector2(300f, 0f);
        // 右側 300px
        rightPanel.anchorMin = new Vector2(1,0);
        rightPanel.anchorMax = new Vector2(1,1);
        rightPanel.pivot     = new Vector2(1,0.5f);
        rightPanel.sizeDelta = new Vector2(300f, 0f);
    }
}

SimulationUIBindings.OnStartClicked() 中呼叫 UIPanelController.OnStart(),隱藏左側、啟用中間世界區和右側控制。


二、標籤動態顯示與排序

需求

  • 畫面保持乾淨 → 預設隱藏所有粒子標籤

  • 想看時才顯示 → 由 UI Toggle 切換 showLabels

  • 每次只顯示「速度最快的前 N 筆」標籤

// ParticleManager.cs
[Header("標籤顯示設定")]
public int  maxLabelsToShow = 20;   // 最多同時顯示的標籤數
public bool showLabels      = false; // Toggle 控制

public void UpdateLabels()
{
    if (!showLabels)
    {
        foreach (var lbl in activeLabels)
            lbl.gameObject.SetActive(false);
        return;
    }

    // 依速度排序,只顯示前 N 筆
    var list = new List<(GameObject p, TMP_Text lbl, float v)>();
    for (int i = 0; i < activeParticles.Count; i++)
        list.Add((activeParticles[i], activeLabels[i],
                  activeParticles[i].GetComponent<Rigidbody2D>().velocity.magnitude));
    list.Sort((a,b)=> b.v.CompareTo(a.v));

    for (int k = 0; k < list.Count; k++)
    {
        var (p,lbl,v) = list[k];
        if (k < maxLabelsToShow)
        {
            lbl.gameObject.SetActive(true);
            // 縮小+抖動以避免重疊
            float s = p.transform.localScale.x;
            Vector3 pos = p.transform.position + Vector3.up*(s+0.1f)
                          + new Vector3(Random.Range(-0.05f,0.05f),Random.Range(-0.05f,0.05f),0);
            lbl.transform.position = pos;
            lbl.fontSize = 14;
            lbl.text = $"ID:{k} | v:{v:F1}";
        }
        else lbl.gameObject.SetActive(false);
    }
}

SimulationController.Update() 中調用 particleManager.UpdateLabels(),並由右側 Toggle 綁定 particleManager.showLabels


三、畫面邊界位置強制約束

需求:粒子有時因物理計算會飛出視野,必須「手動」將位置 clamp 回螢幕範圍。

// ParticleManager.cs
[Header("邊界限制")]
public Camera mainCamera;
private Vector2 minBounds, maxBounds;

public void UpdateBounds()
{
    if (mainCamera==null||!mainCamera.orthographic) return;
    float v = mainCamera.orthographicSize;
    float h = v * mainCamera.aspect;
    Vector3 c = mainCamera.transform.position;
    minBounds = new Vector2(c.x - h, c.y - v);
    maxBounds = new Vector2(c.x + h, c.y + v);
}

public void ClampParticles()
{
    foreach (var p in activeParticles)
    {
        Vector3 pos = p.transform.position;
        pos.x = Mathf.Clamp(pos.x, minBounds.x, maxBounds.x);
        pos.y = Mathf.Clamp(pos.y, minBounds.y, maxBounds.y);
        p.transform.position = pos;
    }
}

SimulationController.Update() 內,於標籤更新後呼叫 ClampParticles(),保證所有粒子都不會跑出螢幕。


四、萬有引力向量可視化

需求:有時看不出粒子是否真的互相吸引,加入 debug 模式下的力向量繪製。

// GravitationalSystem.cs
[Header("調試繪製")]
public bool debugDrawForces = false;
public Color forceLineColor    = Color.cyan;

public void ApplyForces()
{
    var ps = particleManager.GetActiveParticles();
    for (int i = 0; i < ps.Count; i++)
    {
        var rbA = ps[i].GetComponent<Rigidbody2D>();
        for (int j = i+1; j < ps.Count; j++)
        {
            var rbB = ps[j].GetComponent<Rigidbody2D>();
            Vector2 dir = rbB.position - rbA.position;
            float d = dir.magnitude;
            if (d < 0.01f) continue;
            float mag = gravitationalConstant * (rbA.mass * rbB.mass) / (d*d);
            Vector2 f = mag * dir.normalized;
            rbA.AddForce(f);
            rbB.AddForce(-f);
            if (debugDrawForces)
            {
                Debug.DrawLine(rbA.position, rbA.position + f.normalized*0.5f, forceLineColor);
                Debug.DrawLine(rbB.position, rbB.position - f.normalized*0.5f, forceLineColor);
            }
        }
    }
}

gravitationalConstant 調高至 ~10~20,並在 Inspector 勾選 debugDrawForces,即可在 Scene 視窗中看到力向量,確認萬有引力真實存在。


總結

今天重點更新涵蓋:

  • UI 三區塊化:左輸入、中世界、右控制

  • 標籤動態顯示:只顯示前 N 筆、由 Toggle 開關

  • 畫面邊界 Clamp:手動約束所有粒子不飛出螢幕

  • 力向量可視化:Debug 模式下繪製萬有引力方向

0
Subscribe to my newsletter

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

Written by

郭俊鑫
郭俊鑫