Menu
CoddyTech

英雄

本课属于 Coddy Unity 学习旅程的“Unity 游戏架构”单元。第 50 课,共 64 课。

本章将制作 Arena Survivor,这是本课程的最后一个游戏。一波波敌人涌入有围墙的竞技场,你要用剑击退它们:有会朝你走来的杂兵,也有会等待后再冲刺的球体。击败一波敌人后会留下一个强化道具,HUD 会显示你的生命值和当前波次。当生命值耗尽时,这一局就会结束,并保存你的最佳成绩。游戏会在合适的地方使用本单元中的工具:一个包含两种敌人的抽象 Enemy、用于表示所有可受伤对象的 IDamageable、作为 ScriptableObject 资源的波次数据、通过接口实现的拾取物、只监听 C# 事件的 HUD,以及 JSON 存档。项目一开始就包含全部 12 个脚本。目前只有 Hero 能正常工作;其他脚本包含后续课程需要的字段,每节课都会逐步补充更多内容。

英雄的剑会击中前方的目标,因此挥剑时需要判断目标是否“在前方”。Vector3.Dot(a, b) 会将两个向量对应的分量相乘,再把结果相加:a.x * b.x + a.y * b.y + a.z * b.z。如果两个方向的长度都是 1,结果就是它们夹角的余弦值:方向相同时为 1,夹角为 30 度时为 0.866,45 度时为 0.707,垂直时为 0,方向相反时为 -1。因此,“位于我朝向的 30 度范围内”就等于“点积至少为 0.866”,无需计算角度。下面的哨戒机器人会在玩家处于 10 个单位以内且位于它 30 度视野范围内时发出警报:

public Transform player;
public float range = 10f;

void Update()
{
    Vector3 toPlayer = player.position - transform.position;
    toPlayer.y = 0f;
    bool near = toPlayer.magnitude <= range;
    bool inView = Vector3.Dot(transform.forward, toPlayer.normalized) >= 0.866f;
    if (near && inView)
    {
        Debug.Log("Alarm!");
    }
}

两个方向的长度都必须是 1。transform.forward 的长度始终为 1,但 toPlayer 的长度等于距离,因此要先将它归一化;否则,距离加倍的玩家会让点积也加倍。归一化前会先将它的 y 设为 0,因此,站在较高走道上的玩家会根据其在地图上的位置来判断,而不是根据高度。点积的正负可以区分前后:大于 0 表示位于前方半区的任意位置,小于 0 则表示位于后方的任意位置。

challenge icon

挑战

中等

给英雄一把剑。

  1. 在 Update() 中,当按下空格键或点击鼠标左键,并且 Time.time >= nextSwing 时,设置 nextSwing = Time.time + swingCooldown;,然后调用 Swing()。
  2. 在 Swing() 中,遍历 Physics.OverlapSphere(transform.position, reach)。
  3. 销毁每个标签为 Enemy 且其水平朝向 toHit(y 设为 0)满足 Vector3.Dot(transform.forward, toHit.normalized) >= 0.5f 的命中对象。

按下“开始”并挥剑:前方的假人会消失。然后点击“检查”。

亲手试试

using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;

public class Hero : MonoBehaviour
{
    [SerializeField] private float speed = 5f;
    [SerializeField] private float reach = 2f;
    [SerializeField] private float swingCooldown = 0.5f;
    [SerializeField] private GameObject slash;

    private CharacterController controller;
    private InputAction moveAction;
    private float nextSwing;

    void Start()
    {
        controller = GetComponent<CharacterController>();
        moveAction = InputSystem.actions.FindAction("Move");
    }

    void Update()
    {
        Vector2 input = moveAction.ReadValue<Vector2>();
        Vector3 move = new Vector3(input.x, 0f, input.y);
        controller.SimpleMove(move * speed);
        if (move != Vector3.zero)
        {
            transform.rotation = Quaternion.LookRotation(move);
        }

        // 按 Space 键或单击鼠标左键可挥砍,每 swingCooldown 秒最多一次
    }

    void Swing()
    {
        StartCoroutine(ShowSlash());
        // 销毁英雄前方、reach 范围内所有标记为 Enemy 的对象
    }

    IEnumerator ShowSlash()
    {
        slash.SetActive(true);
        yield return new WaitForSeconds(0.1f);
        slash.SetActive(false);
    }
}

Unity 游戏架构中的所有课

自己练习: Unity 在线编辑器