今回はUnity公式サイトに掲載されているチュートリアルの2Dシューティングを改造して自機にHPを設定してみました。
「公式チュートリアル 2Dシューティングに挑戦」の制作記事一覧
HPのUIオブジェクト
まずはゲーム画面に自機のヒットポイントバーを作成します。
[Hierarchy]ウィンドウ > [Create]ボタン > UI > Image を選択。名前をImage HealthBar に変更。このように Rect Transform のパラメータを設定しました。
作成したオブジェクトを複製して名前を Image HealthBar BG に変更。Image HealthBar BG を親にして親子階層を設定します。
Image HealthBar BG を選択して [Inspector]ウィンドウ > Image (Script) > Color を変更しておきます。今回はグレーにしました。自機が攻撃を受けると Image HealthBar のサイズこのように表示されます。
スクリプト HealthBar の作成
UIオブジェクト Image HealthBar の幅サイズを自機のヒットポイントに合わせて変化させる処理を行います。
空オブジェクトを作成してスクリプトを割り当てます。
[Hierarchy]ウィンドウ > [Create]ボタン > Create Empty を選択。して名前を HealthBar に変更。[Project]ウィンドウ > Scripts フォルダに移動して [Create] > C# Script を選択。名前を HealthBar に変更。作成したオブジェクト HealthBar に割り当てておきます。
自機のインスタンスはゲームスタート時のタイトル画面では存在しないので、Start() 関数の中で GameObject.Find() 関数を使って自機のインスタンスを参照しようとするとエラーが発生します。
そこで、スクリプト Manager の ゲーム実行中を判定するフラグ IsPlaying() 関数を参照することにしました。自機がシーンに無くゲームプレイ中の場合に GameObject.Find() 関数を使用してインスタンス Player(Clone) を参照しています。
using System.Collections; using System.Collections.Generic; using UnityEngine; // UI処理のクラスを使用する宣言 using UnityEngine.UI; public class HealthBar : MonoBehaviour { // ヒットポイントバーの RectTransform コンポーネントを格納する変数 public RectTransform healthBar; // 自機オブジェクトのインスタンスを格納する変数 private GameObject playerObj; // 自機オブジェクトのスクリプト Player コンポーネントを格納する変数 private Player player; // ヒットポイントバーのサイズを格納する変数 Vector2 healthBarSize; // 自機ヒットポイントを格納する変数 private float healthPoint; // スクリプト Manager コンポーネントを格納する変数 Manager manager; // ゲームスタート時の処理 void Start() { // ヒットポイントバーのサイズを取得して healthBarSize に格納 healthBarSize = healthBar.sizeDelta; // Manager コンポーネントをシーン内から探して取得して変数 manager に格納 manager = FindObjectOfType<Manager>(); } // ゲーム実行中フレームごとに更新する処理 void Update() { // 自機がシーンに無くゲームプレイ中の場合 if (playerObj == null && manager.IsPlaying()) { // 自機オブジェクトを取得して変数 playerObj に格納 playerObj = GameObject.Find("Player(Clone)"); // スクリプト Player コンポーネントを取得して変数 player に格納 player = playerObj.GetComponent<Player>(); } // 自機がシーンに存在してゲームプレイ中の場合 if (playerObj != null && manager.IsPlaying()) { // スクリプト Player コンポーネントの変数 Health の値を // 自機のヒットポイントをヒットポイントバーの幅サイズに格納 healthBarSize.x = player.health; // ヒットポイントバーのサイズを更新 healthBar.sizeDelta = healthBarSize; } // ゲームプレイ中でない場合 if (!manager.IsPlaying()) {// ヒットポイントバーの幅サイズに 0 を格納 healthBarSize.x = 0; // ヒットポイントバーのサイズを更新 healthBar.sizeDelta = healthBarSize; } } }
スクリプトが完成したらオブジェクト HealthBarを選択して [Inspector]ウィンドウ > HealthBar (Script) コンポーネントの変数フィールドにImage HealthBar を割り当てます。
スクリプト Player の変更とヒットポイントの設定
自機のヒットポイント変数を作成。敵弾が当たるとすぐ自機を破壊する処理を変更して敵弾 Power を自機のダメージとして減算する処理を加えました。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : MonoBehaviour { // スクリプト Spaceship コンポーネントを格納する変数 Spaceship spaceship; // AudioSource コンポーネントを格納する変数 AudioSource seShot; // 自機のヒットポイント public float health; // ゲームのスタート時の処理 void Start() { // スクリプト Spaceship コンポーネントを取得 spaceship = GetComponent(); // AudioSource コンポーネントを取得 seShot = GetComponent (); // 弾の発射処理(コルーチン Shot )を実行 StartCoroutine("Shot"); } // ゲーム実行中の繰り返し処理 void Update() { // 右・左のデジタル入力値を x に渡す float x = Input.GetAxisRaw("Horizontal"); // 上・下のデジタル入力値 y に渡す float y = Input.GetAxisRaw("Vertical"); // 移動する向きを求める // x と y の入力値を正規化して direction に渡す Vector2 direction = new Vector2(x, y).normalized; // Spaceship コンポーネントの Move() 処理を実行 spaceship.Move(direction); // 自機の移動制限処理を実行 Clamp(); } // 自機の移動制限処理 void Clamp() { // 自機の移動座標最小値をビューポートから取得(最小値は0,0) Vector2 min = Camera.main.ViewportToWorldPoint(new Vector2(0.05f, 0.1f)); // 自機の移動座標最大値ををビューポートから取得(最大値は1,1) Vector2 max = Camera.main.ViewportToWorldPoint(new Vector2(0.95f, 0.85f)); // 自機の座標を取得してベクトル pos に格納 Vector2 pos = transform.position; // pos.x の値を最小値 min 最大値 max の範囲に制限する pos.x = Mathf.Clamp(pos.x, min.x, max.x); // pos.y の値を最小値 min 最大値 max の範囲に制限する pos.y = Mathf.Clamp(pos.y, min.y, max.y); // 自機の移動範囲を pos の最小値と最大値の範囲に制限する transform.position = pos; } // トリガーに入った時の処理 // 衝突した相手の Collider2D コンポーネントを引数 c に格納 void OnTriggerEnter2D(Collider2D c) { // レイヤー名を取得して layerName に格納 string layerName = LayerMask.LayerToName(c.gameObject.layer); // レイヤー名が Bullet (Enemy) の場合さらに自機ヒットポイントが 0 より大きい場合 if (layerName == "Bullet (Enemy)" && health > 0) { // 弾の削除 Destroy(c.gameObject); // 自機のダメージ処理を実行 health = health - c.gameObject.GetComponent<Bullet>().power; } // レイヤー名が Enemy の場合または自機のヒットポイントが 0 以下の場合 if (layerName == "Enemy" || health <= 0) { // 自機の破壊処理を実行 DestroyPlayer(); } } // 自機の破壊処理 void DestroyPlayer() { // Manager コンポーネントをシーン内から探して取得し、GameOver メソッドを呼び出す FindObjectOfType<Manager>().GameOver(); // 爆発処理 // スクリプト Spaceship の関数 Explosion() を実行 spaceship.Explosion(); // プレイヤーを削除 Destroy(gameObject); } // 弾の発射処理(コルーチン) IEnumerator Shot() { while (true) { // 弾をプレイヤーと同じ位置/角度で作成 spaceship.Shot(transform); // ショット音を鳴らす seShot.Play(); // shotDelay 秒待つ yield return new WaitForSeconds(spaceship.shotDelay); } } }
スクリプトの変更ができたら プレハブ Player を選択。[Inspector]ウィンドウ > Player (Script) コンポーネント > Health フィールドの値を 400 (Image HealthBar の幅サイズと同じ値)に設定します。
EnemyBullet の設定
プレハブ EnemyBullet の Power 値が初期値の 1 のままなので、50 に変更しました。これで自機が8回被弾するとゲームオーバーになります。
スクリプト Manager の変更
スクリプト Manager の変更
こちらの記事 プレイヤーの移動制限とタイトル作成 ではゲームオーバーテキストオブジェクトを数秒表示してからタイトルテキストオブジェクトを表示する処理をしていました。
しかしこの処理では、ゲームオーバーテキストが表示されている間は、ゲームプレイ中を判定する処理 IsPlaying() 関数の戻り値が true のままで、自機が無いにもかかわらずヒットポイントバーの更新処理も継続されるのでエラーが発生します。
自機が破壊されたらゲームプレイ中を判定する処理 IsPlaying() 関数の戻り値が false になるようにスクリプト Manager を修正します。
using System.Collections; using System.Collections.Generic; using UnityEngine; // UI処理のクラスを使用する宣言 using UnityEngine.UI; public class Manager : MonoBehaviour { // 自機を格納する変数 public GameObject player; // タイトルテキストオブジェクトを格納する変数 public GameObject textTitle; // タイトルテキストオブジェクトの Text コンポーネントを格納する変数 public Text textTitleText; // ゲームオーバーテキストオブジェクトの Text コンポーネントを格納する変数 public Text textGameOver; // ゲームのスタート時の処理 void Start() { // Text コンポーネントを無効にする textGameOver.enabled = false; } // ゲーム実行中の繰り返し処理 void Update() { // ゲーム中ではなく、さらにXキーが押されたらtrueを返す。 if (IsPlaying() == false && Input.GetKeyDown(KeyCode.X)) { // ゲーム開始処理を実行 GameStart(); } } // ゲーム開始処理 void GameStart() { // タイトルオブジェクトを非表示にする textTitle.SetActive(false); // 自機を生成する(プレハブの位置と角度で) Instantiate(player, player.transform.position, player.transform.rotation); } // ゲームオーバー時の処理 public void GameOver() { // テキストオブジェクト表示のコルーチンを実行 StartCoroutine("ShowGameOver"); } // ゲームプレイ中か判定する処理 public bool IsPlaying() { // タイトルオブジェクトが非表示であれば true を返す return textTitle.activeSelf == false; } // ゲームオーバー時にテキストオブジェクトを表示する処理(コルーチン) private IEnumerator ShowGameOver() { // タイトルテキストオブジェクトを表示する textTitle.SetActive(true); // タイトルテキストコンポーネントを無効にする textTitleText.enabled = false; // ゲームオーバーテキストコンポーネントを表示する textGameOver.enabled = true; //3秒後に処理を中断 yield return new WaitForSeconds(3f); // ゲームオーバーテキストコンポーネントを無効にする textGameOver.enabled = false; // タイトルテキストコンポーネントを有効にする textTitleText.enabled = true; // ハイスコアの保存 // スクリプト Score コンポーネントを取得して Save() 関数を実行 FindObjectOfType<Score>().Save(); } }
テキストオブジェクトの階層変更とコンポーネントの設定
変更したスクリプト Manager に合わせてテキストオブジェクトの階層を変更します。
UIオブジェクト Text GameOver をText Title の子階層に移動します。
オブジェクト Manager を選択。[Inspector]ウィンドウ > Manager (Script) コンポーネントの変数フィールドにテキストコンポーネントを割り当てます。
自機の被弾アニメーション
最後に、自機が被弾した時のアニメーションを設定します。
プレハブ Player を[Hierarchy]ウィンドウにドラッグ&ドロップで配置。メニュー:Window > Animation > Animation を選択。[Animation]ウィンドウが開いたら左上にあるアニメーションファイル選択ボタンをクリックしてCreate New Clip を選択。
ファイル保存ウィンドウが開いたら、Assets > Animations > Player フォルダに移動してファイル名 Damage で保存。
残りの手順は公式チュートリアルの エネミーのHP、弾の攻撃力、アニメーションの追加 と同じです。
[Animator]ウィンドウの設定は、Invincible レイヤーに新しいステート Danger を作成してトランジョンを設定します。[Project]ウィンドウ > Assets > Animations > Player > Invincible を選択。ファイルを複製して名前をDamage に変更します。
スクリプト Player に自機の被弾アニメーション処理の追加
スクリプト Player の OnTriggerEnter2D() 関数の被弾処理部分にアニメーション処理を追加します。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : MonoBehaviour { // スクリプト Spaceship コンポーネントを格納する変数 Spaceship spaceship; // AudioSource コンポーネントを格納する変数 AudioSource seShot; // 自機のヒットポイント public float health; // ゲームのスタート時の処理 void Start() { // スクリプト Spaceship コンポーネントを取得 spaceship = GetComponent(); // AudioSource コンポーネントを取得 seShot = GetComponent (); // 弾の発射処理(コルーチン Shot )を実行 StartCoroutine("Shot"); } // ゲーム実行中の繰り返し処理 void Update() { // 右・左のデジタル入力値を x に渡す float x = Input.GetAxisRaw("Horizontal"); // 上・下のデジタル入力値 y に渡す float y = Input.GetAxisRaw("Vertical"); // 移動する向きを求める // x と y の入力値を正規化して direction に渡す Vector2 direction = new Vector2(x, y).normalized; // Spaceship コンポーネントの Move() 処理を実行 spaceship.Move(direction); // 自機の移動制限処理を実行 Clamp(); } // 自機の移動制限処理 void Clamp() { // 自機の移動座標最小値をビューポートから取得(最小値は0,0) Vector2 min = Camera.main.ViewportToWorldPoint(new Vector2(0.05f, 0.1f)); // 自機の移動座標最大値ををビューポートから取得(最大値は1,1) Vector2 max = Camera.main.ViewportToWorldPoint(new Vector2(0.95f, 0.85f)); // 自機の座標を取得してベクトル pos に格納 Vector2 pos = transform.position; // pos.x の値を最小値 min 最大値 max の範囲に制限する pos.x = Mathf.Clamp(pos.x, min.x, max.x); // pos.y の値を最小値 min 最大値 max の範囲に制限する pos.y = Mathf.Clamp(pos.y, min.y, max.y); // 自機の移動範囲を pos の最小値と最大値の範囲に制限する transform.position = pos; } // トリガーに入った時の処理 // 衝突した相手の Collider2D コンポーネントを引数 c に格納 void OnTriggerEnter2D(Collider2D c) { // レイヤー名を取得して layerName に格納 string layerName = LayerMask.LayerToName(c.gameObject.layer); // レイヤー名が Bullet (Enemy) の場合さらに自機ヒットポイントが 0 より大きい場合 if (layerName == "Bullet (Enemy)" && health > 0) { // 弾の削除 Destroy(c.gameObject); // 自機のダメージ処理を実行 health = health - c.gameObject.GetComponent ().power; // アニメーションステートの遷移 // スクリプト Spaceship の関数 GetAnimator() を実行し //Animator コンポーネント のトリガー Damage をセット spaceship.GetAnimator().SetTrigger("Damage"); } // レイヤー名が Enemy の場合または自機のヒットポイントが 0 以下の場合 if (layerName == "Enemy" || health <= 0) { // 自機の破壊処理を実行 DestroyPlayer(); } } // 自機の破壊処理 void DestroyPlayer() { // Manager コンポーネントをシーン内から探して取得し、GameOver メソッドを呼び出す FindObjectOfType ().GameOver(); // 爆発処理 // スクリプト Spaceship の関数 Explosion() を実行 spaceship.Explosion(); // プレイヤーを削除 Destroy(gameObject); } // 弾の発射処理(コルーチン) IEnumerator Shot() { while (true) { // 弾をプレイヤーと同じ位置/角度で作成 spaceship.Shot(transform); // ショット音を鳴らす seShot.Play(); // shotDelay 秒待つ yield return new WaitForSeconds(spaceship.shotDelay); } } }
WebGL
今回の改造内容をWebGLでビルドしてみました。自機が一撃で破壊されなくなったのでゲームの難易度が下がってプレイしやすくなりました。
画像クリックで再生(ファイルサイズ:約7MB)