移动、跳跃并落到方块上
Assets/
Scenes/
Arena.unity
Scripts/
你已经有的这一课新增这一课替换
tab 01 的 SimpleMover 已经让 Knight 能走动了,但它会穿墙、没有重力,而且永远沿世界坐标轴移动。这一课把它换掉 —— 只换 Knight。
Mage 和 Archer 仍然保留 SimpleMover。这正是用意:学完这一课,你可以在同一个场景里按下 1 和 2,直接感受两种移动方式的差别。
移动永远是同样的三步:读取输入、把它变成方向、再乘上 Time.deltaTime 施加出去。
按键变成 Vector3,这个向量再变成每秒多少米。
完整的移动脚本
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : MonoBehaviour
{
[Header("Move")]
[SerializeField] private float walkSpeed = 4f;
[SerializeField] private float runSpeed = 8f;
[SerializeField] private float turnSmooth = 0.08f;
[Header("Jump & gravity")]
[SerializeField] private float jumpHeight = 1.4f;
[SerializeField] private float gravity = -20f;
private CharacterController cc;
private Camera cam;
private float verticalVelocity;
private float turnVelocity;
void Awake()
{
cc = GetComponent<CharacterController>();
cam = Camera.main;
}
void Update()
{
// ---- 1. read input
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
Vector3 input = new Vector3(h, 0f, v).normalized;
// ---- 2. move relative to the camera
Vector3 move = Vector3.zero;
if (input.sqrMagnitude > 0.01f)
{
float targetAngle = Mathf.Atan2(input.x, input.z) * Mathf.Rad2Deg
+ cam.transform.eulerAngles.y;
float angle = Mathf.SmoothDampAngle(
transform.eulerAngles.y, targetAngle, ref turnVelocity, turnSmooth);
transform.rotation = Quaternion.Euler(0f, angle, 0f);
move = Quaternion.Euler(0f, targetAngle, 0f) * Vector3.forward;
}
float speed = Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed;
// ---- 3. gravity and jump
if (cc.isGrounded && verticalVelocity < 0f)
verticalVelocity = -2f; // stick to the ground
if (cc.isGrounded && Input.GetButtonDown("Jump"))
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
verticalVelocity += gravity * Time.deltaTime;
// ---- 4. one single Move call
Vector3 velocity = move * speed + Vector3.up * verticalVelocity;
cc.Move(velocity * Time.deltaTime);
}
}把它挂到 Knight 上
- 1
在 Hierarchy 里选中 Knight,在 SimpleMover 组件上点三个点 → Remove Component。Knight 改用新脚本,Mage 和 Archer 仍然保留原来那个。
- 2
在 Project 的 Assets/Scripts 里:,命名为 PlayerMovement —— 文件名必须和类名完全一致,否则 Unity 不让你挂上去。
- 3
打开它,用上面那段脚本覆盖里面的全部内容,然后保存。
- 4
把 PlayerMovement 从 Project 拖到 Knight 上。Character Controller 会自己冒出来 —— 这就是脚本开头那个 [RequireComponent] 的作用。
- 5
把这个控制器调到和模型贴合,否则胶囊会有一半陷进地板:Center 0, 1, 0 · Radius 0.3 · Height 2。在 Scene 视图里,绿色胶囊应该正好把 Knight 包住。
里面的四个思路
相对相机的移动:按 W 的意思是“远离相机”,不是“朝世界的 +Z”。把相机的 Y 角度加进去,任何机位下操作手感都是对的。
SmoothDampAngle 把瞬间跳变的角度变成平滑的转身,359° → 0° 这种绕回它也处理得对。
重力每帧都累加进 verticalVelocity;所以下落是越来越快,而不是匀速滑动。
Sqrt(h * -2 * g) 是“正好跳高 h 米”的物理公式 —— 所以 Inspector 里的值就是米,调起来很爽。
isGrounded 一直闪
落地时把 verticalVelocity 设成一个小的负数(-2),让控制器始终压在地面上。
检查 CharacterController 上的 Slope Limit 和 Step Offset —— Step Offset 设成 0.3 就能走上矮台阶。
加一点点 coyote time(离开边缘 0.15 秒内仍然能跳)。玩家不会察觉,但会觉得游戏讲道理得多。
让切换角色的功能继续可用
CharacterSwitcher 原本是在启用和禁用 SimpleMover。Knight 已经没有它了,所以列表里要放一个两个脚本都有的东西。
// before
[SerializeField] private List<SimpleMover> characters = new List<SimpleMover>();
// after — MonoBehaviour is the base class of both scripts
[SerializeField] private List<MonoBehaviour> characters = new List<MonoBehaviour>();
// everything below still works unchanged, because .enabled lives on MonoBehaviour
characters[i].enabled = (i == index);private float coyote;
// ...
if (cc.isGrounded) coyote = 0.15f;
else coyote -= Time.deltaTime;
if (coyote > 0f && Input.GetButtonDown("Jump"))
{
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
coyote = 0f;
}