このページに辿り着いた方は、SOLID原則に足を踏み入れ、キレイなコードを書きたい!と思っている中級者なのでしょう。
そして辿り着いた「依存性逆転の原則」という謎の原則!?漢字多すぎてもはや無理!!って感じですか?
わかります。私もそうです。
でも大丈夫。実は意外と簡単なんですよ。
今回は依存性逆転の原則を、実際にUnityに落とし込んでみたものを備忘的に残していきます。
ゲームの全体像
こんな感じのゲームを作りたいと思います。
弾丸でスライムを倒したり。弾丸で大きいボスを倒したり。そんなクソゲー。
弾丸オブジェクト
Bulletオブジェクトにはこんなコンポーネントが付いてます
- Box Collider 2D 当たり判定用に付与
- Rigidbody 2D 当たり判定用に付与
- Bullet Script 弾丸オブジェクトに関するコード
Slimeオブジェクト
Slimeオブジェクトにはこんなコンポーネントが付いています。
- Box Collider 2D 当たり判定用に付与
- Slime Script スライムオブジェクトに関するコード
Bossオブジェクト
Bossオブジェクトにはこんなコンポーネントが付いています。
- Box Collider 2D 当たり判定用に付与
- Boss Script ボスオブジェクトに関するコード
良くない例
ここから「依存性逆転の原則」に迫っていきたいと思います。
弾丸が接触した時の処理(良くない例)
Unity入門書に書いてある処理方法。これは大抵良くないものです。
- if文を使って Slime と Boss で条件分岐させる
- 接触したオブジェクトのTagによって処理を変える
- Enum(列挙型)で処理を変える
// Bulletが当たった時の処理判定イメージ(簡単に)
if (Tag == Slime)
{
// なんかの処理
}
else if(Tag == Boss)
{
// なんかスゴイ処理
}
イメージとしてはこんな感じのコードですね。
このやり方が完全悪だとは言いません。
「Unity1Week」など限られた期間に単発のゲームを作るような場合は最適でしょう。
しかし、作ったアプリ。リリースしたアプリを少しずつアップデートしてどんどん育てていきたい。
そんな場合は、やはり後々困る事になるでしょう。
どこが良くないのか?
弾丸の処理コードがSlimeやBossのTagに依存している事が良くないですね。
上記のような場合、「そうだ!レッドスライムも実装しよう」となった時どうしますか?
- if文の分岐を増やす
- その他、依存関係があるコードを全て直す
これがレッドスライム1匹なら良いですけど「大型アップデート!100種追加!」なんて企画が通ったら地獄の始まりですよね。
良い例
インターフェイスを実装し、そのインターフェイスを介して接触相手の処理を呼び出しましょう。
先ほどの依存関係の図と比べると、矢印の向き、つまり依存関係が逆転している事がわかるかと思います。
そう!インターフェイスを介して処理を行う事こそが「依存関係逆転の原則」なのです。
「う〜ん…もはや着いていけない。」という方も諦めずに実際にコードを見てみましょう。
Bullet Script
弾丸のコードはこうです。
- 毎秒少し右に弾丸が移動する。
- もしコライダーに接触した場合、接触したオブジェクトのインターフェイスを取得する。
- インターフェイスに実装されている ApplyDamageメソッドを呼び出す。
- おしまい
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
Rigidbody2D rb;
void Start()
{
rb = this.GetComponent<Rigidbody2D>();
}
float speed_x = 2;
void Update()
{
rb.velocity = new Vector2(speed_x, 0);
}
private void OnCollisionEnter2D(Collision2D collision)
{
GameObject hitObj = collision.gameObject;
IApplicableDamage d = hitObj.GetComponent<IApplicableDamage>();
if(d != null)
{
d.ApplyDamage();
}
}
}
どこが良いのか?
弾丸に実装したコードのどこが良いのでしょう?
それは、弾丸が接触した相手オブジェクトに指定したインターフェイスが実装されていれば処理が行われる点です。
弾丸としては何にぶつかったかわからないけど、接触相手に「IApplicableDamage」というインターフェイスが実装されていれば処理が行われるという事です。
- SlimeオブジェクトとBossオブジェクトは「IApplicableDamage」というインターフェイスを実装している
- IApplicableDamage を実装しているクラスは必ず「ApplyDamage」という名前のメソッドを実装している。
- ApplyDamage メソッドの中身は、SlimeスクリプトとBossスクリプトでそれぞれ別の処理を書いても良い
Slime Script
じゃあ実際 Slime 側はどうなってんの?という部分です。
内容は「弾丸に当たった事を表示して自身のHPを減らす」というメソッドです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Slime : MonoBehaviour , IApplicableDamage
{
int hp = 100;
int damage = 10;
public void ApplyDamage()
{
Debug.Log("弾がスライムに当たった");
hp -= damage;
Debug.Log("hpは " + hp);
}
}
それでは実行してみましょう。
弾丸オブジェクトがぶつかったタイミングでSlimeスクリプトのApplyDamageメソッドが処理されました。
Boss Script
では続きまして Boss側も見てみましょう。
処理内容は
- 弾丸と接触したことを表示
- 「ガハハ!そんな弾効かぬわ」と表示
- BossのHPが回復しちゃう
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Boss : MonoBehaviour , IApplicableDamage
{
int hp = 500;
int damage = 10;
public void ApplyDamage()
{
Debug.Log("弾はボスに当たった");
Debug.Log("ガハハ!そんな弾効かぬわ");
hp += damage;
Debug.Log("hpは " + hp);
Debug.Log("回復しちゃった...");
}
}
それでは実行してみましょう。
成功です。
Bossに接触すると、Bulletスクリプトを書き替えずともSlime接触時と違う処理をする事ができました。
まとめ
今後、敵キャラが増えた場合でも Bulletスクリプトを修正する必要はなくなりました。
このように「依存性逆転の原則」を使うことでアップデートに強い設計を行う事ができました。
そして、これはあくまで「原則」であるという事に注意しましょう。
この原則に違反した方が良い場合には、違反しても良いという事です。
コメント