この記事で作るもの
2つのボタンで「敵A」「敵B」に10ずつダメージを与える、Unity 6・uGUI・TextMeshProの小さな実験を作ります。画面には各敵のHPを表示します。敵の3Dモデルや戦闘処理は使いません。
共通の初期HPは1つのCommonEnemySettings.assetへ置き、残りHPは各GameObjectのスクリプトへ置きます。Aを攻撃しても、BのHPや共通アセットの値が変わらないことがゴールです。
ScriptableObjectは、GameObjectから独立したデータを扱える型です。今回作る設定データはProject内のアセットになり、GameObjectへコンポーネントとして付けるものではありません。ScriptableObjectの公式資料
1. 初期HPの設定アセットを作る
Project内へEnemySettings.csを作り、次のコードに置き換えます。ファイル名とクラス名を一致させます。
using UnityEngine;
[CreateAssetMenu(fileName = "EnemySettings", menuName = "Learning/Enemy Settings")]
public sealed class EnemySettings : ScriptableObject
{
[SerializeField, Min(1)] private int maxHealth = 100;
public int MaxHealth => Mathf.Max(1, maxHealth);
}
- コンパイルが終わり、Consoleにエラーがないことを確認します。
- Project内を右クリックして
Create > Learning > Enemy Settingsを選びます。 - 作成されたアセットを
CommonEnemySettingsと名付けます。 - アセットのInspectorで
Max Healthを100にします。
CreateAssetMenuは、設定アセットを作る項目をCreateメニューへ追加します。.csはデータの型の定義、作成した.assetは実際の設定データです。後で参照欄へ入れるのはCommonEnemySettingsのアセットです。CreateAssetMenuの公式資料
2. 2体とUIを準備する
- Hierarchyに空のGameObjectを2つ作り、
EnemyAとEnemyBにします。 UI > Button - TextMeshProでボタンを2つ作ります。TMP Essentialsの読み込みを求められたら読み込みます。- Canvasを
Screen Space - Overlayにし、CanvasのGraphic RaycasterとEventSystemを残します。使用している入力設定に対応したUI入力モジュールを使います。 - Canvasの子へ
UI > Text - TextMeshProを2つ作り、HealthLabelAとHealthLabelBにします。ボタン内の文字とは別のオブジェクトです。 - 4つのUIを重ならない位置へ置きます。例として、すべて中央アンカー、サイズ
(320, 48)で次のPos Yを使えます。
| UI | Pos X / Pos Y | 表示文字 |
|---|---|---|
| HealthLabelA | 0 / 120 | HP: 100 / 100 |
| DamageButtonA | 0 / 60 | Aに10ダメージ |
| HealthLabelB | 0 / -40 | HP: 100 / 100 |
| DamageButtonB | 0 / -100 | Bに10ダメージ |
ボタンを作ったときの名前をDamageButtonA、DamageButtonBに変えます。それぞれの子のTMPテキストへ表示文字を設定します。両ButtonのOn Click()は空にします。次のコードがクリックを登録します。
EnemyA ← EnemyHealthDemo
EnemyB ← EnemyHealthDemo
Canvas
HealthLabelA ← TextMeshProUGUI
DamageButtonA ← Button
Text (TMP)
HealthLabelB ← TextMeshProUGUI
DamageButtonB ← Button
Text (TMP)
EventSystem
Project内: CommonEnemySettings.asset
3. 残りHPを各スクリプトに持たせる
EnemyHealthDemo.csを作り、次のコードを保存します。EnemyAとEnemyBの両方へ、このスクリプトを付けます。
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public sealed class EnemyHealthDemo : MonoBehaviour
{
[SerializeField] private EnemySettings settings;
[SerializeField] private TMP_Text healthLabel;
[SerializeField] private Button damageButton;
private int maxHealth;
private int currentHealth;
private void OnEnable()
{
if (settings == null || healthLabel == null || damageButton == null)
{
Debug.LogError("設定アセット、HP表示、Buttonを登録してください。", this);
enabled = false;
return;
}
maxHealth = settings.MaxHealth;
currentHealth = maxHealth;
damageButton.onClick.AddListener(TakeTenDamage);
RefreshLabel();
}
private void TakeTenDamage()
{
currentHealth = Mathf.Max(0, currentHealth - 10);
RefreshLabel();
}
private void RefreshLabel()
{
healthLabel.text = $"HP: {currentHealth} / {maxHealth}";
}
private void OnDisable()
{
if (damageButton != null)
{
damageButton.onClick.RemoveListener(TakeTenDamage);
}
}
}
Inspectorの登録は次のようにします。同じアセットを参照し、UIの参照は各敵に分けます。
| コンポーネント | Settings | Health Label | Damage Button |
|---|---|---|---|
| EnemyA | CommonEnemySettings | HealthLabelA | DamageButtonA |
| EnemyB | CommonEnemySettings | HealthLabelB | DamageButtonB |
有効化されたときにsettings.MaxHealthを読み取り、各コンポーネントのmaxHealthとcurrentHealthへコピーします。ダメージで減らすのは各自のcurrentHealthだけです。この例では、コンポーネントを無効から有効へ戻した場合もHPを初期化します。OnEnableの公式資料
動作確認
| 操作 | 期待する結果 |
|---|---|
| Playする | AもBもHP: 100 / 100 |
| AのButtonを1回押す | Aは90 / 100、Bは100 / 100 |
| BのButtonを2回押す | Aは90 / 100のまま、Bは80 / 100 |
| Aを10回以上攻撃する | AのHPは0で止まり、負の数にならない |
| 実行中にCommonEnemySettingsを見る | Max Healthは100のまま |
| Playを終了し、アセットを150へ変えて再びPlayする | AもBも150 / 150から始まる |
Play中にアセットを編集する手順は、この確認には含めません。Editor上ではアセットの編集が残る場合があるため、共通設定を試しに変更するときはPlayを終了してから行います。実行中にアセットを変えても、このコードがコピー済みのHPを自動で更新することはありません。
うまく動かないとき
- Createメニューに項目がない:Consoleのコンパイルエラー、ファイル名、
CreateAssetMenuの記述を確認します。 - Settingsへスクリプトを入れられない:
.csではなく、作成したCommonEnemySettingsアセットを登録します。 - AのボタンでBの表示が変わる:2体のHealth LabelとDamage Buttonの参照が入れ替わっていないか確認します。
- クリック1回で20減る:On Click()へ追加登録していないか、同じButtonを両コンポーネントへ渡していないか確認します。
- 参照を直しても反応しない:参照不足でコンポーネントが無効になった場合は、有効にし直します。
まとめ
共通の初期値はScriptableObject、変化する残りHPは各EnemyHealthDemoへ持たせます。1つの設定を共有しながら、AとBを独立して変化させられます。この例はセーブ機能を実装していません。終了後のHPを復元したい場合は、セーブデータ設計の基本へ進めます。
この記事のまとめ
Unity 6で共通の初期HPをScriptableObjectに保存し、2体の残りHPを別々に管理する例です。アセットとコンポーネントの違い、参照の登録、設定値のコピーを図と完成コードで確認します。
