【Unity】2Dシューティングを作ってみる話 #22

記事をご覧いただき、誠にありがとうございます。
投稿主の無能です。

前回は、プレイヤーにHPを実装しました。

今回はプレイヤーに残機を持たせる仕組みを実装したいと思います。

プレイヤーに残機を持たせる

プレイヤーに残機を持たせる準備のために、まずはUIを作成します。

スコアの下あたりに、HPと同様の表示でテキストとアイコンで残機を表示します。

残機の初期値を5にします。

プレイヤーの状態を示すUIなので、PlayerStatusオブジェクトを親にして、「RemainingCount_Text」というテキストを作ります。

「RemainingCount_Text」というテキストを作成する

RemainingCount_Textの位置

RemainingCount_Textの設定

残機表示テキストの右側にアイコンを残機数分表示させるようにします。

まずは残機のアイコンを作成します。

「RemainingCountIcon」という空のオブジェクトを作成し、Sprite Rendererをアタッチします。

「RemainingCountIcon」という空のオブジェクトを作成する

RemainingCountIconにSprite Rendererをアタッチする

残機のアイコンは何でも良いですが、分かりやすいものが良いですね。

戦闘機のアイコンにします。
※二次配布に抵触する可能性があるため、リンク先を参照してください。

戦闘機のアイコン - icooon-mono

これをRemainingCountIconのSprite Rendererに設定します。

RemainingCountIconの位置

インポートした画像の設定

RemainingCountIconの設定

残機アイコンを整理するため、PlayerStatusの子オブジェクトに「RemainingIcon」という空のオブジェクトを作成します。

「RemainingIcon」という空のオブジェクトを作成する

そして残機アイコンを子オブジェクトにして、残機の最大値分アイコンを複製します。

今回だと残機の最大数は5なので複製して最大数にします。

RemainingCountIconをRemainingIconの子オブジェクトにする

RemainingCountIconを残機の最大数分複製する

そして複製したアイコンの位置を調整します。

残機アイコンの位置を調整


プレイヤーがやられたら復活する地点を用意する必要があります。

ゲーム開始時の初期位置の-1後ろを復活地点とします。

「PlayerRespawnPos」という空のオブジェクトを作成して配置します。

「PlayerRespawnPos」という空のオブジェクトを作成して配置

プレイヤーの初期位置から-1の座標に配置

配置したらSprite Rendererを非アクティブにする

これでUIの下準備が出来たので、スクリプトで残機を実装させていきます。

スクリプトで残機を実装する

スクリプトで残機を実装するのですが、やることがちょっと多いので、先にやることを整理しておきます。

  • 残機の表示を更新する
  • HPが0以下になったら残機を-1する
  • プレイヤーを非アクティブにして、復活地点へ移動する
  • プレイヤーのHPを最大値に戻す
  • 残機が0未満になったらコンティニュー処理を行う

と、ざっと挙げた感じではこの処理を実装する必要があります。
なかなか多いですね。

処理は多いですが、ここまでやってきたので難しい処理はありません。
一つひとつ丁寧に片付けていきましょう。

このようになります。

PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController : MonoBehaviour
{
    #region Player-Variables
    #region var-Move
    [Header("移動関連")]
    [SerializeField] float moveSpeed = 1f;                  // プレイヤーの移動速度
    [SerializeField] float marginX = 1f, marginY = 1f;      // 余白x・y
    #endregion

    #region var-Shot
    [Header("ショット関連")]
    [SerializeField] GameObject playerBullet;               // 弾のプレハブ
    [SerializeField] float bulletSpeed = 10f;               // 弾の速度
    [SerializeField] Transform playerFirePos;               // 発射ポイント
    [SerializeField] float bulletDelay = 1f;                // 発射の閾値
    #endregion

    #region var-HP
    [Header("プレイヤーのHP")]
    [SerializeField] int playerHPMax = 10;                  // プレイヤーのHP(初期値:10)
    [SerializeField] TMP_Text playerHPText;                 // プレイヤーのHP表示のテキスト
    [SerializeField] Slider playerHPGauge;                  // プレイヤーのHP表示のゲージ
    #endregion

    #region var-Remaining
    [Header("プレイヤーの残機")]
    [SerializeField] TMP_Text remainingCount_Text;          // プレイヤーの残機表示のテキスト
    [SerializeField] GameObject[] remainingCount_Icon;      // プレイヤーの残機表示のアイコン
    [SerializeField] int remainingCountMax = 5;             // プレイヤーの残機の最大数
    [SerializeField] int remainingCountCurrent = 3;         // プレイヤーの残機の現在値
    [SerializeField] Transform respawnPos;                  // プレイヤーの復活地点
    #endregion

    #region var-Controllers
    [Header("バーチャルスティック")][SerializeField] VariableJoystick joyStick;
    [Header("爆発エフェクト")][SerializeField] GameObject playerExplosion;

    [Header("ゲームマネージャー")][SerializeField] GameManager gameManager;
    [Header("エネミーコントローラー")][SerializeField] EnemyController enemyController;
    #endregion

    #region base-Variables
    Rigidbody2D playerRB;                                   // プレイヤーのRigidbody
    Vector2 moveDirection, min, max;                        // プレイヤーのベクトル,画面サイズの最小ベクトル/最大ベクトル
    Vector2 playerPos;                                      // プレイヤーの移動制限用
    float bulletInterval;                                   // 弾の発射間隔管理用
    bool isShotPressed = false;                             // 発射ボタン管理用
    #endregion

    #region Internal-Variables
    int playerHPCurrent, playerHPMin = 0;                   // プレイヤーの現在のHP,HPの最小値
    enum calcType { heal, damage, display }                 // HP表示・計算のタイプ
    int remainingCountMin = 0;                              // プレイヤーの残機の最小値
    #endregion
    #endregion

    #region Method
    #region Start
    void Start()
    {
        // プレイヤーのRigidbodyを取得
        playerRB = GetComponent<Rigidbody2D>();
        // プレイヤーのHPを初期化
        playerHPCurrent = playerHPMax;
        // プレイヤーのHPを表示
        PlayerHPDisplay();
        // プレイヤーの残機を表示
        RemainingCounter();
    }
    #endregion

    #region Update
    void Update()
    {
        // 入力受付
        InputProcess();

        // バーチャルスティック処理 ---
        // バーチャルスティックに何らかの入力が合った場合
        // (joyStickが0では無い場合)
        if (joyStick.Direction != Vector2.zero)
        {
            // moveDirectionをバーチャルスティックの入力にする
            moveDirection = joyStick.Direction;
        }
        // --- ここまで

        // 弾の発射 ---
        // deltaTimeを加算
        bulletInterval += Time.deltaTime;
        // 発射間隔が閾値未満の場合
        if (bulletInterval < bulletDelay)
        {
            // 処理を中断
            return;
        }
        // スペースキーが押された場合/isShotPressedがtrueの場合
        if (Input.GetKey(KeyCode.Space) || isShotPressed)
        {
            // 弾の発射関数を呼ぶ
            PlayerShot(playerFirePos);
        }
        // 発射間隔をリセット
        bulletInterval = 0;
        // --- ここまで

    }
    #endregion

    #region FixedUpdate
    void FixedUpdate()
    {
        // プレイヤーを動かす
        PlayerMove();
    }
    #endregion

    #region InputProcess
    // 入力受付
    void InputProcess()
    {
        // 入力を正規化して受け取る
        float x = Input.GetAxisRaw("Horizontal");
        float y = Input.GetAxisRaw("Vertical");
        // 入力を正規化する
        moveDirection = new Vector2(x, y).normalized;
    }
    #endregion

    #region PlayerMove
    // プレイヤーを動かす
    void PlayerMove()
    {
        // 移動制限
        MoveClamp();
        // プレイヤーを動かす
        playerRB.velocity = moveDirection * moveSpeed;
    }
    #endregion

    #region MoveClamp
    // プレイヤーの移動制限
    void MoveClamp()
    {
        // 最後の移動地点を取得
        playerPos = transform.position;
        // ビューポート座標からワールド座標を取得
        min = Camera.main.ViewportToWorldPoint(Vector2.zero);
        max = Camera.main.ViewportToWorldPoint(Vector2.one);
        // 移動を制限する
        playerPos.x = Mathf.Clamp(playerPos.x, min.x + marginX, max.x - marginX);
        playerPos.y = Mathf.Clamp(playerPos.y, min.y + marginY, max.y - marginY);
        // プレイヤーの位置を取得した最後の地点にする
        transform.position = playerPos;
    }
    #endregion

    #region PlayerShot
    // 弾の発射
    void PlayerShot(Transform firePos)
    {
        // 弾を生成
        GameObject bulletClone = Instantiate(playerBullet, firePos.position, Quaternion.identity);
        // 弾のRigidbodyに速度ベクトルをつける
        bulletClone.GetComponent<Rigidbody2D>().velocity = new Vector2(bulletSpeed, 0);
    }
    #endregion

    #region OnTriggerEnter2D
    // 接触判定
    private void OnTriggerEnter2D(Collider2D collision)
    {
        // 敵の場合
        if (collision.gameObject.CompareTag("Enemy"))
        {
            // ダメージの算出
            PlayerHPChanged((int)calcType.damage, EnemyController.bodyDamage, collision);
        }
        // 敵の弾の場合
        else if (collision.gameObject.CompareTag("EnemyBullet"))
        {
            // ダメージの算出
            PlayerHPChanged((int)calcType.damage, EnemyController.shotDamage, collision);
        }
        // プレイヤーの弾の場合
        else if (collision.gameObject.CompareTag("PlayerBullet"))
        {
            // 処理を中断
            return;
        }
    }
    #endregion

    #region PlayerExplosion
    // 爆発演出
    void PlayerExplosion(Collider2D collision)
    {
        // 接触した方を破棄
        Destroy(collision.gameObject);
        // 自身を非アクティブ
        gameObject.SetActive(false);
        // プレイヤーの位置に爆発オブジェクトを作成
        Instantiate(playerExplosion, playerPos, Quaternion.identity);
    }
    #endregion

    #region OnPointerDown
    // 発射ボタンが押された時のイベント
    public void OnPointerDown()
    {
        // 発射ボタンのフラグを変更
        isShotPressed = true;
    }
    #endregion

    #region OnPointerUp
    // 発射ボタンが元に戻った時のイベント
    public void OnPointerUp()
    {
        // 発射ボタンのフラグを変更
        isShotPressed = false;
    }
    #endregion

    #region Continue
    // コンティニュー
    void Continue()
    {
        // ゲームマネージャーのコンティニューUIを表示
        gameManager.VisibleUI_Continue();
    }
    #endregion

    #region PlayerHPDisplay
    // プレイヤーのHP表示・更新
    void PlayerHPDisplay()
    {
        // HPのテキストを表示・更新
        playerHPText.SetText("PlayerHP : " + playerHPCurrent + " / " + playerHPMax);
        // 現在のHPをゲージに反映
        playerHPGauge.value = playerHPCurrent;
    }
    #endregion

    #region PlayerHPChanged
    // プレイヤーのダメージ算出
    void PlayerHPChanged(int cType, int damage, Collider2D collision)
    {
        // switchで条件分岐:対象→cType
        switch (cType)
        {
            // cTypeが回復だった場合
            case (int)calcType.heal:
                // ダメージ分を加算する
                playerHPCurrent += damage;
                // 接触したオブジェクトを消す
                Destroy(collision.gameObject);
                // プレイヤーのHPが最大値以上の場合
                if (playerHPCurrent >= playerHPMax)
                {
                    // プレイヤーのHPを最大値にする
                    playerHPCurrent = playerHPMax;
                }
                // switch文を抜ける
                break;

            // cTypeがダメージだった場合
            case (int)calcType.damage:
                // ダメージ分を減算する
                playerHPCurrent -= damage;
                // 接触したオブジェクトを消す
                Destroy(collision.gameObject);
                // プレイヤーのHPが0以下(最小値以下)だった場合
                if (playerHPCurrent <= playerHPMin)
                {
                    // 爆発演出
                    PlayerExplosion(collision);
                    // プレイヤーのHPを0(最小値)にする
                    playerHPCurrent = playerHPMin;
                    // 残機を更新
                    RemainingCounter();
                }
                // それ以外の場合
                else
                {
                    // switch文を抜ける
                    break;
                }
                // switch文を抜ける
                break;

            // cTypeが表示だった場合
            case (int)calcType.display:
                // switch文を抜ける
                break;

            // 該当なしの場合
            default:
                // HP表示の分岐へジャンプ
                goto case (int)calcType.display;
        }
        // プレイヤーのHPを表示
        PlayerHPDisplay();
    }
    #endregion

    #region RemainingCountDisplay
    // 残機表示・更新
    void RemainingCountDisplay()
    {
        // プレイヤーの残機表示のテキストを更新
        remainingCount_Text.SetText("Remaining Count:" + remainingCountCurrent + "/" + remainingCountMax);
        // 残機の最大数までループ
        for (int i = 0; i < remainingCountMax; i++)
        {
            // 現在の残機数がi以下の場合
            if (remainingCountCurrent <= i)
            {
                // 残機アイコンを非アクティブ
                remainingCount_Icon[i].SetActive(false);
            }
            // それ以外の場合
            else
            {
                // 残機アイコンをアクティブ
                remainingCount_Icon[i].SetActive(true);
            }
        }
    }
    #endregion

    #region RemainingCounter
    // 残機カウントを更新
    void RemainingCounter()
    {
        // 残機を減らす
        remainingCountCurrent--;
        // 残機が最小値(0)以下の場合
        if (remainingCountCurrent < remainingCountMin)
        {
            // 残機を最小値にする
            remainingCountCurrent = remainingCountMin;
            // コンティニュー処理
            Continue();
        }
        // それ以外の場合
        else
        {
            // プレイヤーを復活地点に移動
            PlayerRespawn(respawnPos);
        }
        // 残機表示を更新
        RemainingCountDisplay();
    }
    #endregion

    #region PlayerRespawn
    // プレイヤーの復活
    void PlayerRespawn(Transform rePos)
    {
        // プレイヤーをアクティブ
        gameObject.SetActive(true);
        // プレイヤーを復活地点に移動
        gameObject.transform.position = rePos.position;
        // プレイヤーのHPを戻す
        playerHPCurrent = playerHPMax;
        // プレイヤーのHPを更新
        PlayerHPDisplay();
    }
    #endregion
    #endregion
}
では見ていきましょう。

まずは追加したメンバ変数を見ていきます。

先程作成したテキストとアイコン、そして復活視点があります。

そして残機の最大数と現在の残機、内部処理用に残機の最小数(0)を作りました。

Start関数では残機を更新するRemainingCounter関数を呼んでいます。

この関数は残機を計算・更新する関数になります。

次にPlayerExplosion関数の、プレイヤー自身を破棄する部分を、非アクティブにしています。

この理由は、プレイヤーを破棄すると作成し直す必要があるためです。

やられた時は非アクティブにして、アクティブになった際に復活地点から動けば、ゲームとしては問題無いからです。

次にPlayerHPChanged関数のダメージを受けた際の処理に変更があります。

ここではHPが0未満になったら、今まではコンティニュー処理を即座に表示していましたが、残機があるので残機の値を更新する処理にしています。

こうすることで、残機がある内は復活地点から再度ゲームに戻れるようになります。

次のRemainingCountDisplay関数では、残機のテキストと残機アイコンの表示・更新を行います。

この中で初めてのfor文に出会いました。

for文はforの右隣にある括弧内の条件を満たす間、繰り返し波括弧内の処理を行うというループ文になります。

for文の見方を説明すると
  • 始めにfor文内で使うためのiという変数を用意します。
  • iが条件に合致していたら波括弧内の処理をします。
  • 最後にiをカウントします。
という流れで処理を行っています。

この流れはセミコロンで区切られている部分に表されます。

変数iを宣言・初期化していて、次のセミコロンまでがif文のような条件になります。

そして波括弧内の処理が行われた後に、条件を更新するためiをカウントアップします。

for文の括弧内は以上の内容に構成されています。

(変数を初期化;ループ継続の条件;{}内の処理が終わったら行う処理)

となっています。

iが初期化された時は0で、RemainingCountMax未満なので波括弧内の処理が行われ、処理後にi++と言う処理を行っています。

この++はインクリメントと呼ばれ、インクリメントが付いた変数に1を足し合わせます。

言い換えると変数=変数+1と同義であるという事です。

因みに--はデクリメントと呼ばれ、デクリメントが付いた変数から-1します。

こちらも言い換えると変数=変数-1と同義であるという事です。

このように簡単にカウントアップ・カウントダウンするものがあるというのを覚えておきましょう。

for文の波括弧内の処理ですが、残機アイコンの表示・非表示を行っています。

RemainingCountMax分forでループして、その時iが現在の残機アイコン以下の場合に非アクティブにして、それ以外の場合にアクティブにしています。

残機の更新は次のRemainingCounter関数でしています。

RemainingCounter関数では最初に残機を1減らします。

早速デクリメントが出てきましたね。

そして次のif文では、現在の残機が0未満の場合に、現在の残機を0にしてコンティニュー処理を呼び出しています。

それ以外の場合はプレイヤーを復活地点に戻しています。

そして関数の最後に残機の表示を更新しています。

最後にプレイヤーを復活地点に移動するPlayerRespawn関数では、プレイヤーをアクティブにして、プレイヤーのpositionを先程作成した復活地点にしています。

そしてHPを最大値に戻してHPの表示を更新しています。

文章で書くと長くなってしまいますが、このような処理を実装しました。

では追加された項目を設定していきましょう。

追加された項目を設定

設定出来たらゲームを実行して確認します。

今回の確認する項目は
  • 残機アイコンは正しく表示されているか、残機のテキスト表示は正しいか
  • HPが0になったら残機が-1される
  • 復活地点に戻る
  • 残機が0未満になったらコンティニュー処理が走る
になります。

では確認しましょう。

残機の表示は配列なので0・1・2になるのでこれでOKです。

残機表示はOK

残機が-1されてHPも最大値に戻りました。
(タイミング悪く、スクリーンショット撮影時に被弾してしまいました)

残機処理もOK

残機が0の時にやられたら、正しくコンティニュー処理が走りました。

コンティニュー処理もOK

これで残機の実装ができました。

まとめ

今回は
  • プレイヤーの残機を実装した
という事をやりました。

次回は、体力回復などのアイテムを実装したいと思います。

では、また次回!

コメント