Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

n2k

Frischling

  • »n2k« ist der Autor dieses Themas
  • Private Nachricht senden

1

19.11.2014, 22:00

InputManager sinnvoll in Architektur einbinden

Hallo zusammen,

ich arbeite gerade an einem kleinen Spiel und habe mir dazu eine kleine "GameEngine"/Grundstruktur auf die Beine gestellt.
Diese sieht folgendermaßen aus:

Ich habe eine Main-Klasse „GameApp“, welche die Applikation kapselt bzw. die eigentliche GameEngine repräsentiert.

In der GameApp-Klasse gibt es eine Methode „GameLoop()“ welche die Hauptschleife des Spiels darstellt (ProcessInput, Update, Render). Diese beinhaltet auch die Win32-Message-Loop, in welcher ich die Inputs vom Betriebssystem beziehe.

Generell besteht meine GameEngine aus GameStates und GameComponents. Ein GameState ist ein Zustand, in welchem sich das Spiel befindet. Beispiel: beim Start des Programms befindet es sich im State „IntroGameState“. Nachdem man beispielsweise den „Play Button“ klickt, findet ein Zustandsübergang zum State „IngameGameState“ statt. Dazu habe ich einfach eine simple StateMachine gebastelt. Außerdem gibt es Komponenten (GameComponents) wie z.B. einem „Player-Objekt“ oder einem „Gegner“, „Grid“, „Menu“, etc.

Ein GameComponent implementiert dabei das Interface IDrawable (renderbar) oder IUpdateable („updatebar“) oder eben beide.

Ein GameState kann wiederum GameComponents enthalten.

Ich möchte nun einen InputManager schreiben. Momentan habe ich dazu ein Objekt der Klasse InputManager, welches von der GameApp-Hauptklasse verwaltet wird.

Der Header des InputManagers sieht wie folgt aus (grob/under construction):

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
enum InputType
{
  Keyboard = 0x01,
  Mouse    = 0x02,
  Joystick = 0x04
};

struct InputContext
{
  InputType InputType;
  std::function<void(void)> Callback; // Signatur ändert sich noch (Input soll dann über die Callback an die jeweilgen Komponenten überreicht werden)

  InputContext(::InputType inputType, std::function<void(void)> callback) :
    InputType(inputType),
    Callback(callback)
  {}
};

class InputManager
{
public:
  InputManager();
  ~InputManager();

  void ProcessRawInputEvent(const sf::Event& rawEvent);
  void Update();
  void Dispatch();
  bool Register(std::string context, std::function<void(void)> callback);

  void SubscribeKeyboardInput(std::string context);
  void SubscribeMouseInput(std::string context);
  void SubscribeJoystickInput(std::string context);
  void SubscribeAll(std::string context);

private:
  std::map<std::string, InputContext> m_activeContexts;
};


Ich habe mir das folgendermaßen gedacht: Der InputManager wird mit RawInput vom OS gefüttert und leitet den Input an interessierte Abonnenten weiter. Dabei können sich z.B. GameComponents beim InputManager registrieren und einen Funktionpointer an diesen geben, damit der InputManager im Falle eines Inputs die entsprechenden Callbacks rufen kann. Außerdem können die Komponenten sich für bestimmte Input-Typen registrieren (Subscribe...).

Die registrierten Komponenten mit deren Callback werden aktuell in einer map gespeichert. Context ist ein einfacher String-Identifier, welcher die verschiedenen Komponenten/Kontexte auseinanderhalten soll.

Meine Frage ist nun: ist das einigermaßen sinnig? Außerdem weiß ich nicht genau, wie ich den Zugriff auf den InputManager effizient/sauber/schön gewährleisten soll. Dieser wird ja von der GameApp verwaltet. Das Spiel besteht aus mehreren GameStates, welche wiederum aus mehreren/bis zu vielen GameComponents bestehen können. Sowohl GameStates als auch GameComponents sollten in der Lage sein, Inputs zu empfangen. Ich finde es aber „unschön“ eine Referenz des InputManagers von der GameApp an die einzelnen States zu reichen und von dort aus wiederum an die einzelnen/vielen GameComponents. Gibt es da eine elegantere Möglichkeit? Die Referenz muss ich durchreichen, damit ich von den jeweiligen Komponenten aus registrieren kann bzw. die Subscribe-Methoden aufrufen kann. Oder sollte ich das Registrieren und Subscriben aus der GameApp-Klasse aus machen? Wäre aber auch unschön, da ich dann alle GameKomponenten in der GameApp kennen müsste. Extern/Global wäre eine weitere Möglichkeit die mir einfallen würde. Ebenfalls unschön :)

Habt ihr vielleicht eine Idee/Tipp(s) wie ich das am saubersten umsetzen kann?

Bei Bedarf kann ich auch mehr Source-Code nachreichen.

Danke im Voraus und Gruß,
n2k

LukasBanana

Alter Hase

Beiträge: 1 097

Beruf: Shader Tools Programmer

  • Private Nachricht senden

2

20.11.2014, 13:48

Das klingt mir alles sehr nach dem Observer Pattern, und das ist ja auch keine Schlechte Idee :-)

Allerdings würde ich nicht alles in Strings speichern.
Zum Einen besteht dabei die Gefahr, dass du mal einen Tippfehler machst, den der Compiler nicht als Fehler erkennt.
Zum Anderen sind String Vergleiche teuer und hier eher unnötig.
Vielleicht reichen dir hier ID Nummern anstatt überall "std::string context" zu verwenden.

BTW: Es heißt "updatable" und nicht "updateable".

Gruß,
Lukas

DeKugelschieber

Community-Fossil

Beiträge: 2 641

Wohnort: Rheda-Wiedenbrück

Beruf: Software-Entwickler

  • Private Nachricht senden

3

20.11.2014, 19:35

Oder übergib direkt Objekte. So mache ich das: C++, Js

n2k

Frischling

  • »n2k« ist der Autor dieses Themas
  • Private Nachricht senden

4

20.11.2014, 22:54

Das klingt mir alles sehr nach dem Observer Pattern, und das ist ja auch keine Schlechte Idee :-)

Hallo LukasBanana,

vielen Dank! Ich hörte schon öfter vom Observer-Pattern. Im Eifer meines Gefechts ist das bei mir total untergegangen :) Dieses Pattern ist aber glaube ich genau das, was ich suche!


BTW: Es heißt "updatable" und nicht "updateable".

Danke für den Typo-Hinweis. Mich beruhigt es, dass große US-Konzerne genauso schlechtes Englisch sprechen/schreiben wie ich: siehe hier :)

Oder übergib direkt Objekte. So mache ich das: C++, Js

Hallo DeKugelschieber, danke für deinen Source. Das sieht auch sehr interessant aus.

Werde mir beides mal näher betrachten.

Gruß
n2k

LukasBanana

Alter Hase

Beiträge: 1 097

Beruf: Shader Tools Programmer

  • Private Nachricht senden

5

21.11.2014, 11:17

Mich beruhigt es, dass große US-Konzerne genauso schlechtes Englisch sprechen/schreiben wie ich: siehe hier :)

Nice :D

buggypixels

Treue Seele

Beiträge: 125

Wohnort: Meerbusch

Beruf: Programmierer

  • Private Nachricht senden

6

21.11.2014, 13:04

Ich finde das mit Listenern oder auch Callbacks immer unschön und sehr ineffizient. Jedes Mal wird dann bei einem "Event" wild durch die Gegend gesprungen etc.
Definiere doch einfach Events. Der InputManager schreibt einfach eine Liste von Events und fertig. Sagen wir mal so:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct InputEvent {
  int state;
 char key;
}

InputEvent events[256];
int nrEvents = inputManager.process(events,256);

int InputManager::process(InputEvent* events,int maxEvents) {
  int num = 0;
 if (..... ) {
   InputEvent& e = events[num++];
  e.state = 12; (key pressed)
  e.char = key;
  }
  return num;
}

Dann reichst Du das einfach an die Objekte weiter, die sich dafür interessieren.
Dadurch ist der InputManager effizient und alle anderen auch. Kein herumspringen im Code und hier eine Callback Methode aufrufen oder da einen Listener.
Man könnte die Sachen auch zu einem struct zusammenfassen:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
struct InputContext {
  InputEvent events[256];
  int num;
  void reset() {
    num = 0;
  }
  void add(int state,char key) {
    assert(num < 256);
    InputEvent& e = events[num++];
   e.state = state;
   e.key = key;
  }
}

Außerdem kann dann jedes Objekt, welches auch diese Events verarbeiten soll einfach durch diese Array gehen und das geht schnell und effektiv.

DeKugelschieber

Community-Fossil

Beiträge: 2 641

Wohnort: Rheda-Wiedenbrück

Beruf: Software-Entwickler

  • Private Nachricht senden

7

21.11.2014, 13:47

Jop ne Event Queue ist auch praktisch, siehe hier.

n2k

Frischling

  • »n2k« ist der Autor dieses Themas
  • Private Nachricht senden

8

21.11.2014, 22:33

Hi zusammen,

danke für eure Beiträge.

@LetsGo: Auch eine gute Idee. Du meinst so ähnlich wie beispielsweise im Event-System in C#.NET (Sender, Object)?
Zum String: Jap. Wollte ich nicht so lassen. War erstmal nur Quick&Dirty. In der Regel mache ich auch einen Bogen um solche Stringvergleiche :)


Dann reichst Du das einfach an die Objekte weiter, die sich dafür interessieren.


Und wie würdest du das am Besten an interessierte Abonnenten weiterreichen? Setter von den Objekten aufrufen? Dann hast du aber auch das "Umhergespringe"? Hast du vielleicht ein kleines Stück Code dazu? Wäre sehr nett. Danke.

@DeKugelschieber: Event Queue klingt auch interessant.

Nochmal zum Thema Observer-Pattern: bin über folgenden Link gestoßen:

Hier wird das Observern-Pattern nochmal ein wenig verallgemeinert (mit Hilfe von Templates und std::function). Außerdem wird die Tiefe der Verbungshirarchie verringert, da Observer jetzt einfache Methoden sein können und nicht mehr unbedingt ein zusätzliches Observer Interface implementien müssen. Der Ansatz gefällt mir auch ganz gut.

Danke für eure Anteilnahme :)

Gruß
n2k

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »n2k« (22.11.2014, 00:41)


buggypixels

Treue Seele

Beiträge: 125

Wohnort: Meerbusch

Beruf: Programmierer

  • Private Nachricht senden

9

23.11.2014, 20:24

Da ich deine Engine nicht kenne und deine Beschreibungen etwas kurz sind, kann ich dir kein konkretes Beispiel geben.
Aber Du weißt doch selber, wer überhaupt solche Events verarbeiten soll. Willst Du in den GameStates verarbeiten?
Oder in den GameObjects?
Aber ich versuche es trotzdem mal (ausgehend von meinem ersten Beispiel):

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Game {

public:
void Game::mainLoop() {
  ....
  m_Events.reset();
  int numEvents = inputProcessor.createEvents(m_Events);
  // current state 
  m_CurrentState->processInput(m_Events,numEvents);
  for ( int i = 0; i < m_Objects.size();++i ) {
    m_Objects[i].processInput(m_Events,numEvents);
  }
}
private:
  GameState* m_CurrentState;
  InputEvent m_Events[256];

Hier ist jetzt aber ganz viel geraten und angenommen. Da m_Events ein festes Array ist, kannst Du schnell und effektiv da durch gehen:

C-/C++-Quelltext

1
2
3
4
5
6
7
void GameObject::processInput(InputEvents* events,int num) {
  for ( int i = 0; i < num; ++i ) {
    if ( events[i].state == 1 ) {
      // do something
    }
  }
}

Das sollten dann im GameState und GameObject virtuelle Methoden sein, damit jeder der möchte sie überschreiben und implementieren kann.
Ich hoffe es ist etwas klarer.

Werbeanzeige