
简介本资源面向Unity游戏开发初学者与需要快速实现移动端交互的开发者聚焦UGUI摇杆的搭建与物体控制。内容从Canvas、RectTransform、Image等基础组件讲起逐步完成摇杆背景与滑块的创建、Joystick脚本编写、输入方向计算及与Rigidbody物体的联动帮助读者理解UI事件与游戏逻辑的衔接方式。压缩包共1575个文件以info、bin、asset、meta、dll等Unity工程与缓存文件为主另含少量xml、txt、png及脚本文件整体约27.28MB属于可直接导入Unity编辑器运行的完整项目结构。目前已有1874人学习下载适合希望跳过繁琐配置、直接参考可运行工程来掌握UGUI摇杆实现思路的开发者也可在此基础上扩展触摸屏支持与移动范围限制。1. 从一次翻车说起UGUI 摇杆到底难在哪很多人第一次做 Unity 摇杆都是直接拿 Image 拼两个圆然后写个脚本让内圈跟着手指跑。跑起来看着没问题真机一测就翻车手指滑出摇杆范围内圈直接飞出去多指同时按屏幕摇杆乱跳换个分辨率摇杆位置全偏。这些坑我在项目里踩过不止一次后来才明白UGUI 摇杆的核心不是画两个圆而是把「屏幕坐标 → 摇杆局部坐标 → 归一化方向向量」这条链路走通并且把边界、多指、分辨率三件事处理干净。这篇笔记拆的是一个用 UGUI 做摇杆、控制物体移动的完整方案。它不依赖任何第三方插件纯 UGUI C# 脚本适合正在做移动端角色控制、又不想引入重型输入框架的开发者。整套东西的核心就三块一个摇杆 UI 结构、一个处理拖拽和方向计算的脚本、一个接收方向向量去驱动物体的控制脚本。下面从结构搭建讲到参数调优再到真机上最容易翻车的几个点照着做基本能一次跑通。2. 摇杆 UI 结构怎么搭从 Canvas 到内外圈的层级设计2.1 为什么用 UGUI 而不是虚拟轴或第三方插件Unity 自带的 Input Manager 里有 Horizontal/Virtual 轴也有 Joystick 相关的输入项但那是给实体手柄用的触摸屏上没法直接用。第三方插件像 Joystick Pack 确实开箱即用但引入外部包意味着版本兼容、包体增大、出问题不好排查。UGUI 方案的优势在于所有东西都在你眼皮底下Image 的锚点、RectTransform 的 pivot、Canvas 的 Render Mode 你都能控制出问题能定位到具体哪一层。选 UGUI 还有一个现实原因移动端 UI 和 3D 物体通常分属不同 Canvas摇杆作为 UI 元素天然适合放在 Screen Space - Overlay 的 Canvas 下不参与世界坐标变换省掉一堆坐标转换的麻烦。常见做法是单独建一个 Canvas 专门放摇杆Sort Order 调高一点避免被其他 UI 挡住。2.2 层级结构Background 与 Handle 的父子关系摇杆的 UI 结构其实就两层Joystick (空物体挂脚本) ├── Background (Image摇杆底盘) │ └── Handle (Image摇杆内圈)Background 是底盘固定不动Handle 是内圈跟着手指在 Background 范围内移动。把 Handle 放在 Background 下面是为了让 Handle 的 localPosition 直接相对于 Background 中心计算省掉一次世界坐标转换。Background 的锚点设成中心对齐pivot 设 (0.5, 0.5)这样它的 localPosition 就是摇杆中心。具体操作步骤在 Hierarchy 右键 → UI → Canvas新建一个 CanvasRender Mode 选 Screen Space - Overlay。在 Canvas 下右键 → UI → Image命名 BackgroundSource Image 选一个圆形 Sprite没有的话用 Unity 自带的 Knob。在 Background 下右键 → UI → Image命名 HandleSource Image 同样选圆形尺寸设成 Background 的一半左右。选中 BackgroundRectTransform 的 Anchor 设 center-middlePos X/Y 设成你想要的摇杆位置比如左下角就设 (150, 150)。选中 HandleAnchor 同样 center-middlePos X/Y 归零让它默认在底盘正中心。这里有个容易忽略的点Background 和 Handle 的 Raycast Target 都要勾上否则拖拽事件收不到。但如果你在摇杆区域上方还叠了其他 UI记得把那些 UI 的 Raycast Target 关掉不然会拦截触摸。2.3 锚点与 pivot 设置对拖拽计算的影响锚点和 pivot 设错是摇杆方向计算偏移的常见原因。Background 的 pivot 必须是 (0.5, 0.5)这样它的中心就是 localPosition 原点。Handle 的 pivot 也设 (0.5, 0.5)这样 Handle 的 localPosition 直接就是从底盘中心出发的偏移向量不用再减去半个宽高。如果 Handle 的 pivot 设成了 (0, 0)那它的 localPosition 表示的是左下角位置算方向时就得手动补偿很容易算错。我一般会在脚本里加一句断言检查 pivot 是不是 (0.5, 0.5)不是就在 Console 里警告省得后面调半天。提示Background 和 Handle 的 Sprite 最好用同一张圆形图避免一个圆一个方导致视觉上方向感错乱。如果暂时没有圆形 Sprite可以用 Unity 内置的 Knob它在 UI 默认资源里。3. 核心脚本怎么写拖拽事件、方向归一化与边界限制3.1 实现 IDragHandler 与 IPointerUpHandler摇杆脚本要挂在一个能接收拖拽事件的物体上。最直接的做法是让脚本实现 IDragHandler、IPointerDownHandler、IPointerUpHandler 三个接口挂在 Background 上。这样手指按在底盘范围内就能触发拖拽Handle 跟着动。using UnityEngine; using UnityEngine.EventSystems; public class Joystick : MonoBehaviour, IDragHandler, IPointerDownHandler, IPointerUpHandler { public RectTransform background; // 底盘 public RectTransform handle; // 内圈 public float handleRange 1f; // 内圈可移动范围系数 public float deadZone 0.1f; // 死区小于此值不输出方向 public Vector2 Direction { get; private set; } // 归一化方向 public float Magnitude { get; private set; } // 0~1 的推杆幅度 private Vector2 inputVector Vector2.zero; private Canvas canvas; void Start() { canvas GetComponentInParentCanvas(); // 确保 pivot 正确否则方向会偏 if (background.pivot ! new Vector2(0.5f, 0.5f)) Debug.LogWarning(Background pivot 建议设为 (0.5, 0.5)); } public void OnPointerDown(PointerEventData eventData) { OnDrag(eventData); } public void OnDrag(PointerEventData eventData) { Vector2 position; // 把屏幕坐标转成 Background 的局部坐标 RectTransformUtility.ScreenPointToLocalPointInRectangle( background, eventData.position, eventData.pressEventCamera, out position); // 归一化到 -1~1 范围 Vector2 radius background.sizeDelta * 0.5f; inputVector new Vector2(position.x / radius.x, position.y / radius.y); inputVector Vector2.ClampMagnitude(inputVector, 1f); // 计算 Handle 位置 handle.anchoredPosition inputVector * radius * handleRange; // 输出方向与幅度 Magnitude inputVector.magnitude; Direction Magnitude deadZone ? inputVector.normalized : Vector2.zero; } public void OnPointerUp(PointerEventData eventData) { inputVector Vector2.zero; handle.anchoredPosition Vector2.zero; Direction Vector2.zero; Magnitude 0f; } }这段代码的逻辑链条是手指按下或拖拽时先把屏幕坐标转成 Background 的局部坐标再除以底盘半径得到 -1~1 的向量用 ClampMagnitude 限制在单位圆内最后把 Handle 的 anchoredPosition 设成这个向量乘以半径。Direction 输出的是归一化方向Magnitude 输出的是推杆幅度两者分开方便后面做「轻推慢走、重推快跑」的速度控制。参数说明handleRange 控制内圈能跑多远设 1 表示内圈边缘刚好贴底盘边缘设 0.8 会留一圈余量。deadZone 是死区手指轻微抖动时不输出方向避免物体原地抖。这两个值我一般设 0.9 和 0.15手感比较稳。3.2 方向向量归一化与死区处理归一化这一步很多人会漏。如果不归一化Direction 的长度会随推杆幅度变化后面给物体加速时速度就不稳定。正确做法是 Direction 只表示方向长度恒为 1Magnitude 单独表示幅度两者相乘才是最终速度系数。死区处理也有讲究。死区太小手指放在摇杆上不动物体也会因为触摸抖动慢慢漂移死区太大轻推没反应手感发木。常见做法是把死区设在 0.1~0.2 之间并且在死区内把 Direction 直接置零而不是把 inputVector 清零这样 Handle 还能跟着手指微动视觉上不会觉得卡。3.3 边界限制ClampMagnitude 与 Handle 回弹边界限制靠 Vector2.ClampMagnitude 完成它把向量长度限制在 1 以内方向不变。这样手指滑出底盘范围时Handle 会停在底盘边缘而不是飞出去。松手时 OnPointerUp 把 Handle 的 anchoredPosition 归零实现回弹。这里有个细节ClampMagnitude 限制的是归一化后的向量所以 Handle 的实际位置是 inputVector * radius * handleRange。如果 handleRange 设 1Handle 中心最远能到 radius 处也就是底盘边缘。如果想让 Handle 完全不出底盘handleRange 要设成 (底盘半径 - Handle 半径) / 底盘半径这个值需要根据实际尺寸算。注意如果摇杆区域上方有其他 UI 元素记得把它们的 Raycast Target 关掉否则 OnDrag 收不到事件Handle 不动。这个坑我踩过排查了半天才发现是上层 UI 拦截了触摸。4. 物体移动怎么接把方向向量转成位移与旋转4.1 用 CharacterController 还是 Transform.Translate摇杆输出方向后驱动物体移动有两种常见方式CharacterController.Move 和 Transform.Translate。CharacterController 自带碰撞检测和斜坡处理适合角色控制Transform.Translate 简单直接适合不需要碰撞的演示场景。如果项目里角色需要走地形、上台阶用 CharacterController 更省事。它的 Move 方法接收一个位移向量内部会处理碰撞滑动。如果只是做个平面移动的 demoTransform.Translate 就够了代码更短。using UnityEngine; public class PlayerController : MonoBehaviour { public Joystick joystick; // 摇杆引用 public float moveSpeed 5f; // 最大移动速度 public float rotateSpeed 10f; // 转向速度 private CharacterController controller; void Start() { controller GetComponentCharacterController(); } void Update() { // 摇杆方向是二维的映射到世界坐标的 XZ 平面 Vector3 moveDir new Vector3(joystick.Direction.x, 0f, joystick.Direction.y); float speed moveSpeed * joystick.Magnitude; if (moveDir.sqrMagnitude 0.001f) { // 移动 controller.Move(moveDir.normalized * speed * Time.deltaTime); // 转向让物体朝向移动方向 Quaternion targetRot Quaternion.LookRotation(moveDir); transform.rotation Quaternion.Slerp( transform.rotation, targetRot, rotateSpeed * Time.deltaTime); } } }这段代码把摇杆的二维方向映射到世界坐标的 XZ 平面Y 轴不动。速度用 moveSpeed 乘以 Magnitude实现推得越远走得越快。转向用 Quaternion.LookRotation 加 Slerp让物体平滑转向移动方向而不是瞬间掉头。参数说明moveSpeed 是满推时的速度一般设 3~8 之间看场景尺度。rotateSpeed 控制转向快慢设 10 左右比较跟手设 3 会有点拖沓。如果不需要转向把 LookRotation 那段删掉即可。4.2 速度曲线Magnitude 与移动速度的映射直接拿 Magnitude 乘 moveSpeed 是线性映射推杆幅度和速度成正比。但实际手感往往需要非线性轻推时速度变化慢重推时速度变化快。常见做法是给 Magnitude 加一条 AnimationCurve或者用 Mathf.Pow 做指数映射。// 指数映射让轻推更细腻 float speedFactor Mathf.Pow(joystick.Magnitude, 1.5f); float speed moveSpeed * speedFactor;指数 1.5 是我常用的值轻推时速度增长慢重推时接近满速。如果觉得太肉可以降到 1.2如果觉得太灵敏升到 2。这个值没有标准答案得在真机上反复试。4.3 摄像机跟随与摇杆方向的坐标系对齐如果摄像机是固定角度俯视摇杆的 Y 轴对应世界 Z 轴方向直觉是对的。但如果摄像机跟着角色旋转摇杆方向就得转换到摄像机坐标系否则会出现「往上推角色往屏幕上方走但世界坐标里是往侧面走」的错位。常见做法是用 Camera.main.transform 的方向做基准Vector3 forward Camera.main.transform.forward; Vector3 right Camera.main.transform.right; forward.y 0f; right.y 0f; forward.Normalize(); right.Normalize(); Vector3 moveDir forward * joystick.Direction.y right * joystick.Direction.x;这样摇杆方向始终相对于摄像机不管摄像机怎么转往上推都是屏幕上方。这个转换在第三人称视角里几乎是必须的第一人称固定视角可以省掉。5. 避坑与排查真机上最容易翻车的五个点5.1 多指触摸时摇杆乱跳现象一根手指按摇杆另一根手指点屏幕其他地方摇杆 Handle 突然跳到另一根手指的位置。原因OnDrag 里用的是 eventData.position它对应的是当前触发拖拽的那根手指。但如果摇杆脚本挂在 Background 上而 Background 的 Raycast Target 范围覆盖了整个屏幕第二根手指按下时也会触发 OnPointerDown把摇杆抢过去。解决把 Background 的 RectTransform 尺寸限制在摇杆可视范围内不要拉满屏幕。或者在 OnPointerDown 里记录 pointerIdOnDrag 里判断 eventData.pointerId 是否匹配不匹配就忽略。private int activePointerId -1; public void OnPointerDown(PointerEventData eventData) { if (activePointerId ! -1) return; // 已有手指在控制 activePointerId eventData.pointerId; OnDrag(eventData); } public void OnDrag(PointerEventData eventData) { if (eventData.pointerId ! activePointerId) return; // ... 原有逻辑 } public void OnPointerUp(PointerEventData eventData) { if (eventData.pointerId ! activePointerId) return; activePointerId -1; // ... 原有归零逻辑 }5.2 分辨率变化后摇杆位置偏移现象在 1080P 手机上摇杆位置正常换到 720P 或平板摇杆跑到屏幕外或位置偏了。原因Background 的锚点设成了绝对坐标没有用相对锚点。Canvas 的 Canvas Scaler 如果设的是 Constant Pixel SizeUI 尺寸不随分辨率缩放位置就会偏。解决Canvas Scaler 的 UI Scale Mode 设成 Scale With Screen SizeReference Resolution 设成 1920×1080Match 设 0.5。Background 的锚点用相对锚点比如左下角就设 anchor 为 (0, 0)pivot 为 (0.5, 0.5)Pos X/Y 设成 (150, 150)这样不管分辨率怎么变摇杆始终在左下角固定距离处。5.3 Handle 飞出底盘范围现象手指快速滑出摇杆区域Handle 跟着飞出去松手后才弹回来。原因OnDrag 里没有做 ClampMagnitude或者 ClampMagnitude 用在了错误的地方。解决确保 inputVector 在计算 Handle 位置之前已经 ClampMagnitude 到 1。如果 handleRange 设了大于 1 的值Handle 也会超出底盘检查这个参数。5.4 摇杆方向与物体移动方向不一致现象往上推摇杆物体往右走或者斜推时方向偏了 45 度。原因坐标系映射错了。摇杆的 Direction 是二维的 (x, y)映射到世界坐标时x 对应世界 X 轴y 对应世界 Z 轴。如果摄像机有旋转还得做摄像机坐标系转换。解决先确认摄像机是否固定。固定视角直接用 (Direction.x, 0, Direction.y)。旋转视角用 4.3 节的摄像机方向转换。另外检查 Background 的 pivot 是不是 (0.5, 0.5)pivot 偏了方向也会偏。5.5 松手后物体继续滑动现象松开摇杆物体还往前滑一段才停。原因Direction 在 OnPointerUp 里没有及时归零或者物体移动用了 Rigidbody 的 velocity 但没有做阻尼。解决OnPointerUp 里把 Direction 和 Magnitude 都置零Handle 的 anchoredPosition 归零。如果用 Rigidbody 移动松手后把 velocity 设成 Vector3.zero或者加一个摩擦力让速度自然衰减。提示排查摇杆问题时可以在 OnDrag 里 Debug.Log 输出 inputVector 和 Direction看数值是否符合预期。真机上可以用 Unity Remote 或者打 Development Build 连 Profiler 看。6. 进阶技巧把摇杆做成可复用组件与手感调优6.1 用 ScriptableObject 存摇杆配置如果项目里有多个摇杆比如移动摇杆和视角摇杆把参数抽成 ScriptableObject 会方便很多。建一个 JoystickConfig存 handleRange、deadZone、moveSpeed、rotateSpeed 这些值不同摇杆引用不同配置改参数不用改代码。using UnityEngine; [CreateAssetMenu(fileName JoystickConfig, menuName Config/Joystick)] public class JoystickConfig : ScriptableObject { public float handleRange 0.9f; public float deadZone 0.15f; public float moveSpeed 5f; public float rotateSpeed 10f; public float speedExponent 1.5f; }Joystick 脚本里加一个 public JoystickConfig configStart 时把 config 的值读进来。这样调手感时只改 asset 文件不用碰脚本也不怕改错代码。6.2 动态摇杆按下位置即摇杆中心固定摇杆有个局限手指必须按在底盘范围内才能触发。动态摇杆的做法是手指按在屏幕任意位置摇杆底盘瞬间移到手指位置松手后隐藏。这种方案在动作游戏里很常见手感更自由。实现思路把 Background 的初始位置设成屏幕外或隐藏OnPointerDown 时把 Background 的 anchoredPosition 设成手指的局部坐标同时显示出来。OnPointerUp 时再隐藏。注意动态摇杆的 Raycast Target 要覆盖整个屏幕否则按在空白处收不到事件。6.3 手感调优死区、回弹速度与灵敏度曲线手感调优没有捷径就是反复试。我一般会按这个顺序调参数作用常用范围调整方向deadZone死区大小0.1~0.2物体漂移就调大轻推没反应就调小handleRange内圈活动范围0.8~1.0内圈飞出底盘就调小moveSpeed最大速度3~8看场景尺度角色太小就调大rotateSpeed转向速度8~15转向拖沓就调大太灵敏就调小speedExponent速度曲线指数1.2~2.0轻推太肉就调小太灵敏就调大调的时候建议在真机上试编辑器里鼠标拖拽和手指触摸的手感差别很大。我一般会打一个 Development Build 装到手机上边玩边改参数改完重新打包。虽然麻烦但比在编辑器里猜准得多。6.4 用 Profiler 检查摇杆脚本的性能开销摇杆脚本本身开销很小但如果 OnDrag 里做了复杂计算或者每帧都在做坐标转换还是值得看一眼。打开 Profiler看 Scripts 部分的耗时正常情况下摇杆脚本应该在 0.1ms 以下。如果超过 0.5ms检查是不是在 OnDrag 里调用了 Camera.main它内部会做 FindGameObjectWithTag开销不小改成在 Start 里缓存 Camera 引用。从那以后我每次做摇杆都会先把 pivot 和锚点检查一遍再在真机上测多指和分辨率最后才调手感参数。这套流程走下来基本不会翻车。希望帮到你。本文还有配套的精品资源点击获取