Ich habe ein grundlegendes 2D-Tower-Defense-Spiel in C ++.
Jede Map ist eine eigene Klasse, die von GameState erbt. Die Karte delegiert die Logik und den Zeichencode an jedes Objekt im Spiel und legt Daten wie den Kartenpfad fest. Im Pseudocode könnte der Logikabschnitt ungefähr so aussehen:
update():
for each creep in creeps:
creep.update()
for each tower in towers:
tower.update()
for each missile in missiles:
missile.update()
Die Objekte (Creeps, Tower und Missiles) werden in Zeigervektoren gespeichert. Die Türme müssen Zugriff auf den Kriechvektor und den Raketenvektor haben, um neue Raketen zu erstellen und Ziele zu identifizieren.
Die Frage ist: Wo deklariere ich die Vektoren? Sollten sie Mitglieder der Map-Klasse sein und als Argumente an die tower.update () -Funktion übergeben werden? Oder global deklariert? Oder gibt es andere Lösungen, die mir völlig fehlen?
Wenn mehrere Klassen auf dieselben Daten zugreifen müssen, wo sollten die Daten deklariert werden?
quelle
Antworten:
Wenn Sie eine einzelne Instanz einer Klasse in Ihrem Programm benötigen, bezeichnen wir diese Klasse als Service . Es gibt verschiedene Standardmethoden zum Implementieren von Diensten in Programmen:
Singletons . Vor 10-15 Jahren waren Singletons das große Designmuster, über das man Bescheid wissen musste. Heutzutage wird jedoch auf sie herabgesehen. Multithreading ist viel einfacher, aber Sie müssen die Verwendung auf jeweils einen Thread beschränken. Dies ist nicht immer das, was Sie möchten. Das Verfolgen von Lebensdauern ist genauso schwierig wie bei globalen Variablen.
Eine typische Singleton-Klasse sieht ungefähr so aus:
Abhängigkeitsinjektion (DI) . Dies bedeutet nur, dass der Service als Konstruktorparameter übergeben wird. Es muss bereits ein Service vorhanden sein, um ihn einer Klasse zuzuordnen. Daher können sich zwei Services nicht aufeinander verlassen. In 98% der Fälle ist dies das, was Sie möchten (und für die anderen 2% können Sie jederzeit eine
setWhatever()
Methode erstellen und den Dienst später übergeben) . Aus diesem Grund hat DI nicht die gleichen Kopplungsprobleme wie die anderen Optionen. Es kann mit Multithreading verwendet werden, da jeder Thread einfach eine eigene Instanz jedes Dienstes haben kann (und nur die freigeben kann, die er unbedingt benötigt). Es macht auch Code Unit-testbar, wenn Sie das interessieren.Das Problem bei der Abhängigkeitsinjektion besteht darin, dass mehr Speicher belegt wird. Jetzt benötigt jede Instanz einer Klasse Verweise auf jeden Dienst, den sie verwenden wird. Außerdem wird es ärgerlich, wenn Sie zu viele Dienste haben. Es gibt Frameworks, die dieses Problem in anderen Sprachen mindern. Aufgrund der mangelnden Reflektion von C ++ sind DI-Frameworks in C ++ jedoch in der Regel noch aufwändiger als nur manuell.
Auf dieser Seite (in der Dokumentation zu Ninject, einem C # DI-Framework) finden Sie ein weiteres Beispiel.
Die Abhängigkeitsinjektion ist die übliche Lösung für dieses Problem und die Antwort, die Sie auf Fragen wie diese auf StackOverflow.com am häufigsten erhalten. DI ist eine Art von Inversion of Control (IoC).
Service Locator . Im Grunde genommen nur eine Klasse, die eine Instanz jedes Dienstes enthält. Sie können dies mit Reflection tun oder einfach jedes Mal, wenn Sie einen neuen Service erstellen möchten, eine neue Instanz hinzufügen. Sie haben immer noch das gleiche Problem wie zuvor - Wie greifen Klassen auf diesen Locator zu? - was auf eine der oben genannten Arten gelöst werden kann, aber jetzt müssen Sie es nur für Ihre
ServiceLocator
Klasse tun , anstatt für Dutzende von Diensten. Diese Methode ist auch Unit-testbar, wenn Sie sich für so etwas interessieren.Service Locators sind eine weitere Form der Inversion of Control (IoC). In der Regel verfügen Frameworks, die die automatische Abhängigkeitsinjektion ausführen, auch über einen Service-Locator.
XNA (Microsoft C # -Spielprogrammierframework) enthält einen Dienstfinder; Weitere Informationen finden Sie in dieser Antwort .
Übrigens, meiner Meinung nach sollten die Türme nichts von den Unheimlichen wissen. Wenn Sie nicht vorhaben, einfach die Liste der Creeps für jeden Turm zu durchlaufen, möchten Sie wahrscheinlich eine nicht-triviale Raumaufteilung implementieren . und diese Art von Logik gehört nicht in die Klasse der Türme.
quelle
Ich persönlich würde hier Polymorphismus verwenden. Warum einen
missile
Vektor, einentower
Vektor und einen Vektor ...creep
wenn alle die gleiche Funktion aufrufen?update
? Warum nicht einen Vektor von Zeigern auf eine BasisklasseEntity
oderGameObject
?Ich finde, ein guter Weg, um zu entwerfen, ist zu denken, ob dies in Bezug auf die Eigentümerschaft Sinn macht. Offensichtlich besitzt ein Turm eine Möglichkeit, sich selbst zu aktualisieren, aber besitzt eine Karte alle Objekte darauf? Wenn Sie sich für global entscheiden, sagen Sie dann, dass nichts die Türme besitzt und schleicht? Global ist normalerweise eine schlechte Lösung - es fördert schlechte Entwurfsmuster, aber es ist viel einfacher, damit zu arbeiten. Überlegen Sie, ob Sie abwägen möchten, ob Sie dies beenden möchten. und "will ich etwas, das ich wiederverwenden kann"?
Ein Weg, dies zu umgehen, ist eine Art Nachrichtensystem. Der
tower
kann eine Nachricht an den sendenmap
(auf die er Zugriff hat, vielleicht einen Verweis auf seinen Besitzer?), Dass er a getroffen hatcreep
, und dermap
meldet dann,creep
dass er getroffen wurde. Dies ist sehr sauber und trennt Daten.Eine andere Möglichkeit besteht darin, die Karte selbst nach dem zu durchsuchen, was sie will. Hier können jedoch Probleme mit der Aktualisierungsreihenfolge auftreten.
quelle
Dies ist ein Fall, in dem die strikte objektorientierte Programmierung (OOP) zusammenbricht.
Gemäß den Prinzipien von OOP sollten Sie Daten mit verwandtem Verhalten mithilfe von Klassen gruppieren. Aber Sie haben ein Verhalten (Targeting) , die Daten benötigt , die nicht miteinander verwandt sind (Türme und kriecht). In dieser Situation werden viele Programmierer versuchen, das Verhalten mit einem Teil der benötigten Daten zu verknüpfen (z. B. Türme behandeln das Targeting, kennen sich jedoch nicht mit Creeps aus), aber es gibt eine andere Option: Gruppieren Sie das Verhalten nicht mit den Daten.
Anstatt das Targeting-Verhalten zu einer Methode der Tower-Klasse zu machen, machen Sie es zu einer freien Funktion, die Türme und Creeps als Argumente akzeptiert. Dies erfordert möglicherweise, dass mehr Mitglieder, die in den Tower- und Creep-Klassen verbleiben, veröffentlicht werden, und das ist in Ordnung. Das Verstecken von Daten ist nützlich, aber es ist ein Mittel, kein Selbstzweck, und Sie sollten kein Sklave dafür sein. Außerdem sind private Mitglieder nicht die einzige Möglichkeit, den Zugriff auf Daten zu steuern. Wenn Daten nicht an eine Funktion übergeben werden und nicht global sind, ist sie dieser Funktion effektiv verborgen. Wenn Sie mit dieser Technik globale Daten vermeiden können, verbessern Sie möglicherweise die Kapselung.
Ein extremes Beispiel für diesen Ansatz ist die Entitätssystemarchitektur .
quelle