antisteo hat geschrieben:Fazit: Es lassen sich Spiele egal welchen Genres in der Engine implementieren
Mit solchen Aussagen wäre ich vorsichtig: es gibt immer Anwendungsszenarien, wo zumindest ein Teil der Engine-Module zu Hindernissen anstatt Helfern degeneriert. Zu Deinem Beispiel hier mit fest eingebautem Terrainmanager fiele mir z.B. ein Space-Shooter als Gegenbeispiel ein.
Wobei ich an der Stelle kurz einfügen möchte, dass es ein guter Test für die eigene Engine ist, wenn man wirklich mal ein kleines Projekt damit baut. Ich habe das je nach Rahmenbedingungen bei der nächsten ZFX-Action vor, und ich würde mich auch brennend dafür interessieren, was Du mit Deiner Engine so zaubern kannst. Hast Du Lust, da mal mitzumachen?
Die Projektstruktur bei uns sieht grob so aus:
Ganz unten - das Framework:
Es gibt eine System-Klasse, die die Message Loop implementiert. Darauf baut ein Input-System auf (OIS von der Ogre-Engine plus eigene Erweiterungen). Es gibt eine aktuell noch etwas monolithische Screen-Klasse, die auch die ganzen DirectX-Befehle entgegennimmt, die Rückgabewerte prüft und unnötige Funktionsaufrufe aussortiert. Im Dunstkreis um die Screenklasse herum gibt es dann die ganzen Resourcen wie Buffer, Shader, Texturen, die von einer gemeinsamen Basisklasse ableiten und auf Wunsch das Verhalten bei Verlust automatisch behandeln - DX-DeviceReset oder VideoRAM-Mangel. Es gibt ein Sound-Modul, dass eigentlich nur FModEx in eine sympathische Struktur presst. Auf Basis des Grafikmoduls gibt es dann Helferklassen für 2D-Sprites und Fontrendering. Und drumherum gibt es zweitausend kleine unabhängige Helfer - Matheklassen, Serialisierung, Timer, Multithread-Aufgaben, Crashreports, Logfiles oder Konsolen mit verschiedenen Views... sowas halt.
Darauf - der Renderer:
Das ist eine Klasse, bei der man Zeichenszenen mit Startparametern eröffnen kann. Startparameter sind da zum Beispiel das Rendertarget, spezielle Rendermodi, die Shader auf andere Shader umleiten, oder die Postprocessing-Konfiguration. Man bekommt dann ein Zeichenszene-Objekt zurück, das man dann mit Renderjobs befüllt. Bei Zurückgabe der Zeichenszene werden dann die Renderjobs zu DrawCalls aufgelöst, auf minimale State Changes sortiert und dann ausgeführt. In der Theorie wollte ich das dann auf einen zweiten Core schieben, in der Praxis wird wohl eher alles andere mal auf andere Cores umziehen und genau dieser Teil bleibt im Mainthread. Dazu gibt es ein Rudel Resourcenverwaltungen für Texturen, Shader, Vertexdeklarationen, Materialien, den Shadergenerator und das PostProcessing-System.
Darauf - die Welt:
Bei uns heißt eine Map, ein Level oder wie auch immer ihr es nennt, "Welt". Es gibt eine Weltverwaltung, die wieder ein paar Resourcenverwaltungen inne hat - der überwiegende Teil ist aber lokal pro Weltinstanz. Die Verwaltungen sorgen auch dafür, dass größere Datenmengen von Platte gestreamt werden. Eine Welt ist im Endeffekt ein Szenegraph, allerdings beschränkt auf Sichtbarkeits- und Lichteinflussbestimmungen. Es gibt ein regelmäßiges Raster aus Zonen, die jeweils ihr eigenes Koordinatensystem definieren. Jede Zone ist auch ein Objekt und bildet die Wurzel eines Objektbaums - jedes Objekt kann 0 bis x Unterobjekte haben, wobei die Kinder Boundingboxen, Transformationen und gewisse Eigenschaften übernehmen. Einige Klassen wie Terrainsegmente, Spiegelungsobjekte oder Blocker/Portale benutzen diese Baumstruktur außerdem für lokal begrenzte Wirkungen. Die Physik wird in einem separaten Graphen abgehandelt, der nur um gewisse Fokuspunkte herum aktiv ist - außerhalb dieser Fokusbereiche gibt es keine Physik. Die Physikanbindung selbst ist dann auch ein separates Modul, das auf PhysX aufbaut. Die Funktionsbeschreibung der einzelnen Objektklassen würde jetzt definitiv zu weit gehen.
Obendrauf - die Spiellogik:
Die Spiellogik besteht wieder aus ein paar Managern, die spielrelevanten Kontext speichern - primär die Vorlagen (Queststrukturen, Dialoge, Gegenstände, Charaktere) und die aktuellen Spielzustände davon (Questzustände, geführte Gespräche, aktuelle Gegenstandszustände, Charaktergedächtnisse). Die primäre Spiellogik sitzt aber in von einem Basisinterface abgeleiteten Logikklassen, die man einem Weltobjekt einimpfen kann. Die Spiellogiken werden dann also von ihren jeweiligen Weltobjekten mit herumgetragen und beeinflussen dieses und die Umgebung.
Ganz obendrauf - das Spiel bzw. der Editor:
Das sind quasi Views des aktuellen Spielzustandes. Das Spiel basiert auf sogenannten Modulen, die quasi optisch aufeinanderliegen - ganz unten das Spielmodul, obendrauf dann je nach Spielsituation Inventar, Cutscene-Modul, die Konsole, das InGame-Menü und andere. Alle Module existieren die ganze Zeit in vordefinierter Reihenfolge, sind aber nicht alle gleichzeitig aktiv und immer nur eins hat den Eingabefokus. Das hat sich als angenehmes System herausgestellt, um alle möglichen Interaktionsszenarien im Spiel zu implementieren. Der Editor ist ein ganz anderes Feld - der besteht aus einem View in die Welt und einer mehr oder minder abstrakten "Aktion"-Schnittstelle, so ne Art Pluginsystem. Jede Aktion wird gefragt, ob sie mit der aktuellen Objektauswahl arbeiten kann. Wenn ja, kann der Nutzer auf die Aktion umschalten. Die Aktion baut dann ihre GUI in einer Toolbar auf und bekommt ab sofort gewisse Nutzereingaben, die sie nach ihrer Aufgabe verarbeitet. Das Ausfüllen dieser Schnittstelle ist recht einfach, weswegen es inzwischen viele Dutzend dieser Aktionen im Editor gibt, mit denen sich jedes Fitzelchen editieren lässt.
Alles bis zum Renderer hoch ist tatsächlich spiel-agnostisch - man könnte damit jedes Spiel schreiben. Sobald es aber an den Szenegraphen geht, merkt man der Engine dann doch an, dass sie für First- bzw. Third Person-Spiele gemacht ist. Ein RTS könnte man damit auch noch zaubern, aber spätestens bei einem Space Shooter stände einem die Weltstruktur nur noch im Weg.
Und noch eine allgemeine Erkenntnis will ich mit euch teilen: globale Variablen stinken. Singletons noch viel mehr, die sind ja nur globale Variablen in überflüssigen Syntax gepackt. Ich war am Anfang noch großer Freund von globalen Managern für jeden Mist. Bis ich immer mehr feststellen musste, dass es bisweilen doch ne feine Sache ist, wenn man dann doch mal den Manager austauschen kann. Sei es bei der Engine, wo ich vor geraumer Weile schon die Renderjob-Queue als Klasse rausgezogen habe - inzwischen merke ich da, dass auch die DX-Schnittstelle selbst, die von der Jobqueue bedient wird, als eigene Klasse eine gute Sache wär. Dann könnte man nämlich auch das parallele Rendern sehr schön umsetzen. Die aktuell gültigen PostProcessing-FX sind inzwischen auch eine Klasse, weil ich schon bald mehrere Effektsätze gleichzeitig haben wollte - das Bloom+HDR+Hitzeflimmern auf dem Mainscreen, der senkrechte Weichzeichner auf Spiegel-Renderings für Wasseroberflächen, das PostProcessing auf der ShadowMap für Penumbra Shadows. Je mehr man mit seiner Engine oder allgemein mit seinen ach so clever designten Klassenstrukturen machen will, desto mehr stellt man fest, dass erst die Übergabe aller nötigen Arbeitsmaterialien als Funktionsparameter eine wirklich schöne saubere Kapselung ermöglicht.
Das haben natürlich schon andere Leute vor mir rausgefunden. Die Fachwelt nennt das Dependency Injection - wurde auch hier auf dem ZFX bereits oft genannt. Ich kann es nur von ganzem Herzen empfehlen. Singletons sind wegen Nutzlosigkeit eh zum Aussterben verurteilt, aber auch globale Variablen werden mit DI immer seltener gebraucht. Eigentlich tauchen die dann nur noch da auf, wo man mit dem Betriebssystem interagiert. Es wird nunmal nur ein Eingabesystem geben. Oder nur eine DirectX-Schnittstelle. Aber schon Screens kann es mehr als einen geben. Logfiles gibt es jetzt bereits mehrere, je nach Zweck und Situation. Es gibt nur eine globale Konsole, aber potentiell einige Views davon. Wenn man die Denkweise drauf hat, erscheint einem die alte Struktur nur noch als verbohrter Unsinn.