Unity 套圈捕捉 UI 实现分享
期望表现效果
《拼贴冒险传 / PatchQuest》 捕捉进度 动态UI
实现效果

- 目标:角色 A 套圈怪物 B,进度环显示围绕角度。
- 技术点:Shader 绘制椭圆环,支持描边、顺/逆时针,需要对两个切口也进行描边。
技术需求 & 准备
- Unity
- RawImage + 自定义 Shader
- Canvas 设置为 World Space,UI 跟随敌人
- C# 脚本控制进度和方向
UI预制体的层级结构
捕捉逻辑
- 玩家位置与敌人位置计算方向向量。
- 计算 DeltaAngle,累积角度。
- 正负值表示顺/逆时针。
- LassoUI GameObject 始终对齐敌人位置。
`
PlayerController.cs捕捉逻辑实现
核心变量定义
csharp
// 角度计算相关变量
float totalAngle; // 累计角度
Vector2 lastDir; // 上一帧的玩家->猎物方向
Vector2 startDir; // 初始方向 玩家->猎物方向
Role prey; // 猎物对象
进入捕捉状态初始化
csharp
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();
}
核心角度计算逻辑
csharp
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();
}
}
抓捕成功处理
csharp
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材质的引用
csharp
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; // 圆环Image
private 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 update
void 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 实现
参数调整
c
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 100
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
ZWrite Off
CGPROGRAM
#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 可随角色位置和方向实时更新。