Designtips benötigt zu "Ereignisverarbeitung"
-
Hallo zusammen,
also ich tue mich beim Design immer noch wahnsinnig schwer, daher hier mal 2 meiner aktuellen Probleme:
1. Design der Ereignisklassen:
Es geht darum, dass eine Aktion (z.B. durch den menschlichen Spieler) ausgelöst wird. Dies kann z.B. über das Drücken eines Button, die Auswahl eines Menüeintrages oder einen Hotkey geschehen. Zusätzlich gibt es verschiedene dieser Aktionen (z.B. will Gebäude errichten, will Angreifen, errichtet Gebäude, greift an, ...). Die Grunddee war, eine abstrakte Basisklasse Activity zu machen, die durch eine parameterlose Methode aktiviert wird. Davon abgeleitet gibt es dann z.B. Klassen für die Aktivitäten "will Gebäude errichten", "will angreifen" usw. Das klappt auch wunderbar. Nun hab ich aber festgestellt, dass diese beiden spezielleren Klassen eigentlich nahezu identisch in ihrem Code sind! D.h., eine Klasse, die bei Aktivierung unterscheiden kann, was passieren soll würde eigentlich langen. Das funktioniert aber nicht mit einer parameterlosen Aktivierungsmethode. Nun könnte ich entweder der Methode einen Paameter mitgeben für die Art der Aktion, oder für jede mögliche Aktione eine eigene Methode bereitstellen. Beides behagt mir nicht so recht!
Qas wäre denn hier eine empfehlenswerte Vorgehensweise?2. Die Koordination zwischen GUI und (mehrstufigen) Folgeaktionen:
Das Thema hatte ich hier (http://www.c-plusplus.net/forum/268900) schon mal aufgebracht und auch in nem anderen Forum schon einiges Feedback bekommen. Problem ist hierbei Mehrstufigkeit von Aktionen. Der Spieler löst die Aktion "will Gebäude errichten" aus. Das sorgt z.B. für visuelles Feedback. Eine mögliche Folgeaktion durch Mausklick ins Spielfeld wäre: "Gebäude errichten". Aber woher weiss ich zu dem Zeitpunkt, dass dies die gewünschte Aktion ist? Setze ich irgendwo im Spielerobjekt oder beim Maus-Ereignisverarbeiter einen entsprechenden Status? Eine andere mögliche Folgeaktion wäre nämlich auch "will doch kein Gebäude errichten", eine weitere wäre "will angreifen". Beide hätten andere Auswirkungen bei einem Klick ins Spielfeld. Wie bekomme ich die Information über die mögliche Folgeaktion denn vermittelt und an wen vermittle ich sie? Ein Vorschlag, den ich erhalten habe ist. dass die Aktion "will Gebäude errichten" als nächste Aktion "Gebäude errichten" an die Ereignisverarbeitung des LMB hängt. Die Aktion "will Gebäude doch nicht errichten" müsste dies dann bei der Ereignisverarbeitung des LMB wieder rückgängig machen. Das gefällt mir aber nicht so ganz, da dann eine Aktion ja die andere kennen müsste! Zudem müssen bei Spieler und Spielfeld später das neue Gebäude ja auch noch eingetragen werden!
Ich hatte mir noch überlegt, ein Objekt für Spieleraktionen zu machen, das immer genau weiss, in welchem Zustand es sich gerade befindet. Dieses Objekt würde dann überall dort eingehanden, wo Ereignisse des menschlichen Spielers verarbeitet werden! Was haltet ihr davon?Ich hoffe, ich konnte die Probleme einigermaßen beschreiben!
Vielen Dank schon mal!
Ciao
-
1. Ginge da nicht ein
enumfür die unterschiedlichen Aktionen?Und warum ist
Activityim Grundzustand inaktiv und muss erst aktiviert werden? Ich würde sonst einen Konstruktor bereitstellen, der die Art der Aktion (eben diesesenum) als Parameter nimmt.2. Ich würde jetzt spontan in einer entsprechenden Verwaltungsklasse zwischenspeichern, was gerade getan wird, und welche Semantik der nächste Mausklick hat. Das ist auch praktisch, weil du diesen Status wahrscheinlich nicht nur für das Ereignis ("beginne Bau") selbst, sondern für andere Dinge wie die Darstellung ("zeige Gebäude-Umriss an der Stelle des Mauszeigers") benutzen kannst.
-
Nexus schrieb:
1. Ginge da nicht ein
enumfür die unterschiedlichen Aktionen?Und warum ist
Activityim Grundzustand inaktiv und muss erst aktiviert werden? Ich würde sonst einen Konstruktor bereitstellen, der die Art der Aktion (eben diesesenum) als Parameter nimmt.Naja, die Activities hängen an den Buttons, Menüeinträgen etc. Bei Auslösen wird die activate()-Methode gerufen. Hier mal ein Auszug:
Abstrakte Basisklasse:
#include "UserActivityC.h" UserActivityC::UserActivityC(AnimObjectC& cursorObject, PlayerC& human) : cursor(cursorObject), humanPlayer(human) { } UserActivityC::~UserActivityC() {} void UserActivityC::changeCursorObject(const int type) { // selber Aktion nochmal aktiviert => type identisch => Sichtbarkeit umschalten // ansonsten Animation wechseln if (cursor.getActiveAnimationType() == type) { cursor.toggleVisible(); } else { cursor.setActiveAnimationType(type); } }Hier wird also schon die Darstellung am MausCursor zentral umgeschalten. Eine speziellere Klasse davon, die z.B. an einem Button hängt ist:
TowerGadgetActivityC::TowerGadgetActivityC(AnimObjectC& cursorObject, PlayerC& human) : UserActivityC(cursorObject, human) { } TowerGadgetActivityC::~TowerGadgetActivityC() {} void TowerGadgetActivityC::activate() { changeCursorObject(GFX_TOWERCURSOR); }Eine andere speziellere Klasse hätte in ihrer activate-Methode nur noch
changeCursorObject(GFX_LIGHTNINGCURSOR);stehen.
-
Reth schrieb:
Naja, die Activities hängen an den Buttons, Menüeinträgen etc. Bei Auslösen wird die activate()-Methode gerufen.
Ah, so eine Art Commands. Die GUI-Elemente kennen den Aktionstyp wahrscheinlich nicht, daher kommt der Konstruktor am ehesten in Frage. Also statt
myButton.AddListener( new TowerGadgetActivity(...) );nimmst du
myButton.AddListener( new GadgetActivity(Tower, ...) );wobei dann
Towerein Enumerator ist. Wäre das was?Übrigens, noch ein paar nebensächliche Codestilanmerkungen (ich will dir nicht zu gross im Stil rumpfuschen, fasse sie als Anregungen auf ;)):
- Warum ein explizites "C"-Postfix, um Klassen zu erkennen? Immerhin kein Präfix, aber trotzdem eventuelle Gegenargumente.
- Willst du nicht ein
enumstattintnehmen, wenn nur eine festgelegte Anzahl an Zuständen möglich sein soll? - Warum definierst du bei abgeleiteten Klassen leere Destruktoren?
- Top-Level-Consts bei Parametern (
const int) würde ich vermeiden, da sie nicht wirklich was bringen (im Gegensatz zu Zeigern und Referenzen aufconst) – siehe auch hier
-
Nexus schrieb:
Reth schrieb:
Naja, die Activities hängen an den Buttons, Menüeinträgen etc. Bei Auslösen wird die activate()-Methode gerufen.
Ah, so eine Art Commands. Die GUI-Elemente kennen den Aktionstyp wahrscheinlich nicht, daher kommt der Konstruktor am ehesten in Frage. Also statt
myButton.AddListener( new TowerGadgetActivity(...) );nimmst du
myButton.AddListener( new GadgetActivity(Tower, ...) );wobei dann
Towerein Enumerator ist. Wäre das was?Genau so ähnlich meinte ich das mit der parametrierbaren activate-Methode. Denn eigentlich bräuchte ich ja dann nicht mehr verschiedene Activity-Objekte pro Button sondern nur noch eines für alle, so lange ich im Code keine Fallunterscheidungen mehr machen müsste, sondern den Typ nur durchreiche (habe dafür schon Symbols, die auch an anderen Stellen verwendet werden).
Nexus schrieb:
Übrigens, noch ein paar nebensächliche Codestilanmerkungen (ich will dir nicht zu gross im Stil rumpfuschen, fasse sie als Anregungen auf ;)):
Danke! Immer gerne!
Nexus schrieb:
[*]Willst du nicht ein
enumstattintnehmen, wenn nur eine festgelegte Anzahl an Zuständen möglich sein soll?Wofür meinst Du? Für die Werte wie: GFX_TOWERCURSOR? Dafür hatte ich mir von Anfang an Symbols definiert. Spricht aber nichts gg. ne enum (was ist denn dann der Vorteil?).
Nexus schrieb:
[*]Warum definierst du bei abgeleiteten Klassen leere Destruktoren?
Öhm, hm, tja. Hatte ich in diesem Thread noch nicht erwähnt, bin noch relativer C++-Anfänger. Braucht man das nicht?
Nexus schrieb:
[*]Top-Level-Consts bei Parametern (
const int) würde ich vermeiden, da sie nicht wirklich was bringen (im Gegensatz zu Zeigern und Referenzen aufconst) – siehe auch hierHier hatte ich mich an nen Grundsatz aus nem C++-Buch gehalten, quasi zu consten, was das Zeug hält. Zumindest kann man dann den Parameter in der Methode nicht mehr versehentlich ändern!
-
Reth schrieb:
Wofür meinst Du? Für die Werte wie: GFX_TOWERCURSOR? Dafür hatte ich mir von Anfang an Symbols definiert. Spricht aber nichts gg. ne enum (was ist denn dann der Vorteil?).
Ja, genau für die Werte. Der Vorteil von
enumgegenüberintist vor allem Typsicherheit:enum Cursor { GFX_TOWERCURSOR, ... }; void f(int cursor); void g(Cursor cursor); f(3); // (fälschlicherweise) ok g(3); // CompilerfehlerAusserdem wird den einzelnen Enumeratoren jeweils ein einzigartiger Wert zugewiesen, d.h. du musst dich nicht um die IDs kümmern. Gegenüber
#definehatenumden zusätzlichen Vorteil, dass der Scope berücksichtigt wird und Debug-Symbole vorhanden sind.Reth schrieb:
Braucht man das nicht?
Nur bei virtuellen Destruktoren, und auch da nur einmal in der Basisklasse (in abgeleiteten Klassen wäre der Destruktor dann automatisch auch virtuell). Der Destruktor gehört zu den automatisch generierten Funktionen und tut meist das Richtige. Erst wenn du spezielle Funktionalität brauchst, musst du ihn selbst definieren – aber dann ist er auch nicht leer.
Reth schrieb:
Hier hatte ich mich an nen Grundsatz aus nem C++-Buch gehalten, quasi zu consten, was das Zeug hält.
Das halte ich für eine schlechte Idee, denn auf diese Weise gehen wichtige
consts unter. Wichtig ist z.B. bei Zeigern/Referenzen auf konstante Objekte, um dem Aufrufer mitzuteilen, dass die Objekte von der Funktion nicht verändert werden. Bei Kopien (wieint) spielt es keine Rolle, was in der Funktion getan wird.Dass die versehentliche Änderung verhindert wird, stimmt natürlich. Das ist ein Grund für
constin der Funktionsdefinition, aber nicht in der -deklaration (die Funktionstypen sind in beiden Fällen identisch). Denn die Deklaration gehört zur Schnittstelle, dort bringt die Information desconstrein gar nichts, aber führt tendenziell zur Verwirrung.