简介:这份资源面向Unity初学者与游戏开发入门者,聚焦键盘WASD控制物体移动这一基础交互需求,帮助读者理解输入检测与位移逻辑的配合方式。包内共1个PDF文件,约46KB,以图文与代码片段形式呈现完整实现思路,便于随时查阅与对照练习。内容围绕在场景中搭建Capsule、将主摄像机置于其下、挂载脚本并编写移动旋转逻辑展开,涉及Input.GetKey检测WASD、空格与F键,以及Input.GetAxis读取鼠标水平垂直移动,并给出MoveCam脚本的完整代码,涵盖移动系数、旋转系数、面向与摄像机同步、Space上升与F下降的边界限制等细节。该资源已有10928人学习,适合想快速掌握第一人称或第三人称移动控制、并在此基础上扩展虚拟现实与增强现实交互的开发者参考。
1. 从一段 MoveCam 脚本说起:WASD 移动到底该怎么落地
很多刚接触 Unity 键盘物体移动的开发者,第一次把 MoveCam 脚本挂到 Capsule 上,按下 W 发现物体确实动了,但紧接着就会撞上一连串问题:摄像机旋转后移动方向不对、空格上升没有上限、鼠标一松视角就跳回原点。这段不到 60 行的脚本,其实把 Unity 角色控制里最核心的几个坑都踩了一遍。它用 Input.GetKey 检测 WASD 和空格、F 键,用 Input.GetAxis 读取鼠标位移,通过 Translate 在 Space.Self 坐标系下推动物体,同时让主角朝向跟随摄像机水平角度。适合正在做第一人称漫游、场景预览工具、数字孪生看板原型的开发者,也适合想搞懂 Transform 与 Camera 联动关系的新手。下面我按「能跑起来 → 能改参数 → 能避开翻车点」的顺序,把这份资源拆开讲透。
2. 场景搭建与脚本挂载:从空场景到能动的 Capsule
2.1 为什么选 Capsule 而不是 Cube 或空物体
原始资源里第一步是「建立一个 Capsule,将主摄像机拖到其物体下」。这个选择不是随便定的。Capsule 自带 CharacterController 常用的碰撞外形,底部是半球,在后续加物理或导航时不会像 Cube 那样卡在台阶边缘。更关键的是,把 Main Camera 拖成 Capsule 的子物体后,摄像机的世界坐标就跟着父物体走,脚本里只需要旋转摄像机本地角度,不用每帧手动同步位置。
如果你用空物体做父级,场景里看不到任何参照,调移动速度时完全没有空间感;用 Cube 则会在贴地移动时因为直角边产生视觉抖动。所以 Capsule 是兼顾可视化和后续扩展的折中方案。
具体操作顺序:
- Hierarchy 面板右键 → 3D Object → Capsule,命名为 Player。
- 把 Main Camera 从 Hierarchy 里直接拖到 Player 上,松手后 Camera 变成 Player 的子物体。
- 选中 Camera,在 Inspector 里把 Transform 的 Position 改成 (0, 0.8, -3) 左右,让视角从 Capsule 后上方看出去。
- 新建 C# 脚本,命名必须和类名一致,这里叫 MoveCam。
- 把 MoveCam.cs 拖到 Player 物体上,或者用 Add Component 搜索添加。
提示:Camera 的 Tag 必须保持 MainCamera,否则脚本里 Camera.main 会返回 null,Start 阶段直接报空引用。
2.2 脚本骨架与字段含义
原始代码的字段很少,但每一个都影响手感:
public class MoveCam : MonoBehaviour { private Vector3 m_camRot; // 累积的摄像机欧拉角 private Transform m_camTransform; // Camera.main 的 Transform private Transform m_transform; // 脚本所在物体(Player)的 Transform public float m_movSpeed = 10; // 移动速度系数 public float m_rotateSpeed = 1; // 鼠标旋转灵敏度 private void Start() { m_camTransform = Camera.main.transform; m_transform = GetComponent<Transform>(); } }m_camRot 是一个持续累积的 Vector3,用来记录鼠标拖动后的俯仰和偏航角。为什么不直接读 m_camTransform.eulerAngles 再叠加?因为 eulerAngles 返回的是 0 到 360 的表示,当你从 359 度继续往上转时会瞬间跳到 0,视角会抽搐。用一个独立变量累积,就绕开了这个欧拉角回绕问题。
m_movSpeed 默认 10 意味着每秒移动 10 个世界单位,在默认 1 单位约等于 1 米的场景里,相当于 10 米每秒,偏快。m_rotateSpeed 默认 1 是鼠标灵敏度的基准值,实际手感取决于 Input Manager 里 Mouse X 的 Sensitivity 设置。
2.3 挂载后的第一次运行检查
脚本挂上去之后不要急着按 Play。先确认三件事:
- Camera 的 Tag 是 MainCamera。
- Player 的 Transform Position 不在 (0,0,0) 以外的异常位置,Scale 保持 1。
- 场景里没有其他脚本也在改 Camera 的 rotation。
运行后按住鼠标左键拖动,摄像机会绕 Player 旋转;松开左键,视角停在当前位置。如果一松手视角就弹回,说明 m_camRot 被某处重置了,检查是否有别的脚本在写 eulerAngles。
这一步跑通,说明输入链路和 Transform 引用都没问题,可以进入移动逻辑的拆解。
3. 移动与旋转逻辑拆解:Translate、Space.Self 和鼠标视角
3.1 鼠标旋转为什么用累积欧拉角
Control 方法里鼠标部分是这样的:
if (Input.GetMouseButton(0)) { float rh = Input.GetAxis("Mouse X"); float rv = Input.GetAxis("Mouse Y"); m_camRot.x -= rv * m_rotateSpeed; m_camRot.y += rh * m_rotateSpeed; } m_camTransform.eulerAngles = m_camRot;Input.GetAxis("Mouse X") 返回的是本帧鼠标水平位移量,已经乘过 Input Manager 里的灵敏度。rv 取负是因为 Unity 里鼠标往上推时 Mouse Y 为正,但摄像机抬头需要 x 角度减小,所以用减法。rh 直接加给 y,是因为鼠标右移时 Mouse X 为正,摄像机应该向右偏航。
这里有个容易被忽略的点:m_camRot 没有对 x 做 clamp。如果你把鼠标一直往上推,x 会超过 90 度,摄像机会翻转,画面上下颠倒。常见做法是在赋值前加一句 m_camRot.x = Mathf.Clamp(m_camRot.x, -80f, 80f),留 10 度余量避免看到万向锁边缘的抖动。
3.2 主角朝向跟随摄像机的取舍
Vector3 camrot = m_camTransform.eulerAngles; camrot.x = 0; camrot.z = 0; m_transform.eulerAngles = camrot;这段把摄像机的俯仰和翻滚清零,只保留水平偏航,赋给 Player。效果是 Player 永远面朝摄像机看的方向,W 键按下去就是往「屏幕里」走。这是第三人称和第一人称漫游里最常见的做法。
但要注意:如果 Player 上挂了 Rigidbody 并且勾了 Freeze Rotation,这行赋值不会生效,物体会保持原朝向。另外,如果后续要做角色动画,直接改 eulerAngles 会绕过 Animator 的 Root Motion,需要改成用 Rotate 或 LookRotation 配合动画状态机。
3.3 WASD 与 Space/F 的位移计算
float xm = 0, ym = 0, zm = 0; if (Input.GetKey(KeyCode.W)) zm += m_movSpeed * Time.deltaTime; else if (Input.GetKey(KeyCode.S)) zm -= m_movSpeed * Time.deltaTime; if (Input.GetKey(KeyCode.A)) xm -= m_movSpeed * Time.deltaTime; else if (Input.GetKey(KeyCode.D)) xm += m_movSpeed * Time.deltaTime; if (Input.GetKey(KeyCode.Space) && m_transform.position.y <= 3) ym += m_movSpeed * Time.deltaTime; if (Input.GetKey(KeyCode.F) && m_transform.position.y >= 1) ym -= m_movSpeed * Time.deltaTime; m_transform.Translate(new Vector3(xm, ym, zm), Space.Self);Time.deltaTime 把「每秒速度」换算成「本帧位移」,保证不同帧率下移动距离一致。如果不乘,帧率越高物体跑得越快,这是新手最常见的翻车点之一。
Space 和 F 分别控制上升和下降,并且用 position.y 做了软限制:y 不超过 3,不低于 1。这个 1 到 3 的范围是写死的,意味着 Capsule 只能在离地 1 到 3 米之间浮动。如果你的场景地面不在 y=0,或者 Capsule 高度不是默认的 2,这两个阈值需要重新算。
Translate 的第二个参数 Space.Self 表示按物体本地坐标系移动。因为前面已经把 Player 的朝向对齐了摄像机水平方向,所以本地坐标的 z 轴就是「前方」,W 键自然往屏幕里走。如果改成 Space.World,物体永远沿世界 z 轴走,摄像机一转方向就全乱了。
3.4 参数调整对照表
| 参数 | 默认值 | 作用 | 调整建议 |
|---|---|---|---|
| m_movSpeed | 10 | 移动速度系数 | 室内漫游 3~5,室外场景 8~15 |
| m_rotateSpeed | 1 | 鼠标旋转灵敏度 | 配合 Input Manager 的 Mouse Sensitivity,一般 0.5~2 |
| Space 上限 | 3 | 上升最高 y 坐标 | 按场景层高改,建议用变量暴露 |
| F 下限 | 1 | 下降最低 y 坐标 | 按地面高度改,避免穿地 |
| Camera 本地 z | -3 | 摄像机后撤距离 | 第一人称改 0,第三人称 2~5 |
把上限下限写成 public 变量而不是硬编码,是这份脚本最该先改的地方。后面调场景时不用每次翻代码。
4. 避坑与排查:WASD 移动最常见的五类翻车
4.1 按 W 物体往侧面走
现象:摄像机转向 90 度后,按 W 物体不是往屏幕里走,而是往世界 z 轴方向走。
原因:Translate 用了 Space.World,或者 Player 的朝向没有跟随摄像机更新。
解决:确认 Translate 第二个参数是 Space.Self;确认 camrot.x 和 camrot.z 清零后赋给了 m_transform.eulerAngles,且赋值发生在 Translate 之前。如果 Player 上有 Rigidbody,检查 constraints 是否冻结了 Y 轴旋转。
4.2 移动速度忽快忽慢
现象:高帧率机器上物体跑得飞快,低帧率机器上像慢动作。
原因:位移计算里漏乘了 Time.deltaTime。
解决:所有和「每秒速度」相关的位移都必须乘 Time.deltaTime。检查 xm、ym、zm 三个分量是否都乘了。如果用了 Rigidbody.MovePosition,则不需要乘,因为物理系统自己处理时间步长。
4.3 鼠标一松视角弹回
现象:拖动鼠标旋转正常,松开左键后摄像机瞬间回到某个固定角度。
原因:m_camRot 在别处被重置,或者每帧从 eulerAngles 重新读取导致累积值丢失。
解决:确保 m_camRot 只在鼠标按下时累加,赋值给 eulerAngles 时不要反向写回 m_camRot。如果场景里有多个脚本操作 Camera,用 Debug.Log 打印 m_camRot 每帧的值,看是哪一帧被改了。
4.4 空格上升穿模或卡住
现象:按空格物体一直往上飞,或者按 F 降到某个高度就下不去。
原因:position.y 的上下限写死,和场景实际地面高度不匹配;或者 Capsule 的碰撞体卡在了其他碰撞体里。
解决:把上下限改成 public 变量,在 Inspector 里按场景调。如果物体卡住,检查是否有 BoxCollider 或 MeshCollider 挡住了路径。另外,Translate 是直接改 Transform,不走物理,所以会穿过碰撞体,需要碰撞就用 CharacterController 或 Rigidbody。
4.5 Camera.main 返回 null
现象:运行后报 NullReferenceException,指向 Start 里的 m_camTransform。
原因:场景里没有 Tag 为 MainCamera 的摄像机,或者摄像机被禁用了。
解决:选中 Camera,在 Inspector 顶部 Tag 下拉选 MainCamera。如果场景里有多个摄像机,确认主摄像机是启用状态且 Tag 正确。也可以改用 public Transform 在 Inspector 里手动拖拽,避免依赖 Tag。
5. 从能跑到好用:把 MoveCam 改造成可配置的角色控制器
原始脚本能跑,但离「好用」还差几步。我一般会做三个改造:把速度、上下限、旋转灵敏度全部暴露成 public 并在 Inspector 里分组;把鼠标旋转的 x 轴 clamp 加上;把 Translate 换成 CharacterController.Move,让移动受碰撞约束。
先看改造后的核心代码:
[Header("Movement")] public float moveSpeed = 5f; public float rotateSpeed = 1.5f; public float minY = 1f; public float maxY = 3f; [Header("Camera")] public Transform camTransform; private float pitch = 0f; private void Start() { if (camTransform == null) camTransform = Camera.main.transform; } private void Update() { // 鼠标旋转 if (Input.GetMouseButton(0)) { float rh = Input.GetAxis("Mouse X"); float rv = Input.GetAxis("Mouse Y"); pitch -= rv * rotateSpeed; pitch = Mathf.Clamp(pitch, -80f, 80f); camTransform.localEulerAngles = new Vector3(pitch, 0, 0); transform.Rotate(0, rh * rotateSpeed, 0); } // 移动 float xm = 0, zm = 0, ym = 0; if (Input.GetKey(KeyCode.W)) zm += 1; if (Input.GetKey(KeyCode.S)) zm -= 1; if (Input.GetKey(KeyCode.A)) xm -= 1; if (Input.GetKey(KeyCode.D)) xm += 1; if (Input.GetKey(KeyCode.Space) && transform.position.y < maxY) ym += 1; if (Input.GetKey(KeyCode.F) && transform.position.y > minY) ym -= 1; Vector3 dir = new Vector3(xm, ym, zm).normalized; transform.Translate(dir * moveSpeed * Time.deltaTime, Space.Self); }这里把摄像机旋转拆成两步:pitch 只作用在 Camera 的本地 x 轴,yaw 作用在 Player 的世界 y 轴。这样摄像机不会因为 Player 的翻滚而倾斜,也不会出现欧拉角回绕。clamp 到 ±80 度是防止抬头低头到极点时画面翻转。
移动方向用 normalized 归一化,避免同时按 W 和 D 时斜向速度变成 1.414 倍。这是很多教程漏掉的细节,斜着走比直着走快,手感很怪。
如果你需要碰撞,把 Translate 换成:
CharacterController cc = GetComponent<CharacterController>(); cc.Move(dir * moveSpeed * Time.deltaTime);CharacterController 会自动处理墙壁和地面碰撞,但不会再走 Space.Self 的本地坐标,需要自己把 dir 从本地转到世界:
Vector3 worldDir = transform.TransformDirection(dir); cc.Move(worldDir * moveSpeed * Time.deltaTime);验证改造是否成功,我习惯用三个检查:斜向移动速度是否和直向一致;鼠标推到极限时画面是否翻转;按空格到 maxY 后是否停住而不是抖动。这三个过了,基本就能直接放进原型里用。
从那以后我每次拿到这类移动脚本,都会先把硬编码的数值全部提成变量,再跑一遍斜向和边界测试,省得后面调场景时反复翻代码。希望帮到你。
本文还有配套的精品资源,点击获取