Menu
CoddyTech

ヒーロー

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より小さければ後方のどこかにいます。

challenge icon

チャレンジ

中級

主人公に剣を持たせましょう。

  1. Update()で、Spaceキーまたは左クリックが押されていて、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秒ごとに最大1回
    }

    void Swing()
    {
        StartCoroutine(ShowSlash());
        // heroの前方かつreachの範囲内にある、Enemyタグの付いたものをすべて破壊する
    }

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

Unityのゲームアーキテクチャのすべてのレッスン

自分で練習してみよう: Unityプレイグラウンド