第三章:游戏原型设计

场景搭建基础

做原型的第一步,就是搭场景。别一上来就想搞个开放世界,那会把自己累死。

我个人习惯用 Unity 的 ProBuilder 快速搭白模。为什么?因为它快。你想想看,原型阶段要的是验证玩法,不是展示美术。用几个 Cube、Plane 拼个平台,加几个斜坡,就够了。

我的经验: 场景搭建时,先把地面铺好,再放障碍物。地面用一个大 Plane,障碍物用 Cube 调整大小。颜色用灰白,别分心。

具体步骤很简单:

  • 新建场景,删除默认的灯光和摄像机(后面我们自己控制)
  • 创建地面:GameObject → 3D Object → Plane,调整位置到 (0, 0, 0)
  • 创建几个平台:Cube,拉伸成扁平状,摆在不同高度
  • 加一个目标点:用 Sphere 或者 Capsule,涂成红色

嗯,这里要注意:地面不要太小。我刚开始做原型时,地面只放了 10x10,角色跑两步就掉出去了。建议至少 20x20。

基本角色控制(移动/跳跃)

角色控制是游戏的核心手感来源。说白了,就是让玩家觉得「这角色听我话」。

我推荐用 CharacterController 组件,而不是 Rigidbody。为什么?CharacterController 自带碰撞和斜坡处理,省去很多调参的麻烦。

先挂组件:选中角色 → Add Component → CharacterController。然后写脚本:

public class PlayerController : MonoBehaviour
{
    public float moveSpeed = 5f;
    public float jumpForce = 8f;
    public float gravity = -9.81f;

    private CharacterController controller;
    private Vector3 velocity;
    private bool isGrounded;

    void Start()
    {
        controller = GetComponent<CharacterController>();
    }

    void Update()
    {
        // 检测是否在地面
        isGrounded = controller.isGrounded;
        if (isGrounded && velocity.y < 0)
            velocity.y = -2f; // 轻微向下,保持贴地

        // 移动输入
        float x = Input.GetAxis("Horizontal");
        float z = Input.GetAxis("Vertical");
        Vector3 move = transform.right * x + transform.forward * z;
        controller.Move(move * moveSpeed * Time.deltaTime);

        // 跳跃
        if (Input.GetButtonDown("Jump") && isGrounded)
        {
            velocity.y = Mathf.Sqrt(jumpForce * -2f * gravity);
        }

        // 重力
        velocity.y += gravity * Time.deltaTime;
        controller.Move(velocity * Time.deltaTime);
    }
}
我曾经踩过的坑: 跳跃时 velocity.y 的计算公式写错了,导致角色跳不起来。记住:velocity.y = Mathf.Sqrt(jumpForce * -2 * gravity),因为 gravity 是负数。

这段代码里有个细节:isGrounded 检测后,我把 velocity.y 设成 -2 而不是 0。为什么?因为如果设成 0,角色在斜坡上会飘起来。设成 -2 能保证始终贴地。

摄像机跟随

摄像机跟随看似简单,但做不好会让人头晕。我见过很多新手直接把摄像机设成角色的子物体,结果角色旋转时摄像机跟着转,玩家直接晕 3D。

正确的做法是:用脚本控制摄像机位置,保持相对偏移。

public class CameraFollow : MonoBehaviour
{
    public Transform target;
    public Vector3 offset = new Vector3(0, 5, -8);
    public float smoothSpeed = 0.125f;

    void LateUpdate()
    {
        Vector3 desiredPosition = target.position + offset;
        Vector3 smoothedPosition = Vector3.Lerp(transform.position, desiredPosition, smoothSpeed);
        transform.position = smoothedPosition;

        transform.LookAt(target);
    }
}

这里用 LateUpdate 而不是 Update,是因为要等角色移动完再更新摄像机位置,避免抖动。

你想想看,如果角色在 Update 里移动,摄像机也在 Update 里跟随,那可能这一帧角色还没动,摄像机就先动了,画面就会卡顿。

我的习惯: offset 的 z 值设为负数,表示摄像机在角色后方。y 值设为正数,表示俯视角度。这样既能看到角色前方,又能看到脚下地形。

简单物理碰撞

物理碰撞其实 CharacterController 已经帮我们处理了大部分。但有些情况需要自己写逻辑,比如:

  • 角色碰到尖刺 → 死亡
  • 角色碰到金币 → 收集
  • 角色碰到传送门 → 过关

这些用 OnControllerColliderHit 或者 OnTriggerEnter 来处理。

void OnTriggerEnter(Collider other)
{
    if (other.CompareTag("Coin"))
    {
        Destroy(other.gameObject);
        Debug.Log("吃到金币!");
    }
    else if (other.CompareTag("Spike"))
    {
        // 角色死亡逻辑
        Debug.Log("角色死亡");
    }
}

注意:触发器需要勾选 Collider 组件的 IsTrigger 选项。如果不勾选,就是物理碰撞,角色会被弹开。

我曾经犯过的错: 给金币加了 Rigidbody 组件,结果角色一碰金币,金币就飞出去了。其实金币只需要 Collider + IsTrigger,不需要 Rigidbody。

原型测试与迭代

原型做完了,别急着高兴。测试才是关键。

我一般会按这个顺序测试:

  1. 移动手感: 角色走起来顺不顺?转弯会不会卡?速度是不是太快或太慢?
  2. 跳跃手感: 跳起来的高度够不够?下落速度自然吗?能不能连续跳?
  3. 碰撞检测: 角色会不会卡进墙里?掉到地面以下?碰到触发器是否正常?
  4. 摄像机视角: 会不会被物体遮挡?跟随是否平滑?

测试完发现问题,马上改。比如移动速度太快,就把 moveSpeed 从 5 调到 3。跳跃太低,就把 jumpForce 从 8 调到 10。

迭代的时候,我建议每次只改一个参数。改完立刻跑一遍测试,看看效果。别一次性改五六个参数,出了问题你都不知道是哪个导致的。

核心思想: 原型阶段,完成比完美重要。先让角色能动、能跳、能碰撞,手感可以慢慢调。很多团队死在「调手感」这一步,调了三个月还在调,项目直接黄了。

嗯,最后说一句:原型测试最好找别人来玩。你自己玩习惯了,感觉不出问题。找个没玩过的同事,看他操作时的反应,哪里卡住了、哪里困惑了,那才是真正的问题。

我记得有一次,我觉得自己的跳跃手感已经很好了,结果测试同事一上手,连续三次跳不上平台。我一看,原来是平台边缘判定太严格,角色必须站在正中间才能跳上去。后来我把平台的 Collider 稍微扩大了一点,问题就解决了。

这就是原型迭代的意义——发现问题,快速修正,然后继续测试。循环往复,直到手感满意为止。