現場レベルのゲーム制作が、すべてここで学べます。
この講座は3Dダンジョン探索型RPGの作り方について説明しています。今回はその第5回目になります。
前回は戦闘画面用のシーンを新規作成し、そこで表示するUIオブジェクトを用意していきました。
前回の記事:

ここからは主にスクリプトを拡張し、戦闘システムの実現に向けて順番に開発を進めていきます。
キャラクターデータを定義・作成
戦闘シーンで使用される味方(アクター)および敵(エネミー)のデータはそれぞれキャラクターデータ(ScriptableObjectのクラス)として作成し扱います。
アクターとエネミーで必要になるパラメータが異なることもあれば共通することもあります。共通するパラメータの例として現在・最大HPや状態異常、行動速度などがあります。
アクタークラスとエネミークラスを完全に別のクラスとして作成しても問題はありませんが、前述した共通部分をまとめた1つのベースクラスから作成し、アクタークラス・エネミークラスについてはベースクラスから派生する形にした方が管理がしやすくスマートです。
基底クラス・派生クラスとは?
C#で開発を行うにあたって、似ているけれど少しだけ機能が違うクラスを複数作る場合があります。
前述のように、アクターもエネミーも同じキャラクターというくくりで扱う事ができるのもその一例です。
このような場合にはC#の機能の1つである継承という仕組みを用いて基底クラスや派生クラスを作成するのが便利です。
継承とは、あるクラスが持つ機能(変数やメソッド)を、別のクラスがそのまま受け継いで利用できる仕組みのことです。
この時、機能の元となる設計図のようなクラスを「基底クラス」(または親クラス)、その設計図を受け継いで作られるクラスを「派生クラス」(または子クラス)と呼びます。
基底クラスと派生クラスの例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// (基底クラス)キャラクターの基本的な機能を持つ public class Character { public string characterName; public int hp; } // (派生クラス)Characterクラスを継承して、戦士クラスを作る public class Warrior : Character { // characterNameやhpは自動的に受け継がれる public int attackPower; // Warriorクラス独自の変数 } |
この例では基底クラスとしてCharacterを定義し、その下ではそれを継承する形でWarriorクラスを定義しています。
クラス名を宣言する部分の右側を :(コロン) で区切り、その右に継承元のクラス名を書きます。普段はMonoBehaviourやScriptableObjectなどを入れている場所です。
このようにする事でクラスの継承が行え、継承元が持っている変数やメソッドをそのまま引き継ぐようになります。
virtual と override
基底クラスの機能を受け継ぐだけでは、どの派生クラスも全く同じ動きしかしません。「攻撃する」という処理を、キャラクターの種類(派生クラス)ごとに違う内容にしたい場合があります。
そんな時に使うのがvirtualとoverrideです。
-
virtual:基底クラスのメソッドに付けて、「このメソッドは、派生クラスで処理内容を上書き(オーバーライド)しても良いですよ」と許可を与えるキーワードです。
-
override:派生クラスのメソッドに付けて、「基底クラスのvirtualが付いたメソッドを、このクラス独自の処理で上書きします」と宣言するキーワードです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// (基底クラス) public class Character { // このAttackメソッドは上書き可能にする public virtual void Attack() { Debug.Log("通常攻撃!"); } } // (派生クラス) public class Magician : Character { // CharacterクラスのAttackメソッドを上書きする public override void Attack() { Debug.Log("魔法で攻撃!"); } } |
このように設定すると、MagicianクラスのインスタンスでAttack()を呼び出すと「魔法で攻撃!」と表示されるようになります。
難しい言葉が並びましたが、ひとまず「クラスの機能を使い回しつつ、一部だけ独自のものに変える仕組み」という認識で問題ありません。
この仕組みを上手く使うことでコードの重複を減らし、より整理されたプログラムを作ることができるようになります。今後の講座でも登場しますので少しずつ慣れていきましょう。
(関連記事:UnityC# クラスの継承・抽象メソッドとオーバライド is, as, null, this, baseの使い方も解説)

キャラクターデータのベースクラス作成
それでは最初にベースクラスとなるCharacterDataクラスを作成しましょう。フォルダ分けをする場合はScripts/Definesフォルダ以下に作成するのが良いでしょう。
CharacterData.cs (新規)
|
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 |
using System.Collections.Generic; using UnityEngine; /// <summary> /// キャラクターデータ定義クラス(敵味方共通の基底クラス) /// </summary> [System.Serializable] public class CharacterData : ScriptableObject { // キャラクター名 [HideInInspector] public string characterName; // 現在ステータス [HideInInspector] public int currentHP; // 現在HP [HideInInspector] public int currentTP; // 現在TP [HideInInspector] public bool isKnockedOut; // 戦闘不能フラグ // 基礎能力 public int baseMaxHP; // 最大HP public int baseMaxTP; // 最大TP public int baseAtk; // 物理攻撃力 public int baseDef; // 物理防御力 public int baseMAtk; // 魔法攻撃力 public int baseMDef; // 魔法防御力 public int baseLuc; // 運 public int baseSpeed; // 速度 // 属性耐性(0.0fが基準、1.0fでダメージ無効化、-1.0fでダメージ2倍) public float resist_Fire; // 火耐性 public float resist_Ice; // 氷耐性 public float resist_Light; // 光耐性 public float resist_Dark; // 闇耐性 } |
- 7行目の[System.Serializable]は、今後このデータをゲーム内でセーブ&ロードする機能を実装する時に必要になります。
アクターもエネミーも共通で持つことになるであろうパラメータを1つずつ定義しました。
またScriptableObjectでデータを作成することを想定し、HPやTPの現在値などはInspectorで表示しないようにしています。
これだけでも戦闘システムを作成するにあたって必要最低限のものは揃っていますが、のちほど状態異常のシステムを実装したり、一時的に変化したパラメータを取得する機能(たとえば装備によって上昇する攻撃力など)を取得するための拡張の余地が欲しいです。
C#の基底クラス・派生クラスの仕組みをうまく活用し、今後の拡張を行いやすい形を整えていきましょう。
CharacterData.cs
|
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 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
using System.Collections.Generic; using UnityEngine; /// <summary> /// キャラクターデータ定義クラス(敵味方共通の基底クラス) /// </summary> [System.Serializable] public class CharacterData : ScriptableObject { // キャラクター名 [HideInInspector] public string characterName; // 現在ステータス [HideInInspector] public int currentHP; // 現在HP [HideInInspector] public int currentTP; // 現在TP [HideInInspector] public bool isKnockedOut; // 戦闘不能フラグ // 現在の状態異常と残りターン数(Dictionaryによってペアで管理) [HideInInspector] public Dictionary<CharacterState, int> currentStates; // 基礎能力 public int baseMaxHP; // 最大HP public int baseMaxTP; // 最大TP public int baseAtk; // 物理攻撃力 public int baseDef; // 物理防御力 public int baseMAtk; // 魔法攻撃力 public int baseMDef; // 魔法防御力 public int baseLuc; // 運 public int baseSpeed; // 速度 // 属性耐性(0.0fが基準、1.0fでダメージ無効化、-1.0fでダメージ2倍) public float resist_Fire; // 火耐性 public float resist_Ice; // 氷耐性 public float resist_Light; // 光耐性 public float resist_Dark; // 闇耐性 // 定数定義 private const float StatusBuffMulti = 1.5f; // ステータス上昇系のバフがある場合の計算時倍率 private const float StatusDebuffMulti = 0.6f; // ステータス低下系のデバフがある場合の計算時倍率 #region 状態異常関連 /// <summary> /// 状態異常をセットする /// </summary> public void SetState (CharacterState targetState, int turnNum) { // 戦闘不能時は処理しない if (isKnockedOut) return; // 状態異常追加 if (currentStates.ContainsKey (targetState)) {// 既に該当の状態異常にかかっている // 残りターン数を更新 if (currentStates[targetState] < turnNum) currentStates[targetState] = turnNum; } else {// 新規付与 // 状態異常DictionaryにKey(状態異常の種類)とValue(残りターン数)を追加 currentStates.Add (targetState, turnNum); } } /// <summary> /// 状態異常を解除する /// </summary> public void RemoveState (CharacterState targetState) { // 状態異常解除(状態異常DictionaryからKeyを削除) if (currentStates.ContainsKey (targetState)) currentStates.Remove (targetState); } /// <summary> /// 全ての状態異常を解除する /// </summary> public void RemoveAllStates () { // 状態異常Dictionaryを初期化 currentStates.Clear (); } /// <summary> /// 全状態異常残りターンを1ずつ減らす /// </summary> public void DecreaseCharacterStates () { // 状態異常Dictionary内のKeyをリストで取得 List<CharacterState> states = new List<CharacterState> (currentStates.Keys); // 各KeyごとにDictionary内で格納している残りターン数をデクリメント foreach (var targetState in states) { currentStates[targetState]--; // 残りターン数が0以下ならDictionaryからKeyを削除 if (currentStates[targetState] <= 0) currentStates.Remove (targetState); } } #endregion #region 計算後ステータス取得 // MaxHP取得 public virtual int GetCalculatedMaxHP () { return baseMaxHP; } // MaxTP取得 public virtual int GetCalculatedMaxTP () { return baseMaxTP; } // Atk取得 public virtual int GetCalculatedAtk () { return baseAtk; } // Def取得 public virtual int GetCalculatedDef () { return baseDef; } // MAtk取得 public virtual int GetCalculatedMAtk () { return baseMAtk; } // MDef取得 public virtual int GetCalculatedMDef () { return baseMDef; } // Luc取得 public virtual int GetCalculatedLuc () { return baseLuc; } // Speed取得 public virtual int GetCalculatedSpeed () { int value = baseSpeed; // 状態異常による補正を適用 if (currentStates.ContainsKey (CharacterState.SpeedUp)) value = (int)(value * StatusBuffMulti); if (currentStates.ContainsKey (CharacterState.SpeedDown)) value = (int)(value * StatusDebuffMulti); return value; } // 各種耐性値取得 public virtual float GetCalculatedResist (ElementType elementType) { float value; switch (elementType) { case ElementType.Fire: // 火耐性 value = resist_Fire; break; case ElementType.Ice: // 氷耐性 value = resist_Ice; break; case ElementType.Light: // 光耐性 value = resist_Light; break; case ElementType.Dark: // 闇耐性 value = resist_Dark; break; default: // その他 value = 0.0f; break; } return value; } #endregion #region 属性ダメージ倍率取得 /// <summary> /// 指定した属性の攻撃スキルを受けた時のダメージ倍率を返す /// </summary> public float GetElementDamage (ElementType elementType) { // 各属性の耐性値を取得 float resistValue = GetCalculatedResist (elementType); // ダメージ倍率を計算して返す return 1.0f - resistValue; } #endregion } // (enum)キャラクター状態異常 public enum CharacterState { Poison, // 毒 Stun, // スタン Protection, // かばう Mark, // 標的 Guard, // ガード SpeedUp, // 速度上昇 SpeedDown, // 速度低下 MagicWall, // 魔法壁 } // (enum)属性種類 public enum ElementType { None, // 無属性 Fire, // 火属性 Ice, // 氷属性 Light, // 光・雷属性 Dark, // 闇属性 } |
主に以下の拡張を加えています。
- 状態異常の種類および攻撃の属性をenumで定義。
- 「現在かかっている状態異常」を、残りターン数とともにDictionaryで管理。
- 状態異常Dictionaryを操作し、状態異常の追加や残りターン数の更新を行えるメソッドを追加。
- 現在の各パラメータ(攻撃力など)を取得するためのGet系メソッドを追加。virtualプロパティを付与することで上書き可能にする。
- 属性ダメージの計算式を追加。戦闘システムの深い部分を実装する時に関わってくる。今は無視してOK。その属性の耐性値が高いほど、その属性から受けるダメージが減少するようにしている。
状態異常および属性の種類(enum)はクラス外で定義しています。他のファイルからよく参照する情報になるためクラス外に置いていますが、本来はファイルごと分けた上でnamespaceで区切った方が好ましいです。
状態異常の管理はDictionaryで行っています。単にそのキャラクターが抱えている状態異常を保存したいだけならList<CharacterState>を使用する事で実現可能ですが、実際には状態異常の種類だけでなく「あと何ターンで解除されるか(残りターン数)」という情報もそれぞれペアで持っておきたいですね。
Dictionaryを使用すると、任意の型でKey(キー)とValue(値)のペアの情報をリストとして管理できます。Keyは一意(他の要素と重複しない)のものである必要がありますが、これによりKeyを渡せばそれに対応するValueを取得する事ができます。
ここで用意したメソッドは戦闘システム実装の際にターン経過処理時に呼び出されるようにします。
攻撃力などのパラメータの現在値を取得するメソッドもそれぞれ用意しています。virtual属性を付けることによって、この後作成する派生クラスにて拡張できる形にしています。
今はあまり入り組んだ仕様にはしないため基本的にbaseの変数をそのまま返すようにしていますが、例えば速度取得メソッドのように「特定の状態異常にかかっているなら値に補正を掛けてから返す」ような機能が必要であればここで用意しておくと良いでしょう。
アクターデータクラス作成
このCharacterDataクラスから継承した、アクターデータを管理するクラスを作成しておきましょう。
ActorData.cs (新規)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using System.Collections.Generic; using UnityEngine; /// <summary> /// アクターデータクラス(CharacterDataから派生) /// </summary> public class ActorData : CharacterData { // 職業データ public int jobID; // 立ち絵ID public int pictureID; // 現在レベル public int nowLevel; // 現在経験値 public int nowEXP; // 最大経験値 public int maxEXP; } |
5つの変数しか宣言していませんが、CharacterDataがもつ全ての変数もこのクラスは併せ持っている形になります。
このActorDataクラスはすぐには使用しませんが、どの職業に就いているかや立ち絵を番号で指定できるようにし、レベルと経験値を保存するようにしています。
なおScriptableObjectも間接的に継承しているためアセットとして作成できるようにすることも可能ですが、アクターデータは全てプログラム内で作成されるので今回は不要です。
エネミーデータクラス作成
続けてエネミーデータを管理する方のクラスも作成してみましょう。
EnemyData.cs (新規)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System.Collections.Generic; using UnityEngine; /// <summary> /// エネミーデータクラス(CharacterDataから派生) /// </summary> [CreateAssetMenu (fileName = "Enemy", menuName = "Dungeon/EnemySO")] public class EnemyData : CharacterData { [Header ("名前(日本語)")] public string enemyName_JP; [Header ("名前(英語)")] public string enemyName_EN; [Header ("立ち絵画像")] public Sprite standingSprite; [Header ("顔グラフィック画像")] public Sprite faceIconSprite; [Header ("撃破時の獲得Exp")] public int enemyExp; [Header ("撃破時の獲得Gold")] public int enemyGold; } |
エネミーのデータについては、それぞれScriptableObjectで予め用意しておく必要があるためそれを踏まえた設定をしています。
立ち絵画像をInspectorから指定できるようにする事でこの後の表示確認に用いていきます。
エネミーデータのScriptableObject作成
エネミーデータクラスのScriptableObjectを作成できるようになったので、ここで試しに1~2体分のデータを用意してみましょう。
アセットはCreate→Dungeon→EnemySOの順にクリックで作成が可能です。ScriptableObjectフォルダ以下にエネミーデータ格納用のフォルダを用意しておく事をおすすめします。

Inspectorを開くと、EnemyDataクラス内で用意した5つの項目の他にCharacterDataクラスのパラメータのほとんどが表示されていることを確認できるはずです。これらの値を編集してエネミーのデータを作成していきます。
値の設定にあたってのポイントは以下です。
- HPやTP、攻撃力などのパラメータ(速度と耐性以外)は基礎値を入力します。各キャラクターのレベル1のパラメータにはそれぞれ基礎値の5倍の値がセットされ、1レベル上昇するごとに基礎値分の値が追加されます。
- 属性耐性値(Resist_)はレベルによって変化しません。0で耐性なし、1で耐性あり(ダメージ無効)、-1で弱点(ダメージ2倍)になります。
- Enemy_Name_JPには日本語でのエネミー名、Enemy_Name_ENには英語でのエネミー名を入力します。今後ローカライズ機能を実装する時に用いられます。
- 立ち絵画像は好きな画像を使用してください。
- 顔グラフィック画像も同様に任意の画像を使用可能ですが、アスペクト比(縦横比)が 2:1 の画像が好ましいです。(例:横幅150px・縦幅75pxの画像)
- Enemy_ExpおよびEnemy_Goldは、その敵1体を撃破した時の獲得経験値量・獲得ゴールド量を設定します。今は適当な値でOKです。

複数の敵との戦闘もテストできるようにするため、もう1体ほど作っておくと良いでしょう。バランス調整は後で行えば良いため値は深く考える必要はありません。

戦闘シーンにエネミーを表示する
立ち絵画像の情報をともなったエネミーのデータを用意できたので、シーン開始時(戦闘開始時)にエネミーインスタンスを作成して表示する所までを行ってみましょう。
まずはBattleシーンにおけるエネミー管理クラスとしてEnemyManagerを作成します。ここではエネミーデータにまつわる全般を扱います。
EnemyManager.cs (新規)
まとめ

基底クラスや派生クラス、シングルトンの機能を活かしてキャラクターやゲームデータを取り扱うクラスを作成しました。
引き続き戦闘システムの開発を続け、アクターやエネミーが通常攻撃を行える状態を目指していきます。
次の記事:

現場レベルのゲーム制作が、すべてここで学べます。






コメント