Torchlight III | Update der Entwickler - Abenteuergeschichten

Update der Entwickler - Abenteuergeschichten

Ein technischer Blick auf die Datenfehler, auf die wir während der Entwicklung von Torchlight III gestoßen sind.

Schaut auf DiscordFacebookTwitter und Twitch vorbei, um mehr News und Updates zu erhalten.

 

 

Das Entwickler-Update dieses Monats kommt von Guy Somberg, Lead Programmer von Echtra. Um die Zukunft eines Projektes zu sehen, muss man manchmal seine Vergangenheit verstehen. Das nächste Entwickler-Update wird einen genaueren Blick auf kommende Updates, bestehende Probleme und anderes Community-Feedback werfen, doch dieser Blog ist für jene unter euch, die gern etwas Technisches lesen und die „Abenteuergeschichten“ unserer internen Entwicklung erfahren möchten.

 

Einleitung

Bei der Entwicklung von Spielen bezieht sich nicht jedes Problem auf Leistung, Funktionen oder Funktionalität, das die Spieler sehen. Manchmal gibt es einfach etwas Unordnung, die aufgeräumt werden muss, und da muss man einfach loslegen und anpacken.

Es gibt einen Ausdruck für diese Art von Arbeit: „Yak-Scheren“. Ursprünglich war es ein Bezug auf die Zeichentrickserie „Ren und Stimpy“, nun bezeichnet es eine Arbeit, die scheinbar nichts mit dem Endergebnis zu tun hat, aber die man erledigen muss, um es zu erreichen. Ein Beispiel: „Ich will eine Steinbrücke über einen Bach bauen. Ich schere ein Yak, damit ich die Wolle mit einem Garnmacher tauschen kann, der mir dafür seinen Wagen leiht, mit dem ich im Steinbruch ein paar Steine abholen kann.“ Das Scheren des Yaks ist kein wichtiger Teil für den Bau der Brücke, aber wir machen keinen Fortschritt, solange das Yak noch seine Wolle hat!

Dies ist eine Sammlung an Beispielen, bei denen wir Yak-Scheren-Probleme hatten, die einfach gelöst werden mussten.

 

0D0D0A

Verwaltungssysteme sind ein wichtiges Werkzeug für Spieleentwickler (eigentlich für alle Entwickler). Es ist eine Datenbank mit allen Dateien des Spiels – Quellcode, Assets, Klänge, einfach alles.

Vor etwa drei Jahren haben wir unser System gewechselt. Dabei ist unwichtig, um welche Systeme es sich handelt. Das alte System versagte unter der hohen Belastung, daher brauchten wir ein neues. Wir haben uns gut informiert, die Alternativen untersucht und uns entschieden.

Wenn man Verwaltungssysteme wechselt, kann man dies auf zwei Arten machen. Der einfache Weg ist, alle aus dem System auszusperren, eine Momentaufnahme zu machen, sie ins neue System zu importieren, an das neue System anzupassen und dann das System zu aktivieren. Dies hat den Vorteil, dass es einfach funktioniert, allerdings verliert man dabei den gesamten Verlauf vor dem Wechsel. In diesen Umgebungen gibt es oft einen Zeitpunkt, der sagt: „Alles importiert. Für den älteren Verlauf bitte im alten System nachsehen.“ Das ist okay, solange es das alte System noch gibt und man darauf zugreifen kann, aber oft gehen die früheren Daten verloren.

Der kompliziertere Weg ist der Import des Verlaufs vom alten System ins neue. Die meisten Systeme erlauben das, aber es ist zeitaufwendig, fehleranfällig und erfordert manuelle Eingaben, um die Eigenarten des neuen Systems anzupassen. Auch wenn es im Voraus mehr Arbeit erfordert, ist es im Nachhinein von unschätzbarem Wert, den gesamten Verlauf zu erhalten.

Wir haben uns für den Import entschieden, der – zumindest oberflächlich – zu funktionieren schien. Wir haben den Verlauf und die Dateien gesehen und wir stellten sicher, dass alles in Ordnung war. Einige Dateien hatten sonderbare Abstandsfehler, die allerdings kein großes Problem darstellten.

Aber als wir es zusammensetzen wollten, brach alles zusammen. Der Visual Studio Compiler beschwerte sich über „Mac-Zeilen-Endungen“ und wollte nichts übersetzen.

Was?! Was könnte das sein?

Etwas Hintergrundwissen: Wenn ein Computer ein Zeichen darstellen will, dann muss er eine Zeichencodierung wählen. Die gebräuchlichste Zeichencodierung nennt sich UTF-8, die alle Buchstaben und Satzzeichen der englischen Sprache in ein einzelnes Byte an Daten kodieren kann. (Mit mehreren Bytes kann man Daten in fast jeder Sprache kodieren, aber das nur am Rande.)

Zwei dieser Steuerzeichen sind der Wagenrücklauf (Carriage Return: CR) und der Zeilenvorschub (Line Feed: LF), die auf die Zeit zurückgehen, in der Computer mit automatischen Schreibmaschinen statt Bildschirmen verbunden waren. In diesen Zeiten würde man dem Drucker sagen, dass er zur Startspalte zurückkehren soll, indem man einen CR-Code sendet, und man würde die Papierrolle zur nächsten Zeile rollen lassen, indem man einen LF-Code schickt. Wenn man also am Anfang der nächsten Zeile schreiben wollte, würde man die Sequenz CR LF senden.

Mit dem Wechsel zu grafischen Anzeigen wurde diese CR-LF-Konvention für die Abwärtskompatibilität beibehalten. Allerdings wurden die Entwickler von neuen Systemen, wie dem Apple Macintosh und dem Unix-System von Bell Labs, nicht davon abgehalten, neue Entscheidungen zu treffen.

Es stellte sich heraus, dass die drei gebräuchlichsten Systeme der Welt unterschiedliche Entscheidungen getroffen haben: DOS verwendet CR LF, Macintosh verwendet CR und Unix verwendet LF. Windows hat das Zeilenende von DOS übernommen und MacOS verwendet nun LF (genau wie Unix).

Mit der Zeit haben sich die Unterschiede verlaufen. Die Software ist üblicherweise in der Lage, im „Textmodus“ zu arbeiten und dem Anwender mit den notwendigen Zeilenendungen für ihr System zu versorgen. Die Details dieser Unterschiede treten manchmal noch hervor, aber sind für gewöhnlich kein Problem.

Dieses Hintergrundwissen tauchte in unseren Köpfen auf, als wir den Fehler über Mac-Zeilenendungen sahen. Wovon war hier die Rede? Wir entwickeln auf Windows, also sollten alle Zeilenendungen auch Windows-Zeilenendungen (CR LF) sein – oder zumindest eine Kombination von Windows- und Unix-Zeilenendungen. Wo kamen diese einzelnen CR-Zeichen her?

Und dann erinnerten wir uns an diese sonderbaren Abstandsfehler – unser gesamter Quellcode schien einen doppelten Abstand zu haben. Wo kamen diese extra Leerzeichen her?

Hier kam jemand auf die Idee, die Datei in einem Hex-Editor zu öffnen – ein Werkzeug, mit dem man die binäre Darstellung der Textdateien sehen kann, indem der Wert jedes Bytes im Hexadezimal-System angezeigt wird. In einer Datei mit Windows-Zeilenendungen würde man eine Zeile aus Text und dann ein CR (13 oder 0D im Hexadezimal-System) und ein LF (10 oder 0A) erwarten. Aus irgendeinem Grund hatten die fehlerhaften Zeilen ein CR (0D), ein weiteres CR (0D) und dann ein LF (0A), also ein 0D0D0A.

Während der Umwandlung von einem System ins andere, beschloss das Umwandlungsprogramm, dass die Datei Unix-Zeilenendungen besaß und ersetzte alle LF mit CR LF, selbst wenn es bereits ein CR gab! Das erklärte alles. Unser Editor konnte CR als leere Zeile interpretieren und CRLF in eine leere Zeile umwandeln, dadurch erhielt unser Code einen doppelten Abstand. Im Gegensatz dazu interpretierte der Visual Studio Compiler CR LF als neue Zeile, aber gab für das folgende CR einen Fehler aus.

Ich habe dies mit einem kleinen Programm in C++ korrigiert. Es würde den Quellcode-Pfad überprüfen, jede Textdatei öffnen, Muster von 0D0D0A suchen und sie mit 0D0A ersetzen. Wir werden unser Verwaltungssystem bestimmt nicht nochmal ersetzen, also ist der Code für dieses Tool für immer verloren. (Ergänzung: Zumindest dachten wir das! Nach der Fertigstellung dieses Artikels ist eine Festplatte mit dem Quellcode für dieses Programm aufgetaucht, also haben wir den Code für die Nachwelt in unser Archiv hochgeladen.)

Es haben nur zwei oder drei von uns an diesem Problem gearbeitet, aber man kann uns leicht zum Zucken bringen, wenn man „oh doh doa“ sagt.

 

Rautenzeichen-Fixer

Vor einigen Jahren fuhren unser Sound Designer und Komponist nach Bratislava, Slowakei, um unsere Musik mit einem Orchester aufzunehmen. Es war eine fantastische Reise (hat man mir gesagt) und sie haben in den wenigen Tagen viel geschafft.

Ein Ergebnis dieser Reise war eine Reihe von Inhalten, die wir „vzory“ nennen – slowakisch für „Muster“. Dies sind kleine Orchesterstücke, die in unzähligen Arten miteinander kombiniert werden können, um neue Musik zu erschaffen. Sie wurden in verschiedenen Kombinationen aus Grundtönen und Vorzeichen aufgezeichnet. Das Ergebnis waren Muster in G, G#, F, F# etc.

Unser Komponist hat seine natürliche Arbeit gemacht – er hat die Inhalte geschnitten und organisiert, sie in entsprechende Ordner sortiert und dann alles in unser Audio-Programm FMOD-Studio importiert. FMOD ist mit unserem Verwaltungssystem verknüpft, das die neuen Dateien hinzugefügt und kontrolliert hat.

So weit so gut. Bis Leute rätselhafte Warnungen über Dateinamen erhielten, wenn sie von unserem Verwaltungssystem die neuesten Daten erhielten. Es waren nur Warnungen – sie haben niemanden vom Arbeiten abgehalten – aber es war etwas, das wir nicht ignorieren wollten.

Die vzory-Stücke waren der Übeltäter. Es stellte sich heraus, dass unser Verwaltungssystem keine Dateien mit einem Rautezeichen (#, Raute, Doppelkreuz, Hashtag) mag. Es wird sie akzeptieren, aber lauthals protestieren. Unser Komponist hat die Ordner und Dateien für die vzory-Stücke mit dem Grundton Ais (A sharp) mit dem Namen „A#“ bezeichnet – logisch! (Die anderen Grundtöne mit Kreuz-Vorzeichen wurden ähnlich verwendet.)

Das Verwaltungssystem war über diese Wahl äußerst verärgert.

Die Umbenennung der Dateien war nicht genug, denn FMOD verfolgt Datei- und Ordner-Metadaten in XML-Dateien – jede mit einer GUID (eine Folge von Buchstaben, Zahlen und Querstrichen) als Dateinamen.

Wieder einmal musste uns ein Programm retten. Dieses Mal wurde das Programm in C# geschrieben (wie ironisch), das alle Dateien und Unterordner eines Pfads überprüfte, das Rautezeichen im Namen suchte und „#“ mit dem Wort „sharp“ ersetzte. So wurde aus „A#“ ein „Asharp“. Dann hat es die XML-Dateien im Pfad überprüft, alle mit einem Rautezeichen in den Dateiinhalten gesucht (die Metadaten zu den umbenannten Dateien oder Ordnern enthielten) und das „#“ mit dem Wort „sharp“ ersetzt.

Außer unseren Leuten zu sagen, dass sie das nicht tun sollen, gibt es nicht viel, um so etwas zu vermeiden. Diesmal haben wir den Quellcode aufbewahrt, damit wir diesen Fehler beim nächsten Mal schnell beheben können.

 

PO-Fixer

Lokalisierung und Internalisierung sind wichtige Bestandteile eines jeden Projekts. Wir verwenden die Unreal-Engine, die bereits eine Reihe von Lokalisierungswerkzeugen mit eingebaut hat. Durch den Einsatz einer speziellen Datenstruktur in unseren Dateien, kann Unreal alle nichtlokalisierten Zeilen im Spiel finden. Dann können wir sie in ein standardisiertes Format, einer sogenannten „Portable Object“-Datei (.po), exportieren.

Für dieses Format haben unsere Übersetzer Programme, um damit zu arbeiten. Sie nehmen die Dateien, übersetzen die Zeilen und schicken sie zurück. Dann können wir sie importieren und Unreal wird den Text anzeigen. Alles sehr schön, solange man nicht über den Rand malt und den Anweisungen der Unreal-Engine folgt.

Natürlich haben wir auch einige unserer eigenen Sachen entwickelt, die innerhalb der Unreal-Systeme arbeiten, aber fremd genug sind, dass sie die anderen Unreal-Systeme nicht erkennen. Eins davon ist das System für lokalisierte Zeichenketten, das unsere tollen Assets nicht erkannte.

Wir haben ein Programm geschrieben, dass sie sichtbar macht und es dabei belassen. Dann haben wir die ersten Dateien an die Übersetzer geschickt. Als wir sie importierten wollten, … wurde keine unserer Zeichenketten importiert!

Was war passiert?

Unreal erlaubt uns, jede lokalisierte Zeichenkette durch ein Textpaar zu identifizieren: eine Kategorie und einen Eintrag in dieser Kategorie. Wenn man diese nicht angibt, werden sie erstellt. Es stellte sich heraus, dass unser Programm, das unsere Assets für die Übersetzer sichtbar machte, jedes Mal eine neue Kategorie und einen neuen Eintrag für jede lokalisierte Zeichenkette erstellte. Dadurch erhielt jede Textzeile jedes Mal einen anderen Code, wenn wir einen Import oder Export durchführten.

Ach herrje. Wir haben das zugrundeliegende Problem behoben und die Kategorien und Einträge konsistent gemacht, aber wir hatten immer noch all die Zeichenketten in allen Sprachen, die mit den korrigierten Daten inkompatibel waren! Wir mussten einen Weg finden, diese Zeichenketten in einem Rutsch aneinander zuzuordnen.

Glücklicherweise enthielt jede Zeichenkette eine Menge Metadaten über ihren Inhalt und Herkunft. Diese Metadaten hatten sich nicht sehr geändert. Es stellte sich heraus, dass diese Metadaten ausreichten, um eine importierte Zeile mit einer neu exportierten Zeile zu vergleichen und die Zeichenketten aneinander zuzuordnen.

Wie schon zuvor, war Code schreiben die Antwort. Wir haben ein Programm (wieder in C++) geschrieben, um die übersetzten Dateien (mit den alten, falschen Kategorien und Einträgen) und einer neu exportierten Datei (mit den neuen, richtigen Kategorien und Einträgen) zu lesen, die Metadaten zuzuordnen und eine korrigierte Version der Datei mit dem übersetzten Text und den richtigen Kategorien und Einträgen zu erstellen.

Dies war eine Situation, in der die Korrektur der Daten nicht ausreichte. Wir mussten zuerst das zugrundeliegende Problem lösen, bevor wir die Daten korrigieren konnten.

 

Fazit

All diese Probleme haben ein gemeinsames Thema: durch eine Folge von Ereignissen – menschliche oder maschinelle Fehler – erschienen wichtige Dateien als fehlerhaft. Für diejenigen, die mit diesen Daten arbeiten, ist es egal, warum diese Dinge passieren. Sie wollen einfach die fehlerhaften Dateien korrigieren. Es ist immer ein wertvolles Bestreben, den Grund herauszufinden und Wiederholungen zu vermeiden, aber manchmal muss man einfach arbeiten. Keine nachträglichen Analysen und vorbeugende Maßnahmen helfen den Leuten, ihre Arbeit zu machen, wenn sie mit fehlerhaften Dateien arbeiten müssen.

Die genannten Beispiele waren alles wichtige Arbeiten, die erledigt werden mussten, doch alle drei Programme wurden nur einmal ausgeführt. Es hat sich herausgestellt, dass viele unserer Systeme kompliziert sind und dass diese Art von Problemen als Teil der Entwicklung auftreten.

Manchmal muss man ein Programm schreiben, dass man nur einmal ausführt, und das ist kein Problem – man muss einfach die Zähne zusammenbeißen und das Yak scheren.

- Guy Somberg