Skripte debuggen


Wie selbst die sehr guten Programmierer es tun so wird es auch bei Dir sein: Du wirst Fehler machen. Bugs sind ein ständiger Begleiter eines Skripters und sollte euch nicht entmutigen, sondern euch etwas lehren. Normalerweise kannst Du in IDEs wie Visual Studio einen sog. "Debug-Modus" anschalten, der Dir dann Dateinamen und Zeilennummern und sogar Haltepunkte gibt, mit denen man an ausgewählten Stellen anhalten und den Code Schritt für Schritt überprüfen kann. Das gibt es in Space Engineers Ingame-Skripte leider nicht.

Der Grund dafür ist ganz einfach: Debuggable-Skripte laufen viel langsamer als optimierte Skripte. Aus diesem Grund enthalten die kompilierten Skripte in Space Engineers überhaupt keine Debug-Informationen.

Trotzdem gibt es ein paar Kniffe wie ihr euch ein Debug(-ähnliche) Ausgabe schaffen könnt:

Der Befehl Echo

Beginnen wir mit dem einfachen Befehl Echo. Dies ist eine Methode der Skript-Basisklasse MyGridProgram. Mit dieser Methode kann Text in den Detailbereich des programmierbaren Blocks ausgegeben werden. Zum Beispiel dies:

public void Main() 
{
    Echo("SpaceEngineering.de - Wiki");
}

Das ergibt folgende Ausgabe:

Indem Du solche Echo-Ausgaben an strategisch wichtigen Stellen im Code platzierst, kannst Du herausfinden, wie weit der Code geht, bevor ein Fehler auftritt - und so den Suchbereich einschränken.

Um jedoch eine vollständige Abdeckung zu gewährleisten, benötigst Du Echos überall.

Der Stack Trace

Ein Stack-Trace ist eine Beschreibung, die Dir die Abfolge von Codeaufrufen anzeigt bis der Fehler aufgetreten ist. Eine Ebene der Abfolge kannst Du in Space Engineers abfangen. Das geht mit dem sogenanten Try-Catch Block. Füge diesen in Hauptfunktionen deines Skripts hinzu: Program(), Save() und vor allem Main().

Beginnen wir in unserem Beispiel mit einem einfachen Beispiel ohne Stack Tracing .

public void Main()  
{ 
    var block = GridTerminalSystem.GetBlockWithName("Mich gibt es nicht");
    ZeigeBlockInfo(block);
}

public void ZeigeBlockInfo(IMyTerminalBlock block)
{
    // Hier versuchen wir verschiedene Infos über den Block darzustellen, wie z.B. seinen Namen
    Echo(block.CustomName);
}

Vorausgesetzt den Block Namens "Mich gibt es nicht" existiert nicht in deinem Grid wird im Detailfeld  des Programmierbaren Blocks folgendes erscheinen:

Das ist weniger als hilfreich. Wir wissen, dass irgendein Objekt irgendwo Null war, aber das war's. Um das zu beheben, müssen wir unserem Beispiel ein wenig Code hinzufügen, den eben erwähnten Try-Catch-Block:

public void Main()  
{ 
    try
    {
        // Das ist der alte Main-Methoden-Inhalt
        var block = GridTerminalSystem.GetBlockWithName("Mich gibt es nicht");
        ZeigeBlockInfo(block);
    }
    catch (Exception e)
    {
        // Gebe den Exception Inhalt aus
        Echo("Eim Fehler ist während der Skriptausführung aufgetreten:");
        Echo($"Exception: {e}\n---");

        // Werfe wiederrum die Exception um den programmierbaren Block zum abrupten Anhalten zu zwingen
        throw;
    }
}

public void ZeigeBlockInfo(IMyTerminalBlock block)
{    
    Echo(block.CustomName);
}

Leider ist der Zeilenumbruch und das Wordwrapping nicht das beste. Hier steht eigentlich:

An error occurred during script execution.
Exception:
System.NullReferenceException:
Object reference not set to an instance of an object. at Program.ShowBlockInfo(IMyTerminalBlock block) at Program.Main() ---
Caught exception during execution of script:
Object reference not set to an instance of an object.

Die letzte Zeile, nach '---', ist offensichtlich die gleiche wie zuvor, so dass wir das einfach ignorieren werden. Der Rest der Ausgabe stammt aus unseren Änderungen im Skript, und das liefert mehr Informationen. Das sagt es uns:

  • Welche Exception aufgetreten ist (System.NullReferenceException)
  • Die spezifische Fehlermeldung (Objektreferenz nicht auf eine Instanz eines Objekts gesetzt.)
  • Die erste Zeile unter Program.ShowBlockInfo(IMyTerminalBlock block) zeigt Ihnen, in welcher Methode der Fehler aufgetreten ist.
  • Die zweite Zeile bei Program.Main() sagt uns, woher die Methode in der vorherigen Zeile aufgerufen wurde.
  • Wenn die Anrufkette tiefer wäre, würden wir mehr erfahren.

Jetzt wissen wir, welche Methode(n) wir benötigen, um mit Echo's zu dekorieren, wir müssen unsere Anwendung nicht vollständig ausfüllen.

Leider ist auch diese Methode ungenau. Der Compiler-Optimierer ist ziemlich clever. Manchmal nimmt es einige deiner kleineren Methoden und schließt sie zu größeren Methoden zusammen, weil er das für schneller hält. Das bedeutet, dass Du bestimmte Methoden im Stack-Trace nicht sehen wirst, denn was den kompilierten Code betrifft, existieren sie so nicht. Aber es sind Anhaltspunkte wo du mit der Suche beginne kannst.

Echo, Performance und Tricks

Leider gibt es einen kleinen Nachteil, wenn man Echo zu oft benutzt. Im Singeplayer gibt es wenig bis gar keine Probleme, aber im Multiplayer-Modus muss der Text vom Server, auf dem das Skript läuft, und mit Ihrem Client synchronisiert werden. Das braucht Zeit. Wenn viele Skripte eine Menge Text in jedem Frame wiedergeben, wird das Performance-Auswirkungen haben. Aus diesem Grund solltest du nicht immer alles mit Echo ausgeben lassen. Man kann aber die aufgerufene Methode Echo durch eine beliebige andere, eigene ersetzen.

IMyTextPanel _logAusgabeLCD;

public Program()
{
    // Echo ersetzen
    Echo = EchoToLCD;

    // Nimm einen LCD zum loggen
    _logAusgabeLCD = GridTerminalSystem.GetBlockWithName("Log-LCD") as IMyTextPanel;
}

public void EchoToLCD(string text)
{
    // Füge den Text als neue Zeile an den LCD Text an
    // Ein kleiner netter C# Trick:
    // - Das ?. nach _logAusgabeLCD bedeutet "nur machen wenn _logAusgabeLCD nicht null ist".
    _logAusgabeLCD?.WritePublicText($"{text}\n", true);
}