ブラウンチップ☆ 2019/12/21 19:53

【DotFeather】便利なオブジェクトを作る

はいどーもこんばんは。
今日も昨日の続きでDoFeatherを使っていきたいと思いますよ。
本日のお題は便利なオブジェクトです。

便利なオブジェクトとは?

前回の記事で「クリックでイベントを発火するテキストオブジェクト」を作りました。
これはこれでいいのですが、以下のような機能も盛り込みたくはならないでしょうか?

  • マウスポインタが重なった瞬間にイベント発火
  • マウスポインタが離れた瞬間にイベント発火
  • 長押しされている間ずっと発火

ノベルゲーだと欲しそうですよね。
さらに言うなら、この辺の機能ってテキストに限らずスプライトでも欲しいですよね。
では設計を考えてみましょう。

愚直な実装方法

もっとも単純な実装方法は、昨日作ったClickableTextクラスに上述した機能を一つ一つ盛り込み、イベントを追加してあげることです。
これでもよいのですが、以下の問題点があります。

  • 不要なイベントまで付与されてしまう
    • 例えばクリック機能だけでいいのに、マウスポインタが重なった瞬間のイベントも実装されてします。ただし中身を空にすれば動作自体に支障はない
  • スプライトに使い回せない
    • あくまでもテキストに対してのみしか機能が有効でなく、スプライトでも似たような機能が欲しくなったとき、似たようなコードを書かなければならない

そこで今回は

  • コンポーネント方式

を採用しようと思います。
つまりUnityのAddComponent的なやつですね。あるいはデコレータパターンとして作ってあげてもいいかもしれません。
まあこの辺はお好みで。

コンポーネント方式の基盤

上記では「スプライトで使い回せない」と書きましたが、とりあえずのところスプライトは無視して考えます。
というわけで今考えるのはTextDrawableのみですね。
コンポーネントのコードはこんな感じでどうでしょうか。


           internal abstract class TextComponentBase {

        ////=============================================================================
        //// Properties
        ////  
        ////=============================================================================

        public GameTextObject Parent { get; private set; }

        /// <summary>
        /// 何らかのタイミングで発火させたいイベント
        /// </summary>
        public event EventHandler Subject;

        ////=============================================================================
        //// Public Methods
        ////  
        ////=============================================================================

        public abstract void Update();

        /// <summary>
        /// GameTextObject以外からセットして欲しくない
        /// </summary>
        public void SetParent(GameTextObject parent) {
            Parent = parent;
        }

        /// <summary>
        /// 親からのコンポーネントを取得
        /// </summary>
        /// <typeparam name="T">取得したい型</typeparam>
        /// <returns></returns>
        public T GetComponent<T>() where T : TextComponentBase {
            return Parent.GetComponent<T>();
        }
        ////=============================================================================
        //// Protected Methods
        ////  
        ////=============================================================================

        protected virtual void OnEvent(EventArgs e) {
            Subject?.Invoke(this, e);
        }
    }

特に見るべき箇所はないですね。
OnEventの名前が気に入らないだとか(本当はイベントごとに名前を変えたい)、Parentが実質どこからでもセットできちゃうとか、問題は色々ありますが……。
とりあえずはこれで行きましょう! 問題が出てきそうなら変える方針で。

TextDrawableを継承するクラスの方はこんな感じ。

    internal class GameTextObject : TextDrawable, IUpdatable {
        ////=============================================================================
        //// Local Members
        ////  
        ////=============================================================================

        /// <summary>
        /// コンポーネントのリスト
        /// </summary>
        private readonly List<TextComponentBase> _componentList = new List<TextComponentBase>();

        ////=============================================================================
        //// コンストラクタ
        //// 
        ////=============================================================================

        public GameTextObject(string text, DotFeather.Font font, Color? color = null) : base(text, font, color) {
        }

        public GameTextObject(string text, float fontSize = 16, DotFeather.FontStyle fontStyle = DotFeather.FontStyle.Normal, Color? color = null) : base(text, fontSize, fontStyle, color) {
        }


        ////=============================================================================
        //// Public Methods
        ////  
        ////=============================================================================
        public virtual void OnUpdate(GameBase game) {
            _componentList.ForEach(x => x.Update());
        }

        public GameTextObject AddComponent(TextComponentBase component) {
            if(CanAddComponent(component)) {
                _componentList.Add(component);
                component.SetParent(this);
            }
            return this;
        }

        public T GetComponent<T>() where T : TextComponentBase {
            var result = _componentList.FirstOrDefault(x => {
                var component = x as T;
                return component != null;
            });
            return result as T;
        }


        ////=============================================================================
        //// Private Methods
        ////  
        ////=============================================================================

        private bool CanAddComponent(TextComponentBase component) {
            var isComponentNull = component == null;
            var isAlreadyContainComponent = _componentList.Contains(component);
            var isAddedType = (_componentList.FirstOrDefault(x => x.GetType() == component.GetType()) != null);

            return !isComponentNull && !isAlreadyContainComponent && !isAddedType;
        }
    }

AddComponentはfluentな作りになっているため、AddComponentからの戻り値からさらにAddComponentすることができます。

hoge.AddComponent(fuga).AddComponent(piyo);

みたいなやつですね。
GetComponentについてはUnityのGetComponentみたいに使えたらなーという感じで作りました。

hoge.GetComponent<FugaComponent>()

みたいな感じで付与されているコンポーネントを取得できます。
細かいエラー処理とか何も入ってないので問題あるかもですが、まあとりあえずはまあこんなもんで。

各コンポーネントを作ってみる

ここまででコンポーネント方式の基盤ができました。
あとは各コンポーネントを作りましょう。
具体的には

  • オブジェクトがクリックされたら発火するコンポーネント
  • オブジェクトの中にマウスが入ったら発火するコンポーネント
  • オブジェクトの外にマウスが出ていったら発火するコンポーネント
  • MAXHPと現在HPを持つコンポーネント
  • 現在HPが減ると発火するコンポーネント
  • 死ぬ(現在HPが0になる)と発火するコンポーネント

上の3つはいいですね。
問題なのは下の2つです。
「現在HPが半分以下のとき発火するコンポーネント」は「MAXHPと現在HPを持つコンポーネント」のことを知っていなければなりません。
コンポーネント同士はお互いのことを知っていてほしくないので、これは不都合です。
また文字を描画するものに対して「現在HP」というのは、かなり特殊なコンポーネントな気がしますね。

そこで今回はTextEnemyCore(GameTextObjectから継承)TextEnemyComponentBaseを作成し、Coreは他のコンポーネントから知ってもらえることとします(Core自体をコンポーネントとして用意する方法も考えましたが、AddComponentする順番を間違えると死んじゃうのでこうしました)。
すなわち、
TextEnemyComponentBaseはTextComponentBaseを継承して作成**し、TextEnemyCoreコンポーネントへの参照を持つことにします。

各コンポーネントはCoreのイベントを参照し、そこから登録していく感じです。
これならコンポーネントごとに処理をわけることも可能です。
さらに言うなら、各イベントをTextEnemyCoreの方につけてあげると、例えばクリック時に「音を鳴らす処理」「画像を切り替える処理」「HPを減らす処理」を同時に(1フレーム以内に)おこないたいと、別クラスにわけちゃうこともできそうです。
もしそうしなければ「オブジェクトがクリックされたときに発火するコンポーネント」の中に上記3つの処理全てを入れなければなりません。
これはSOLID原則の1つである「単一責務の原則」に反しますね。
クラスが変更される理由はなるべく一つにしてあげましょう。

以上をまとめると、以下のようになります。

  • Coreオブジェクト
    • MAXHPと現在HPを持つ
  • Coreオブジェクトへの参照を持つベースコンポーネント
  • 現在HPが減ると発火するコンポーネント
    • HPが減るのはクリックされたらとする
      • HPが減ると音を鳴らす
  • 死ぬと発火するコンポーネント
    • 死ぬときに音を鳴らす
      • 死ぬと文字色が変わる
  • マウスポインタが重なったときに発火するコンポーネント
    • これをロック状態として、死んでいないならば色が変わる
  • マウスポインタが離れたときに発火するコンポーネント
    • これをアンロック状態として、死んでいないならば色が変わる

実際にはCoreは

  • クリックされたら発火するコンポーネント
  • マウスが重なったら発火するコンポーネント
  • マウスが離れたら発火するコンポーネント

を内部で生成し、それらのイベントになんやらわかりやすい名前をつけていきます(クリックされると発火~というより死ぬと発火~のほうが動作がわかりやすい)。

さあ設計方針が決まりました。
あとはガンガン作っていくだけです!

フォロワー以上限定無料

特典はほとんどありませんが、入ってくださる方が増えれば増えるほど僕が喜ぶプランです(笑)

無料

この記事が良かったらチップを贈って支援しましょう!

チップを贈るにはユーザー登録が必要です。チップについてはこちら

記事のタグから探す

月別アーカイブ

記事を検索