Unity 套圈捕捉 UI 實現分享
期望表現效果
《拼貼冒險傳 / PatchQuest》 捕捉進度 動態UI
實現效果
- 目標:角色 A 套圈怪物 B,進度環顯示圍繞角度。
- 技術點:Shader 繪制橢圓環,支持描邊、順/逆時針,需要對兩個切口也進行描邊。
技術需求 & 準備
- Unity
- RawImage + 自定義 Shader
- Canvas 設置為 World Space,UI 跟隨敵人
- C# 腳本控制進度和方向
UI預制體的層級結構
捕捉邏輯
- 玩家位置與敵人位置計算方向向量。
- 計算 DeltaAngle,累積角度。
- 正負值表示順/逆時針。
- LassoUI GameObject 始終對齊敵人位置。
`
PlayerController.cs捕捉邏輯實現
核心變量定義
// 角度計算相關變量
float totalAngle; // 累計角度
Vector2 lastDir; // 上一幀的玩家->獵物方向
Vector2 startDir; // 初始方向 玩家->獵物方向
Role prey; // 獵物對象
進入捕捉狀態初始化
private void Catching_Enter()
{// UI跟隨獵物位置lassoUI.transform.position = prey.transform.position;lassoUI.SetRequiredAngle(prey.NeedAngle);// 初始化方向向量startDir = (transform.position - prey.transform.position).normalized;lassoUI.InitStartDir(startDir); lastDir = startDir;totalAngle = 0f;// 綁定滿圈事件lassoUI.OnFullRotation += HandleLassoFullRotation;lassoUI.Show();
}
核心角度計算邏輯
private void Catching_Update()
{// 讓LassoUI跟隨獵物位置if (lassoUI != null && prey != null){lassoUI.transform.position = prey.transform.position;}// 計算當前方向向量Vector2 currentDir = (transform.position - prey.transform.position).normalized;// 計算角度變化(相對上一次)float delta = Mathf.DeltaAngle(Mathf.Atan2(lastDir.y, lastDir.x) * Mathf.Rad2Deg,Mathf.Atan2(currentDir.y, currentDir.x) * Mathf.Rad2Deg);totalAngle += delta; // 累計總角度(正負都可以)lastDir = currentDir;lassoUI.UpdateProgress(totalAngle);// 檢查是否滿圈if (Mathf.Abs(totalAngle) >= prey.NeedAngle){HandleLassoFullRotation();lassoUI.ResetProgress();}
}
抓捕成功處理
void HandleLassoFullRotation()
{// 滿圈了,執行抓捕成功邏輯Debug.Log("執行抓捕成功");// 調用UI彈出動畫UIManager.instance.GetPanel<BattleMainPanel>().ShowImagePopUp();// 銷毀獵物if (prey != null){prey.Dead();}// 退出抓捕狀態,回到射擊模式fsm.ChangeState(PlayerControllerStates.Shooting);
}
關鍵技術點說明
1. 角度計算原理
- 使用
Mathf.Atan2()
將方向向量轉換為角度 - 使用
Mathf.DeltaAngle()
計算相對角度變化,自動處理角度跨越問題 - 支持順時針和逆時針旋轉,正負值自動處理
2. UI跟隨機制
- 每幀更新
lassoUI.transform.position = prey.transform.position
- 確保UI始終跟隨獵物位置
3. 狀態管理
- 使用狀態機管理不同游戲狀態(射擊、狩獵、捕捉)
- 進入捕捉狀態時初始化角度計算
- 退出時清理事件綁定
4. 事件驅動
- 通過
OnFullRotation
事件觸發抓捕成功邏輯 - 實現UI和游戲邏輯的解耦
UI 進度計算
LassoUI.cs
ringMaterial 為shader材質的引用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System;namespace Gameplay.Battle
{public class LassoUI : MonoBehaviour{// [SerializeField] private Image progressCircle; // 圓環Imageprivate CanvasGroup canvasGroup; // 控制顯示隱藏的透明度private float accumulatedAngle = 0f; // 累計角度private float requiredAngle = 360f; // 默認1圈public Material ringMaterial;public Vector2 startDir = Vector2.up; // 初始方向 玩家->獵物方向public event Action OnFullRotation; // 觸發滿圈事件// Start is called before the first frame updatevoid Start(){canvasGroup = GetComponent<CanvasGroup>();Hide();}public void Show(){canvasGroup.alpha = 1;canvasGroup.blocksRaycasts = true;canvasGroup.interactable = true;}public void Hide(){canvasGroup.alpha = 0;canvasGroup.blocksRaycasts = false;canvasGroup.interactable = false;}public void InitStartDir(Vector2 dir){startDir = dir;float startAngle = Mathf.Atan2(startDir.y, startDir.x) * Mathf.Rad2Deg;// 只設置起始角度,不設置進度ringMaterial.SetFloat("_StartAngle", startAngle);ringMaterial.SetFloat("_Progress", 0f); // 進度從0開始Debug.Log($"LassoUI: 初始化角度 = {startAngle}°");}public void SetRequiredAngle(float angle){requiredAngle = angle;Debug.Log($"LassoUI: 設置所需角度 = {requiredAngle}°");}public void ResetProgress(){accumulatedAngle = 0f;}public void UpdateProgress(float angle){var Progress = Mathf.Clamp(angle / requiredAngle,-1f,1f);ringMaterial.SetFloat("_Progress", Progress);}}
}
Shader 實現
參數調整
Shader "Unlit/EllipseRingProgress"
{Properties{_MainColor ("Fill Color", Color) = (1,0.5,0,1) // 內圈填充顏色_EdgeColor ("Edge Color", Color) = (0,0,0,1) // 描邊顏色_Progress ("Progress", Range(-1,1)) = 0 // 進度,負數順時針,正數逆時針_Thickness ("Ring Thickness", Range(0.01,2)) = 1 // 環寬_EdgeWidth ("Edge Width", Range(0.001,0.1)) = 0.02 // 內外描邊寬度_CapEdgeAngle ("Cap Edge Width (Degrees)", Range(0,5)) = 1.0 // 封口兩端描邊角度_EllipseA ("Ellipse Semi-major Axis", Float) = 1 // 橢圓長軸_EllipseB ("Ellipse Semi-minor Axis", Float) = 1 // 橢圓短軸_StartAngle ("Start Angle Offset (Degrees)", Range(-180,180)) = 0 // 起始角度}SubShader{Tags { "Queue"="Transparent" "RenderType"="Transparent" }LOD 100Pass{Blend SrcAlpha OneMinusSrcAlphaCull OffZWrite OffCGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"fixed4 _MainColor;fixed4 _EdgeColor;float _Progress;float _Thickness;float _EdgeWidth;float _CapEdgeAngle;float _EllipseA;float _EllipseB;float _StartAngle;struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv : TEXCOORD0;float4 vertex : SV_POSITION;};// 頂點程序v2f vert(appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);// 將 UV 從 [0,1] 映射到 [-1,1],中心在 (0,0)o.uv = v.uv * 2 - 1;return o;}fixed4 frag(v2f i) : SV_Target{float2 pos = i.uv;// 1?? 計算橢圓歸一化距離float ellipseDist = (pos.x * pos.x) / (_EllipseA * _EllipseA) +(pos.y * pos.y) / (_EllipseB * _EllipseB);float halfThickness = _Thickness * 0.5;float innerBoundary = 1.0 - halfThickness;float outerBoundary = 1.0 + halfThickness;// 不在環內的點直接丟棄if (ellipseDist < innerBoundary || ellipseDist > outerBoundary)discard;// 2?? 計算極角 (0~360)float angleRad = atan2(pos.y / _EllipseB, pos.x / _EllipseA);float angleDeg = degrees(angleRad);if (angleDeg < 0) angleDeg += 360;float relativeAngle = fmod(angleDeg - _StartAngle + 360, 360);// 3?? 處理順/逆時針顯示float absProgress = abs(_Progress); // 進度長度bool clockwise = (_Progress < 0); // 順時針方向float progressAngle = absProgress * 360;if (clockwise){// 順時針:從起點往回走if (relativeAngle < (360 - progressAngle) && relativeAngle > 0)discard;}else{// 逆時針:原邏輯if (relativeAngle > progressAngle)discard;}// 4?? 內外描邊bool radialEdge = abs(ellipseDist - (1.0 - halfThickness)) < _EdgeWidth ||abs(ellipseDist - (1.0 + halfThickness)) < _EdgeWidth;// 5?? 封口描邊計算float startCap = 0;float endCap = progressAngle;if (clockwise){startCap = 360 - progressAngle;endCap = 360;}bool capEdge = (relativeAngle < _CapEdgeAngle) ||(abs(relativeAngle - startCap) < _CapEdgeAngle) ||(abs(relativeAngle - endCap) < _CapEdgeAngle);// 6?? 返回顏色if (radialEdge || capEdge)return _EdgeColor; // 描邊return _MainColor; // 填充}ENDCG}}
}
說明
-
_Progress
:- 負值 → 順時針
- 正值 → 逆時針
-
_StartAngle
:- 控制環起點位置
-
_EdgeWidth
:- 調整環內外描邊粗細
-
_CapEdgeAngle
:- 調整封口角度寬度
-
_EllipseA/B
:- 控制橢圓比例,可實現圓形或拉長效果
-
_Thickness
:- 環寬
📌 總結
- 通過 Shader 對橢圓環的歸一化計算,實現動態進度顯示。
- 支持順/逆時針顯示。
- 封口描邊、內外描邊,增強視覺效果。
- C# 控制
_Progress
和_StartAngle
,UI 可隨角色位置和方向實時更新。