ゲームの「ビュー」処理システムを設計しています。目標は、順番に表示したり、積み重ねて表示したりできるさまざまな「ビュー」を持つことができるようにすることです。たとえば、最初のスプラッシュスクリーンは1つのView
であり、スプラッシュスクリーンに続くメインメニューもView
であり、スプラッシュスクリーンの終了時にレンダリングされます。ゲームの世界のカメラビューもView
であり、HUDはView
であり、ゲームの上にレンダリングされますView
。
View
のインターフェイスを定義しているときに、View
sのスタックされた性質が入力フォーカスの優先順位に対応していることに気付きました。つまりスタックの一番上にあるView
は、他のView
sより前にユーザー入力に作用するように提供する必要があります。これは、本質的にフォーカスの「フォアグラウンド」にあるためです。
したがって、View
のインターフェースの設計は、現時点では次のようになります。
/**
* Interface for a "view". A view is a renderable target that can accept user input.
*
* One can think of it as a "layer", many layers can be drawn over each other in a
* stack like fashion where the input focus travels from the top to the bottom of
* the stack.
*/
class IView{
public:
virtual ~IView() {}
IView(const IView&) = delete; // Not allowed
void operator = (const IView&) = delete; // Not allowed
/**
* Called to handle user keyboard input. May be called multiple times per update.
* @param aKeyEvent The type of input event.
* @param aKeyCode The key code of the event key.
* @param aKeyboardState The current state of they keyboard.
* @return True if the event was handled. False if the event should continue to lower views.
*/
virtual bool handleInput(KeyEvent aKeyEvent, KeyCode aKeyCode, const KeyboardState& aKeyboardState) = 0;
/**
* Called to handle user mouse clicks. May be called multiple times per update.
* @param aMouseButtonEvent The event to handle.
* @param aMouseButton Which button it was.
* @param aMouseState The current mouse state.
* @return True if the event was handled. False if the event should continue to lower views.
*/
virtual bool handleInput(MouseButtonEvent aMouseButtonEvent, MouseButton aMouseButton,
const MouseState& aMouseState) = 0;
/**
* Called to handle user mouse movement. Only called once per update.
* @param aMouseEvent The event to handle.
* @param aMouseState
* @return True if the event was handled. False if the event should continue to lower views.
*/
virtual bool handleInput(MouseMoveEvent aMouseMoveEvent, const MouseState& aMouseState) = 0;
/**
* Called when this view becomes the foreground view.
*/
virtual void foreground() = 0;
/**
* Called when this view loses its foreground status and
* has another view drawn over it.
*/
virtual void background() = 0;
/**
* Called once every frame to perform periodic processing and rendering.
*/
virtual void update() = 0;
};
これは最終的なインターフェースではなく、今はプロトタイプを作成しているだけです。しかし、それはあなたにアイデアを与えます。
HUDが上にあるゲームView
を考えてみます。その後、ゲームが一時停止され、HUDの上にポーズView
がレンダリングされます。一時停止View
は、ユーザー入力に基づいてゲームを続行する最初の機会を提供し、他のすべての入力を飲み込みます。私にはこれは完全に理にかなっています。
現在、すべてのView
sが必ずしも入力の受信に関心を持っているわけではありません。マウスイベントだけに関心がある人もいれば、キーボードイベントだけに関心がある人もいます。私の最初のアプローチは、単にパススルー入力ハンドラーを持たせることでした。しかし、私は Interface Segregation Principle を思い出したので、さらに2つのインターフェイスIKeyboardHandler
およびIMouseHandler
を定義し、関連するView
クラスに実装させることが理にかなっています。必要に応じて。しかし、それからdynamic_cast
入力をView
オブジェクトに渡して、特定のView
がIKeyboardHandler
などを実装しているかどうかを確認します。
このアプローチが空の入力ハンドラーよりもエレガントであるかどうかはわかりません。デザインに関する入力とアイデアを探しています。
インターフェイス分離の原則は、完全に無関係な機能により重点を置いています。ここではそれほど明確ではありません。
ISPの違反の露骨な例は、スクロールバーにテキストが表示されない場合に、Scrollbar
から継承されたsetText
関数を持つWidget
インターフェイスです。そのような場合、関数は呼び出し元に関係がないと文書化されている場合があり、スクロールバーでそのような関数を呼び出そうとすると、論理エラーを示す場合があります。
このようなISPの露骨な違反は、最もファウルである傾向があり、モノリシックインターフェイスには200の機能があり、問題の実際のオブジェクトに関連するのは20だけです。大規模な継承階層に基づいたGUI設計や、無関係な派生機能の膨大な量を除外しようとするドキュメントメカニズムがよくあるGUIでよく見られます。
あなたの場合、次のような関数が与えられているので、少し違うと思います:
virtual bool handleInput(KeyEvent aKeyEvent, ...) = 0;
...ビューがキーボード入力を処理するかどうかに関係なく、クライアントには依然として関係があります。そうでないという事実はまだ関連情報であり、調べるために関数を呼び出す価値があります。だから私はこれをISPの明らかな違反とは考えていません。
インターフェースをIKeyboardHandler
のような複数のインターフェースに分割することもできますが、これは物事をより簡単にするのではなく、難しくする可能性があります。ここでは、依存関係の観点からスコープに注目する価値があります。
通常、ゲームはそのようなビューをほんの少ししか持っていません。ある種のメインメニュー画面、スプラッシュ画面、HUD、ゲームビュー、ゲーム内のパイメニューなどがあります。ほんの一握りのビューです。クライアントも数が少ない傾向があります。オペレーティングシステムからのイベントを処理し、関連するビュー関数を呼び出すのは、たった1つの広範なクライアントです。したがって、依存関係の数に関しては、範囲/規模が非常に制限されています。これは、優れたエンジニアリングソリューションを思いつくために一生懸命に努力する必要がないことを示すものであり、そうすることは目標と非常によく矛盾する可能性がありますメンテナンスが容易(変更が容易)。
動的キャストの回避
とはいえ、完全を期すために、このようなシナリオでキャストを回避する簡単な方法を紹介します。あなたはこれを行うことができます:
class IView{
public:
...
// Returns null if keyboard handling is unsupported.
virtual IKeyboardHandler* keyboard() = 0;
// Returns null if mouse handling is unsupported.
virtual IMouseHandler* mouse() = 0;
};
...(実装)IKeyboardHandler
から継承する必要のないビュータイプは、単にnullを返すことができます。できるものは単にreturn this;
。このようにして、ビューがキャストを含まないインターフェイスクエリを使用したキーボード処理をサポートしているかどうかを確認し、サポートしている場合は、そのビューのキーボード入力を処理します。
この手法をさらに柔軟に進めると、多くの場合、COMスタイルのデザイン(キャストは含まれますが、クライアントからは非表示になります)、さらにはエンティティコンポーネントシステム(これについて知っておくと非常に便利です)にアプローチすることになります。ゲーム開発者ですが、おそらくこのビューのコンテキスト外です)。それでも、これは基本的なフォームを使用した場合でも、絶対的なoverkillであり、後で何も軽減せずに前もって負荷を追加するだけの可能性が高いと思います。
しかし、上に示したこの基本バージョンは、たとえばIKeyboardHandler
で厳密に機能するシステム内の関数があり、このキーボード処理インターフェイスを実装するビュー以外のものがある可能性がある場合、有用な設計手法になる可能性があります。
誰かが良いアイデアだと言っているからといって、パターンを使用しないでください。次のデザインパターンはアンチパターンです。
キーボードとマウスの処理は、View
の一部であり、他のものではないように見えます。
おそらく、ChainedView
を実装するView
基本クラスがあるはずです。これは、キーボードとマウスの処理のための仮想メソッドのデフォルト実装を提供します。一時停止のような一部のView
sは、それをオーバーライドして、理解した入力のみを受け入れ、残りを無視します。あるいは、ChainedView
にならないこともあります。ほとんどのView
sはそれをオーバーライドして、可能なものを処理し、残りを次の連鎖ビューに渡します。入力自体を処理する必要のないView
は自動的に渡されるため、すでに分離されています。