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

Wirago

Alter Hase

  • »Wirago« ist der Autor dieses Themas

Beiträge: 1 193

Wohnort: Stockerau

Beruf: CRM Application Manager

  • Private Nachricht senden

1

07.08.2017, 13:04

Inputhandling > Zusammenfassen von If-Statements

Hallo Zusammen,

eigentlich ist es hier eher ein Syntax- bzw. Designthema, da die gewünschte Funktionalität gegeben ist. Allerdings gefällt mir der Aufbau des Codes nicht, habe aber keine wirklich gute Idee wie das besser umzusetzen ist. Vielleicht hat jemand hier eine Lösung:

Grundsätzlich geht es um eine Methode um User-Input per Tastatur entgegen zu nehmen. Sie hat zwei Parameter (aktuellen KeyboardStateund neuen KeyboardState) um mehrfaches Auslösen zu verhindern (zB. um Inventarfenster zu öffnen/schließen).

Handelt es sich bei der Eingabe um eine mit dem der Actionbar verknüpften Taste, dann wird - abhängig der hinterlegten Funktion des Actionbarslots - die auszuführende Aktion weitergegeben. Bei fixen Aktionen (Inventar öffnen/schließen) wird sie direkt angesteuert.

Soweit, so einfach.
Das Problem ergibt sich nun, dass im Laufe des Projektes durchaus mehrere Tasten belegt sein können, und dann wird die Methode entsprechend lang und schlecht wartbar:

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
public string HandleKeyStroke(KeyboardState ks, KeyboardState oldks)
{

    if (ks.IsKeyDown(GameplayButtons.ACTIONBAR_0) && oldks.IsKeyUp(GameplayButtons.ACTIONBAR_0))
        return UIManager.actionToBeTriggered("actionbar_0");
    if (ks.IsKeyDown(GameplayButtons.ACTIONBAR_1) && oldks.IsKeyUp(GameplayButtons.ACTIONBAR_1))
        return UIManager.actionToBeTriggered("actionbar_1");
    if (ks.IsKeyDown(GameplayButtons.ACTIONBAR_2) && oldks.IsKeyUp(GameplayButtons.ACTIONBAR_2))
        return UIManager.actionToBeTriggered("actionbar_2");
    if (ks.IsKeyDown(GameplayButtons.ACTIONBAR_3) && oldks.IsKeyUp(GameplayButtons.ACTIONBAR_3))
        return UIManager.actionToBeTriggered("actionbar_3");

    if (ks.IsKeyDown(GameplayButtons.CHARACTER) && oldks.IsKeyUp(GameplayButtons.CHARACTER))
        UIManager.ToggleWindowById("button_Character");

    if (ks.IsKeyDown(GameplayButtons.INVENTORY) && oldks.IsKeyUp(GameplayButtons.INVENTORY))
        UIManager.ToggleWindowById("button_Inventory");

    if (ks.IsKeyDown(GameplayButtons.QUESTLOG) && oldks.IsKeyUp(GameplayButtons.QUESTLOG))
        UIManager.ToggleWindowById("button_Questlog");

    if (ks.IsKeyDown(GameplayButtons.MAP) && oldks.IsKeyUp(GameplayButtons.MAP))
        UIManager.ToggleWindowById("button_Map");

    return "";
}


Jetzt würde ich das gerne irgendwie zusammen fassen, damit das nicht so eine lange Wurst aus If-Statements wird. :hmm:

Legend

Alter Hase

Beiträge: 731

Beruf: Softwareentwickler

  • Private Nachricht senden

2

07.08.2017, 13:36

Vielleicht würde als erste Verbesserung ein Dictionary, dass einen GameplayButton auf eine actionToBeTriggered/ToogleWindowById mappt, helfen.

Als große Verbesserung könnte sich vielleicht eine Variante des Commandpatterns anbieten.
"Wir müssen uns auf unsere Kernkompetenzen konzentrieren!" - "Juhu, wir machen eine Farm auf!"

Netzwerkbibliothek von mir, C#, LGPL: https://sourceforge.net/projects/statetransmitt/

Lares

1x Contest-Sieger

  • Private Nachricht senden

3

07.08.2017, 13:53

Naja eine erste Verbesserung wäre die && Abfragen in einzelne Funktionen umzuwandeln mMn. Also anstatt

C-/C++-Quelltext

1
2
 if (ks.IsKeyDown(GameplayButtons.ACTIONBAR_0) && oldks.IsKeyUp(GameplayButtons.ACTIONBAR_0))
        return UIManager.actionToBeTriggered("actionbar_0");

folgendes

C-/C++-Quelltext

1
2
 if (IsKeyPressed(GameplayButtons.ACTIONBAR_0))
        return UIManager.actionToBeTriggered("actionbar_0");


Ich verwende für sowas gerne folgende Begriffe:
Pressed -> 1. Frame in dem die Taste unten ist
Down -> Mindestens 2. Frame in dem die Taste unten ist
Released -> 1. Frame in dem die Taste oben ist
Up -> Mindestens 2. Frame in dem die Taste oben ist

Danach könntest du noch schauen, ob du Die Tastenabfragen selbst iwie gruppieren kannst (Systemkombinationen wie z.B. Alt-F4, GUI-Abfragen und Figurbewegung). Dadurch hättest du zumindest eine bessere Übersicht. Alles andere würde ich erstmal außen vor lassen, da du sonst unter Umständen viel Aufwand in etwas steckst, was das eigentliche Programm aus Nutzersicht nicht verbessert.

Jar

Treue Seele

Beiträge: 197

Wohnort: Lübeck

Beruf: Softwareentwickler

  • Private Nachricht senden

4

07.08.2017, 13:55

Irgendwo müssen diese Abfragen leider gemacht werden.
Für InputHandling speziell bei Spielen gibt es aber zum Beispiel ein schönes Command Pattern. http://gameprogrammingpatterns.com/command.html
Das kann man dann auch wunderbar erweitern und in einer Event Queue einbauen. http://gameprogrammingpatterns.com/event-queue.html

Wirago

Alter Hase

  • »Wirago« ist der Autor dieses Themas

Beiträge: 1 193

Wohnort: Stockerau

Beruf: CRM Application Manager

  • Private Nachricht senden

5

07.08.2017, 14:02

Danke für euren Input. Command Pattern ist wohl das, was ich gesucht hab :)

TGGC

1x Rätselkönig

Beiträge: 1 799

Beruf: Software Entwickler

  • Private Nachricht senden

6

08.08.2017, 11:12

Das Command Pattern scheint mir totaler Schwachsinn für diese Aufgabe, speziell so wie auf der Seite erklärt. Die if Kaskade, die man aufwendig pflegen muss hat man so trotzdem noch. Plus zig Klassen mit Ableitungen rundrum. Und dann baut man sich auch noch eine Einschränkung ein, die einem in einem echten Projekt wahrscheinlich sofort um die Ohren fliegt. Command* InputHandler::handleInput() kann genau ein Command zurueckgeben, was ist wenn ich schiessend nach links laufe und dann springen will. Dann baue ich ein Command das genau die 3 Sachen macht und dann auch noch eins für alle 2^n Kombinationen meiner n buttons?

Was sich bei mir in jahrelanger Praxis bewährt hat, ist ein simples Array, das die Infos zu allen Aktionen hält. Der Index lässt sich am einfachsten über Enums/Konstanten abbilden, so in der Art enum Action {left, right, jump, shoot}. Der simpelste usecase ist dann eine Funktion keyPressed(left), die man direkt dort im Code aufruft, wo sie gebraucht wird. Die Funktion schaut dann in das array, da steht drin welche Taste konkret hinter left steht, schaut nach ob die gedrueckt ist und gibt die Antwort. Angenommen ich hab Top Down Steuerung, das könnte dann so ausshehen:

C-/C++-Quelltext

1
2
3
vec moveXY((keyPressed(right)?1:0) - (keyPressed(left)?1:0), (keyPressed(up)?1:0) - (keyPressed(down)?1:0);
moveXY.normalize();
pos += moveXY * maxspeed;


Mach das mal mit dem komischen Pattern, da schreibt man 4 Klassen und braucht noch zig temporäre Variablen bevor man den Vektor sinnvoll zusammengebaut hat.


Fehlt noch der zweite usecase, der ist nuetzlich für Menus oder so Shortcuts wie Waffenwechseln. Mit dem Pattern da oben muss man sich irgendwie merken das man grad Waffen gewechselt hat, sonst wechselt man nächsten Frame wieder die Waffe. Was man eigentlich will: ein Event, wenn eine Taste gedrueckt oder losgelassen wird. Kann man natürlich einfach irgendwo global eine Funktion aufrufen, welche dann wieder die if Kaskade drin hat oder eleganter sein switch über das enum mit Aktionen. Noch schöner ist wenn man einen Callback registrieren kann: registerkeyDown(changeWeapon, OnWeaponChanged()); registerkeyUp(select, OnPauseMenuOpen());. Noch cooler wirds natuerlich wenn man Sachen wie OnWeaponChanged() aus eine Scriptsprache aufrufen kann und man dann im Config File diese Zuweisungen tätigen kann ohne. So kann das auch ein Nichtprogrammier recht leicht anpassen, wenn man ihm die Liste mit Befehlen gibt.

Renegade

Alter Hase

Beiträge: 494

Wohnort: Berlin

Beruf: Certified Unity Developer

  • Private Nachricht senden

7

08.08.2017, 16:44

Das Command Pattern scheint mir totaler Schwachsinn für diese Aufgabe, speziell so wie auf der Seite erklärt. Die if Kaskade, die man aufwendig pflegen muss hat man so trotzdem noch. Plus zig Klassen mit Ableitungen rundrum. Und dann baut man sich auch noch eine Einschränkung ein, die einem in einem echten Projekt wahrscheinlich sofort um die Ohren fliegt. Command* InputHandler::handleInput() kann genau ein Command zurueckgeben, was ist wenn ich schiessend nach links laufe und dann springen will. Dann baue ich ein Command das genau die 3 Sachen macht und dann auch noch eins für alle 2^n Kombinationen meiner n buttons?


Da muss ich dir komplett widersprechen. Das Command Pattern ist in dem Fall sehr sinnvoll. Das Command kapselt einzelnes Verhalten und ermöglicht diese sequentiell zu verarbeiten. Ich zitiere: "Encapsulate a request as an object, thereby letting users parameterize clients with different requests, queue or log requests, and support undoable operations." Dafür benötigst du lediglich ein Command für jede Art des Inputs und nicht für jede Kombination. Sollten in einer Execute mehrere Inputs abgearbeitet werden, dann werden sie das eben in einer Queue. Deine Variante führt zu unflexiblen Code und folgt nicht der allgemeinen Regel statischen von veränderlichen Code zu trennen. (Clean Code - Uncle Bob) Ggf. bitte nochmal das Pattern in "Design Patterns - Elements of Reusable Object-Oriented Software" von der Gang of Four nachlesen oder hier von Bob Nystrom als aufgearbeitetes Beispiel für Games.
Liebe Grüße,
René

Schorsch

Supermoderator

Beiträge: 5 145

Wohnort: Wickede

Beruf: Softwareentwickler

  • Private Nachricht senden

8

08.08.2017, 17:54

Das Command kapselt einzelnes Verhalten und ermöglicht diese sequentiell zu verarbeiten. Ich zitiere: "Encapsulate a request as an object, thereby letting users parameterize clients with different requests, queue or log requests, and support undoable operations." Dafür benötigst du lediglich ein Command für jede Art des Inputs und nicht für jede Kombination. Sollten in einer Execute mehrere Inputs abgearbeitet werden, dann werden sie das eben in einer Queue.

Naja aber das ist doch genau der Punkt. Du hast jeweils ein Command um dich nach rechts, links, oben und unten zu bewegen. Wenn dein "inputHandler" nur ein einzelnes Command zurück gibt dann musst du eben auch Commands für die diagonalen Bewegungsrichtungen schaffen. Abhilfe kann man sich natürlich schaffen indem so ein Input Handler mehrere Commands zurück geben kann. Dann könnten die Abfragen die jeweiligen Commands generieren und würden diese zurück geben. Danach könnten sie in die Queue gestopft werden um am Ende irgendwo verarbeitet zu werden. Darauf möchtest du vermutlich auch hinaus. In dem Beispiel wird das aber so erst mal nicht umgesetzt. An sich ist Command ein schönes Pattern, man sollte sich aber überlegen ob man es am Ende wirklich benötigt. Vergleich einfach mal den kurzen Code den TGGC da zeigt mit dem Code den ein Command Pattern hier benötigen würde. Flexibilität ist natürlich ein Argument, wobei ich mir die auch mit anderen weniger aufwendigeren Mitteln schaffen kann. Man sollte halt erst nachdenken bevor man solche Patterns benutzt. Die können einem halt viel Arbeit einbringen.
„Es ist doch so. Zwei und zwei macht irgendwas, und vier und vier macht irgendwas. Leider nicht dasselbe, dann wär's leicht.
Das ist aber auch schon höhere Mathematik.“

Wirago

Alter Hase

  • »Wirago« ist der Autor dieses Themas

Beiträge: 1 193

Wohnort: Stockerau

Beruf: CRM Application Manager

  • Private Nachricht senden

9

08.08.2017, 18:11

Ich habe die Bewegung der Spielfigur extra ausgelagert um zu verhindern, dass sich Aktionen und Bewegung gegenseitig blockieren.
Dort wird im Prinzip nur auf die Bewegungstasten reagiert, die Bewegung berechnet und dann die Figur bewegt. Drückt man also Links und Rechts gleichzeitig, dann steht die Figur still.

Zitat

Sollten in einer Execute mehrere Inputs abgearbeitet werden, dann werden sie das eben in einer Queue.


Hm, möchte ich das in einem RPG? Eigentlich möchte ich doch normal keine Attacken etc. queuen können, oder?

Renegade

Alter Hase

Beiträge: 494

Wohnort: Berlin

Beruf: Certified Unity Developer

  • Private Nachricht senden

10

08.08.2017, 18:52

Naja aber das ist doch genau der Punkt. Du hast jeweils ein Command um dich nach rechts, links, oben und unten zu bewegen. Wenn dein "inputHandler" nur ein einzelnes Command zurück gibt dann musst du eben auch Commands für die diagonalen Bewegungsrichtungen schaffen. Abhilfe kann man sich natürlich schaffen indem so ein Input Handler mehrere Commands zurück geben kann. Dann könnten die Abfragen die jeweiligen Commands generieren und würden diese zurück geben. Danach könnten sie in die Queue gestopft werden um am Ende irgendwo verarbeitet zu werden. Darauf möchtest du vermutlich auch hinaus. In dem Beispiel wird das aber so erst mal nicht umgesetzt. An sich ist Command ein schönes Pattern, man sollte sich aber überlegen ob man es am Ende wirklich benötigt. Vergleich einfach mal den kurzen Code den TGGC da zeigt mit dem Code den ein Command Pattern hier benötigen würde. Flexibilität ist natürlich ein Argument, wobei ich mir die auch mit anderen weniger aufwendigeren Mitteln schaffen kann. Man sollte halt erst nachdenken bevor man solche Patterns benutzt. Die können einem halt viel Arbeit einbringen.

Man könnte die Bewegungsrichtungen auch zusammenfassen zu einem MoveCommand und daraus einen Richtungsvektor berechnen (Wie sonst soll das klappen? 360 Commands erstellen für jeden Grad des Kreises?). Außerdem ist die Erweiterung, das mehrere zutreffende Commands zurück gegeben werden, jetzt nicht gerade schwer. Das hier zu schreiben dauert länger, als die Abänderung im Beispiel von Nystrom hinzuzufügen.
Das TGGCs Code kürzer ist bleibt unbestritten. Aber die Argumentation, dass das Command in einem echten Projekt um die Ohren fliegt ist absolut nicht haltbar. Ich kenne kaum ein großes Projekt welches den Input (jeglicher Art, ob Remote/Network/Proxy, KI etc. pp.) nicht in Commands kapselt. Es gehört zu einen der wichtigsten Patterns in der Spieleentwicklung und ist super flexibel.
Ich habe die Bewegung der Spielfigur extra ausgelagert um zu verhindern, dass sich Aktionen und Bewegung gegenseitig blockieren.
Dort wird im Prinzip nur auf die Bewegungstasten reagiert, die Bewegung berechnet und dann die Figur bewegt. Drückt man also Links und Rechts gleichzeitig, dann steht die Figur still.

Das kannst du auch sehr gerne weiterhin so lösen. Das sollte aber meiner Meinung nach nicht im Input geschehen, sondern in der Simulation deines Spiels. Inputs und deren Commands werden erstmal alle erstellt und entgegen genommen. Welche du dann filterst oder nicht bleibt dir im nächsten Schritt. Zum Beispiel könntest du dann sagen, sobald das Command Array von Bewegungen größer 0 ist, reagierst du nicht mehr auf Commands von Aktionen.
Liebe Grüße,
René

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »Renegade« (08.08.2017, 18:57)


Werbeanzeige