エネミーを実装しよう
なんてことはない、ごく普通のエネミーコンポーネントである。
すごいエネミーは移動する
普通に実装すれば、恐らくはこうなる。
public class SugoiiEnemy : MonoBehaviour {
private void Update() {
// 移動処理
}
}
エネミーは動いたよ、やったね!
エネミーは体力が0を下回ると死亡する
苦痛の始まりである。
簡単だ、さっそく実装しよう。
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というプレファブにつける、という手法がある。
この手法のメリットは、継承では対処が困難な、
- 歩行エネミー
- 飛行エネミー
- 歩行+飛行エネミー
- 不死身の飛行エネミー
のような、変則的な実装にもスマートに対応できるところだ。
ただし、各コンポーネントの設計が従来のやり方とは少し異なるため、慣れるまでは面倒に思うかもしれない。
責務とか責任とかをクラスごとにきっちり分けたい、イカしたエンジニアはこうしているはずだ。
このやり方であれば、少なくとも今回のような単純な場合においては、三つ子問題は起きない。
コンポーネントを継承して新しいコンポーネントを実装するときには起きるが、今までのやり方ほど影響範囲は大きくはないだろう。
しかし起きる可能性がある以上、問題は解決したとは言えない。
たせけて
本当はイカした方法があって、私が知らないだけだということを切に願う。
偉い人、愚かな私をたせけてください。
P.S. base.XXX();
書くのしんどいです、自動で最初に呼び出してくれるイケメン機能はないですか?