今回はゲーム開発でほぼ必須の機能の作り方に関する話題で、タイトルの通り
を丁寧に解説するという内容になっています。
ダメージ処理というとアクションゲームをはじめとして様々なゲームで導入されており、ゲームでは「あって当たり前」のような機能ですよね。しかしいざ作ってみるとなると、ダメージ処理をきちんと設計・実装するのは意外と難しいので初心者の方からしたら悩ましい部分なのではないでしょうか。
そこでここでは、C#の「インターフェイス」を使った汎用性の高いダメージ処理の作り方をサンプルスクリプト付きでご紹介しますね。
ダメージ処理の設計について
でははじめに、そもそもの話として「ダメージ処理はどういう設計にしたらいいのか?」という点から説明していこうと思います。
ダメージ処理の要点の整理
まず、皆さん何となくご存じかもしれませんがダメージ処理の要点を整理しておきましょう。
ダメージ処理は上の図のように
- 与ダメージ側
- 被ダメージ側
の2者が関わる処理となります。一見するとどちらに処理を書いたらいいか迷いますが、基本的にダメージ処理は被ダメージ側のHPを減らす処理なので
形にすると分かりやすいと思います。
初心者にありがちなダメージ処理の設計とその問題点
次に初心者の方が陥りがちなダメージ処理の設計とその問題点について解説します。初心者の方が陥りがちなダメな設計は次の2種類です。
- クラスごとに別のダメージ処理を作ってしまう
- 基底クラスにダメージ処理を作り、派生クラスで処理を実装する
ダメな例1:クラスごとに別のダメージ処理を作ってしまう
まず一つ目のダメな例は、クラスごとに別のダメージ処理を作ってしまうことです。例えば下のような感じですね。
using UnityEngine; public class Player : MonoBehaviour { public void Damage(int value) { // ここに被ダメージ時の具体的な処理 } } public class Enemy : MonoBehaviour { public void Damage(int value) { // ここに被ダメージ時の具体的な処理 } }
これでは別々のクラスについていちいちGetComponentするなどしてダメージ処理を呼び出さないといけません。めちゃくちゃ面倒くさいことになるのでこんな設計は論外です。
ダメな例2:基底クラスにダメージ処理を作り、派生クラスで処理を実装する
次に2つ目のダメな例は、基底クラスにダメージ処理を作って派生クラスで具体的な処理を書くやり方です。
using UnityEngine; public class BaseCharacter : MonoBehaviour { public virtual void Damage(int value) { } } public class Player : BaseCharacter { public override void Damage(int value) { // ここに被ダメージ時の具体的な処理 } }
一見するとこれでも良さそうに見えますよね。しかし、これだと例えばBaseCharacterクラスとは全く別の機能を持つクラスにもダメージ処理を実装したくなった場合などに困ってしまいます。
※例:
BaseCharacterクラスを「動くキャラクター用の基底クラス」とした場合、例えば全く動かない箱とかにもダメージ処理を実装しようとすると面倒くさいことになる。
上記2つのダメな例に共通する問題点は?
ではここで上記2つのダメな例に共通する問題点をまとめておきましょう。その問題点とは、一言でいえば
ことです。ダメージ処理は汎用性が高い処理なのに、それが特定のクラスに依存していると汎用性が潰れて面倒くさいことになってしまうのです。
C#の「インターフェイス」を使ってクラスに依存しないダメージ処理を作ろう
じゃあどうすればいいのかというと、C#には「インターフェイス」という便利な機能があります。インターフェイスとは簡単に言えば
です。C#ではインターフェイスを使って各クラスが共通の規格になっていれば、同じ部分は同じように使えます。
ただこの説明だけだと「なんかよく分からんな…」って感じなのでダメージ処理でインターフェイスを活用した場合をザックリと見ていきましょう。
インターフェイスの定義
まずはダメージ処理の規格を定めるためのインターフェイス「IDamageable」を定義します。
public interface IDamageable { public void Damage(int value); public void Death(); }
このインターフェイス内には「Damage関数」と「Death関数」があります。ただし、インターフェイスの定義では具体的な処理内容は一切書きません。
インターフェイスの実装
次にダメージ処理を追加したいクラスに先ほどのIDamageableを継承させて、Damage関数とDeath関数の具体的な処理を書きます。
using UnityEngine; public class Character : MonoBehaviour, IDamageable { public void Damage(int value) { // ここに具体的なダメージ処理 } public void Death() { // ここに具体的な死亡処理 } }
インターフェイスをGetComponentしてダメージ処理を呼び出す!
あとは与ダメージ側でIDamageableをGetComponentして、Damage関数を呼び出せばOKです。
using UnityEngine; public class Bullet : MonoBehaviour { [SerializeField] int damage = 10; void OnCollisionEnter(Collision collision) { IDamageable damageable = collision.gameObject.GetComponent<IDamageable>(); if(damageable != null) { damageable.Damage(damage); } } }
このようにIDamageableをGetComponentするだけでクラスに関わらずダメージ処理を実行することができるので便利です。
ダメージ処理のC#スクリプト
では最後に、シンプルなダメージ処理を行うためのサンプルスクリプトを掲載しておきます。
using UnityEngine; public class Character : MonoBehaviour, IDamageable { [SerializeField] int maxHp = 100; int hp; public int Hp { get { return hp; } set { hp = Mathf.Clamp(value, 0, maxHp); if(hp <= 0) { Death(); } } } void Start() { Hp = maxHp; } public void Damage(int value) { Hp -= value; } public void Death() { Destroy(gameObject); } } public interface IDamageable { public void Damage(int value); public void Death(); }
拡張しやすい構造になっているので、ぜひ必要に応じて
- FPSでよくあるシールド
- スタミナ
- 被ダメージ時の無敵時間
などを追加してみてください。
なお、このCharacterクラスでは基本的にはダメージ処理や死亡処理といった「キャラクターの生存状態」にかかわる処理のみを記述するようにした方がいいと思います。移動処理などは別のクラスに書いておくとコードがスッキリしますよ。
おわりに
以上、Unityでのダメージ処理の作り方についてご説明しました。
C#のインターフェイスは一見するとややこしい概念ですが、使いこなせると非常に便利なので「今まで使ったことなかったよ」という方はぜひこの機会にマスターしていただければと思います。
この記事がUnityでのゲーム開発のお役に立てば幸いです。