🎮 Unity Handles 完全指南:打造强大的场景交互手柄

💡 场景交互的力量

  • 想在 Scene 视图中创建自定义的可视化工具?
  • 如何实现类似 Unity 内置的移动、旋转、缩放手柄?
  • OnSceneGUI 和 Handles 怎么配合使用?
  • 想让工具在场景视图中更直观、更易用?

这篇文章! 将深入讲解 Unity Handles 机制,帮你打造专业级场景交互工具!

一、Handles 概述

Handles 类提供了一套在 Scene 视图中绘制 3D GUI 控件的能力,是自定义编辑器工具的重要组成部分。

1.1 核心概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────┐
│ Scene 场景视图 │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ OnSceneGUI 绘制区域 │ │
│ │ │ │
│ │ ┌───────────────────┐ │ │
│ │ │ PositionHandle │ ← 位置手柄 │ │
│ │ └───────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────┐ │ │
│ │ │ RotationHandle │ ← 旋转手柄 │ │
│ │ └───────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────┐ │ │
│ │ │ RadiusHandle │ ← 范围手柄 │ │
│ │ └───────────────────┘ │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

1.2 使用场景

场景 描述 示例
🎯 路径编辑 可视化编辑路径点 曲线编辑器、Waypoint 系统
🏰 区域编辑 可视化设置影响范围 AI 巡逻区域、触发区域
🔧 自定义工具 创建专用编辑工具 地形刷子、灯光布置
📐 辅助线 绘制调试信息 射线、包围盒、方向指示

1.3 基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(目标组件))]
public class 自定义编辑器 : Editor
{
void OnSceneGUI()
{
// 获取目标组件
目标组件组件 targetComponent = (目标组件)target;

// 开始绘制 Handles
Handles.color = Color.yellow;

// 使用各种 Handle 函数...

// 如果修改了值,需要标记场景为已修改
if (GUI.changed)
{
EditorUtility.SetDirty(targetComponent);
}
}
}

二、内置 Handle 类型

Unity 提供了多种内置 Handle 类型,满足大部分常见需求。

2.1 位置手柄 (PositionHandle)

在场景中创建一个可拖动的位置手柄。

1
2
3
4
5
6
7
8
9
10
11
Vector3 position = transform.position;

// 使用 Handles.PositionHandle 创建位置手柄
// 参数: 当前位置, 旋转方向
// 返回: 新位置
position = Handles.PositionHandle(position, Quaternion.identity);

if (EditorGUI.EndChangeCheck())
{
transform.position = position;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌────────────────────────────────────┐
│ PositionHandle │
│ │
│ Y (绿色) │
│ ↑ │
│ /│ │
│ / │ │
│ / │ │
│ ────●────→ X (红色) │
│ / Z (蓝色) │
│ ↙ │
│ │
│ 可沿 XYZ 三轴拖动 │
└────────────────────────────────────┘

2.2 旋转手柄 (RotationHandle)

创建一个可视化的旋转手柄。

1
2
3
4
5
6
7
8
9
10
11
Quaternion rotation = transform.rotation;

// 使用 Handles.RotationHandle 创建旋转手柄
// 参数: 当前旋转, 中心位置
// 返回: 新旋转
rotation = Handles.RotationHandle(rotation, transform.position);

if (EditorGUI.EndChangeCheck())
{
transform.rotation = rotation;
}

2.3 缩放手柄 (ScaleHandle)

创建缩放手柄。

1
2
3
4
5
6
7
8
9
10
11
Vector3 scale = transform.localScale;

// 使用 Handles.ScaleHandle 创建缩放手柄
// 参数: 当前缩放, 中心位置, 旋转方向, 大小
// 返回: 新缩放值
scale = Handles.ScaleHandle(scale, transform.position, Quaternion.identity, 1f);

if (EditorGUI.EndChangeCheck())
{
transform.localScale = scale;
}

2.4 半径手柄 (RadiusHandle)

创建一个圆形的半径调节手柄。

1
2
3
4
5
6
7
8
9
10
11
float radius = 5f;

// 使用 Handles.RadiusHandle 创建半径手柄
// 参数: 旋转方向, 中心位置, 当前半径
// 返回: 新半径值
radius = Handles.RadiusHandle(Quaternion.identity, transform.position, radius);

if (GUI.changed)
{
Debug.Log($"新半径: {radius}");
}
1
2
3
4
5
6
7
8
9
10
11
12
┌────────────────────────────────────┐
│ RadiusHandle │
│ │
│ ╭────────╮ │
│ ╭──╯ ╰──╮ │
│ │ ●───────● │ ← 拖动圆周 │
│ │ ╲ ╱ │ 调整半径 │
│ ╰──╯ ╭──╯ │
│ ╰────────╯ │
│ │
│ 常用于: AI 巡逻范围、触发区域 │
└────────────────────────────────────┘

2.5 方向手柄 (ArrowHandle)

创建一个指向性箭头。

1
2
3
4
5
6
7
Vector3 position = transform.position;
Vector3 direction = transform.forward;
float size = 1f;

// 使用 Handles.ArrowHandle 创建箭头手柄
// 参数: 位置, 方向, 大小
Handles.ArrowHandle(0, position, Quaternion.LookRotation(direction), size);

2.6 数值缩放手柄 (ScaleValueHandle)

创建一个可拖动的滑块来调整数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
float size = 1f;

// 使用 Handles.ScaleValueHandle 创建数值缩放手柄
// 参数: 当前值, 位置, 旋转, 大小, 手柄类型, 截断比例
// 返回: 新数值
size = Handles.ScaleValueHandle(
size,
transform.position + Vector3.up * 2,
Quaternion.identity,
2f,
Handles.ArrowHandleCap,
0.5f
);

三、Handle Caps(手柄样式)

Handle Cap 是手柄的视觉表现样式。

3.1 内置 Cap 类型

Cap 类型 描述 适用场景
SphereCap 球体 点位置标记
CircleCap 圆形 区域标记
RectangleCap 矩形 矩形区域
ArrowCap 箭头 方向指示
CubeCap 立方体 体积标记
CylinderCap 圆柱 柱状区域
ConeCap 圆锥 方向+体积
DotCap 精确位置

3.2 使用 Cap 的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
void OnSceneGUI()
{
Vector3 position = transform.position;
float size = 1f;

// 绘制不同类型的 Cap
Handles.SphereHandle(0, position + Vector3.left * 2, Quaternion.identity, size, EventType.Repaint);
Handles.CircleHandle(0, position + Vector3.right * 2, Quaternion.identity, size, EventType.Repaint);
Handles.CubeHandle(0, position + Vector3.up * 2, Quaternion.identity, size, EventType.Repaint);

// 自定义绘制 ArrowCap
Handles.ArrowCap(0, position + Vector3.down * 2, Quaternion.identity, size);
}

四、绘制形状与线条

Handles 类还提供了直接绘制形状和线条的方法。

4.1 基本形状绘制

方法 描述 示例
Handles.DrawLine() 绘制线段 DrawLine(start, end)
Handles.DrawDottedLine() 绘制虚线 DrawDottedLine(start, end)
Handles.DrawWireDisc() 绘制圆形轮廓 DrawWireDisc(center, normal, radius)
Handles.DrawSolidDisc() 绘制实心圆 DrawSolidDisc(center, normal, radius)
Handles.DrawWireArc() 绘制圆弧 DrawWireArc(center, normal, from, angle, radius)

4.2 形状绘制示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void OnSceneGUI()
{
Transform targetTransform = ((MonoBehaviour)target).transform;

// 绘制连接线
Handles.color = Color.white;
Handles.DrawLine(targetTransform.position, targetTransform.position + Vector3.forward * 5);

// 绘制虚线
Handles.color = Color.gray;
Handles.DrawDottedLine(targetTransform.position, targetTransform.position + Vector3.right * 5);

// 绘制圆形区域
Handles.color = new Color(1, 0, 0, 0.3f);
Handles.DrawWireDisc(targetTransform.position, Vector3.up, 3f);

// 绘制圆弧
Handles.color = Color.green;
Handles.DrawWireArc(
targetTransform.position + Vector3.up * 2,
Vector3.forward,
Vector3.right,
90f,
2f
);
}

4.3 绘制 3D 形状

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void OnSceneGUI()
{
Vector3 position = transform.position;

// 绘制线框立方体
Handles.color = Color.cyan;
Handles.DrawWireCube(position, Vector3.one * 2);

// 绘制线框球体
Handles.color = Color.yellow;
Handles.DrawWireSphere(position + Vector3.right * 3, 1f);

// 绘制线框胶囊体
Handles.color = Color.magenta;
Handles.DrawWireCapsule(position + Vector3.left * 3, Quaternion.identity, 2f, 0.5f);
}

五、颜色与样式

5.1 设置颜色

1
2
3
4
5
6
7
8
9
10
void OnSceneGUI()
{
// 设置所有后续 Handles 的颜色
Handles.color = Color.red;

Handles.DrawWireDisc(transform.position, Vector3.up, 2f);

// 也可以设置带透明度的颜色
Handles.color = new Color(1, 0, 0, 0.5f); // 半透明红色
}

5.2 矩阵变换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void OnSceneGUI()
{
// 保存当前矩阵
Matrix4x4 originalMatrix = Handles.matrix;

// 应用自定义变换矩阵
Handles.matrix = Matrix4x4.TRS(
transform.position, // 位置
transform.rotation, // 旋转
transform.localScale // 缩放
);

// 在变换后的空间中绘制
Handles.DrawWireCube(Vector3.zero, Vector3.one);

// 恢复原始矩阵
Handles.matrix = originalMatrix;
}

5.3 光照

1
2
3
4
5
6
7
8
void OnSceneGUI()
{
// 设置光照方向
Handles.lighting = true;

// 绘制带有光照效果的形状
Handles.SphereHandle(0, transform.position, Quaternion.identity, 1f, EventType.Repaint);
}

六、标签与辅助信息

6.1 Label 标签

1
2
3
4
5
6
7
8
9
10
11
12
void OnSceneGUI()
{
Vector3 position = transform.position;

// 在场景中绘制文本标签
Handles.Label(
position + Vector3.up * 2,
$"<size=24><color=yellow>{gameObject.name}</color></size>\n" +
$"Pos: {position}\n" +
$"HP: {100}"
);
}

6.2 使用 GUIStyle

1
2
3
4
5
6
7
8
9
10
11
12
void OnSceneGUI()
{
GUIStyle style = new GUIStyle()
{
fontSize = 16,
fontStyle = FontStyle.Bold,
normal = { textColor = Color.yellow },
alignment = TextAnchor.MiddleCenter
};

Handles.Label(transform.position + Vector3.up * 3, "自定义标签样式", style);
}

七、完整示例:路径编辑器

下面是一个完整的路径点编辑器示例。

7.1 目标组件

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;

/// <summary>
/// 路径组件 - 定义一系列路径点
/// </summary>
public class PathComponent : MonoBehaviour
{
[Header("路径设置")]
public Vector3[] waypoints = new Vector3[0];
public bool loop = false;
public Color pathColor = Color.cyan;
public float pointSize = 0.2f;
}

7.2 自定义编辑器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(PathComponent))]
public class PathComponentEditor : Editor
{
private PathComponent path;
private SerializedProperty waypointsProperty;
private SerializedProperty loopProperty;
private SerializedProperty pathColorProperty;

void OnEnable()
{
path = (PathComponent)target;
waypointsProperty = serializedObject.FindProperty("waypoints");
loopProperty = serializedObject.FindProperty("loop");
pathColorProperty = serializedObject.FindProperty("pathColor");
}

public override void OnInspectorGUI()
{
serializedObject.Update();

EditorGUILayout.LabelField("路径编辑器", EditorStyles.boldLabel);
EditorGUILayout.Space();

EditorGUILayout.PropertyField(loopProperty);
EditorGUILayout.PropertyField(pathColorProperty);
EditorGUILayout.PropertyField(waypointsProperty, true);

// 显示路径总长度
if (path.waypoints != null && path.waypoints.Length > 1)
{
float length = CalculatePathLength();
EditorGUILayout.HelpBox($"路径总长度: {length:F2} 单位", MessageType.Info);
}

EditorGUILayout.Space();

// 添加/清除按钮
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("添加当前相机位置"))
{
AddWaypointFromCamera();
}
if (GUILayout.Button("清除所有点"))
{
if (EditorUtility.DisplayDialog("确认", "确定要清除所有路径点吗?", "确定", "取消"))
{
waypointsProperty.ClearArray();
serializedObject.ApplyModifiedProperties();
}
}
EditorGUILayout.EndHorizontal();

serializedObject.ApplyModifiedProperties();
}

void OnSceneGUI()
{
if (path.waypoints == null || path.waypoints.Length == 0)
return;

// 设置颜色
Handles.color = path.pathColor;

// 绘制路径线
for (int i = 0; i < path.waypoints.Length - 1; i++)
{
Handles.DrawDottedLine(
path.waypoints[i],
path.waypoints[i + 1],
2f
);
}

// 绘制循环线
if (path.loop && path.waypoints.Length > 2)
{
Handles.DrawDottedLine(
path.waypoints[path.waypoints.Length - 1],
path.waypoints[0],
2f
);
}

// 绘制和编辑路径点
for (int i = 0; i < path.waypoints.Length; i++)
{
EditorGUI.BeginChangeCheck();

// 使用 SphereCap 作为手柄样式
Vector3 newPosition = Handles.PositionHandle(
path.waypoints[i],
Quaternion.identity
);

// 绘制点序号
Handles.Label(
path.waypoints[i] + Vector3.up * 0.5f,
$"<color=yellow>{i}</color>"
);

if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(path, "Move Waypoint");
path.waypoints[i] = newPosition;
EditorUtility.SetDirty(path);
}
}

// 绘制方向指示
DrawPathDirection();
}

private float CalculatePathLength()
{
if (path.waypoints.Length < 2)
return 0f;

float length = 0f;
for (int i = 0; i < path.waypoints.Length - 1; i++)
{
length += Vector3.Distance(path.waypoints[i], path.waypoints[i + 1]);
}

if (path.loop && path.waypoints.Length > 2)
{
length += Vector3.Distance(
path.waypoints[path.waypoints.Length - 1],
path.waypoints[0]
);
}

return length;
}

private void AddWaypointFromCamera()
{
Vector3 cameraPos = SceneView.lastActiveSceneView.camera.transform.position;
waypointsProperty.arraySize++;
waypointsProperty.GetArrayElementAtIndex(waypointsProperty.arraySize - 1).vector3Value = cameraPos;
serializedObject.ApplyModifiedProperties();
}

private void DrawPathDirection()
{
if (path.waypoints.Length < 2)
return;

Handles.color = Color.yellow;

for (int i = 0; i < path.waypoints.Length - 1; i++)
{
Vector3 dir = (path.waypoints[i + 1] - path.waypoints[i]).normalized;
Vector3 mid = (path.waypoints[i] + path.waypoints[i + 1]) * 0.5f;
Handles.ArrowCap(0, mid, Quaternion.LookRotation(dir), 0.5f);
}

if (path.loop && path.waypoints.Length > 2)
{
Vector3 dir = (path.waypoints[0] - path.waypoints[path.waypoints.Length - 1]).normalized;
Vector3 mid = (path.waypoints[0] + path.waypoints[path.waypoints.Length - 1]) * 0.5f;
Handles.ArrowCap(0, mid, Quaternion.LookRotation(dir), 0.5f);
}
}
}

7.3 效果展示

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────────────────────────┐
│ Scene 场景视图 │
│ │
│ 0 ──────→ 1 ──────→ 2 ──────→ 3 │
│ │ ╲ │
│ │ ╲ │
│ ←──── 5 ←──── 4 │
│ │
│ ● = PositionHandle (可拖动) │
│ → = 方向指示箭头 │
│ ── = 路径连接线 (虚线) │
│ 数字 = 点序号标签 │
└─────────────────────────────────────────────────────────┘

八、完整示例:AI 巡逻范围编辑器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
using UnityEngine;
using UnityEditor;

/// <summary>
/// AI 巡逻组件
/// </summary>
public class AIPatrol : MonoBehaviour
{
[Header("巡逻设置")]
public float patrolRadius = 5f;
public float chaseRadius = 10f;
public Color patrolColor = new Color(0, 1, 0, 0.3f);
public Color chaseColor = new Color(1, 0, 0, 0.3f);
}

/// <summary>
/// AI 巡逻编辑器
/// </summary>
[CustomEditor(typeof(AIPatrol))]
public class AIPatrolEditor : Editor
{
private AIPatrol patrol;
private SerializedProperty patrolRadiusProp;
private SerializedProperty chaseRadiusProp;

void OnEnable()
{
patrol = (AIPatrol)target;
patrolRadiusProp = serializedObject.FindProperty("patrolRadius");
chaseRadiusProp = serializedObject.FindProperty("chaseRadius");
}

void OnSceneGUI()
{
Vector3 position = patrol.transform.position;
position.y = 0f; // 保持在平面上

// 绘制巡逻范围(绿色)
Handles.color = patrol.patrolColor;
EditorGUI.BeginChangeCheck();
float newPatrolRadius = Handles.RadiusHandle(
Quaternion.identity,
position,
patrol.patrolRadius
);

if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(patrol, "Change Patrol Radius");
patrol.patrolRadius = newPatrolRadius;
}

// 绘制追击范围(红色)
Handles.color = patrol.chaseColor;
EditorGUI.BeginChangeCheck();
float newChaseRadius = Handles.RadiusHandle(
Quaternion.identity,
position,
patrol.chaseRadius
);

if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(patrol, "Change Chase Radius");
patrol.chaseRadius = newChaseRadius;
}

// 绘制标签
Handles.Label(
position + Vector3.up * 0.1f,
$"<color=green>巡逻: {patrol.patrolRadius:F1}m</color>\n" +
$"<color=red>追击: {patrol.chaseRadius:F1}m</color>"
);

if (GUI.changed)
{
EditorUtility.SetDirty(patrol);
}
}

public override void OnInspectorGUI()
{
serializedObject.Update();

EditorGUILayout.LabelField("AI 巡逻范围设置", EditorStyles.boldLabel);
EditorGUILayout.Space();

EditorGUILayout.PropertyField(patrolRadiusProp, new GUIContent("巡逻半径"));
EditorGUILayout.PropertyField(chaseRadiusProp, new GUIContent("追击半径"));

serializedObject.ApplyModifiedProperties();
}
}

九、Handles 常用方法速查表

方法 描述 返回值
位置相关
PositionHandle() 位置手柄 Vector3
旋转相关
RotationHandle() 旋转手柄 Quaternion
缩放相关
ScaleHandle() 缩放手柄 Vector3
ScaleValueHandle() 数值缩放手柄 float
形状相关
RadiusHandle() 半径手柄 float
Slider() 滑动手柄 float
ArrowHandle() 箭头手柄 -
绘制相关
DrawLine() 绘制线段 -
DrawDottedLine() 绘制虚线 -
DrawWireDisc() 绘制圆轮廓 -
DrawSolidDisc() 绘制实心圆 -
DrawWireArc() 绘制圆弧 -
DrawWireCube() 绘制线框立方体 -
DrawWireSphere() 绘制线框球体 -
辅助相关
Label() 绘制文本标签 -
DotHandle() 绘制点标记 -

十、最佳实践

10.1 变更检测

1
2
3
4
5
6
7
8
9
10
11
12
void OnSceneGUI()
{
EditorGUI.BeginChangeCheck();

// Handle 操作...

if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(target, "Handle Change");
EditorUtility.SetDirty(target);
}
}

10.2 性能优化

1
2
3
4
5
6
7
8
9
10
11
// 只在重绘事件时绘制复杂形状
void OnSceneGUI()
{
if (Event.current.type == EventType.Repaint)
{
// 复杂绘制操作...
}

// Handle 操作始终执行
position = Handles.PositionHandle(position, Quaternion.identity);
}

10.3 用户体验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用有意义的颜色
Handles.color = Color.green; // 安全区域
Handles.color = Color.yellow; // 警告区域
Handles.color = Color.red; // 危险区域

// 添加标签说明
Handles.Label(position, "拖动调整位置");

// 提供视觉反馈
if (value > maxValue)
{
Handles.color = Color.red;
Handles.Label(position, "值超出范围!");
}

十一、总结

本文介绍了 Unity Handles 的核心知识:

主题 要点
基本概念 Scene 视图中的 3D GUI 控件
核心方法 OnSceneGUI 中调用 Handles API
常用 Handle PositionHandle, RotationHandle, ScaleHandle, RadiusHandle
Handle Caps SphereCap, CircleCap, ArrowCap 等样式
绘制方法 DrawLine, DrawWireDisc, DrawWireArc 等
辅助功能 Label 标签, color 颜色, matrix 变换
最佳实践 使用 Undo 记录,检测变更,优化性能

💡 开发建议

  • 所有 Handle 操作都应该放在 OnSceneGUI 中
  • 使用 EditorGUI.BeginChangeCheck/EndChangeCheck 检测变更
  • 修改对象前调用 Undo.RecordObject 记录撤销
  • 使用合适的颜色和标签提升用户体验
  • 复杂绘制只在 EventType.Repaint 时执行

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1487842110@qq.com