シンプルなアクションゲームを作ってみようの第17回です。
今回はプレイヤーキャラクターのHPをScriptableObjectでアセットファイルとして保持し、それをUIに表示したり、敵に接近された時にHPを減らす機能を作成していきます。
前回は障害物を作成しました。
シンプルなアクションゲームを作ってみようの他の記事は
シンプルなアクションゲームを作るのを通してUnityの使い方を学ぶカテゴリです。
から参照出来ます。
ステータスを管理するスクリプトを作成する
プレイヤーキャラクターのPlayerControllerスクリプトでは、キャラクターの操作に関する処理を記述しました。
ここにキャラクターのhpに関するフィールドを用意し、敵にダメージを受けた時のメソッドを書いていくということも出来ます。
ですが、今回はHPに関する処理をステータスを管理する別のスクリプトに処理をまかせるようにします。
なのでPlayerControllerでは敵にダメージを受けたというメソッドを用意し、実際にHPを減らすのはそのスクリプトで行うという感じです。
このステータスを管理するスクリプトはScriptableObjectを継承したクラスとし、そのインスタンスをアセットにして使います。
ScriptableObjectはゲームオブジェクトに取り付ける必要がないスクリプトで、大量のデータを使う時等に便利です。
また、ひとつのデータを複数のゲームオブジェクトで共有したい時にも便利です。
ScriptableObjectのアセットを作るには、スクリプトでScriptableObjectを継承してクラスを作成し、アトリビュートでメニューからアセットを作れるように設定するという流れです。
ScriptableObjectでステータスアセットを作成する
文章だけを見ても分かり辛いので実際に作ってみましょう。
まずはAssets/Scriptsフォルダに新しくPlayerStatusスクリプトを作成し以下のように記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | using System.Collections; using System.Collections.Generic; using UnityEngine; [CreateAssetMenu(fileName = "PlayerStatus", menuName = "CreatePlayerStatus")] public class PlayerStatus : ScriptableObject { [SerializeField] private int hp; public int SetHp(int hp) { if(hp <= 0) { hp = 0; } this.hp = hp; return this.hp; } public int GetHp() { return this.hp; } public void Reset() { this.hp = 10; } } |
PlayerStatusはScriptableObjectを継承するようにします。
また、アトリビュートでCreateAssetMenuを取り付け、UnityエディターのメニューのAssetsにCreatePlayerStatusという項目を追加します。
また作成するアセットの名前はPlayerStatusという名前にします。
hpはフィールドでプレイヤーキャラクターのHPを入れます。
SetHpメソッドは引数で受け取ったhpをプレイヤーキャラクターのHPを設定します。
ただし、hpが0以下だった場合は0にします。
またHPを設定するだけでなく、その値を呼び出し元にも返しています。
GetHpメソッドはプレイヤーキャラクターのHPを返すメソッドです。
ResetメソッドはHPを10にセットするメソッドです。
これはプレイヤーキャラクターのHPの最大値を10に想定しているので10というマジックナンバーをそのまま入れています。
マジックナンバーというのはいきなり現れた数値等の事をいい(いきなり現れるので何を表しているか分かり辛い)、普通はフィールド等で用意しておいて10の部分をフィールド名にする方が良いです。
これでスクリプトが出来ました。
PlayerStatusアセットの作成
先ほど作成したスクリプトでCreateAssetMenuアトリビュートを取り付けたので、UnityエディターのメニューのAssetsに新しい項目が表示されています。
UnityメニューのAssets→Create→CreatePlayerStatusを選択します。
すると開いているProjectフォルダに新しくPlayerStatusフォルダが作成されます。
PlayerStatusのHPのデフォルト値はResetメソッドで設定した値になるようです。
新しく出来たPlayerStatusファイルはAssets/Dataフォルダを作成し、その中に入れていくようにします。
これでHPの管理をするアセットが出来ました。
PlayerStatusを選択してインスペクタを確認すると以下のようにHPを確認出来ます。
HP表示用のUIを作成する
次にプレイヤーキャラクターのHPを表示するUIを作成していきます。
UIゲームオブジェクトを選択した状態で、右クリックからUI→Canvasを選択し、名前をLifeとします。
次にLifeゲームオブジェクトを選択した状態で右クリックからUI→Panelを選択し、名前をLifePanelとします。
次にヒエラルキーのLifePanelを選択し、シーンビューでLifePanelの四隅に矢印があるのを確認したら、右上か左上の矢印をShiftキーを押しながらドラッグし、ライフを表示する場所をゲーム画面の下にします。
ライフを表示する領域はゲーム画面の横の比率は100%で縦の比率が10%ぐらいにしました。
ライフを表示する領域の背景イメージは使わないので、ヒエラルキーでLifePanelを選択し、Imageコンポーネントのチェックを外します。
ライフの表示領域が出来たので次は表示するライフゲージを作成します。
LifePanelを選択した状態で右クリックからUI→Imageを選択し、名前をLifeとします。
ヒエラルキーでLifeゲームオブジェクトを選択し、インスペクタの設定をします。
ImageコンポーネントのSource Imageの丸のアイコンを押し、検索窓にbackgroundと入力して出てくる画像を設定します。
これでライフゲージが一つ出来たのでヒエラルキーのLifeを選択した状態でCtrl+Dキーを9回押してライフゲージを10個にします。
これで10個のライフゲージが出来ました。
ライフゲージを整列させる
ライフゲージは10個出来ましたが、同じ位置に表示されています。
個々のLifeのRect Transformの数値を変更したり、シーンビューで少しずつズラすということも出来ますが、ここではLifePanelゲームオブジェクトにHorizontal Layout Groupというコンポーネントを取り付け、子要素のゲームオブジェクトのサイズをコントロールし、横に綺麗に並ぶようにします。
ヒエラルキーのLifePanelゲームオブジェクトを選択し、インスペクタのAdd ComponentからLayout→Horizontal Layout Groupを選択し取り付けます。
PaddingのLeft、Right、Top、Bottomに2を入れ、表示領域の各方向からの距離を設定しています。
Child Alignmentは子要素の配置方法で中央の左側に表示します。
Control Child Sizeは子要素のライフゲージの幅と高さをHorizontal Layout Groupでコントロールする場合にチェックを入れます。
ここにチェックを入れると子要素のゲームオブジェクトの幅と高さが変更されます(チェックを外しても元に戻りません)。
Child Force Expandは子要素のゲームオブジェクトを強制的に拡張するかどうかで、高さのみ拡張するようにします。
Horizontal Layout Groupの設定を行うとライフゲージが横に整列されたと思います。
これでライフゲージのUIが出来ました。
ライフゲージ更新スクリプトの作成
ライフゲージを更新するLifeGaugeUpdateScriptスクリプトを作成し、LifePanelゲームオブジェクトに取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class LifeGaugeUpdateScript : MonoBehaviour { // PlayerStatus [SerializeField] private PlayerStatus playerStatus; // Start is called before the first frame update void Start() { // ライフゲージの設定 UpdateLifeGauge(); } // ライフゲージのアップデート public void UpdateLifeGauge() { for (int i = 0; i < transform.childCount; i++) { if (i < playerStatus.GetHp()) { transform.GetChild(i).GetComponent<Image>().color = new Color(0f, 0f, 1f); } else { transform.GetChild(i).GetComponent<Image>().color = new Color(1f, 1f, 1f); } } } } |
playerStatusは先ほど作ったPlayerStatusアセットをインスペクタで設定します。
StartメソッドではUpdateLifeGaugeメソッドを呼んでライフゲージを更新しています。
UpdateLifeGaugeメソッドではfor文を使って繰り返し処理をしていますが、条件でtransform.childCountを使って子要素のライフゲージ分の繰り返しを行います。
iの値とplayerStatusのHPを比較し、iがHPより低い場合はtransform.GetChild(i)でLifePanelの子要素のi番目のTransformを取得し、そこからImageコンポーネントに青色を設定しています。
これはHPがある分のライフゲージを青色に変更しています。
それ以外の時、つまり既にHPが減っている部分のライフゲージは白色に設定します。
これでスクリプトが出来たのでLifePanelのインスペクタのLifeGaugeUpdateScriptのPlayer StatusにAssets/DataフォルダにあるPlayerStatusアセットをドラッグ&ドロップします。
PlayerControllerスクリプトに処理を追加する
PlayerControllerスクリプトに処理を追加します。
まずはフィールドを追加します。
1 2 3 4 5 6 7 8 | // プレイヤーのステータスデータ [SerializeField] private PlayerStatus playerStatus; // ライフゲージ更新スクリプト [SerializeField] private LifeGaugeUpdateScript lifeGaugeUpdateScript; |
playerStatusはインスペクタでPlayerStatusアセットを設定します。
lifeGaugeUpdateScriptはインスペクタにLifePanelをドラッグ&ドロップしてLifeGaugeUpdateScriptを設定します。
次に敵にダメージを受けた時に呼び出すメソッドを追加します。
1 2 3 4 5 6 7 8 9 10 11 | // ダメージ処理 public void TakeDamage(int damage) { // HPを減らす if (playerStatus.SetHp(playerStatus.GetHp() - damage) <= 0) { gameManager.EndGame(); } // ライフゲージを減らす lifeGaugeUpdateScript.UpdateLifeGauge(); } |
引数で受けるダメージ数を受け取ります。
playerStatusのGetHpメソッドで現在のプレイヤーのHPを取得し、ダメージ数分を引いた値をplayerStatusのSetHpメソッドで設定しています。
返ってきた値(HP)が0以下だった時はgameManagerのEndGameメソッドを呼んでゲームを終了します。
その後lifeGaugeUpdateScriptのUpdateLifeGaugeメソッドを呼んでライフゲージの更新をしています。
スクリプトへの追加が出来たのでPlayerゲームオブジェクトのPlayerControllerの設定をします。
Player StatusにはPlayerStatusアセットをドラッグ&ドロップして、Life Gauge Update ScriptにはLifePanelをドラッグ&ドロップしてLifeGaugeUpdateScriptを設定します。
敵が接近したらダメージを与える
最後に敵が接近したらプレイヤーキャラクターにダメージを与える処理をEnemyControllerスクリプトに追加します。
まずはフィールドを追加します。
1 2 3 4 5 | [SerializeField] private int attackPower = 2; private PlayerController playerController; |
attackPowerは敵の攻撃力で、playerControllerはプレイヤーに取り付けてあるPlayerControllerを入れます。
次にStartメソッドに処理を追加します。
1 2 3 4 | player = GameObject.Find("Player").transform; playerController = player.GetComponent<PlayerController>(); |
playerに代入している処理の後にplayerからPlayerControllerを取得する処理を追加します。
次にUpdateメソッドに処理を追加しますが、一部だけだと分かり辛いのでUpdateメソッド全部を載せました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | void Update() { if(!canControl) { return; } // ゲームオーバー時は停止してこれ以降何もしない if (gameManager.GameOver) { navMeshAgent.isStopped = true; animator.SetFloat("Speed", 0f); canControl = false; return; } // 目的地の再設定 navMeshAgent.SetDestination(player.position); // プレイヤーキャラクターと敵キャラクターの距離 var characterDistance = Vector2.Distance(new Vector2(player.position.x, player.position.z), new Vector2(transform.position.x, transform.position.z)); // 止まっている時 if (navMeshAgent.isStopped) { // 距離が開いたら再度追いかける(Y軸は無視) if (characterDistance > 2f) { navMeshAgent.isStopped = false; } } else { // 目的地との距離が開いている場合 if (characterDistance > 0.8f) { navMeshAgent.isStopped = false; animator.SetFloat("Speed", navMeshAgent.speed); } else { navMeshAgent.isStopped = true; animator.SetFloat("Speed", 0f); // プレイヤーに近づいたのでダメージを与える playerController.TakeDamage(attackPower); } } } |
敵がプレイヤーにダメージを与えるのはプレイヤーに接近した時なのでそこでPlayerControllerのTakeDamageメソッドを呼び出してダメージを与えています。
実行して確認してみる
機能が出来たので実行して確認してみましょう。
上のようになりました。
現時点ではHPが減ると回復させていないので、HPが減ったらPlayerStatsuアセットのHPに10を入力して回復してください。
またHPが0になったらゲームオーバーになっていますが、ゲームオーバーであることを示すUIの作成もしていないので分かり辛くなっております。
終わりに
今回はプレイヤーキャラクターのHPの作成とそれをUIで表示、また敵キャラクターが接近した時のダメージ処理などを作りました。
次回は敵キャラクターとは別に大砲を作ってプレイヤーを狙う機能を作成していきます。