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 可随角色位置和方向实时更新。