简介:本资源围绕Unity引擎中基于键盘WASD与鼠标配合实现物体移动的典型需求整理,适合Unity初学者、游戏开发入门者以及需要快速实现第一人称或第三人称移动控制的开发者。资源为一个PDF文档,压缩包共1个文件,大小约46KB,内容包含从场景搭建、脚本挂载到核心代码的完整讲解,重点介绍了Input.GetKey与Input.GetAxis等方法的使用逻辑,并详细演示了WASD控制前后左右、空格与F键控制Y轴升降、鼠标左键拖拽旋转视角等操作。文档还分析了该方案的优缺点及适用场景,例如第一人称射击游戏、模拟类应用和虚拟现实交互原型。目前该资源已有10925人学习,读者可直接参考其中完整的MoveCam示例代码,理解物体位移与摄像机旋转的配合方式,为后续扩展角色控制、碰撞检测或视角平滑等功能打下基础。
1. 一条WASD移动代码的背后,是输入、坐标空间和物理帧三件事
Unity里让物体跟着WASD动,最直觉的写法就是读两个轴、给Transform的position加一个方向向量。这个流程很多入门教程三分钟就带过,但真正落到项目里,你会发现同样的代码在第三人称下按W不走"屏幕前方",斜着走速度突然变成1.4倍,加了Rigidbody之后还会穿墙抖动。问题不出在"移动"本身,而在于你选了哪套输入接口、移动的是Transform还是Rigidbody、方向向量算在世界空间还是本地空间。这三点决定了一段WASD移动代码能不能直接用,也决定了它是入门第一课还是后面返工的重灾区。这篇就顺着这条链路,把每层的选择和参数讲清楚。
2. 用Input类接住键盘:WASD映射、按键检测与轴输入的区别
2.1 先说Input.GetKey与Input.GetAxis两套读键方式
Unity里读键盘最直接的方法是Input.GetKey(KeyCode.W),返回布尔值,按着就是true,松开就是false。配合GetKeyDown和GetKeyUp可以捕捉按下瞬间和抬起瞬间,适合做单次触发逻辑,比如跳跃、互动、UI快捷键。而Input.GetAxis("Horizontal")返回的是一个从-1到1的浮点数,按下A返回负值,按下D返回正值,数值变化还受Input Manager里的灵敏度、重力参数影响,自带平滑过渡。键盘本来就是数字输入,为什么要一个带平滑的浮点?因为同样的输入接口还要兼容手柄摇杆,摇杆推一点就返回0.3,推满才返回1。移動代码和速度相乘时,用一个float比判断一堆bool要干净得多。
默认工程已经配置好了Edit > Project Settings > Input Manager里的Horizontal和Vertical两组轴,分别绑定了A/D、W/S,另外还带上下方向键。大多数情况下不用手动改映射,直接读这两个轴就行。
2.2 最小可动代码:Update里移动Transform
2.2.1 基于Translate的写法与参数说明
先看一段能直接挂到物体上跑起来的代码:
using UnityEngine; public class SimpleMove : MonoBehaviour { public float speed = 5f; void Update() { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); Vector3 dir = new Vector3(h, 0f, v); transform.Translate(dir * speed * Time.deltaTime, Space.World); } }这段代码的逻辑:h对应A/D轴,v对应W/S轴,两者拼成一个三维向量dir,水平方向是X,前后是Z,Y固定为0。dir乘以speed得到每秒位移量,再乘Time.deltaTime把"每帧位移"换算成"每秒位移",最后用Translate加到位置上。Space.World表示该方向向量按世界坐标解释,物体自身旋转了也不会影响W键的方向;如果改成Space.Self,方向会跟随物体的本地坐标,旋转后W键可能变成"沿着物体鼻子方向走",这在某些俯视游戏里没问题,但第三人称角色一旦转向,就会得到很诡异的手感。
2.2.2 为什么必须乘Time.deltaTime
Update的调用次数取决于设备帧率,60帧下每秒跑60次,30帧下只有30次。如果不乘Time.deltaTime,同样是speed = 5,每帧位移5单位,60帧下物体每秒跑300单位,30帧下只有150单位,速度直接差一倍。deltaTime是上一帧消耗的真实时间,乘以它以后,位移变成"每秒5单位"的恒定量,帧率变化不影响实际速度。
提示:不要在
Update里直接写transform.position += dir * speed并期望不同设备手感一致,漏掉deltaTime是Unity新手移动代码里最常见的致命伤。
2.3 新版Input System带来的差异与兼容
Unity从2019开始主推新的Input System包,它把输入处理从Input.GetAxis这种全局静态方法换成了可配置的InputAction资产。新系统下默认不启用旧的Input Manager,如果项目创建时选了新模板,直接写Input.GetAxis会找不到方法。需要在Player Settings > Active Input Handling里把选项设为Both,让新旧输入代码同时生效,老项目才能平滑迁移。
新旧方案的核心差异在于:旧Input Manager的轴名称写死在Input Manager窗口里,平台差异化处理比较繁琐;新Input System支持运行时重新绑定按键、在UI里显示图标、方便处理手柄和触屏,代价是学习曲线更陡。做简单的WASD键盘移动时,旧方案的代码量确实更少,但如果你预判项目后期要接手柄或自定义按键,建议一开始就用新Input System,省得后面把移动逻辑从静态方法改成Action回调时大面积返工。
| 方式 | 返回值 | 典型用途 | 注意点 |
|---|---|---|---|
| Input.GetKey | bool | 单键触发、快捷键 | 需要自己处理按下/抬起逻辑 |
| Input.GetAxis | float(-1到1) | 连续移动、手柄兼容 | 默认带平滑,响应有延迟 |
| Input.GetAxisRaw | float(-1/0/1) | 需要立即响应的键盘移动 | 无平滑,数值跳跃 |
| 新Input System | InputAction | 跨平台、可重绑定的复杂输入 | 需要配置Action资产,代码结构不同 |
如果追求跟手,移动方向计算建议用GetAxisRaw,它没有平滑过渡,按键瞬间就是满值。平滑交给后面的移动代码自己处理,比让输入层半遮半掩地做更可控。
3. 移动谁?Transform与Rigidbody的选择和参数陷阱
3.1 Transform.Translate适合哪些物体,又会在哪里失控
Transform.Translate直接修改GameObject的position,完全绕开物理引擎。对不需要碰撞的物体来说,这是最轻量的方式,比如移动一个纯视觉的粒子占位、做摄像机跟随的中间节点。但一旦物体带有Collider和Rigidbody,直接改Transform会出问题。物理引擎每帧基于刚体速度做碰撞检测,你直接瞬移位置,引擎来不及更新碰撞体状态,高速移动时可能直接穿过薄墙;也可能因为帧率波动造成位置抖动,毕竟物理步长是固定的,而Transform移动是跟渲染帧走的。
回到标题里的场景:如果只是"物体移动",没有说一定要走物理,那么简洁方案就是Translate。但到了项目里,物体要撞墙停下来、要触发碰撞事件,就必须考虑物理接口。
3.2 用Rigidbody做物理移动:MovePosition、AddForce与velocity的取舍
凡是带Rigidbody的角色,推荐按需求在三种方式里选:
AddForce:给刚体施加力,由物理引擎计算速度。优点是真实,适合玩家踩到弹簧垫、被爆炸弹飞;缺点是很难精确控制速度,手感偏"飘",不用于常规角色位移。rb.velocity:直接给刚体的速度向量赋值,效率和可控性最好,FPS角色常用。但完全覆盖物理计算结果,所以要保留重力的那部分速度。rb.MovePosition:把刚体移动到指定位置,且会经过碰撞检测,适合传送和滑行类移动,但不改变刚体的瞬时速度,球体相撞等依赖速度的效果表现不佳。
角色移动最常用的是velocity赋值,写出来是这样:
using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class RbMove : MonoBehaviour { public float speed = 5f; private Rigidbody rb; private Vector3 dir; void Start() { rb = GetComponent<Rigidbody>(); rb.freezeRotation = true; } void Update() { float h = Input.GetAxisRaw("Horizontal"); float v = Input.GetAxisRaw("Vertical"); dir = new Vector3(h, 0f, v); dir.Normalize(); } void FixedUpdate() { Vector3 velocity = dir * speed; velocity.y = rb.velocity.y; rb.velocity = velocity; } }这里把dir的读取放Update,把速度赋值放FixedUpdate。原因是Input状态在Update里更新,而物理计算在FixedUpdate里跑,固定帧率(默认50Hz),不在FixedUpdate里改刚体速度就会引入帧率耦合。velocity.y = rb.velocity.y保留了重力累积出的下落速度,避免角色被钉在空中。
3.3 让手感不发飘的三个刚体参数
3.3.1 velocity赋值与MovePosition的帧率差异
rb.velocity是每物理帧生效的,赋值后后续物理模拟基于这个速度推进位置。它和MovePosition最大的区别在于:velocity改变的是运动状态,碰撞滑块、推箱子时物理表现更自然;MovePosition是"告诉物理引擎我要移到这个位置",本质上是位置更新的视觉结果,遇到障碍物时会表现为被挡住,但不会产生推挤效果。做玩家移动时选velocity,做机关平台移动时选MovePosition更恰当。
3.3.2 Interpolate与Collision Detection的配合
刚体组件上有两个肉眼可见的参数:Interpolate和Collision Detection。Interpolate建议设为Interpolate,它能让物体位置在两个物理帧之间做插值渲染,消除低帧率下的抖动。注意它只影响渲染位置,不影响物理逻辑。Collision Detection默认是Discrete,高速移动可能穿透薄墙,改成Continuous会基于扫掠形状检测,代价是性能开销增加。普通小物体、速度在10以内,Discrete够用;子弹、高速赛车这类物体再考虑Continuous。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Mass | 1 到 5 | 影响AddForce和碰撞反作用力,不影响velocity直接赋值 |
| Drag | 1 到 3 | 数值越大,松开按键后减速越快 |
| Interpolate | Interpolate | 平滑渲染位置,减少抖动 |
| Collision Detection | Continuous(高速时) | 防止穿过碰撞体,但开销更大 |
4. WASD在三维空间里的方向问题:相机相对移动与斜向速度修正
4.1 为什么W朝世界的Z轴走,而不是朝屏幕里走
上一章的Translate和velocity代码,输入向量(h, 0, v)是直接按世界X轴和Z轴解释的。物体没旋转时,按W就是往世界Z轴正方向走,A往X轴负方向走。这在俯视视角或者固定网格游戏里逻辑正确,切换到第三人称后,相机放在角色后上方,玩家按W期望的是"角色朝屏幕里远离镜头",也就是相机的前方向投影到地面上。如果不修正方向,按W角色会沿着世界Z轴走,而相机朝向可能已经绕Y轴转过了90度,两者完全错位。
4.2 把输入向量从屏幕方向换算到世界方向
常见的修正是先取相机的forward和right,把它们的Y分量清零,再归一化得到地面投影方向,然后和输入向量组合:
Transform cam = Camera.main.transform; Vector3 forward = cam.forward; forward.y = 0f; forward.Normalize(); Vector3 right = cam.right; right.y = 0f; right.Normalize(); float h = Input.GetAxisRaw("Horizontal"); float v = Input.GetAxisRaw("Vertical"); Vector3 move = forward * v + right * h; move = Vector3.ClampMagnitude(move, 1f); transform.Translate(move * speed * Time.deltaTime, Space.World);这里的语义是:相机正对着的方向定义为"前",按W时v = 1,forward * 1让角色沿相机前方地面投影移动;按D时h = 1,right * 1让角色沿相机右方移动。两个方向叠加后就能得到任意键盘组合对应的世界空间方向。如果项目里相机没有俯仰角,也就是完全俯视,forward.y本来就是0,这步可以省略,但为了兼容自由视角还是保留为好。
另一种等效做法是用四元数把输入向量绕Y轴旋转到相机的朝向上:
Vector3 rawDir = new Vector3(h, 0f, v); Vector3 move = Quaternion.Euler(0f, cam.eulerAngles.y, 0f) * rawDir;因为角色移动只需要地面的平面方向,取eulerAngles.y就够,不需要把相机的俯仰角也转进去。这种方式代码更短,但新手容易误写成cam.rotation * rawDir,那样会连俯仰角一起带进来,产生一个斜向上的方向,角色就会往天上飞。
4.3 斜向速度1.414倍的修正方法
4.3.1 normalized与ClampMagnitude的区别
同时按W和D时,move向量的X和Z分量都是1,向量长度变成$\sqrt{2}$,约1.414。如果不处理,斜向移动速度会比直线快约41%,等式子里的speed就不再是实际速度。修正方式有两种:
move.normalized:直接把向量除以长度,强制变为长度1。这种方式会丢失向量原有的模长信息,适合键盘这种非0即1的输入。Vector3.ClampMagnitude(move, 1f):当模长超过上限时截断,没超过时保留原始值。它会保留手柄摇杆推一半时0.5的输入幅度,达到"小幅度输入就走慢点"的效果。
对纯键盘WASD来说,两者效果几乎一样;如果项目同一套代码要支持手柄,用ClampMagnitude更好。还有一种情况是移动速度超调,比如加了加速Buff让speed变成10,但方向向量本身没变,这时设置的上限应该和移动速度解耦,只截方向长度就好。
4.3.2 朝向随移动方向旋转的常用做法
很多游戏里角色不是平移着走,而是身体朝向移动方向。最常见的是用LookRotation:
if (move.magnitude > 0.01f) { Quaternion targetRotation = Quaternion.LookRotation(move); transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * 10f); }move.magnitude > 0.01f是为了避免原地站立时方向向量为零,导致LookRotation产生undefined行为。Slerp第三个参数是旋转速度,Time.deltaTime * 10表示每秒转过角度随距离增大而变化的渐近方式,10是常用起始值,角色转身会更跟手。如果想要"瞬间转身"效果,直接赋值transform.rotation = targetRotation,不插值。
5. 让移动手感阶跃的三个技巧:平滑加速、地面判定与速度可视化
5.1 用SmoothDamp模拟惯性的最小写法
键盘输入是二值的,按下瞬间速度就能达到满值,松开立即归零,手感生硬。用Vector3.SmoothDamp做一个速度缓冲,让实际移动向量缓慢追上输入向量:
public float speed = 6f; private Vector3 currentVelocity; private Vector3 targetMove; void Update() { float h = Input.GetAxisRaw("Horizontal"); float v = Input.GetAxisRaw("Vertical"); Vector3 inputDir = new Vector3(h, 0f, v).normalized; targetMove = inputDir * speed; Vector3 smoothedMove = Vector3.SmoothDamp( transform.position, transform.position + targetMove, ref currentVelocity, 0.1f ); transform.position = smoothedMove; }SmoothDamp接收当前值、目标值、参考速度和一个smoothTime参数。这里把当前位置和目标位置之间做了平滑追赶,smoothTime = 0.1f表示大约0.1秒追上目标。数值越小越跟手,越大越滑。注意currentVelocity需要声明为字段,因为算法内部依赖上一次的计算结果。这个方案比Mathf.Lerp好在这个系数是时间量,改起来直观。
5.2 斜坡与地面判定的常见修正
如果角色走斜坡,直接用法线方向做移动会导致滑动。常规做法是把移动向量投影到地面法线所在的平面:
RaycastHit hit; if (Physics.Raycast(transform.position, Vector3.down, out hit, 1.2f, groundMask)) { Vector3 slopeDir = Vector3.ProjectOnPlane(move, hit.normal); transform.Translate(slopeDir * speed * Time.deltaTime, Space.World); }用一个groundMask过滤哪些层算地面。如果角色需要跳跃,还要配合一个isGrounded标志,通过射线距离判断;只有isGrounded为true时才允许Input.GetKeyDown(KeyCode.Space)触发跳跃,否则会出现二段跳的漏洞。
5.3 用Gizmos实时画出速度向量辅助调参
调手感最怕"感觉不对但说不出哪里不对"。给移动脚本加一个OnDrawGizmos,把当前实际速度画出来,就能在场景视图直观看到加速度滞后、斜向超速这一类问题:
private void OnDrawGizmos() { Gizmos.color = Color.green; Gizmos.DrawRay(transform.position, rb ? rb.velocity : transform.forward * speed); }这样Play运行后,绿色箭头就代表当前移动向量。如果按W和按W+D箭头长度不同,说明斜向速度修正没做;如果松开按键箭头还拉很长,说明平滑时间太大或Drag太小。配合这个可视化,再调上一节的smoothTime和刚体Drag,基本就能找到适合自己的手感。
本文还有配套的精品资源,点击获取