U
章 02角色与战斗9 课 — 点击打开
02 / 09 · 12 分钟
角色与战斗

移动、跳跃并落到方块上

到目前为止的项目

Assets/

Scenes/

Arena.unity

KnightCharacterController

Scripts/

CharacterStats.csCharacterSwitcher.csSimpleMover.cs (on Knight)→PlayerMovement.cs

你已经有的这一课新增这一课替换

tab 01 的 SimpleMover 已经让 Knight 能走动了,但它会穿墙、没有重力,而且永远沿世界坐标轴移动。这一课把它换掉 —— 只换 Knight。

Mage 和 Archer 仍然保留 SimpleMover。这正是用意:学完这一课,你可以在同一个场景里按下 1 和 2,直接感受两种移动方式的差别。

移动永远是同样的三步:读取输入、把它变成方向、再乘上 Time.deltaTime 施加出去。

动态图解

按键变成 Vector3,这个向量再变成每秒多少米。

完整的移动脚本

PlayerMovement.cs — remove SimpleMover from Knight first
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. 1

    在 Hierarchy 里选中 Knight,在 SimpleMover 组件上点三个点 → Remove Component。Knight 改用新脚本,Mage 和 Archer 仍然保留原来那个。

  2. 2

    在 Project 的 Assets/Scripts 里:Assets▸Create▸C# Script,命名为 PlayerMovement —— 文件名必须和类名完全一致,否则 Unity 不让你挂上去。

  3. 3

    打开它,用上面那段脚本覆盖里面的全部内容,然后保存。

  4. 4

    把 PlayerMovement 从 Project 拖到 Knight 上。Character Controller 会自己冒出来 —— 这就是脚本开头那个 [RequireComponent] 的作用。

  5. 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 已经没有它了,所以列表里要放一个两个脚本都有的东西。

CharacterSwitcher.cs — one word changes
// 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);
coyote time
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;
}