Ich habe in Unity ein einfaches RTS-Spiel gemacht, das Hunderte von Charakteren wie Crusader Kings 2 enthält. Zum Speichern wäre es am einfachsten, skriptfähige Objekte zu verwenden. Dies ist jedoch keine gute Lösung, da Sie zur Laufzeit keine neuen erstellen können.
Also habe ich eine C # -Klasse namens "Character" erstellt, die alle Daten enthält. Alles funktioniert gut, aber während das Spiel simuliert, werden ständig neue Charaktere erstellt und einige Charaktere getötet (wenn Ereignisse im Spiel auftreten). Während das Spiel kontinuierlich simuliert, werden Tausende von Charakteren erstellt. Ich habe eine einfache Überprüfung hinzugefügt, um sicherzustellen, dass ein Charakter "lebendig" ist, während er seine Funktion verarbeitet. Es hilft also der Leistung, aber ich kann "Charakter" nicht entfernen, wenn er / sie tot ist, da ich seine / ihre Informationen beim Erstellen des Stammbaums benötige.
Ist eine Liste der beste Weg, um Daten für mein Spiel zu speichern? Oder wird es Probleme geben, wenn 10000s eines Charakters erstellt wurden? Eine mögliche Lösung besteht darin, eine weitere Liste zu erstellen, sobald die Liste eine bestimmte Menge erreicht hat, und alle toten Zeichen darin zu verschieben.
GameObjects
. In CK2 war die maximale Anzahl von Charakteren, die ich gleichzeitig sah, als ich auf meinen Platz schaute und dort 10-15 Leute sah (ich spielte allerdings nicht sehr weit). Genauso gut ist eine Armee mit 3000 Soldaten nur eineGameObject
, die die Nummer "3000" anzeigt.Antworten:
Es gibt drei Dinge, die Sie berücksichtigen sollten:
Verursacht es tatsächlich ein Leistungsproblem? 1000er sind eigentlich nicht viele. Moderne Computer sind furchtbar schnell und können mit vielen Dingen umgehen. Überwachen Sie, wie viel Zeit die Zeichenverarbeitung in Anspruch nimmt, und prüfen Sie, ob sie tatsächlich ein Problem verursacht, bevor Sie sich zu viele Sorgen machen.
Wiedergabetreue von derzeit minimal aktiven Zeichen. Ein häufiger Fehler von Anfänger-Spielprogrammierern ist die Besessenheit, Off-Screen-Charaktere genau wie On-Screen-Charaktere genau zu aktualisieren. Das ist ein Fehler, niemand kümmert sich darum. Stattdessen müssen Sie versuchen, den Eindruck zu erwecken, dass Off-Screen-Charaktere immer noch agieren. Indem Sie die Anzahl der Aktualisierungszeichen reduzieren, die außerhalb des Bildschirms empfangen werden, können Sie die Verarbeitungszeiten erheblich verkürzen.
Betrachten Sie datenorientiertes Design. Anstatt 1000 Zeichenobjekte zu haben und für jedes dieselbe Funktion aufzurufen, sollten Sie ein Array der Daten für die 1000 Zeichen haben und eine Funktionsschleife über die 1000 Zeichen haben, die jeweils nacheinander aktualisiert werden. Diese Art der Optimierung kann die Leistung erheblich verbessern.
quelle
In dieser Situation würde ich die Verwendung von Komposition vorschlagen :
In diesem Fall klingt es so, als wäre Ihre
Character
Klasse gottähnlich geworden und enthält alle Details darüber, wie ein Charakter in allen Phasen seines Lebenszyklus funktioniert .Sie stellen beispielsweise fest, dass die toten Zeichen weiterhin benötigt werden - wie sie in den Stammbäumen verwendet werden. Es ist jedoch unwahrscheinlich, dass alle Informationen und Funktionen Ihrer lebenden Charaktere noch benötigt werden, um sie in einem Stammbaum anzuzeigen. Sie benötigen beispielsweise einfach Namen, Geburtsdatum und ein Porträtsymbol.
Die Lösung besteht darin, die einzelnen Teile von Ihnen
Character
in Unterklassen aufzuteilen , derenCharacter
Instanz der Eigentümer besitzt. Zum Beispiel:CharacterInfo
kann eine einfache Datenstruktur mit dem Namen, dem Geburtsdatum, dem Todesdatum und der Fraktion sein,Equipment
kann alle Gegenstände haben, die dein Charakter hat, oder deren aktuelles Vermögen. Es kann auch die Logik haben, die diese in Funktionen verwaltet.CharacterAI
oderCharacterController
kann alle Informationen enthalten, die über das aktuelle Ziel des Charakters, seine Nutzenfunktionen usw. benötigt werden. Und es kann auch die tatsächliche Aktualisierungslogik enthalten, die die Entscheidungsfindung / Interaktion zwischen seinen einzelnen Teilen koordiniert.Sobald Sie den Charakter aufgeteilt haben, müssen Sie kein Alive / Dead-Flag mehr in der Update-Schleife aktivieren.
Stattdessen würden Sie einfach ein machen ,
AliveCharacterObject
dass die hatCharacterController
,CharacterEquipment
undCharacterInfo
Skripte angebracht. Um den Charakter zu "töten", entfernen Sie einfach die Teile, die nicht mehr relevant sind (wie dieCharacterController
) - es wird jetzt weder Speicher noch Verarbeitungszeit verschwendet.Beachten Sie, dass dies
CharacterInfo
wahrscheinlich die einzigen Daten sind, die tatsächlich für den Stammbaum benötigt werden. Indem Sie Ihre Klassen in kleinere Funktionen zerlegen, können Sie dieses kleine Datenobjekt nach dem Tod einfacher behalten, ohne den gesamten KI-gesteuerten Charakter behalten zu müssen.Es ist erwähnenswert, dass dieses Paradigma das ist, für das Unity entwickelt wurde - und deshalb werden Dinge mit vielen separaten Skripten behandelt. Das Erstellen großer Gottobjekte ist selten der beste Weg, um Ihre Daten in Unity zu verarbeiten.
quelle
Wenn Sie eine große Datenmenge verarbeiten müssen und nicht jeder Datenpunkt durch ein tatsächliches Spielobjekt dargestellt wird, ist es normalerweise keine schlechte Idee, auf Unity-spezifische Klassen zu verzichten und einfach mit alten C # -Objekten zu arbeiten. Auf diese Weise minimieren Sie den Overhead. Sie scheinen hier also auf dem richtigen Weg zu sein.
Das Speichern aller lebenden oder toten Zeichen in einer Liste ( oder einem Array ) kann nützlich sein, da der Index in dieser Liste als kanonische Zeichen-ID dienen kann. Der Zugriff auf eine Listenposition über den Index ist sehr schnell. Es kann jedoch nützlich sein, eine separate Liste der IDs aller lebenden Zeichen zu führen, da Sie diese wahrscheinlich weitaus häufiger wiederholen müssen als die toten Zeichen.
Wenn Ihre Implementierung Ihrer Spielmechanik Fortschritte macht, möchten Sie möglicherweise auch untersuchen, welche anderen Arten von Suchvorgängen Sie am häufigsten durchführen. Wie "alle lebenden Charaktere an einem bestimmten Ort" oder "alle lebenden oder toten Vorfahren eines bestimmten Charakters". Es kann vorteilhaft sein, weitere sekundäre Datenstrukturen zu erstellen, die für diese Art von Abfragen optimiert sind. Denken Sie daran, dass jeder von ihnen auf dem neuesten Stand gehalten werden muss. Dies erfordert zusätzliche Programmierung und führt zu zusätzlichen Fehlern. Tun Sie dies also nur, wenn Sie eine bemerkenswerte Leistungssteigerung erwarten.
CKII " beschneidet " Zeichen aus seiner Datenbank, wenn es sie für unwichtig hält, um Ressourcen zu sparen. Wenn Ihr Stapel toter Charaktere in einem lang laufenden Spiel zu viele Ressourcen verbraucht, möchten Sie möglicherweise etwas Ähnliches tun (ich möchte dies nicht als "Garbage Collection" bezeichnen. Vielleicht als "respektvoller Inkremator"?).
Wenn Sie tatsächlich ein Spielobjekt für jeden Charakter im Spiel haben, kann das neue Unity ECS- und Jobs-System für Sie hilfreich sein. Es ist für den performanten Umgang mit einer Vielzahl sehr ähnlicher Spielobjekte optimiert. Aber es zwingt Ihre Softwarearchitektur in einige sehr starre Muster.
Übrigens, ich mag CKII sehr und die Art und Weise, wie es eine Welt mit Tausenden von einzigartigen AI-gesteuerten Charakteren simuliert. Ich freue mich darauf, Ihre Sicht auf das Genre zu spielen.
quelle
Like "all living characters in a specific location" or "all living or dead ancestors of a specific character". It might be beneficial to create some more secondary data-structures optimized for these kinds of queries.
Aus meiner Erfahrung mit CK2-Modding kommt dies dem Umgang von CK2 mit den Daten sehr nahe. CK2 scheint Indizes zu verwenden, die im Wesentlichen grundlegende Datenbankindizes sind, wodurch es schneller wird, Zeichen für eine bestimmte Situation zu finden. Anstatt eine Liste mit Zeichen zu haben, scheint es eine interne Datenbank mit Zeichen zu geben, die alle Nachteile und Vorteile mit sich bringt.Sie müssen nicht Tausende von Charakteren simulieren / aktualisieren, wenn sich nur wenige in der Nähe des Players befinden. Sie müssen nur aktualisieren, was der Spieler zum aktuellen Zeitpunkt tatsächlich sehen kann. Daher sollten Charaktere, die weiter vom Spieler entfernt sind, gesperrt werden, bis der Spieler näher an ihnen ist.
Wenn dies nicht funktioniert, weil Ihre Spielmechanik entfernte Charaktere benötigt, um den Zeitablauf anzuzeigen, können Sie sie in einem "großen" Update aktualisieren, wenn der Spieler näher kommt. Wenn Ihre Spielmechanik erfordert, dass jeder Charakter tatsächlich auf Ereignisse im Spiel reagiert, unabhängig davon, wo sich der Charakter in Bezug auf den Spieler oder das Ereignis befindet, kann dies dazu beitragen, die Häufigkeit zu verringern, mit der sich Charaktere weiter vom Spieler entfernt befinden aktualisiert (dh sie werden immer noch synchron mit dem Rest des Spiels aktualisiert, aber nicht so oft, so dass es eine leichte Verzögerung gibt, bevor entfernte Charaktere auf ein Ereignis reagieren, aber es ist unwahrscheinlich, dass dies ein Problem verursacht oder sogar vom Spieler bemerkt wird ). Alternativ können Sie einen hybriden Ansatz verwenden.
quelle
Klärung der Frage
In dieser Antwort gehe ich davon aus, dass das ganze Spiel wie CK2 sein soll, anstatt nur den Teil, in dem es darum geht, viele Charaktere zu haben. Alles, was Sie in CK2 auf dem Bildschirm sehen, ist einfach zu erledigen und gefährdet weder Ihre Leistung noch ist die Implementierung in Unity kompliziert. Die Daten dahinter werden dort komplex.
Character
Klassen ohne FunktionalitätGut, denn ein Charakter in Ihrem Spiel besteht nur aus Daten. Was Sie auf dem Bildschirm sehen, ist nur eine Darstellung dieser Daten. Diese
Character
Klassen sind das Herzstück des Spiels und laufen als solche Gefahr, " Gottobjekte " zu werden. Daher würde ich extreme Maßnahmen dagegen empfehlen: Entfernen Sie alle Funktionen aus diesen Klassen. Eine MethodeGetFullName()
, die den Vor- und Nachnamen kombiniert, OK, aber keinen Code, der tatsächlich "etwas tut". Fügen Sie diesen Code in dedizierte Klassen ein, die eine Aktion ausführen. Beispiel: Eine KlasseBirther
mit einer MethodeCharacter CreateCharacter(Character father, Character mother)
wird viel sauberer als diese Funktionalität in derCharacter
Klasse.Speichern Sie keine Daten im Code
Nein. Speichern Sie sie im JSON-Format mit JsonUtility von Unity. Bei diesen
Character
Klassen ohne Funktionalität sollte dies trivial sein. Dies funktioniert sowohl für die Ersteinrichtung des Spiels als auch für die Speicherung in Savegames. Es ist jedoch immer noch langweilig, deshalb habe ich nur die einfachste Option in Ihrer Situation angegeben. Sie können auch XML oder YAML oder ein beliebiges Format verwenden, sofern es noch von Menschen gelesen werden kann, wenn es in einer Textdatei gespeichert ist. CK2 macht das Gleiche, eigentlich machen es die meisten Spiele. Es ist auch ein ausgezeichnetes Setup, um es Leuten zu ermöglichen, Ihr Spiel zu modifizieren, aber das ist ein Gedanke für viel später.Denken Sie abstrakt
Dieser ist leichter gesagt als getan, weil er oft mit der natürlichen Denkweise kollidiert. Sie denken auf "natürliche" Weise an einen "Charakter". In Bezug auf Ihr Spiel scheint es jedoch mindestens zwei verschiedene Arten von Daten zu geben, die "ein Charakter" sind: Ich werde es
ActingCharacter
und nennenFamilyTreeEntry
. Ein toter CharakterFamilyTreeEntry
muss nicht aktualisiert werden und benötigt wahrscheinlich viel weniger Daten als ein aktiverActingCharacter
.quelle
Ich werde aus ein wenig Erfahrung sprechen, von einem starren OO-Design zu einem Entity-Component-System (ECS) -Design.
Vor einiger Zeit war ich genau wie Sie , ich hatte eine Reihe verschiedener Arten von Dingen, die ähnliche Eigenschaften hatten, und ich baute verschiedene Objekte aus und versuchte, sie mithilfe der Vererbung zu lösen. Eine sehr kluge Person sagte mir , dass ich das nicht tun und stattdessen Entity-Component-System verwenden soll.
Jetzt ist ECS ein großes Konzept, und es ist schwierig, es richtig zu machen. Es steckt viel Arbeit dahinter, Entitäten, Komponenten und Systeme richtig aufzubauen. Bevor wir dies jedoch tun können, müssen wir die Begriffe definieren.
Also, hier ist, wohin ich damit gehen würde:
Erstellen
ID
Sie in erster Linie eine für Ihre Charaktere. Einint
,Guid
, was Sie wollen. Dies ist die "Entität".Zweitens sollten Sie über die verschiedenen Verhaltensweisen nachdenken, die Sie haben. Dinge wie der "Stammbaum" - das ist ein Verhalten. Erstellen Sie ein System, das alle diese Informationen enthält, anstatt dies als Attribute für die Entität zu modellieren . Das System kann dann entscheiden, was damit zu tun ist.
Ebenso wollen wir ein System für "Ist der Charakter lebendig oder tot?" Dies ist eines der wichtigsten Systeme in Ihrem Design, da es alle anderen beeinflusst. Einige Systeme können die "toten" Zeichen löschen (z. B. das "Sprite" -System), andere Systeme können die Dinge intern neu anordnen, um den neuen Status besser zu unterstützen.
Sie bauen beispielsweise ein "Sprite" - oder "Drawing" - oder "Rendering" -System auf. Dieses System hat die Verantwortung zu bestimmen, mit welchem Sprite der Charakter angezeigt werden soll und wie er angezeigt werden soll. Wenn ein Charakter stirbt, entferne ihn.
Zusätzlich ein "KI" -System, das einem Charakter sagen kann, was zu tun ist, wohin er gehen soll usw. Dies sollte mit vielen anderen Systemen interagieren und basierend auf diesen Entscheidungen treffen. Auch hier können tote Charaktere wahrscheinlich aus diesem System entfernt werden, da sie eigentlich nichts mehr tun.
Ihr "Name" -System und Ihr "Family Tree" -System sollten den Charakter (lebendig oder tot) wahrscheinlich im Speicher behalten. Dieses System muss diese Informationen abrufen, unabhängig vom Status des Charakters. (Jim ist immer noch Jim, auch nachdem wir ihn begraben haben.)
Dies bietet Ihnen auch den Vorteil einer Änderung, wenn ein System effizienter reagiert : Das System verfügt über einen eigenen Timer. Einige Systeme müssen schnell ausgelöst werden, andere nicht. Hier beginnen wir mit dem, was ein Spiel effizient laufen lässt. Wir müssen das Wetter nicht jede Millisekunde neu berechnen, wir können das wahrscheinlich alle 5 oder so tun.
Es gibt Ihnen auch mehr kreative Hebelwirkung: Sie können ein "Pathfinder" -System erstellen, das die Berechnung eines Pfades von A nach B übernimmt und bei Bedarf aktualisiert werden kann, sodass das Bewegungssystem sagen kann, "wo muss ich hin" als nächstes gehen? " Wir können diese Bedenken jetzt vollständig trennen und effektiver darüber nachdenken. Bewegung muss nicht den Weg finden, sondern nur dorthin gelangen.
Sie sollten einige Teile eines Systems nach außen aussetzen. In Ihrem
Pathfinder
System möchten Sie wahrscheinlich eineVector2 NextPosition(int entity)
. Auf diese Weise können Sie diese Elemente in streng kontrollierten Arrays oder Listen aufbewahren. Sie können kleinerestruct
Typen verwenden, mit denen Sie Komponenten in kleineren, zusammenhängenden Speicherblöcken aufbewahren können, wodurch Systemaktualisierungen viel schneller durchgeführt werden können. (Insbesondere wenn die externen Einflüsse auf ein System minimal sind, muss es sich nur noch um seinen internen Zustand kümmern, wie zName
.Aber, und ich kann das nicht genug betonen, jetzt
Entity
ist es nur einID
, einschließlich Kacheln, Objekte usw. Wenn eine Entität nicht zu einem System gehört, wird das System sie nicht verfolgen. Dies bedeutet, dass wir unsere "Baum" -Objekte erstellen, sie in den SystemenSprite
und speichern könnenMovement
(die Bäume bewegen sich nicht, aber sie haben eine "Position" -Komponente) und sie von den anderen Systemen fernhalten können. Wir brauchen keine spezielle Liste mehr für Bäume, da das Rendern eines Baumes abgesehen von Paperdolling nicht anders ist als ein Charakter. (Was dasSprite
System steuern kann oder was das System steuernPaperdoll
kann.) JetztNextPosition
kann unser leicht umgeschrieben werden:Vector2? NextPosition(int entity)
und es kann einenull
Position für Entitäten zurückgeben, die es nicht interessiert. Wir wenden dies auch auf unsere anNameSystem.GetName(int entity)
, es kehrtnull
für Bäume und Felsen zurück.Ich werde dies zu Ende bringen, aber die Idee hier ist, Ihnen einige Hintergrundinformationen zu ECS zu geben und wie Sie es wirklich nutzen können, um Ihnen ein besseres Design für Ihr Spiel zu geben. Sie können die Leistung steigern, nicht verwandte Elemente entkoppeln und die Dinge besser organisieren. (Dies passt auch gut zu funktionalen Sprachen / Setups wie F # und LINQ. Ich empfehle dringend, F # zu überprüfen, wenn Sie dies noch nicht getan haben. Es passt sehr gut zu C #, wenn Sie sie zusammen verwenden.)
quelle
GameObject
, die nicht viel bewirken, aber eine Liste vonComponent
Klassen haben, die die eigentliche Arbeit erledigen. Vor einem Jahrzehnt gab es einen Paradigmenwechsel in Bezug auf den ECS-Kreisverkehr , da es sauberer ist, den handelnden Code in separate Systemklassen einzuteilen. Unity hat kürzlich auch ein solches System implementiert, aber ihrGameObject
System ist und war immer ein ECS. OP verwendet bereits ein ECS.Wenn Sie dies in Unity tun, ist der einfachste Ansatz folgender:
In Ihrem Code können Sie Verweise auf die Objekte in einer Liste aufbewahren, um zu verhindern, dass Sie Find () und seine Variationen ständig verwenden. Sie tauschen CPU-Zyklen gegen Speicher, aber eine Liste von Zeigern ist ziemlich klein, sodass selbst mit ein paar tausend Objekten kein großes Problem auftreten sollte.
Während Sie Ihr Spiel durcharbeiten, werden Sie feststellen, dass einzelne Spielobjekte Ihnen unzählige Vorteile bieten, einschließlich Navigation und KI.
quelle