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

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

前回は、敵の種類を増やして敵毎に動きを付けました。

今回はプレイヤーにシールド機能を実装したいと思います。

プレイヤーのシールド機能

敵の種類が増えて手強くなってきたので、プレイヤーも強化したいと思います。

そこでシールド機能を付けて、敵の弾や敵本体からのダメージを防ぐ、または軽減するようにしたいと思います。

シールドを作成する

まずはシールドを作ります。

2DオブジェクトからCircleを作り、「PlayerShield」としてプレイヤーの子オブジェクトにします。

GameObject > 2D Object > Sprite > Circle でCircleを作成

「PlayerShield」という名前でプレイヤーの子オブジェクトにする


作成したPlayerShieldをPlayerと同じposition(0, 0, 0)にします。

Playerと同じ位置にする


当然ですが重なってPlayerが見えません。

そこでこのCircleの透明度を変更し、Player全体を包み込むようにします。

透明度は色相環のAというアルファ値で変更できます。

色相環のアルファ値で透明度を変更できる

ではシールドを青っぽくして、Player全体を包み込みます。

シールドを青っぽくしてアルファ値を調整する

シールドの大きさを調整

プレイヤー全体を包み込んだ

これでシールドっぽくなりました。

あとはシールドを非アクティブにしておけば準備OKです。

シールドを非アクティブにする

シールドを持つようになるきっかけを作る

シールドを非アクティブにしたので、どのようにしてアクティブにするかと言うと、シールドのアイテムを取得したらアクティブにします。

そうすればアイテムの価値がグッと出てきますね。

ということでシールドのアイテムを作りましょう。

アイテムはシールドが青っぽいので青にしました。

アイテムの素材をインポート

画像の大きさを設定

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

ではスクリプトでの実装になります。

アイテムの分岐を増やす方から追加するとこのようになります。

ItemController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]

public class ItemController : MonoBehaviour
{
    #region Variables
    #region var-CapsuleBase
    [Header("アイテムの基本パラメーター")]
    [SerializeField] float rotateSpeed = 10.0f;             // アイテムの回転速度
    [SerializeField] float dropSpeed = 1.0f;                // アイテムの進行速度
    [SerializeField] int itemType = 0;                      // アイテムの種類
    #endregion

    #region var-CapsuleIndividual
    [Header("アイテム個別のパラメーター")]
    [SerializeField] int healPoint = 5;                         // 回復アイテムの回復量
    [SerializeField] int itemScore = 5000;                      // スコアアイテムのスコア増加量
    [SerializeField] int remainingAdd = 1;                      // 残機の増加量
    #endregion

    #region var-SE
    [Header("効果音関連")]
    [SerializeField] AudioClip SE_GetItem;                      // アイテム取得時の効果音
    [SerializeField] float getItemVol = 0.4f;                   // 取得時の効果音の音量
    #endregion

    #region var-Controller
    [Header("コントローラー関連")]
    [SerializeField] PlayerController player;                   // プレイヤーコントローラー
    [SerializeField] GameManager gameManager;                   // ゲームマネージャー
    Rigidbody2D capsuleRB;                                      // アイテムのリジッドボディ
    #endregion

    #region others
    Collider2D itemCollider;                                // アイテムのコライダー
    #endregion

    #region Internal
    enum itemEffectType { heal, addScore, oneUp, shield };      // 内部処理用(アイテムの効果の種類)
    #endregion
    #endregion

    #region Methods
    #region Start
    void Start()
    {
        // カプセルのリジッドボディを取得
        capsuleRB = GetComponent<Rigidbody2D%gt;();
        // ゲームマネージャーを取得
        gameManager = GameObject.FindWithTag("GameManager").GetComponent<GameManager>();
        // プレイヤーコントローラーを取得
        player = GameObject.FindWithTag("Player").GetComponent<PlayerController>();
        // アイテムのコライダーを取得
        itemCollider = GetComponent<Collider2D>();
    }
    #endregion

    #region Update
    void Update()
    {
        // アイテムをプレイヤーの方向へ動かす
        capsuleRB.velocity = new Vector2(-dropSpeed, 0);
        // アイテムを回転させる
        capsuleRB.transform.Rotate(0, 0, rotateSpeed);
    }
    #endregion

    #region OnTriggerEnter2D
    // 接触判定
    private void OnTriggerEnter2D(Collider2D collision)
    {
        // 接触したオブジェクトのタグがPlayerだった場合
        if (collision.gameObject.CompareTag("Player"))
        {
            // アイテムの効果を呼び出す
            ItemEffect(itemType, itemCollider);
        }
    }
    #endregion

    #region ItemEffect
    // アイテムの効果
    void ItemEffect(int iType, Collider2D collision)
    {
        // 引数iTypeで分岐
        switch (iType)
        {
            // 回復効果の場合
            case (int)itemEffectType.heal:
                // プレイヤーのHP表示を更新
                player.PlayerHPChanged(iType, healPoint, collision);
                // switch文を抜ける
                break;

            // スコア加算の場合
            case (int)itemEffectType.addScore:
                // スコアを加算する
                gameManager.ScoreAdded(itemScore);
                // 自身を破棄
                Destroy(gameObject);
                // switch文を抜ける
                break;

            // 1upの場合
            case (int)itemEffectType.oneUp:
                // 残機を増やす
                player.RemainingCounter(remainingAdd);
                // 自身を破棄
                Destroy(gameObject);
                // switch文を抜ける
                break;

            // シールドの場合
            case (int)itemEffectType.shield:
                // シールドを有効にする
                player.PlayerShieldActivate();
                // 自身を破棄
                Destroy(gameObject);
                // switch文を抜ける
                break;

            // 該当なしの場合
            default:
                // switch文を抜ける
                break;
        }
        // 取得時に効果音を再生
        AudioSource.PlayClipAtPoint(SE_GetItem, Camera.main.transform.position, getItemVol);
    }
    #endregion
    #endregion
}

今回はenumのitemEffectTypeにshieldを追加しました。

ItemEffect関数のswitch文の分岐にshieldの場合を追加して、PlayerControllerのPlayerShieldActivate関数を呼んで、アイテム自身を破棄します。

いつものアイテムの処理ですね。

では次にPlayerControllerの方を追記するとこうなります。

PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using TMPro;
using Unity.VisualScripting;
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] int playerHPCurrent;                       // プレイヤーの現在のHP
    [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-Shield
    [Header("シールド関連")]
    [SerializeField] GameObject playerShield;                                   // プレイヤーのシールド
    [SerializeField] int ShieldHPMax = 5;                                       // シールドの耐久値の最大値
    #endregion

    #region var-SE
    [Header("効果音関連")]
    [SerializeField] AudioClip SE_playerDamaged;                                // プレイヤー被弾時の効果音
    [SerializeField] AudioClip SE_ShieldDamaged;                                // シールドが攻撃された効果音
    [SerializeField] float playerDamagedVol = 0.4f, shieldDamagedVol = 0.4f;    // 被弾時の効果音の音量(プレイヤー,シールド)
    #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 playerHPMin = 0;                                                        // HPの最小値
    enum calcType { heal, damage, display }                                     // HP表示・計算のタイプ
    int remainingCountMin = 0;                                                  // プレイヤーの残機の最小値
    int changeRemaining = -1, remainingCountDisp = 0;                           // プレイヤーの残機減少用,表示用
    int shieldHPCurrent, shieldHPMin = 0;                                       // シールドの現在の耐久値
    #endregion
    #endregion

    #region Method
    #region Start
    void Start()
    {
        // プレイヤーのRigidbodyを取得
        playerRB = GetComponent<Rigidbody2D>();
        // プレイヤーのHPを初期化
        playerHPCurrent = playerHPMax;
        // プレイヤーのHPを表示
        PlayerHPDisplay();
        // プレイヤーの残機を表示
        RemainingCounter(remainingCountDisp);
    }
    #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
    // プレイヤーのダメージ算出
    public 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:
                // シールドがアクティブになっている場合
                if (playerShield.activeInHierarchy)
                {
                    // シールドの耐久値を減算する
                    PlayerShieldIsActive(damage, collision);
                }
                else
                {
                    // ダメージ分を減算する
                    playerHPCurrent -= damage;
                }

                // 接触したオブジェクトを消す
                Destroy(collision.gameObject);

                // プレイヤーのHPが0以下(最小値以下)だった場合
                if (playerHPCurrent <= playerHPMin)
                {
                    // 爆発演出
                    PlayerExplosion(collision);
                    // プレイヤーのHPを0(最小値)にする
                    playerHPCurrent = playerHPMin;
                    // 残機を更新
                    RemainingCounter(changeRemaining);
                }
                // それ以外の場合
                else
                {
                    if (playerShield.activeInHierarchy)
                    {
                        // 被弾時に効果音を再生(シールドへのダメージ)
                        AudioSource.PlayClipAtPoint(SE_ShieldDamaged, Camera.main.transform.position, shieldDamagedVol);
                    }
                    else
                    {
                        // 被弾時に効果音を再生(プレイヤーへのダメージ)
                        AudioSource.PlayClipAtPoint(SE_playerDamaged, Camera.main.transform.position, playerDamagedVol);
                    }
                    // 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()
    {
        // 現在の残機が最大値を超えている場合
        if (remainingCountCurrent > remainingCountMax)
        {
            // 残機を最大値にする
            remainingCountCurrent = remainingCountMax;
        }
        // プレイヤーの残機表示のテキストを更新
        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
    // 残機カウントを更新
    public void RemainingCounter(int addRemaining)
    {
        // 残機に変更がない場合(addRemainingが0の場合)
        if (addRemaining == remainingCountDisp)
        {
            // 残機を表示する
            RemainingCountDisplay();
        }
        // 残機を増やす場合(addRemainingがプラスの場合)
        else if (addRemaining > remainingCountDisp)
        {
            // 残機を増やす
            remainingCountCurrent += addRemaining;
            // 残機が最大値を超える場合
            if (remainingCountCurrent > remainingCountMax)
            {
                // 現在の残機を最大値にする
                remainingCountCurrent = remainingCountMax;
            }
        }
        // 残機を減らす場合(addRemainingがマイナスの場合)
        else if (addRemaining < remainingCountDisp)
        {
            // 残機を減らす
            remainingCountCurrent += addRemaining;
            // 残機が最小値(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

    #region PlayerShieldActivate
    // シールドをアクティブにする
    public void PlayerShieldActivate()
    {
        // シールドをアクティブ
        playerShield.SetActive(true);
        // シールドの耐久値を最大にする
        shieldHPCurrent = ShieldHPMax;
    }
    #endregion

    #region PlayerShieldIsActive
    // シールドのダメージ
    void PlayerShieldIsActive(int shieldDamage, Collider2D collision)
    {
        // シールドで受けたダメージを減算
        shieldHPCurrent -= shieldDamage;
        // 耐久値が最小値(0)未満になった場合
        if (shieldHPCurrent < shieldHPMin)
        {
            // 耐久値を超過した分をexcessDamageに代入
            int excessDamage = shieldHPCurrent;
            // シールドを非アクティブ
            playerShield.SetActive(false);
            // 超過したダメージ分はプレイヤーへダメージ
            PlayerHPChanged((int)calcType.damage, -excessDamage, collision);
        }
    }
    #endregion
    #endregion
}

では見ていきましょう。

メンバ変数に
  • シールドのゲームオブジェクト
  • シールドの耐久値の最大値
  • シールドの耐久値の最小値
  • 現在のシールドの耐久値
  • シールドがダメージを受けた時の効果音
  • 効果音のボリューム
を追加しました。

PlayerHPChanged関数のダメージを受ける分岐で、シールドがアクティブになっている場合の条件分岐を追加しています。

activeInHierarchyについてはドキュメントで詳細を確認してください。

GameObject.activeInHierarchy - Unity Documentation

activeInHierarchyは名前からも推測できる通り、対象のゲームオブジェクトがHierarchyビュー上(シーン上)でアクティブかどうかbool値を返すものです。

シールドがアクティブになっているかで条件を分岐させ、シールドがアクティブであればシールドから、非アクティブであればプレイヤーからダメージを減算します。

そしてダメージを受けた時の効果音も、同様にシールドとプレイヤーで分けています。

PlayerShieldActivate関数は、アイテムによってシールドがアクティブになります。

シールドがアクティブになった際に耐久値を最大にしています。

そしてシールドの耐久値とダメージを計算するPlayerShieldIsActive関数では、まずシールドが受けたダメージを現在の耐久値から減算します。

その際にシールドの耐久値が0未満になった場合に、まずはシールドを非アクティブにします。

そしてダメージの超過分をexcessDamageに代入して、PlayerHPChanged関数で超過分のダメージをプレイヤーのHPから減算する処理にしています。

これで処理が正常に行われるか、追加された項目を設定して確認しましょう。

まずは追加分の素材のインポートです。

シールドのアイテム取得時の効果音も変更するので、シールドアイテム取得時とシールドへのダメージの二つになります。

素材をインポート

ShieldActiveをシールドアイテム取得時の効果音に設定します。

同時にアイテムの種類も設定します。

アイテムの種類と効果音を設定

後はアイテムリストへ追加します。

アイテムリストへシールドを追加

プレイヤーの追加された項目も設定します。

追加された項目を設定

ではゲームを実行して確認します。

今回確認することは
  • アイテムを取得したらシールドがアクティブになる
  • ダメージが耐久値(初期値は5に設定)を超えたら非アクティブになる
  • 耐久値を超過したダメージがプレイヤーに入る
となります。

アイテムを取得したらシールドがアクティブになる:OK

ダメージが耐久値(初期値は5に設定)を超えたら非アクティブになる:OK
耐久値を超過したダメージがプレイヤーに入る:OK

これでシールドの実装が出来ました。

まとめ

今回は
  • プレイヤーにシールドを実装した
という事をやりました。

次回は、ゲーム全体を調整してから、ここまでの状態を各プラットフォームへビルドしたいと思います。

以前ビルドした時に比べて大分様変わりしたので、そろそろ各プラットフォームで試してみましょう。

では、また次回!

コメント