英雄
本课属于 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 则表示位于后方的任意位置。
挑战
中等给英雄一把剑。
- 在
Update()中,当按下空格键或点击鼠标左键,并且Time.time >= nextSwing时,设置nextSwing = Time.time + swingCooldown;,然后调用Swing()。 - 在
Swing()中,遍历Physics.OverlapSphere(transform.position, reach)。 - 销毁每个标签为
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 在线编辑器