ヒーロー
CoddyのUnityジャーニー「Unityのゲームアーキテクチャ」セクションの一部。レッスン 50/64。
この章では、コース最後のゲームであるArena Survivorを作ります。敵のウェーブが壁に囲まれたアリーナになだれ込み、剣で撃退します。こちらに向かって歩いてくる雑魚敵と、待ち構えてから突進してくるオーブが登場します。ウェーブを倒すとパワーアップアイテムが残され、HUDには体力とウェーブが表示されます。体力がなくなるとプレイが終了し、最高記録が保存されます。このゲームでは、このセクションで学ぶツールを適切な箇所で使います。2種類の敵を持つ抽象クラスEnemy、ダメージを受けるものすべてに使うIDamageable、ScriptableObjectアセットとしてのウェーブ、インターフェースを介したピックアップアイテム、C#イベントを受け取るだけのHUD、そしてJSON形式のセーブデータです。12個のスクリプトはすべて、最初からプロジェクトに入っています。現時点で動作するのはHeroだけです。ほかのスクリプトには後のレッスンで必要になるフィールドが用意されており、レッスンを進めるごとに実装が追加されます。
ヒーローの剣は正面にあるものに当たるため、振ったときに「正面にある」かどうかを判定する必要があります。Vector3.Dot(a, b)は、2つのベクトルの対応する要素を掛け合わせて合計します。つまり、a.x * b.x + a.y * b.y + a.z * b.zです。長さが1の2つの方向ベクトルの場合、この値はその間の角度の余弦になります。同じ方向を向いているときは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の長さは距離によって変わるため、最初に正規化します。そうしないと、プレイヤーが2倍遠くにいると内積も2倍になります。正規化する前にyを0にしているので、高い通路にいるプレイヤーも、高さではなくマップ上の位置で判定されます。内積の符号で前方と後方を区別できます。0より大きければ前方の半分のどこかに、0より小さければ後方のどこかにいます。
チャレンジ
中級主人公に剣を持たせましょう。
Update()で、Spaceキーまたは左クリックが押されていて、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秒ごとに最大1回
}
void Swing()
{
StartCoroutine(ShowSlash());
// heroの前方かつreachの範囲内にある、Enemyタグの付いたものをすべて破壊する
}
IEnumerator ShowSlash()
{
slash.SetActive(true);
yield return new WaitForSeconds(0.1f);
slash.SetActive(false);
}
}
Unityのゲームアーキテクチャのすべてのレッスン
自分で練習してみよう: Unityプレイグラウンド