【Unity】シューティングゲームのリファクタリング(衝突のイベント化に注視)

リファクタリングのポイント

マジックナンバーの廃止

コード中に直接「数値」や「文字列」が書かれているケースでは、値の変更の調整のたびにメソッド自体を更新することになり、影響が大きくなります
マジックナンバーの箇所はプロパティやフィールドとし、宣言と初期化はここで実行します
また、publicアクセス修飾子や[SerializeField]属性をつけることでインスペクターで値を調整できるようにします

変数名をわかりやすい英単語になる

初期のコードでは、p1やr1などの変数名が付けられていますが、意味が通じにくいです
短縮するのではなく、意味のわかる英単語に置き換えます

移動量の計算

初期のコードでは、フレームごとに transform.Translate メソッドを使用して、矢を等速で落下させています。しかし、フレームレートによっては、矢の落下速度が変わってしまうことがあります。

より安定した落下速度を得るためには、時間に依存する計算を行う必要があります。例えば、Time.deltaTime を掛けることで、フレームレートに関係なく一定の速度で移動できます。

transform.Translate(0, -0.1f * Time.deltaTime, 0);

Instantiateメソッドのオーバーロードを使ってコードを簡略化

変更前サンプル

GameObject go = Instantiate(arrowPrefab);
int px = Random.Range(-6, 7);
go.transform.position = new Vector3(px, 7, 0);

変更後サンプル

第2引数にインスタンスを作成する場所、第3引数の回転状態を設定することができます

Instantiate(arrowPrefab,new Vector3(Random.Range(-6, 7), 7, 0),Quaternion.identity);

プロジェクトウィンドウのフォルダの構成の例

Unityのプロジェクトウィンドウは、プロジェクト内のすべてのファイルとフォルダーを表示するウィンドウです。プロジェクトウィンドウの管理方法は以下の通りです。

  1. フォルダーの作成: プロジェクトウィンドウ内で右クリックして、新しいフォルダーを作成することができます。フォルダーは、ファイルを整理するために使用できます。
  2. ファイルのインポート: プロジェクトウィンドウの上部にある「Import」ボタンをクリックして、ファイルをインポートできます。インポートするファイルには、テクスチャ、モデル、オーディオ、スクリプトなどがあります。
  3. タブの管理: プロジェクトウィンドウ内で複数のフォルダーを開いている場合、各フォルダーのタブがプロジェクトウィンドウの上部に表示されます。タブを右クリックして、タブを閉じたり、他のタブとグループ化したりすることができます。
  4. 検索機能の使用: プロジェクトウィンドウの上部にある検索バーを使用して、特定のファイルを検索することができます。検索バーにキーワードを入力すると、そのキーワードに一致するすべてのファイルが表示されます。
  5. ファイルの操作: プロジェクトウィンドウ内でファイルを選択して右クリックすると、ファイルの削除、リネーム、コピー、貼り付けなどの操作ができます。
  6. ファイルのフィルタリング: プロジェクトウィンドウ内でファイルの種類に応じてフィルタリングすることができます。フィルタリングボタンをクリックして、テクスチャ、オーディオ、スクリプトなどのファイルの種類を選択することができます。

これらの方法を使用して、プロジェクトウィンドウを管理して、プロジェクト内のファイルを効果的に整理することができます。

プロジェクトウィンドウでのアセットの管理方法は色々考えられます
最後の章にもこの他の例を示しましたので参考にしてください

構成イメージ

AssetStoreからさまざまなアセットをダウンロードする場合、どのフォルダに保存されるかは、製作者の意図によります
なので、自身で作成するものについては、あらためて、Projct等のフォルダ名をつけて新しくフォルダを作成し、その配下にシーンごとに管理する方法が良いと思います

以下の構成サンプルを参考にしてください

実際のプロジェクトウィンドウ

Unityエディター上での構成を見てみましょう

シーン構成(初期の各ゲームオブジェクトの一覧)

Unityのシーンとは、Unityエディター上で作成されたゲームの1つの「ステージ」や「画面」のことを指します。シーンは、ゲームの各パート(メニューやタイトル画面、プレイ画面など)を表します。

シーンは、カメラ、ライト、3Dオブジェクト、テキスト、UI、エフェクト、サウンドなど、様々なアセットを含めることができます。また、シーン内のオブジェクトやアセットは、スクリプトを使って制御することができます。

Unityのプロジェクトには、複数のシーンを含めることができます。それぞれのシーンは、独自の名前を持っており、別々のファイルに保存されます。複数のシーンを作成して、異なるシーン間を切り替えて、ゲームの進行を制御することができます。

例えば、2D横スクロールアクションゲームの場合、ゲームスタート時に表示するタイトル画面やオプション画面、ゲームプレイ画面などを別々のシーンとして作成し、それらを切り替えてゲームを進行させることができます。また、複数のシーンを同時にロードすることもでき、複数のシーンを組み合わせて、より複雑なゲームを作成することもできます。

GameManagerゲームオブジェクト

GameDirectorスクリプトがアタッチされています

Canvasゲームオブジェクト

GaugeControllerスクリプトがアタッチされています
UI関連の処理についてはここで処理されます
hpGaugeの画像を更新するため、HpGaugeImageにhpGaugeゲームオブジェクトおwアウトレット接続します

(参考)hpGauge

プロジェクトウィンドウに保存するプレファブについて

このゲームのに必要なオブジェクトのプレファブを保存しておきます

プレファブ(Prefab)

UnityのPrefabは、ゲームオブジェクトとそのコンポーネントの集合を再利用可能なテンプレートとして保存し、他のシーンやプロジェクトで再利用できるようにする機能です。

Prefabは、オブジェクトの構造、プロパティ、コンポーネント、子オブジェクトなどを保存し、同じ構造を持つ複数のオブジェクトを簡単に作成できます。例えば、敵キャラクターのプレハブを作成しておけば、同じ敵キャラクターを複数の場所で必要に応じて作成できます。

Prefabは、シーン内でのオブジェクトの作成と同じように、Inspectorウィンドウで編集できます。Prefabを編集すると、元のPrefab自体と、それを元に作成されたすべてのインスタンスが変更されます。

Prefabは、プロジェクト内で共有することができ、チームで作業する場合には非常に便利です。また、Prefabは、ゲームオブジェクトのライブラリとして使用することもできます。

playerPrefab

ArrowPrefab

スクリプト

Unityのスクリプトとは、C#のプログラミング言語を使って、Unityで作成したゲームオブジェクトの動作や振る舞いを制御するためのコードのことを指します。

スクリプトを使用することで、ゲームオブジェクトの位置、角度、スピード、アニメーション、衝突判定、サウンド再生、プレイヤーの入力など、様々な要素を制御することができます。スクリプトは、Unityのエディター上で作成することができ、Unityのコンポーネントとしてゲームオブジェクトにアタッチすることができます。

Unityでは、スクリプトを使って、プログラマーが自由にゲームをカスタマイズし、自分たちのアイデアを実現することができます。

GameManegerゲームオブジェクト

ゲーム全体を管理、コントロールするために空のオブジェクトをコンテナ(入れ物)として利用します
基本的にスクリプトのみをアタッチしていきます

GameDirectorスクリプト

このコードは、プレイヤーと矢を生成してプレイヤーにダメージを与えるゲームを制御するためのスクリプトです

using UnityEngine;

public class GameDirector : MonoBehaviour
{
    // Arrowゲームオブジェクトをアウトレット接続((矢はこのオブジェクトの子として生成)
    [SerializeField] Transform arrowsParent;

    // 各Prefabをアウトレット接続
    [SerializeField] public GameObject playerPrefab;
    [SerializeField] public GameObject arrowPrefab;

    // gaugeControllerをアウトレット接続(Canvasゲームオブジェクトをドラッグ&ドロップします)
    [SerializeField] GaugeController gaugeController;

    GameObject player;

    // 矢を生成する間隔
    [SerializeField] float arrowGanereteRepertTime = 1;

    void Start()
    {
        // プレイヤーのインスタンス作成
        player = Instantiate(playerPrefab);
        // 矢のインスタンスを繰り返し作成
        InvokeRepeating(nameof(GenerateArrow), 0, arrowGanereteRepertTime);
    }

    // 繰り返し矢を生成するメソッド
    void GenerateArrow()
    {
        // 矢を生成する位置を計算(x軸の値はランダム)
        Vector3 createPos = new Vector3(Random.Range(-6, 7), 7, 0);

        // 矢のPrefabからインスタンスを生成
        // Instantiate(生成するオブジェクト, 位置, 回転, 親オブジェクト); // arrowsParentの子オブジェクトとして生成
        GameObject arrowObject = Instantiate(arrowPrefab, createPos, Quaternion.identity, arrowsParent);

        // 矢にアタッチされているArrowControllerコンポーネントの取得
        ArrowController arrowController = arrowObject.GetComponent<ArrowController>();

        // 矢のほうで衝突を検知したときに呼び出されるイベントハンドラを登録
        arrowController.OnArrowCollision.AddListener(OnArrowCollition);
    }

    // 矢のほうで衝突を検知したときに呼び出されるイベントハンドラを作成
    public void OnArrowCollition(int damagePoint)
    {
        // PlayerにアタッチされているPlayerStatusコンポーネントを取得
        PlayerStatus playerStatus = player.GetComponent<PlayerStatus>();

        // damegePoint分、プレイヤーのHPを減少させる
        playerStatus.hp -= damagePoint;

        // ゲージを現在のplayerのhpで更新する
        gaugeController.ViewHp(playerStatus.hp);
    }
}

[SerializeField]は、UnityにおけるC#スクリプトで、変数やプロパティをシリアル化するための属性(Attribute)です。

シリアル化とは、オブジェクトのデータをファイルやネットワーク転送などで扱える形式に変換することを指します。Unityでは、シリアル化された変数やプロパティは、Unityエディタ上で編集可能なインスペクター(Inspector)に表示されます。

[SerializeField]を使うことで、Unityエディタ上で変数やプロパティの値を直接変更できるため、開発者が必要な情報を簡単に編集できます。また、シリアル化された変数やプロパティは、スクリプトの実行中に値を保持することができるため、シーン間でデータを受け渡すこともできます。

例えば、以下のように[SerializeField]を使って、変数「playerName」をシリアル化することができます。

[SerializeField]
private string playerName;

これにより、Unityエディタ上で「playerName」の値を編集できるようになります。また、この変数はシーン間でデータを受け渡すことも可能です。

ransform型のarrowsParentは、このゲームオブジェクトの子として矢を生成するためのアウトレット接続です

playerPrefabとarrowPrefabは、プレイヤーと矢のプレハブを指定するためのアウトレット接続です

gaugeControllerは、CanvasゲームオブジェクトにアタッチされたGaugeControllerスクリプトを指定するためのアウトレット接続です

GameObject型のplayerフィールドは、プレイヤーのインスタンスを格納するために使用されます

Startメソッドは、ゲーム開始時に呼び出されます。このメソッドでは、プレイヤーのインスタンスを生成し、InvokeRepeatingメソッドを使用して、繰り返し矢を生成するGenerateArrowメソッドを呼び出します

GenerateArrowメソッドは、ランダムな位置に矢を生成し、その矢にArrowControllerコンポーネントをアタッチします。そして、OnArrowCollitionメソッドをarrowController.OnArrowCollision.AddListenerを使用して登録します

OnArrowCollitionメソッドは、矢がプレイヤーに当たったときに呼び出されます。このメソッドでは、プレイヤーのPlayerStatusコンポーネントを取得し、ダメージポイントを使ってプレイヤーのHPを減らします。そして、ゲージを現在のプレイヤーのHPで更新します

アタッチされたゲームオブジェクトは、矢を生成するためのメソッド Generate() を公開します。

Instantiateメソッドの第4引数は、ヒエラルキーでの親ゲームオブジェクトを登録しています

Quaternion.identityは、回転を表すQuaternionオブジェクトのデフォルト値を表します。具体的には、x、y、z、wのすべての値が0のQuaternionオブジェクトで、回転がない状態を表します。

Unityの3D空間では、オブジェクトの回転はQuaternionオブジェクトで表されます。Quaternion.identityは、回転が必要ない場合に便利で、以下のような場合に使用できます。

  • オブジェクトを作成するときに、回転を0に設定する必要がある場合。
  • オブジェクトを回転する必要がない場合、例えばスケール変換を適用するときに回転を避けたい場合。

Quaternion.identityを使用すると、回転がない状態でオブジェクトを操作でき、コードがより簡潔になります。

Playerゲームオブジェクト

プレイヤーについて機能を持ったオブジェクトです
イラストの情報や表示、移動などのコンポーネントがアタッチされています

PlayerControllerスクリプト

このコードは、プレイヤーの移動を制御するためのスクリプトです

using UnityEngine;
using UnityEngine.UI;

public class PlayerController : MonoBehaviour
{
    // 左ボタン
    Button leftButton;
    // 右ボタン
    Button rightButton;

    // プレイヤーの横の移動速度
    public float playerMoveSpeed = 3.0f;

    void Start()
    {
        // LButtonゲームオブジェクトにアタッチされているButtonコンポーネントを取得
        leftButton = GameObject.Find("LButton").GetComponent<Button>();

        // Buttonコンポーネントのマウスクリックイベントにイベントハンドラ(MoveLeft)を登録
        // マウスがクリックされると、MoveLeftメソッドが実行されるようにします
        leftButton.onClick.AddListener(MoveLeft);

        rightButton = GameObject.Find("RButton").GetComponent<Button>();
        rightButton.onClick.AddListener(MoveRight);
    }

    void Update()
    {
        // 左矢印が押された時
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            MoveLeft();
        }

        // 右矢印が押された時
        if (Input.GetKey(KeyCode.RightArrow))
        {
            MoveRight();
        }

        // 左右の移動制限の計算
        float x = transform.position.x;
        float y = transform.position.y;

        transform.position = new Vector2(Mathf.Clamp(x, -8, 8), y);
    }

    public void MoveLeft()
    {
        transform.Translate(-playerMoveSpeed * Time.deltaTime, 0, 0);
    }

    public void MoveRight()
    {
        transform.Translate(playerMoveSpeed * Time.deltaTime, 0, 0);
    }
}


具体的には、左右に移動するプレイヤーキャラクターを制御します
Button はUnityのUI機能で、ボタンを表します。leftButton と rightButton は、それぞれ左右の移動を制御するためのボタンです
public float playerMoveSpeed = 3.0f; は、プレイヤーの横移動速度を示す変数です。初期値は 3.0f に設定されています
void Start() は、スクリプトが開始されたときに実行されるメソッドです
GameObject.Find と GetComponent を使用して、ボタンオブジェクトを検索し、左右のボタンに対して onClick.AddListener を呼び出して、左右の移動を制御するメソッドを登録しています


void Update() は、フレームごとに実行されるメソッドです
このメソッドでは、左右のキー入力があった場合に、それぞれ MoveLeft メソッドと MoveRight メソッドを呼び出して、プレイヤーを移動させています
また、プレイヤーの移動制限を Mathf.Clamp を使用して計算して、画面の端に到達した場合には、それ以上移動しないようにしています。
public void MoveLeft() と public void MoveRight() は、それぞれプレイヤーを左右に移動させるメソッドですtransform.Translate を使用して、プレイヤーの座標を移動させています
Time.deltaTime を乗算することで、フレームレートの変化による移動速度の変化を吸収します

PlayerStatusスクリプト

プレイヤーの状態を管理するスクリプトです

using UnityEngine;

// プレイヤーの状態管理
public class PlayerStatus : MonoBehaviour
{
    public int hp = 100;
}

public int hp = 100; は、プレイヤーの体力を表す hp 変数を宣言して、初期値を100に設定しています
この変数は、他のスクリプトからアクセスできます

Arrowゲームオブジェクト

矢について機能を持ったオブジェクトです
イラストの情報や表示、移動などのコンポーネントがアタッチされています

ArrowControllerスクリプト

矢の移動や衝突判定を管理するスクリプトです
衝突時、登録されたイベントハンドラを実行します

using UnityEngine;
using UnityEngine.Events;

public class ArrowController : MonoBehaviour
{
    GameObject player;

    // 衝突時に呼び出すイベントハンドラを登録するフィールド変数
    public UnityEvent<int> OnArrowCollision;

    // 矢の半径
    [SerializeField] float arrowRadius = 0.5f;
    // プレイヤの半径
    [SerializeField] float playerRadius = 1.0f;
    // 落下速度
    [SerializeField] float dropSpeed = -2;
    // 落下時の自動消去するYの値
    [SerializeField] float destroyPositionY = -5.0f;

    public int damegePoint = 10;

    void Start()
    {
        player = GameObject.Find("playerPrefab(Clone)");
    }

    void Update()
    {
        // フレームごとに等速で落下させる
        transform.Translate(0, dropSpeed * Time.deltaTime, 0);

        //画面外判定
        CheckOutOfFrame();

        // 当たり判定
        CheckDetect();
    }

    private void CheckOutOfFrame()
    {
        // 画面外に出たらオブジェクトを破棄する
        if (transform.position.y < destroyPositionY)
        {
            Destroy(gameObject);
        }
    }

    // 当たり判定
    private void CheckDetect()
    {
        // プレイヤーと矢の距離
        Vector3 distanceFromArrowtoPlayer = transform.position - player.transform.position;
        // 三平方の定理で直線距離を計算
        float distance = distanceFromArrowtoPlayer.magnitude;

        // 外観が接触した!
        if (distance < arrowRadius + playerRadius)
        {
            // 登録されているイベントハンドラを実行する
            OnArrowCollision?.Invoke(damegePoint);

            // 衝突した場合は矢を消す
            Destroy(gameObject);
        }
    }
}

このコードで、OnArrowCollisionは「public UnityEvent<int>」型の変数であり、矢が何かに衝突したときに発生するイベントを表しています
このイベントは、整数値を引数に受け取ります。具体的には、このイベントは、矢が何かに当たったときに、そのオブジェクトにダメージを与えるために使用されます。OnArrowCollision変数は、ArrowControllerクラス内の他のメソッドからもアクセスできるように、publicで定義されています。また、OnArrowCollision変数には、UnityEvent<int>型のインスタンスが新しく作られ、初期化されます。

UnityEventは、Unityのイベントシステムを使用して、複数のイベントリスナーを登録できる強力な機能です。これにより、複数の処理を実行することができます。例えば、矢が衝突したときに、音を再生するとともに、ダメージを与えることもできます。

このクラスは、矢の制御を担当しており、矢が画面外に出たときに自動的に破棄されるように設計されています
また、矢がプレイヤーに衝突した場合、指定されたダメージポイントでOnArrowCollisionイベントを発生させるようにもなっています

さらに、このクラスは、以下の3つのフィールド変数を持っています。

  • OnArrowCollision:衝突時に呼び出すイベントハンドラを登録するフィールド変数
  • arrowRadius:矢の半径
  • playerRadius:プレイヤの半径

これらのフィールド変数は、インスペクターペインで編集することができます。また、他にも落下速度や自動消去するYの値などのパラメータも定義されています。

このクラスには、StartメソッドとUpdateメソッドが含まれています。

Startメソッドは、矢が作成されたときに呼び出され、プレイヤーオブジェクトを見つけてplayerフィールド変数に割り当てます。

Updateメソッドは、矢の位置をフレームごとに等速で落下させ、画面外判定と当たり判定を実行します。CheckOutOfFrameメソッドは、矢が画面外に出た場合にオブジェクトを破棄します。CheckDetectメソッドは、矢がプレイヤーに当たった場合にOnArrowCollisionイベントを発生させ、矢を破棄します。

このクラスは、GameDirectorスクリプトから参照されます

UIゲームオブジェクト(Canvas)

Unityにおいて、CanvasはUIを描画するためのオブジェクトです。UI要素を画面上に表示するために使用され、ユーザーがゲーム内のメニューやHUDを操作することができます。

Canvasは、画面上に表示されるUI要素のグループを表します。また、UI要素を配置するために使用される「RectTransform」コンポーネントも含まれます。Canvasを作成すると、デフォルトで画面全体を覆うようなサイズと位置が設定されます。また、Canvasはレンダリングモードを指定することができ、2DのUI要素を描画する際に使用される「Screen Space – Overlay」や、「Screen Space – Camera」、3DのオブジェクトをUI要素として描画する「World Space」などがあります。

Canvasは、Unityのインスペクタウィンドウ内で作成でき、必要に応じて子オブジェクトにUI要素を追加して構成することができます。例えば、テキストやイメージ、ボタン、スライダー、ドロップダウンなどのUI要素をCanvas内に配置することができます。Canvasを作成することで、UIの作成や配置が簡単になり、直感的に操作することができます。

GaugeControllerスクリプト

このコードは、HPゲージ表示を制御するためのスクリプトです

using UnityEngine;
using UnityEngine.UI;

public class GaugeController : MonoBehaviour
{
    // hpGaugeゲームオブジェクトにアタッチされているImageコンポーネントをアウトレット接続
    [SerializeField] Image hpGaugeImage;

    // ゲージの円の画像を更新する
    public void ViewHp(int value)
    {
        hpGaugeImage.fillAmount = value / 100.0f;
    }
}

[SerializeField]属性を使用して、hpGaugeImageという名前のImageコンポーネントをアウトレット接続しています
これは、Unityのインスペクタービューで、GaugeControllerスクリプトをアタッチしたゲームオブジェクトのImageコンポーネントを指定することができます

ViewHpメソッドでは、int型のvalueパラメータを受け取ります
このvalueパラメータは、現在のHPの値を表します

このメソッドは、hpGaugeImageのfillAmountプロパティを、value / 100.0fで更新します
fillAmountは、Imageコンポーネントのゲージを表すプロパティであり、0から1までの範囲の値を取ります。ここでは、valueを100で割ることで、0から1までの範囲に変換しています。これにより、HPゲージの円の表示が更新されます

クラス図

その他のプロジェクトウィンドウのフォルダ構成例

Unityのプロジェクトウィンドウのフォルダ構成は、プロジェクトによって異なりますが、以下のような構成を取ることもよく使われるパターンです

  1. Assetsフォルダー: Unityプロジェクトの中心的なフォルダーで、すべてのアセット(テクスチャ、モデル、音声、スクリプトなど)が含まれています。
  2. Scenesフォルダー: Unityのシーンファイルが保存されるフォルダーです。シーンファイルは、プロジェクト内の異なる場所やレベルに配置されたオブジェクト、カメラ、照明などを含みます。
  3. Scriptsフォルダー: Unityプロジェクトで使用されるスクリプトファイルが含まれています。スクリプトは、C#、JavaScript、Booなどのプログラミング言語で書かれています。
  4. Prefabsフォルダー: 再利用可能なPrefabオブジェクトが保存されるフォルダーです。Prefabは、ゲームオブジェクトとそのコンポーネントの集合を再利用可能なテンプレートとして保存し、他のシーンやプロジェクトで再利用できるようにする機能です。
  5. Pluginsフォルダー: Unityプロジェクトで使用される外部プラグイン(DLLなど)が含まれています。
  6. Editorフォルダー: Unityエディターで使用されるスクリプトが含まれています。このフォルダー内に配置されたスクリプトは、プロジェクトの実行時には実行されず、エディター上でのみ実行されます。
  7. Resourcesフォルダー: プロジェクトで使用されるリソースファイル(画像、音声、動画など)が含まれています。Resourcesフォルダーに配置されたリソースは、UnityのAPIからロードできます。
  8. StreamingAssetsフォルダー: プロジェクトで使用されるストリーミングリソース(ビデオ、音声、XMLなど)が含まれています。ストリーミングリソースは、ビルドされたプロジェクト内で使用されます。

これらのフォルダーは、プロジェクトの構成に応じてカスタマイズできます