今回はアクションゲームやRPGゲーム等でキャラクターがダメージを受けた時に、キャラクターのHPを表しているバーで受けたダメージ量をわかりやすく表示する機能を作ってみたいと思います。
受けたダメージ分のHPを一気に減らしたり、徐々に減らすというだけでもいい感じなんですが、受けたダメージ量がどの程度なのか?がわかるというのも「この敵の攻撃はこんなにダメージがでかいのか!」と見た目でわかるというのもいい感じです。
HPを徐々に減らしていく方法は既に以下の記事で作っていて、今回の記事でもスクリプト等は大体同じですが、以下の記事だとHPを数値で表していて数値の変化が段々緩やかになるようにしていました。
今回はHPをバーで表示し、段々緩やかに減っていく方法だと変な感じになるので一定量ずつ減っていくように変化させます。
今回の機能を作成すると以下のような感じになります。
キャラクターの配置と設定
まずは攻撃対象であるキャラクターを配置します。
今回はスタンダードアセットのEthanを配置することにします(キャラクターは何でも構いません)。
配置したらインスペクタのAdd ComponentからPhysics→CharacterControllerを選択肢取り付けてコライダのサイズを調整します。
Ethanをカメラに近づけるように移動させます。
また攻撃を受けた時の判定に利用するため、Layerに新しくPlayerレイヤーを作成し、設定します(Ethan本体のみで子はDefaultのまま)。
ここまでで以下のようなインスペクタになりました。
キャラクターのHP表示バーの作成
次にこのキャラクターのHP表示バーを作成します。
徐々に減らすHPバーの作成
Ethanを選択した状態で右クリックからUI→Sliderを選択し、名前をHPSliderとします。
ヒエラルキーのHPSliderの子のHandle Slide Areaは使わないので選択してDeleteキーを押して削除します。
次にヒエラルキーのCanvasを選択し、CanvasコンポーネントのRender ModeをWorld Spaceに変更します。
これはこのCanvasを他のゲームオブジェクトと同じように3D上を移動出来るようにする為です。
変更したらRect Transformの数値を変更し、キャラクターの頭上に表示されるようにします。
次にヒエラルキーのHPSliderを選択し、インスペクタのRectTransformのAnchorと位置を調整します。
Anchor PresetsでShiftキーとAltキーを押しながらstretch stretchを選択します。
先ほどヒエラルキーのHandle Slide Areaを削除したのでSliderコンポーネントのTarget GraphicがMissingになっているので選択してDeleteキーを押しNoneにしておきます。
HPSliderと同様に子のBackgroundとFill AreaのAnchorもstretch stretchに変更しておきます。
これはCanvasのサイズに合わせて背景とバーのサイズを合わせる為に行います。
次にFill Areaの子のFillを選択し、インスペクタのWidthを0にします。
また、Fillはダメージを受けた時に徐々にHPを減らすバーとして使うのでFillのインスペクタのImageコンポーネントのColorで色を変更します。
ここまで作ったらヒエラルキーのHPSliderを選択し、インスペクタのSliderのValueの値を変更し以下のようになるかを確認します。
これで徐々に減らすHPバーの作成が出来ました。
一括で減らすHPバーの作成
次にダメージを受けた時に一括でダメージ分のHPを減らすバーを作成します。
ヒエラルキーのHPSliderを選択し、Ctrl+Dキーを押して複製し、名前をBulkHPSliderとします。
BulkHPSliderの子のBackgroundを選択し、Deleteキーを押して削除します。
ヒエラルキーのBulkHPSliderの子のFillを選択し、ImageコンポーネントのColorを青色に変更しておきます。
これで一括で減らすHPバーの作成が出来ました。
キャラクターとHPバーの階層は以下のようになります。
スクリプトの作成
攻撃対象のキャラクターとそのキャラクターが持つHPのバーの作成が出来たので、次はHPのバーをカメラの方に向けるスクリプト、ダメージを受けた時にHPバーの変更を行うスクリプトやマウスクリックした時にキャラクターをクリックしたかどうかを判定するスクリプトを作成していきます。
HPバーをカメラの方向に向けるスクリプト
HPバーを常にカメラの方向に向けるRotateDamageUIスクリプトを作成し、Canvasゲームオブジェクトに取り付けます。
1 2 3 4 5 6 7 8 9 10 11 | using UnityEngine; using System.Collections; public class RotateDamageUI : MonoBehaviour { void Update() { transform.rotation = Camera.main.transform.rotation; } } |
キャラクターが回転した時にHPバーが一瞬乱れる場合はUpdateメソッドをLateUpdateメソッドに変更してください。
HPバーを変更するスクリプト
敵のHPを管理するEnemyHPManagerスクリプトを作成し、キャラクター(Ethan)のゲームオブジェクトに取り付けます。
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | using UnityEngine; using System.Collections; using UnityEngine.UI; public class EnemyHPManager : MonoBehaviour { // 最大HP [SerializeField] private int maxHP = 10000; // 徐々に減らしていくhp計測に使う [SerializeField] private int hp; // 最終的なhp計測に使う private int finalHP; // HPを一度減らしてからの経過時間 private float countTime = 0f; // 次にHPを減らすまでの時間 [SerializeField] private float nextCountTime = 0f; // HP表示用スライダー private Slider hpSlider; // 一括HP表示用スライダー private Slider bulkHPSlider; // 現在のダメージ量 private int damage = 0; // 一回に減らすダメージ量 [SerializeField] private int amountOfDamageAtOneTime = 100; // HPを減らしているかどうか private bool isReducing; // HP用表示スライダーを減らすまでの待機時間 [SerializeField] private float delayTime = 1f; void Start() { hpSlider = transform.Find("Canvas/HPSlider").GetComponent<Slider>(); bulkHPSlider = transform.Find("Canvas/BulkHPSlider").GetComponent<Slider>(); hp = maxHP; finalHP = maxHP; hpSlider.value = 1; bulkHPSlider.value = 1; } void Update() { // ダメージなければ何もしない if (!isReducing) { return; } // 次に減らす時間がきたら if (countTime >= nextCountTime) { int tempDamage; // 決められた量よりも残りダメージ量が小さければ小さい方を1回のダメージに設定 tempDamage = Mathf.Min(amountOfDamageAtOneTime, damage); hp -= tempDamage; // 全体の比率を求める hpSlider.value = (float)hp / maxHP; // 全ダメージ量から1回で減らしたダメージ量を減らす damage -= tempDamage; // 全ダメージ量が0より下になったら0を設定 damage = Mathf.Max(damage, 0); countTime = 0f; // ダメージがなくなったらHPバーの変更処理をしないようにする if(damage <= 0) { isReducing = false; } // HPが0以下になったら敵を削除 if (hp <= 0) { Destroy(gameObject); } } countTime += Time.deltaTime; } // ダメージ値を追加するメソッド public void TakeDamage(int damage) { // ダメージを受けた時に一括HP用のバーの値を変更する var tempHP = Mathf.Max(finalHP -= damage, 0); bulkHPSlider.value = (float)tempHP / maxHP; this.damage += damage; countTime = 0f; // 一定時間後にHPバーを減らすフラグを設定 Invoke("StartReduceHP", delayTime); } // 徐々にHPバーを減らすのをスタート public void StartReduceHP() { isReducing = true; } } |
maxHPはキャラクターの最大HP
hpは現時点でのhp
finalHPはダメージを受けた後の最終的なhp
countTimeは前回HPバーを減らしてからの経過時間
nextCountTimeは次にHPバーを減らすまでの間隔時間
hpSliderはHPSliderゲームオブジェクトのSliderコンポーネントを入れます。
damageは現在のダメージ量
amountOfDamageAtOneTimeは1回に減らすダメージ量
isReducingは現在HPをへらしているかどうか
delayTimeはダメージを受けてから実際にHPバーを減らしていくまでの待機時間
となっています。
Startメソッドではキャラクターゲームオブジェクトの階層を辿ってHPSliderとBulkHPSliderのSliderコンポーネントを取得しフィールドに保持しています。
その他はHPとHPバー用のSliderの初期設定です。
UpdateメソッドではisReducingがfalseの時はreturnを使ってそれ以降の処理をしません。
countTimeがnextCountTime以上になったらMathf.Minを使ってamountOfDamageAtOneTimeとdamageの小さい方をtempDamageに入れています。
これは1回で減らすダメージ量を計算したいんですが、残りのダメージ(damage)の方が小さいと実際に受けたダメージよりも多いamountOfDamageAtOneTimeの値を1回のダメージとして与えてしまう為です。
なので小さい方の値を1回のダメージとして計算する為にMathf.Minで小さい方の値を取得します。
1回のダメージ量を計算したらhpからそのダメージ量を減らし、maxHPで割って現在のHPの最大HPに対する割合を計算します。
Sliderの値の範囲はデフォルト値では0~1の間になっているので(変更していない為)最大HPに対する現在のhpの割合を計算し、0~1の値にしています。
(float)でhpの値をfloat型にキャストしていますが、これはhpとmaxHPはint型で割った時に小数点の値を取得出来ない為で、一旦hpをfloat型にすることで計算結果にfloat値を得られるようにしています。
その後、1回に与えたダメージ量分のダメージをdamageから減らします。
次にMathf.Max(大きい方の値を取得)を使ってdamageと0の大きい方をdamageに入れなおしています。
これはダメージ量がマイナスになるのを防ぐ為で、damageは最低でも0になるようにしています。
ダメージ量が0以下の場合は与えるダメージがもうないのでisReducingをfalseにします。
countTimeにTime.deltaTimeを足して経過時間を計算します。
TakeDamageメソッドはキャラクターが攻撃を受けた時に呼び出すメソッドです。
finalHPからdamageを減らしてfinalHPに入れます。
またそのfinalHPと0の大きい方をtempHPに入れます。
tempHPをmaxHPで割ったものをbulkHPSliderの値に入れます。
こちらは一括でダメージ分を減らす為にTakeDamageメソッド内で値を変更しています。
フィールドのdamageに今受けたダメージ分を足して合計ダメージ量を増やします。
Invokeメソッドを使ってdelayTime時間後にStartReduceHPメソッドを実行するようにします。
こうすることでダメージを受けてからdelayTime経った後にHPバーを徐々に減らす処理を実行することになります。
StartReduceHPメソッドではisReducingをtrueにしているだけです。
キャラクターを攻撃するスクリプトの作成
最後にキャラクターをマウスで押した時にダメージを与えるClickAttackScriptスクリプトを作成し、MainCamera(他のゲームオブジェクトでもOK)に取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class ClickAttackScript : MonoBehaviour { private Camera mainCamera; [SerializeField] private int attackPower = 500; // Start is called before the first frame update void Start() { mainCamera = Camera.main; } // Update is called once per frame void Update() { if(Input.GetMouseButtonDown(0)) { Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); if(Physics.Raycast(ray, out RaycastHit hit, 1000f, LayerMask.GetMask("Player"))) { hit.transform.GetComponent<EnemyHPManager>().TakeDamage(attackPower); } } } } |
mainCameraにはメインカメラを入れます。
attackPowerは攻撃力を設定します。
Startメソッドでメインカメラを取得しmainCameraに保持して置きます。
Updateメソッドではマウスの左ボタンを押した時にメインカメラからマウスを押した場所にレイを飛ばしその情報をrayに保持しています。
そのrayの情報を使ってメインカメラからマウスを押した場所の方向に1000m先までレイを飛ばし、Playerレイヤーを持つゲームオブジェクトと接触したらその情報をhitに入れます。
その相手からEnemyHPManagerコンポーネントを取得し、TakeDamageメソッドにattackPowerを渡して実行します。
これで機能が完成しました。
終わりに
どれだけのダメージを受けたのか視覚的に確認出来ると便利ですね。
今PS4のニーアレプリカントをプレイしているんですが、ちょうど今回のような機能があったので作ってみました。