Ein frohes neues Jahr zusammen, ich hoffe ihr seit gut gerutscht Ich war trotz der Feiertage fleißig, und hab wieder paar Neuigkeiten zu verkünden. Mal abgesehen von den technischen Neuerungen geht die Portierung gut voran, ich bin mittlerweile an Crysta dran. Ich muss am Ende noch alle einzelnen Szenen irgendwie zusammenfügen, aber im großen und ganzen sollte es bald erledigt sein. Der größte Blocker dürfte noch das Inventar-System bzw. das Truhen-Menü werden.
Aber jetzt erstmal zu den ganzen tollen neuen Änderungen (die meisten das Event-System betreffend):
- Das neue Breakpoint-Debugging-System hat sich aus dem Bedarf ergeben, Code direkt im C++-Aufruf eines Triggers auszuführen. Bisher war das Sstem nämlich relativ komplex, und hat erfordert dass der Update-Schritt der Event-Entities speziell mit Breakpoint-Code ausgestattet wird. Das wäre zu viel Aufwand gewesen, wenn man das nun bei jedem Trigger-Aufruf machen müsste. Im Gamedev-Forum hat mich dann jemand auf die Idee gebracht, einfach den C++-Code an der Stelle mit einer while(true)-Schleife anzuhalten, wenn ein Breakpoint getroffen wird. So ein ähnliches System hatte ich bereits bei den Dialog-Boxen im GUI angewendet, also war das nicht allzu kompliziert zum implementieren. Faktisch ändert sich an der Verwendung recht wenig: Der Editor bleibt weiterhin responsiv, wenn ein Breakpoint getroffen wird, und das Spielgeschehen hält an. Der Unterschied ist bloß, dass dieses System nun 100% deterministisch ist, d.h. es macht keinen Unterschied in der Ausführung ob ein Breakpoint getroffen wurde oder nicht. Mit dem alten System konnten unter ganz bestimmten Umständen Seiteneffekt durch Breakpoints eintreten. Außerdem macht es das neue Breakpoint-System auch leichter im C++-Code zu debuggen, da der C++-Callstack jetzt genau dorthin zeigt, wo der Breakpoint aufgerufen wurde.
- Die direkte Ausführung des Trigger-Codes war ja wie gesagt an das neue Breakpoint-System geknüpft. Warum muss der Code überhaupt sofort ausgeführt werden? Weil es sonst zu Fehlern kommen kann. Das Player-Event z.B. regiert auf den BeginTalkTrigger, und hält die Bewegung des Spielers an. Ein Teleporter-Event aktiviert nun zuerst den Talk-Modus (d.h. BeginTalkTrigger wird abgefeuer), und bewegt den Spieler danach in Richtung des Teleporters. Würde jetzt der BeginTalkTrigger verzögert ausgeführt, hätte das zur Folge, dass der Spieler zuerst den Move-Befehl des Teleporters bekommt, und dann den StopMove-Befehl des BeginTalkTriggers, wodurch er im Endeffekt stehen bleibt. Die Lösung davor war es, einen "SkipFrame"-Befehl zwischen den "DoTalk" und "MovePlayer"-Block zu setzen, was bedeutet hat dass die Ausführung erst nächsten Frame fortgesetzt wurde und er Move-Befehl korrekt ankam. Dass das nicht sehr schön ist, weil es Wissen über diesen Seiteneffekt erfordert, und außerdem noch manuelles Eingreifen an bestimmten Stellen erfordert, ist klar. Die Lösung ist auch einfach: Sobald der Trigger aufgerufen wird, wird der angehängt Code direkt ausgeführt. Natürlich bloß solange, bis das erste blockende Event (Wait, While, etc...) kommt, das wird dann in die Ausführungsschleife gehängt. Dass ich es bisher nicht so gemacht habe, hat eigentlich lediglich historische Gründe, am Anfang war das System bei weitem nicht so komplex und groß wie jetzt.
- Das Stack-System. Oh Mann, das hab ich mir lange aufgehoben. Am Anfang des Systems, wo es noch nichts gab außer normalen Nodes und ein paar Triggern, war es eigentlich die logische einfachste Wahl, zur Ausführung einfach das Event zu kopieren, und dieses dann auszuführen. Mit dem aufkommen von Methoden, Ableitung, etc... wurde das langsam zum Problem. So müsste auf diese Weise jeder Methoden-Node eine Kopie der Methode besitzen, was unter Umständen einem enormen Speicherverbraucht bedeutet. Rekursion (eine Methode ruft sich selber auf) ist so auch nicht wirklich möglich, da das zu einer unendlichen Vervielfältigung der Methode geführt hätte. Die Lösung ist also simpel: Der Event-Code und die Ausführung gehört getrennt. Das läuft über eine Stack-Klasse, welche im Prinzip blop den aktuellen State des Events speichert (statt diese Information in den Nodes selber zu speichern). Damit braucht jede Event-Entity bloß noch einen Zeiger auf das Event, statt einer vollen Kopie. Technisch hat das mehrere Auswirkungen. Zum einen sinkt die Ladezeit, weil weniger kopiert werden muss. An vielen Stellen kann der Code vereinfacht werden, weil die Nodes selber nicht mehr resettet werden müssen. Die Auswirkung auf die Performance ist schwer abzuschätzen. Einerseits müssen im aktuellen System öfters Speicherallokationen gemacht werden. Das könnte ich wegoptimieren, allerdings könnte man dann Events wieder nicht zur Laufzeit verändern. Weiters sind mehr Map-Zugriffe notwendig, welche ich auf einen linearen Array-Zugriff verbessern könnte. Dafür fallen einige virtuelle Methoden und Benachrichtigungen über Signals im Vergleich zu vorher weg. Alles in allem schwierig zu sagen, einen echten Unterschied habe ich soweit nicht bemerkt, müsste ich aber einmal Benchmarken.
- Das Local-System für die Event-Commands ist vor allem nötig, um den Event-State serialisieren zu können, um eben z.B. exakte Replay-Savestate erzeugen zu können. Denn bestimmte Nodes mussten einen State speichern. zB. wieviel Zeit von einem "Wait"-Node noch zu warten ist. Das war bisher einfach eine Member-Variable der Node, und daher nicht automatisch serialisierbar. Das hätte ich dann extra für jeden Node ausimplementieren müssen, was sehr aufwändig wäre. Mit dem neuen System werden jetzt solche Variablen mit meinem dynamischen Typsystem deklariert, als "Variable"-Klasse gespeichert, und können dann vom System automatisch ausgewertet werden. Das hat einen leicht negativen Einfluss auf die Performance, sollte sich aber in Grenzen halten.
_____________________________________________________________________________-
Das waren die wichtigsten Änderungen. Leider muss ich noch das Debugging mit dem Stack-System reimplementieren. Danach werde ich vmtl die Serialisierung das Stack-States einbauen, und die Anzeige der Locales im Debugging-Modus. Dann halt weiter im Portieren, und dann sollte schon das Inventar-System drankommen.