入力デバイスからデータを読み取るためのシステムの設計(パート1)

ゲームエンジンで作業するときは、すぐに適切に設計する必要があります。これにより、後で苦痛を伴うリファクタリングに時間を浪費することがなくなります。 インスピレーションを求めてエンジンを開発したとき、他のゲームエンジンのソースを調べて、特定の実装に至りました(記事の最後にあるリンクで見つけることができます)。 この記事では、入力デバイスからデータを読み取るシステムを設計するという問題の解決策を提案したいと思います。



これは複雑に思えます。マウス、キーボード、ジョイスティックからデータを読み取り、適切な場所でそれらを呼び出しました。 そのため、ほとんどの場合、このようなコードの類似性はゲームエンジンで見つけることができます。



// ,     cotrols->Update() ... void Player::Move() { if (controls->MouseButonPressed(0)) { ... } if (controls->KeyPressed(KEY_SPACE)) { ... } if (controls->JoystickButtonPressed(0)) { ... } }
      
      





このアプローチで私に合わないものは何ですか? まず、ジョイスティックなどの特定のデバイスからデータを読み取りたい場合、特定のデバイスからデータを受信するメソッドを使用します。 第二に、コードではハードコード、つまり ゲームコードの中には、特定のキーと特定のデバイスの調査があります。 後で、ゲームメニューでキーを再定義するために、すべてを一掃し、その場でキーバインドを再定義する機能を使用して、何らかのサブシステムを再マッピングする必要があるため、これは良くありません。 したがって、最も単純な実装では、すべてがそれほど優れているわけではありません。



問題を解決するために何を提案できますか?



解決策は簡単です。入力デバイスをポーリングするときは、抽象名を使用します。別名は、「ACTION_JUMP」、「ACTION_SHOOT」など、アクションがバインドされているキーの名前ではなく、アクションに由来する別の構成ファイルに書き込まれます。 エイリアス名自体を使用しないために、エイリアス識別子を取得するメソッドを追加します。



 int GetAlias(const char* name);
      
      





状態ポーリング自体は、次の2つの方法になります。



 enum AliasAction { Active, Activated }; bool GetAliasState(int alias, AliasAction action); float GetAliasValue(int alias, bool delta);
      
      





2つの方法を使用する理由を説明します。 キーの状態をポーリングするときは、ブール値で十分ですが、スティックの状態をポーリングするときは、ジョイスティックが数値を取得する必要があります。 そのため、2つのメソッドが追加されました。 状態の場合、2番目のパラメーターでアクションのタイプを渡します。 それらの2つだけがあります:アクティブ(エイリアスがアクティブ、たとえば、キーが押される)またはアクティブ化(エイリアスがアクティブ状態に切り替わりました)。 たとえば、手g弾を投げるキーを処理する必要があります。 これは歩行などの永続的な動作ではないため、手ren弾投げキーが押されたという事実を判断する必要があり、キーが押され続けている場合はこれに応答しないでください。 エイリアスの数値をポーリングするとき、ブール値フラグを2番目のパラメーターとして渡します。これは、値自体が必要かどうか、または現在の値と最後のフレームの値の差が必要かどうかを示します。



カメラコントロールを実装するコードの例を示します。



 void FreeCamera::Init() { proj.BuildProjection(45.0f * RADIAN, 600.0f / 800.0f, 1.0f, 1000.0f); angles = Vector2(0.0f, -0.5f); pos = Vector(0.0f, 6.0f, 0.0f); alias_forward = controls.GetAlias("FreeCamera.MOVE_FORWARD"); alias_strafe = controls.GetAlias("FreeCamera.MOVE_STRAFE"); alias_fast = controls.GetAlias("FreeCamera.MOVE_FAST"); alias_rotate_active = controls.GetAlias("FreeCamera.ROTATE_ACTIVE"); alias_rotate_x = controls.GetAlias("FreeCamera.ROTATE_X"); alias_rotate_y = controls.GetAlias("FreeCamera.ROTATE_Y"); alias_reset_view = controls.GetAlias("FreeCamera.RESET_VIEW"); } void FreeCamera::Update(float dt) { if (controls.GetAliasState(alias_reset_view)) { angles = Vector2(0.0f, -0.5f); pos = Vector(0.0f, 6.0f, 0.0f); } if (controls.GetAliasState(alias_rotate_active, Controls::Active)) { angles.x -= controls.GetAliasValue(alias_rotate_x, true) * 0.01f; angles.y -= controls.GetAliasValue(alias_rotate_y, true) * 0.01f; if (angles.y > HALF_PI) { angles.y = HALF_PI; } if (angles.y < -HALF_PI) { angles.y = -HALF_PI; } } float forward = controls.GetAliasValue(alias_forward, false); float strafe = controls.GetAliasValue(alias_strafe, false); float fast = controls.GetAliasValue(alias_fast, false); float speed = (3.0f + 12.0f * fast) * dt; Vector dir = Vector(cosf(angles.x), sinf(angles.y), sinf(angles.x)); pos += dir * speed * forward; Vector dir_strafe = Vector(dir.z, 0,-dir.x); pos += dir_strafe * speed * strafe; view.BuildView(pos, pos + Vector(cosf(angles.x), sinf(angles.y), sinf(angles.x)), Vector(0, 1, 0)); render.SetTransform(Render::View, view); proj.BuildProjection(45.0f * RADIAN, (float)render.GetDevice()->GetHeight() / (float)render.GetDevice()->GetWidth(), 1.0f, 1000.0f); render.SetTransform(Render::Projection, proj); }
      
      





エイリアス名はFreeCameraプレフィックスを使用することに注意してください。 これは、エイリアスがどのオブジェクトに属しているかを理解できるようにする特定の命名規則を順守するために行われます。 これが行われない場合、さらなる開発によりエイリアスの数が増加し、時間が経つにつれて、相互に参照するエイリアスの束を取得できます。これはすべて制御の対象ではありません。 エラーのある仕事を見つけるのは非常に難しく、時間がかかります。 したがって、命名規則の導​​入が必要です。



最も興味深い部分に移りましょう-エイリアス自体を設定します。 それらはjsonファイルに保存されます。 カメラのエイリアスを記述するファイルは次のようになります。



 { "Aliases" : [ { "name" : "FreeCamera.MOVE_FORWARD", "AliasesRef" : [ { "names" : ["KEY_W"], "modifier" : 1.0 }, { "names" : ["KEY_I"], "modifier" : 1.0 }, { "names" : ["KEY_S"], "modifier" : -1.0 }, { "names" : ["KEY_K"], "modifier" : -1.0 } ]}, { "name" : "FreeCamera.MOVE_STRAFE", "AliasesRef" : [ { "names" : ["KEY_A"], "modifier" : -1.0 }, { "names" : ["KEY_J"], "modifier" : -1.0 }, { "names" : ["KEY_D"], "modifier" : 1.0 }, { "names" : ["KEY_L"], "modifier" : 1.0 } ]}, { "name" : "FreeCamera.MOVE_FAST", "AliasesRef" : [ { "names" : ["KEY_LSHIFT"] } ]}, { "name" : "FreeCamera.ROTATE_ACTIVE", "AliasesRef" : [ { "names" : ["MS_BTN1"] } ]}, { "name" : "FreeCamera.ROTATE_X", "AliasesRef" : [ { "names" : ["MS_X"] } ]}, { "name" : "FreeCamera.ROTATE_Y", "AliasesRef" : [ { "names" : ["MS_Y"] } ]}, { "name" : "FreeCamera.RESET_VIEW", "AliasesRef" : [ { "names" : ["KEY_R", "KEY_LCONTROL"] } ]} ] }
      
      





エイリアスは非常に簡単に説明されています。名前をエイリアスに設定し(パラメータ名)、エイリアスへのリンクの配列を設定します(パラメータAliasesRef)。 各エイリアス参照に対して、modificatorパラメーターを指定できます。このパラメーターは、GetAliasValueメソッドが呼び出されたときに取得される値に適用される乗数として使用されます。 エイリアスMOVE_FORWARDおよびMOVE_STRAFEは、このパラメーターを使用して、ジョイスティックのスティックの動作をシミュレートします。 2つの軸のそれぞれに対して[-1..1]の範囲の値を与えるのはジョイスティックスティックです。 キーの組み合わせ、つまり ホットキー、namesパラメーターは名前の配列です。 エイリアスRESET_VIEWは、LCTRL + Rキーの組み合わせのホットキーを設定する例です。



たとえば、KEY_W、MS_BTN1など、エイリアスへの参照で検出される名前をさらに詳しく考えてみましょう。 実際には、何らかの方法で特定のキーへのリンクが必要であり、そのようなリンクはハードウェアエイリアスと呼ばれます。 したがって、私たちのシステムには、ユーザー定義(コード内で処理します)とハードウェアエイリアスの2種類のエイリアスがあります。 メソッド自体は次のとおりです。



 bool GetAliasState(int alias, bool exclusive, AliasAction action); float GetAliasValue(int alias, bool delta);
      
      





入力メソッドは、GetAliasメソッドが呼び出されたときに受け取ったユーザーエイリアスの識別子を受け入れます。 この制限は、ハードウェアエイリアスを直接使用する誘惑を回避するために導入されたもので、ユーザー定義のエイリアスのみが常に使用されていました。



デバッグを含むデバッグホットキーを挿入する場合は、次の2つの方法のいずれかを使用します。



 bool DebugKeyPressed(const char* name, AliasAction action); bool DebugHotKeyPressed(const char* name, const char* name2, const char* name3);
      
      





どちらの方法も、ハードウェアエイリアスの名前を受け入れます。 したがって、販売ホットキーの処理には2つの方法のいずれかが使用されるため、すべての販売ホットキーの処理を無効にする設定を追加することは難しくありません。 セールスホットキーの処理を無効にする別のコードは必要ありません。 システム自体がそれらを無効にします。 したがって、リリースビルドにはリリース機能は含まれません。



実装のより詳細な説明に移りましょう。 コードのロジックのみを以下に説明します。 キーボードとマウスを使用するために、DirectInputを使用したため、DirectInputを操作するためのコードはスキップされます。



ハードウェアエイリアスの構造の説明から始めましょう。



 enum Device { Keyboard, Mouse, Joystick }; struct HardwareAlias { std::string name; Device device; int index; float value; };
      
      





次に、エイリアスの構造について説明します。



 struct AliasRefState { std::string name; int aliasIndex = -1; bool refer2hardware = false; }; struct AliasRef { float modifier = 1.0f; std::vector<AliasRefState> refs; }; struct Alias { std::string name; bool visited = false; std::vector<AliasRef> aliasesRef; };
      
      





次に、メソッドの実装に取り​​かかりましょう。 初期化メソッドから始めましょう:



 bool Controls::Init(const char* name_haliases, bool allowDebugKeys) { this->allowDebugKeys = allowDebugKeys; //Init input devices and related stuff JSONReader* reader = new JSONReader(); if (reader->Parse(name_haliases)) { while (reader->EnterBlock("keyboard")) { haliases.push_back(HardwareAlias()); HardwareAlias& halias = haliases[haliases.size() - 1]; halias.device = Keyboard; reader->Read("name", halias.name); reader->Read("index", halias.index); debeugMap[halias.name] = (int)haliases.size() - 1; reader->LeaveBlock(); } while (reader->EnterBlock("mouse")) { haliases.push_back(HardwareAlias()); HardwareAlias& halias = haliases[(int)haliases.size() - 1]; halias.device = Mouse; reader->Read("name", halias.name); reader->Read("index", halias.index); debeugMap[halias.name] = (int)haliases.size() - 1; reader->LeaveBlock(); } } reader->Release(); return true; }
      
      





カスタムエイリアスを読み込むために、LoadAliasesメソッドについて説明します。 エイリアスを記述するファイルが変更された場合、たとえば、ユーザーが設定でコントロールを再定義した場合、同じ方法が使用されます。



 bool Controls::LoadAliases(const char* name_aliases) { JSONReader* reader = new JSONReader(); bool res = false; if (reader->Parse(name_aliases)) { res = true; while (reader->EnterBlock("Aliases")) { std::string name; reader->Read("name", name); int index = GetAlias(name.c_str()); Alias* alias; if (index == -1) { aliases.push_back(Alias()); alias = &aliases.back(); alias->name = name; aliasesMap[name] = (int)aliases.size() - 1; } else { alias = &aliases[index]; alias->aliasesRef.clear(); } while (reader->EnterBlock("AliasesRef")) { alias->aliasesRef.push_back(AliasRef()); AliasRef& aliasRef = alias->aliasesRef.back(); while (reader->EnterBlock("names")) { aliasRef.refs.push_back(AliasRefState()); AliasRefState& ref = aliasRef.refs.back(); reader->Read("", ref.name); reader->LeaveBlock(); } reader->Read("modifier", aliasRef.modifier); reader->LeaveBlock(); } reader->LeaveBlock(); } ResolveAliases(); } reader->Release(); }
      
      





ResolveAliases()メソッドは、ダウンロードコードにあります。 このメソッドは、ロードされたエイリアスをリンクします。 リンクコードは次のようになります。



 void Controls::ResolveAliases() { for (auto& alias : aliases) { for (auto& aliasRef : alias.aliasesRef) { for (auto& ref : aliasRef.refs) { int index = GetAlias(ref.name.c_str()); if (index != -1) { ref.aliasIndex = index; ref.refer2hardware = false; } else { for (int l = 0; l < haliases.size(); l++) { if (StringUtils::IsEqual(haliases[l].name.c_str(), ref.name.c_str())) { ref.aliasIndex = l; ref.refer2hardware = true; break; } } } if (index == -1) { printf("alias %s has invalid reference %s", alias.name.c_str(), ref.name.c_str()); } } } } for (auto& alias : aliases) { CheckDeadEnds(alias); } }
      
      





リンクコードにはCheckDeadEndsメソッドが含まれています。 メソッドの目的は、循環参照を識別することです。なぜなら、 そのようなリンクは処理できず、それらに対する保護が必要です。



 void Controls::CheckDeadEnds(Alias& alias) { alias.visited = true; for (auto& aliasRef : alias.aliasesRef) { for (auto& ref : aliasRef.refs) { if (ref.aliasIndex != -1 && !ref.refer2hardware) { if (aliases[ref.aliasIndex].visited) { ref.aliasIndex = -1; printf("alias %s has circular reference %s", alias.name.c_str(), ref.name.c_str()); } else { CheckDeadEnds(aliases[ref.aliasIndex]); } } } } alias.visited = false; }
      
      





次に、ハードウェアエイリアスの状態を調べる方法に移ります。



 bool Controls::GetHardwareAliasState(int index, AliasAction action) { HardwareAlias& halias = haliases[index]; switch (halias.device) { case Keyboard: { //code that access to state of keyboard break; } case Mouse: { //code that access to state of mouse break; } } return false; } bool Controls::GetHardwareAliasValue(int index, bool delta) { HardwareAlias& halias = haliases[index]; switch (halias.device) { case Keyboard: { //code that access to state of keyboard break; } case Mouse: { //code that access to state of mouse break; } } return 0.0f; }
      
      





エイリアス自体のポーリングコード:



 bool Controls::GetAliasState(int index, AliasAction action) { if (index == -1 || index >= aliases.size()) { return 0.0f; } Alias& alias = aliases[index]; for (auto& aliasRef : alias.aliasesRef) { bool val = true; for (auto& ref : aliasRef.refs) { if (ref.aliasIndex == -1) { continue; } if (ref.refer2hardware) { val &= GetHardwareAliasState(ref.aliasIndex, Active); } else { val &= GetAliasState(ref.aliasIndex, Active); } } if (action == Activated && val) { val = false; for (auto& ref : aliasRef.refs) { if (ref.aliasIndex == -1) { continue; } if (ref.refer2hardware) { val |= GetHardwareAliasState(ref.aliasIndex, Activated); } else { val |= GetAliasState(ref.aliasIndex, Activated); } } } if (val) { return true; } } return false; } float Controls::GetAliasValue(int index, bool delta) { if (index == -1 || index >= aliases.size()) { return 0.0f; } Alias& alias = aliases[index]; for (auto& aliasRef : alias.aliasesRef) { float val = 0.0f; for (auto& ref : aliasRef.refs) { if (ref.aliasIndex == -1) { continue; } if (ref.refer2hardware) { val = GetHardwareAliasValue(ref.aliasIndex, delta); } else { val = GetAliasValue(ref.aliasIndex, delta); } } if (fabs(val) > 0.01f) { return val * aliasRef.modifier; } } return 0.0f; }
      
      





最後のキーはキーキーのポーリングです。



 bool Controls::DebugKeyPressed(const char* name, AliasAction action) { if (!allowDebugKeys || !name) { return false; } if (debeugMap.find(name) == debeugMap.end()) { return false; } return GetHardwareAliasState(debeugMap[name], action); } bool Controls::DebugHotKeyPressed(const char* name, const char* name2, const char* name3) { if (!allowDebugKeys) { return false; } bool active = DebugKeyPressed(name, Active) & DebugKeyPressed(name2, Active); if (name3) { active &= DebugKeyPressed(name3, Active); } if (active) { if (DebugKeyPressed(name) | DebugKeyPressed(name2) | DebugKeyPressed(name3)) { return true; } } return false; }
      
      





状態を更新するための機能はまだあります:



 void Controls::Update(float dt) { //update state of input devices }
      
      





以上です。 システムは、最小限のコードで非常にシンプルであることがわかりました。 同時に、入力デバイスのステータスをポーリングする問題を効果的に解決します。



稼働中のシステムの使用例へのリンク



また、このシステムはAtumというエンジン用に作成されました。 すべてのエンジンソースのリポジトリ -彼らは多くの興味深いものを持っています。



All Articles