Cのコンポーネント指向エンジン#

コンポーネント指向プログラミング 」に関するインターネットの記事で何度か出会いましたが、その一般的な考え方は、各複雑なオブジェクトを独立した機能ブロックのセット-コンポーネントとして提示することです。



理論的には、これは非常に興味深いように思えます。すべての独立した小さく複雑なオブジェクトは、単純なオブジェクトの異なる組み合わせにすぎません。 最近、私はそのような法律に従って機能するシステムを書くことを試みることに決めました、そして、すべてがそれほど些細ではないことが判明しました。 詳細については、猫に招待します。



おそらく、誰もが自分の決定を他の人と共有して話し合い、他の人の意見を見つけ、おそらく誰かを助けたいという気持ちを知っています。



個人トレーニングの目的でプロジェクトに取り組んでいる間、私は「コンポーネント指向」アプローチを思い出し、再びそのすべての利点を感じ、実装を真剣に取り上げることに決めました。



このコンポーネントの向きは何ですか? オブジェクトはもはやトレンドではありませんか?



システムの複雑なオブジェクトは、それぞれが独自のタスクのみを実行できる小さなオブジェクトのセットのように見えますが、コンポーネントのこのシンフォニーが一緒に必要な動作を正確に生成すると想像してください。 あとは、すべての複雑なエンティティを構築して、アプリケーションを起動するだけです。



オブジェクトがデータ(フィールド)と動作(メソッド)のセットではなく、コンポーネント(他のオブジェクト)であるため、「 コンポーネント指向プログラミング 」と言われるのは、おそらくこれが理由です。



このアイデアはどのように魅力的に見えましたか?



第一に、どのコンポーネントも1つの問題を解決します- 単一責任原則 (SOLID)を知らなくても、これが良いことは直感的に明らかです。



第二に、コンポーネントは互いに独立しています。誰もが自分が何をし、これに必要なものだけを知っています。 このようなコンポーネントは、保守、変更、再利用、テストが簡単です。



たぶん私はロマンティックなのかもしれませんが、これらの2つのポジションは、そのようなソリューションを実装する方法について深く考えるのに十分です。



コンテナオブジェクトにアタッチされた各コンポーネントが何らかの形で後者の可能な動作に影響を与えるというルールを基本とする場合、いくつかの問題が発生します。クライアントコードは、この抽象的なコンテナを見て、そこにあるコンポーネントのセットについて学習します位置していますか?



別のニュアンス:両方のコンポーネントが同じコンテナにある場合、一方のコンポーネントが他方と「通信」する必要がある可能性を提供する必要があります。



始めましょう



常識の規則に従って、将来のシステムの各要素がどのように機能するかのプロセスを可能な限り抽象的に記述しようとする必要があります。 最も単純な結果は、2つの抽象的なエンティティの割り当てです。





最も単純で直観的な例を使用して複雑なことを解決するのが大好きなので、従来のオブジェクトスタイルの非常に単純なエンティティ「プレーヤー」を想像してみましょう。



public class Player { public int Health; // . public int Mana; // . public int Strength; // . public int Agility; // . public int Intellect; // . public WeaponSlot WeaponSlot; //   . }
      
      





次に、すべてをコンポーネントスタイルで表示する方法を示します。



 //      . public class Player : ComponentContainer { // -  ,   . } //   . public class BaseStats : Component { public int Health; // . public int Mana; // . } //    . public class CreatureStats : Component { public int Strength; // . public int Agility; // . public int Intellect; // . } //    . public class WeaponSlot : Component { public Weapon Weapon; // . }
      
      





ご覧のとおり、どのコンポーネントも、他のコンポーネントやプレーヤー自身についての手がかりはありません。 それをどのように作成しますか?



 //  -     . public Player CreatePlayer() { var player = new Player(); //  new ComponentContainer(); player.AddComponent<BaseStats>(); player.AddComponent<CreatureStats>(); player.AddComponent<WeaponSlot>(); return player; }
      
      





システム設計の有効性が変更が生じたときに正確に現れる場合、現在のバージョンでは、より複雑な例を考えようとすると問題が発生します。非ゲームキャラクター(NPC)との相互作用です。



そのため、問題のコンテキストは次のとおりです。ある時点で、プレーヤー NPCモデルをクリックして(またはキーボードのボタンを押して)ダイアログボックス呼び出しをアクティブにします。 レベルの制限を考慮して、プレーヤーが現在利用できるすべてのタスクを表示する必要があります。

これがどのように見えるかの簡単な概要をスケッチしようとします。



 // ...    . // -    " " -  . var player = GetPlayer(); var questGiverNpc = GetQuestGiver(); var playerStats = player.GetComponent<StatsComponent>(); if (playerStats == null) return; //        ,     10. if (playerStats.Level < 10) return; var questList = questGiverNpc.GetComponent<QuestList>(); if (questList == null) return; //     . var availableQuests = questList.Where(quest => quest.Level >= playerStats.Level); // ... -    .
      
      





ご覧のとおり、コンテナの内容について何も知らない(そして知りたくない)ことを考えると、唯一の方法は適切なコンポーネントを取得することです。 時々、そのような解決策で十分ですが、さらに進んで、これで他に何ができるか、そしてそれを非常に便利なモデルに変えるためにそれを変換する方法を理解したかったです。



最初のステップ:コンテナーオブジェクトの相互作用を別のレイヤーに移動します。 したがって、 インタラクターの抽象化が表示されます(英語のインタラクションから-インタラクション、したがってインタラクターはインタラクターです)。



システムを開発するとき、最高レベルのコードがどのように見えるか、つまり「これがフレームワークである場合、エンドユーザーはどのようにそれを使用するか」を想像するのが好きです。



過去の例のタスクのコードを基礎としてください:



 // ...     NPC. var player = GetPlayer(); var questGiver = GetQuestGiver(); player.Interact(questGiver).Using<QuestDialogInteractor>();
      
      





すべての陰謀はQuestDialogInteractorクラスに行きました。 結果を魔法のように整理する方法は? 前の例に基づいて、最も単純で明白なものを示します。



 public class QuestDialogInteractor : Interactor { public void Interact(ComponentContainer a, ComponentContainer b) { var player = a as Player; if (player == null) return; var questGiver = b as QuestGiver; if (questGiver == null) return; var playerStats = player.GetComponent<StatsComponent>(); if (playerStats == null) return; if (playerStats.Level < 10) return; var questList = questGiverNpc.GetComponent<QuestList>(); if (questList == null) return; var availableQuests = questList.Where(quest => quest.Level >= playerStats.Level); //   . } }
      
      





ほとんどすぐに、現在の実装がひどいことは明らかです。 まず、チェックに完全に関与しました。



 if (playerStats.Level < 10)
      
      





1人のキャラクターが第5レベルのタスクを発行し、別のキャラクターが第27レベルのタスクを発行します。 第二に、最も深刻なパンク: PlayerおよびQuestGiverタイプに依存しています。 それらをComponentContainerに置き換えることができますが、特定の型への参照が必要な場合はどうすればよいですか? そして、それらはこれらのタイプに必要でした、彼らは他の人に必要です。 変更は、 オープン/クローズド原則 (ソリッド)に違反します。



リフレクション



この解決策は、.NETで提案されているメタデータメカニズムで見つかりました。これにより、多数のルールと制限を導入できます。



一般的な考えは、 Component Containerから派生した型を持つ2つのパラメーターを取るInteractor型の継承者のメソッドを定義できるようにすることです。 このようなメソッドは、 [InteractionMethod]属性でマークしないと機能しません。



したがって、以前のコードは直感的になります。



 public class QuestDialogInteractor : Interactor { [InteractionMethod] public void PlayerAndQuestGiver(Player player, QuestGiver questGiver) { var playerStats = player.GetComponent<StatsComponent>(); if (playerStats == null) return; if (playerStats.Level < 10) return; var questList = questGiverNpc.GetComponent<QuestList>(); if (questList == null) return; var availableQuests = questList.Where(quest => quest.Level >= playerStats.Level); //   . } }
      
      





コンテナからコンポーネントを取り出すためのこれらの「試み」は、どこかで削除したいのですが、それでも印象的です。



同じツールを使用して、属性[RequiresComponent(parameterName、ComponentType)]の形式で追加のコントラクトを導入します



 public class QuestDialogInteractor : Interactor { [InteractionMethod] [RequiresComponent("player", typeof(StatsComponent))] [RequiresComponent("questGiver", typeof(QuestList))] public void PlayerAndQuestGiver(Player player, QuestGiver questGiver) { var playerStats = player.GetComponent<StatsComponent>(); if (playerStats.Level < 10) return; var questList = questGiverNpc.GetComponent<QuestList>(); var availableQuests = questList.Where(quest => quest.Level >= playerStats.Level); //   . } }
      
      





これですべてがきれいに整頓されました。 元のバージョンからの変更点:





2つのコンテナオブジェクトの相互作用に加えて、同じコンテナ内の複数のコンポーネント間の相互作用を保証する方法に問題がありました。 この問題を解決するために、前の方法と同様の方法を使用しました。ユーザーがコンポーネントをコンテナに追加すると、最初のハンドラーメソッドを呼び出して、パラメーターとして自分自身を渡します。



 public class ComponentContainer { public void AddComponent(Component component) { // ... , . component.OnAttach(this); } }
      
      





OnAttachメソッド 、特定のタイプのコンポーネント(リフレクションとポリモーフィズムを使用)で[AttachHandler]属性でマークされたメソッドを検索します。これは、特定のタイプのコンテナで機能します。



そのようなコンポーネントが動作するためにコンテナの他のコンポーネントが必要な場合、そのクラスは同じ属性[RequiresComponent(ComponentType)]でマークできます。



XNAライブラリを使用してテクスチャを描画することがタスクであるコンポーネントの例を考えてみましょう。



 [RequiresComponent(typeof(PositionComponent))] [RequiresComponent(typeof(SpriteBatch))] public class TextureDrawComponent : Component { [AttachHandler] public void OnTextureHolderAttach(ITexture2DHolder textureHolder) { //   ITexture2DHolder   GetComponent,  //     ComponentContainer,      Component, //    protected  GetComponent    Component. var spriteBatch = GetComponent<SpriteBatch>(); spriteBatch.Draw(textureHolder.Texture2D, GetComponent<Position>(), Color.White); } }
      
      





最後に、相互作用の非常に単純な例をいくつか示します。



 //   . player.Interact(monster).Using<AttackInteractor>(); //    . player.Interact(healthPotion).Using<ItemUsingInteractor>(); //      . player.Interact(monster).Using<LootInteractor>();
      
      





まとめ



これまでのところ、システムは完全には準備ができていません:拡張と最適化のためのいくつかのウィンドウがあります(それでも、リフレクションがアクティブに使用され、キャッシュをスキップしないでください)、非常に複雑なエンティティの相互作用をより慎重に考慮する必要があります。

コードが契約に準拠していない場合(例外を無視またはスローする場合)、 [RequiresComponent]属性に動作オプションを追加したいと思います。



結局、この拡張性と利便性の「レース」は良いものにはならないかもしれませんが、いずれにせよ、貴重な体験があります。



このアプローチをCIM - Component Interactor Modelと呼び、次の「ホーム」プロジェクトでパフォーマンスを慎重にテストする予定です。 誰かがこのトピックに興味がある場合、次のパートでは、 ComponentComponentContainerUsing <T>およびInteractorに関連する実装などのクラスのソースコードを検討できます。



第二部。



ご清聴ありがとうございました!



All Articles