
简介这份资源面向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. 场景搭建与脚本挂载从空场景到能动的 Capsule2.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 会返回 nullStart 阶段直接报空引用。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 GetComponentTransform(); } }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 米之间浮动。如果你的场景地面不在 y0或者 Capsule 高度不是默认的 2这两个阈值需要重新算。Translate 的第二个参数 Space.Self 表示按物体本地坐标系移动。因为前面已经把 Player 的朝向对齐了摄像机水平方向所以本地坐标的 z 轴就是「前方」W 键自然往屏幕里走。如果改成 Space.World物体永远沿世界 z 轴走摄像机一转方向就全乱了。3.4 参数调整对照表参数默认值作用调整建议m_movSpeed10移动速度系数室内漫游 3~5室外场景 8~15m_rotateSpeed1鼠标旋转灵敏度配合 Input Manager 的 Mouse Sensitivity一般 0.5~2Space 上限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 GetComponentCharacterController(); cc.Move(dir * moveSpeed * Time.deltaTime);CharacterController 会自动处理墙壁和地面碰撞但不会再走 Space.Self 的本地坐标需要自己把 dir 从本地转到世界Vector3 worldDir transform.TransformDirection(dir); cc.Move(worldDir * moveSpeed * Time.deltaTime);验证改造是否成功我习惯用三个检查斜向移动速度是否和直向一致鼠标推到极限时画面是否翻转按空格到 maxY 后是否停住而不是抖动。这三个过了基本就能直接放进原型里用。从那以后我每次拿到这类移动脚本都会先把硬编码的数值全部提成变量再跑一遍斜向和边界测试省得后面调场景时反复翻代码。希望帮到你。本文还有配套的精品资源点击获取