U
13 / 22 · 8 分钟
常见问题

移动角色:三种方式里哪种适合你

简短回答

怎么让角色移动?

三种方式里只选一种,绝不同时用两种:由你直接操控的玩家用 CharacterController,需要靠物理推动的物体用 Rigidbody,而 transform.Translate 只用于不发生碰撞的物体。所有速度都要乘以 Time.deltaTime,否则机器越快、游戏跑得越快。

在 Unity 里移动物体有三种方式,它们不能混着用。选错方式,角色就会陷进墙里、干脆不动,或者在 120 Hz 的手机上跑出两倍速。

动态图解

同一面墙,三种方式去撞。只有 CharacterController 会贴着墙滑行,只有 Rigidbody 能推开木箱,而 transform.Translate 根本不知道那里有墙。

选一个 —— 全部的决定都在这里

  • CharacterController —— 你直接操控的角色就用它。它会贴着墙滑行、能爬上台阶,也不会被物理引擎推得乱晃。绝大多数第一人称和第三人称游戏用的都是它。

  • Rigidbody —— 该交给物理引擎打理的一切:滚动的球、你推的木箱、布娃娃。你只负责发出移动指令,实际发生什么由物理引擎决定。

  • transform.Translate —— 只用于永远不参与碰撞的东西:漂浮的拾取物、飘动的云、UI 元素。它是瞬移,推不动任何东西。

CharacterController

挂上组件后,用 Move() 来移动。它接收的是这一帧要走多远,而不是速度 —— 所以里面才有 deltaTime。

PlayerMove.cs — attach to the object that has the CharacterController
using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class PlayerMove : MonoBehaviour
{
    public float speed = 6f;
    public float gravity = -20f;

    CharacterController cc;
    float verticalSpeed;

    void Awake()
    {
        cc = GetComponent<CharacterController>();
    }

    void Update()
    {
        // Input is a direction, length 0 to 1. Never a distance.
        Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
        if (input.sqrMagnitude > 1f) input.Normalize();

        // Gravity has to be applied by hand: a CharacterController ignores physics.
        if (cc.isGrounded && verticalSpeed < 0f) verticalSpeed = -2f;
        verticalSpeed += gravity * Time.deltaTime;

        Vector3 move = input * speed + Vector3.up * verticalSpeed;
        cc.Move(move * Time.deltaTime);
    }
}

Rigidbody

记住两条规则,剩下的水到渠成:要写 Rigidbody 而不是 transform,要在 FixedUpdate 里做而不是 Update 里。物理引擎按自己的时钟运行,你直接改 transform 它毫不知情,转头就把物体拽回原位。

BallMove.cs — attach to the object that has the Rigidbody
using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class BallMove : MonoBehaviour
{
    public float speed = 8f;

    Rigidbody rb;
    Vector3 input;

    void Awake()
    {
        rb = GetComponent<Rigidbody>();
    }

    void Update()
    {
        // Read input here: Update runs once per frame, so no press is missed.
        input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
        if (input.sqrMagnitude > 1f) input.Normalize();
    }

    void FixedUpdate()
    {
        // Move here: FixedUpdate runs on the physics clock.
        rb.MovePosition(rb.position + input * speed * Time.fixedDeltaTime);
    }
}

为什么 input 在 Update 里读取、在 FixedUpdate 里使用

  • FixedUpdate 默认每秒跑 50 次,而你的游戏画面可能每秒渲染 120 帧。所以在 FixedUpdate 里读 Input.GetKeyDown,会把两次物理步之间按下的键漏掉。

  • 在 FixedUpdate 里用 Time.fixedDeltaTime,在 Update 里用 Time.deltaTime。其实 Unity 在哪边都会返回正确的值,但写对名字,读代码的人一眼就明白你的意图。

  • 如果相机跟着它走,就把 Rigidbody 的 Interpolate 设为 Interpolate,不然那运动看起来就像在以 50 Hz 抖动。

transform.Translate

Drifter.cs — for something with no collider, or one set to trigger
void Update()
{
    transform.Translate(Vector3.forward * speed * Time.deltaTime);
}

做出这个的那一课

移动、跳跃并落到方块上

这篇指南单独成篇,是一份可以直接照着做的配方。在课程里,同样的东西会作为贯穿全部五个章的那个项目的一部分来搭建 —— 角色与战斗,第 02 课。