LoginSignup
24
14

More than 3 years have passed since last update.

Unityの継承周りの苦痛をどうにかしたい

Last updated at Posted at 2020-02-29

エネミーを実装しよう

image.png

なんてことはない、ごく普通のエネミーコンポーネントである。

すごいエネミーは移動する

image.png

普通に実装すれば、恐らくはこうなる。

public class SugoiiEnemy : MonoBehaviour {
  private void Update() {
    // 移動処理
  }
}

エネミーは動いたよ、やったね!

エネミーは体力が0を下回ると死亡する

苦痛の始まりである。

image.png

簡単だ、さっそく実装しよう。

public class Enemy : MonoBehaviour {
  public int hp = 10;
  private void Update() {
    if(hp <= 0) Destroy(gameObject);
  }
}

あれ? Enemy死なないよ?
おっと、間違えた。
SugoiiEnemyでもUpdateを実装しているから、こうでなくては。

public class Enemy : MonoBehaviour {
  public int hp = 10;
  protected virtual void Update() {
    if(hp <= 0) Destroy(gameObject);
  }
}

あれ? これでも死なない?
あ、SugoiiEnemyの方もも変えなくては。

public class SugoiiEnemy : MonoBehaviour {
  protected override void Update() {
    base.Update();
    // 移動処理
  }
}

苦痛だ。

苦痛だ

例はよくないかもしれないが、ニュアンスは伝わったはずだ。
今回はEnemyの派生先クラスがSugoiiEnemy 1つだったから、これだけで勘弁してもらえたものの、気合入れて20個作った、みたいな場合だともう目も当てられない。
継承のネストがもっと深い場合は、もう寝てよし。

三つ子問題

見返してみると、私のコードには3種類のUpdateがあった。

// 継承しておらず、かつ継承されない場合
private void Update(){}
// 継承される場合
protected virtual void Update(){}
// 継承する場合
protected override void Update(){base.Update()}

ギャグか? ギャグなのか?
何かしらの県の条例に違反してるとしか思えない所業である。

この3つのUpdateを状況に応じて使い分けるのがプログラマーの仕事であるのなら、私は今すぐプログラマーをやめたい。

なぜこんな問題が起こっているのか

MonoBehaviourにprotected virtual void Update(){}のようなコードがないからだ。
MonoBehaviourにprotected virtual void Update(){}のようなコードがないから、我々は3種類のUpdateを使い分けるなどという愚行ができてしまうのだ。

解決せねば。
解決するか、Unityをやめるかの二択だ。

解決法 全部定義してしまう

ないなら足せばいい。
BaseBehaviourみたいなクラスを作って、MonoBehaviourの代わりに継承すればいい。

    public class BaseBehaviour: MonoBehaviour {
        protected virtual void Reset() {}
        protected virtual void Awake() {}
        protected virtual void OnEnable() {}
        protected virtual void Start() {}
        protected virtual void FixedUpdate() {}
        protected virtual void OnTriggerEnter(Collider other) {}
        protected virtual void OnTriggerEnter2D(Collider2D other) {}
        protected virtual void OnTriggerStay(Collider other) {}
        protected virtual void OnTriggerStay2D(Collider2D other) {}
        protected virtual void OnTriggerExit(Collider other) {}
        protected virtual void OnTriggerExit2D(Collider2D other) {}
        protected virtual void OnCollisionEnter(Collision other) {}
        protected virtual void OnCollisionEnter2D(Collision2D other) {}
        protected virtual void OnCollisionStay(Collision other) {}
        protected virtual void OnCollisionStay2D(Collision2D other) {}
        protected virtual void OnCollisionExit(Collision other) {}
        protected virtual void OnCollisionExit2D(Collision2D other) {}
        protected virtual void OnMouseEnter() {}
        protected virtual void OnMouseOver() {}
        protected virtual void OnMouseUp() {}
        protected virtual void OnMouseDrag() {}
        protected virtual void OnMouseDown() {}
        protected virtual void OnMouseUpAsButton() {}
        protected virtual void OnMouseExit() {}
        protected virtual void Update() {}
        protected virtual void LateUpdate() {}
        protected virtual void OnWillRenderObject() {}
        protected virtual void OnPreCull() {}
        protected virtual void OnBecameVisible() {}
        protected virtual void OnBecameInvisible() {}
        protected virtual void OnPreRender() {}
        protected virtual void OnRenderObject() {}
        protected virtual void OnPostRender() {}
        protected virtual void OnRenderImage(RenderTexture src, RenderTexture dest) {}
        protected virtual void OnDrawGizmos() {}
        protected virtual void OnGUI() {}
        protected virtual void OnApplicationPause(bool pauseStatus) {}
        protected virtual void OnDisable() {}
        protected virtual void OnDestroy() {}
        protected virtual void OnApplicationQuit() {}
        protected virtual void OnApplicationFocus(bool focusStatus) {}
    }

イベント関数の実行順を参考に列挙した。
これでコードは常にprotected override void XXX(){base.XXX();}に統一される。
詳しくは知らないが、パフォーマンスはやばそうである。

軽減法 コンポーネント指向

そもそも、Enemyを継承して別タイプのEnemyを作る、という手法自体が時代遅れなものだ(たぶんな)。
もっとスマートなやり方として、

  • ステータスコンポーネント
  • 死亡コンポーネント
  • 移動コンポーネント
  • 上記のコンポーネントを制御する頭脳コンポーネント(Enemyコンポーネント)

という四つのコンポーネントを作り、SugoiiEnemyというプレファブにつける、という手法がある。
この手法のメリットは、継承では対処が困難な、

  1. 歩行エネミー
  2. 飛行エネミー
  3. 歩行+飛行エネミー
  4. 不死身の飛行エネミー

のような、変則的な実装にもスマートに対応できるところだ。

ただし、各コンポーネントの設計が従来のやり方とは少し異なるため、慣れるまでは面倒に思うかもしれない。
責務とか責任とかをクラスごとにきっちり分けたい、イカしたエンジニアはこうしているはずだ。

このやり方であれば、少なくとも今回のような単純な場合においては、三つ子問題は起きない。
コンポーネントを継承して新しいコンポーネントを実装するときには起きるが、今までのやり方ほど影響範囲は大きくはないだろう。
しかし起きる可能性がある以上、問題は解決したとは言えない。

たせけて

本当はイカした方法があって、私が知らないだけだということを切に願う。
偉い人、愚かな私をたせけてください。

P.S. base.XXX();書くのしんどいです、自動で最初に呼び出してくれるイケメン機能はないですか?

24
14
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
14