Open-Source-Projekt

WarScript - einbettbare Skriptsprache

WarScript

Eine leichtgewichtige Skriptsprache mit Bytecode-VM, geschrieben in C#. Entwickelt für die Einbettung in Spiele.

Was ist WarScript?

Skripting, das zu Bytecode kompiliert

WarScript ist eine Skriptsprache mit Interpreter und Bytecode-Compiler, geschrieben in C#. Quellcode wird geparst, zu Bytecode kompiliert und auf einer stapelbasierten virtuellen Maschine ausgeführt. Der AST wird nach der Kompilierung verworfen, sodass nur der Bytecode im Speicher bleibt.

Die Sprache hat eine eigene saubere Syntax mit Klassen, Vererbung, Coroutinen, Fehlerbehandlung und einem Importsystem. Aber die echte Stärke ist die Native-Binding-Schicht: dein C#-Spielcode und WarScript kommunizieren direkt, ohne Serialisierung oder Übersetzung dazwischen.

Entstanden aus der Entwicklung von Warman und getestet in einem veröffentlichten Spiel, handhabt WarScript alles von Event-Hooks und Gameplay-Logik bis zum Spawnen von Einheiten und Anwenden von Modifikatoren.

game-logic.ws
# Klassen mit Eigenschaften und Methoden
class Entity [name, hp]
    fun take_damage [amount]
        this :: hp = this :: hp - amount
    end
    fun is_alive []
        return this :: hp > 0
    end
end

hero = new Entity ["Hero", 100]
hero :: take_damage [30]
assert hero :: hp == 70

# Fehlerbehandlung
begin
    risky_operation []
rescue err
    print err :: message
ensure
    print "cleanup done"
end
patrol.ws
# Gemeinsame Bibliotheken importieren
import "lib/vectors.ws"

# Coroutine mit yield
fun patrol_loop [unit, waypoints]
    loop wp in waypoints
        Unit_move_to [unit, wp]
        yield wait 2.0
    end
end

# Eine Endlos-Coroutine starten
start_coroutine_loop ["patrol_loop",
    {guard, {pos_a, pos_b, pos_c}}]

# Wird jeden Tick vom Host aufgerufen
fun tick [dt]
    players = get_players []
    loop player in players
        if Unit_get_hp [player] < 20
            Unit_apply_modifier [player, "Regen"]
        end
    end
end

Die Sprache

Saubere Syntax, echte Features

WarScript nutzt fun/end-Blöcke, Argumente in eckigen Klammern, loop/in-Iteration und #-Kommentare. Die Syntax ist bewusst einfach, damit Modder sie schnell erlernen können.

Aber es ist keine Spielzeugsprache. WarScript hat Klassen mit Vererbung, is- und as-Operatoren für Typprüfung und -umwandlung, begin/rescue/ensure-Fehlerbehandlung, ein import-System mit zirkulärer Abhängigkeitserkennung und Caching, Coroutinen mit yield und yield wait, sowie einen vollständigen Satz an arithmetischen, Vergleichs- und logischen Operatoren.

Die Native-Binding-Schicht ermöglicht Skripten, direkt in deinen C#-Spielcode zu rufen und umgekehrt. Funktionen wie Unit_apply_modifier und spawn_unit sind C#-Methoden, die Skripte nativ aufrufen. Deine Spiel-API wird zur Skript-API.

Architektur

Vom Quellcode zum Bytecode

Bytecode-Compiler

Quellcode wird gelext, in einen AST geparst und dann zu Bytecode kompiliert. Der AST wird nach der Kompilierung verworfen, um Speicher freizugeben. Der Bytecode wird gecacht und bei jedem Aufruf wiederverwendet.

Stapelbasierte VM

Eine speziell gebaute virtuelle Maschine führt den Bytecode aus. Sie nutzt Superinstruktionen (fusionierte Opcode-Paare wie Vergleich-und-Sprung), um den Dispatch-Overhead auf heißen Pfaden zu reduzieren.

Hot Reload

Quellcode zur Laufzeit austauschen und dabei alle globalen Variablen beibehalten. Funktionsdefinitionen werden sofort aktualisiert. Bestehende Klasseninstanzen funktionieren weiter. Aktive Coroutinen werden sauber gestoppt.

Bytecode-Serialisierung

Einmal kompilieren, sofort laden. Kompilierten Bytecode zur Build-Zeit in einen Binärstream speichern, dann zur Laufzeit laden und Lexer, Parser und Compiler komplett überspringen.

Sandboxing

Pro-Aufruf-Limits für Bytecode-Instruktionen und Heap-Allokationen setzen. Wenn ein Budget überschritten wird, wirft die VM eine abfangbare Exception und stoppt sauber. Unverzichtbar für nicht vertrauenswürdige Mod-Skripte.

Source-Map-Debugger

Breakpoints nach Zeilennummer setzen, durch die Ausführung steppen und lokale Variablen inspizieren. Der Debug-Hook empfängt Funktionsname, Zeile und alle Lokalen an jedem Haltepunkt. Null Overhead wenn deaktiviert.

Native Bindings

Source Generator erledigt die Verdrahtung

WarScript wird mit einem Roslyn Source Generator ausgeliefert. Markiere eine C#-Klasse mit [WsModule] und ihre Methoden mit [WsFunction], und der Generator erzeugt automatisch den gesamten Marshalling-Code. Parametertypen (double, int, string, bool, WarValue) werden an der Grenze konvertiert. Rückgabetypen werden gewrappt. Kein manueller Glue-Code.

Für Instanzmethoden fängt der Generator this im Lambda ein. Für variadische Funktionen markiere einen Parameter mit [WsRawArgs], um die rohe Argumentliste zu empfangen. Der generierte Code speist direkt in die Bibliotheks-Registry, die auch der Docs-Exporter liest.

Du kannst native Funktionen auch manuell registrieren. Die NativeFunctionDefinition-API nimmt einen Namen, eine Argumentliste und ein Lambda entgegen. Beide Ansätze koexistieren.

MathModule.cs
// C# mit Source-Generator-Attributen
[WsModule("math",
    Description = "Math functions")]
public static partial class MathModule
{
    [WsFunction("pow")]
    public static double Pow(
        double @base, double exp)
        => Math.Pow(@base, exp);

    [WsFunction("lerp")]
    public static double Lerp(
        double a, double b, double t)
        => a + (b - a) * t;

    [WsFunction("clamp")]
    public static double Clamp(
        double n, double lo, double hi)
        => Math.Max(lo, Math.Min(hi, n));
}

Math

pow, sqrt, floor, ceil, round, abs, min, max, clamp, sign, lerp.

Array

length, contains, index_of, remove, remove_at, pop, insert, copy, clear, append.

Coroutine

start_coroutine, start_coroutine_loop, stop_coroutine, stop_all_coroutines. Yield mit Wartezeiten.

Utility

is_null und Typprüfungs-Helfer. Die Registry ist so konzipiert, dass neue Bibliotheken einfach hinzugefügt werden können.

Integrierte Bibliotheken

Batterien inklusive

WarScript wird mit vier Standard-Bibliotheken ausgeliefert, die automatisch über die WarScriptLibraryRegistry registriert werden. Jede Bibliothek stellt ihre Funktionen mit vollständigen Beschreibungen und Rückgabetyp-Annotationen bereit, sodass der Docs-Exporter Referenzdokumentation für sie zusammen mit deinen spielspezifischen Bindings generieren kann.

Die Coroutine-Bibliothek verdient besondere Erwähnung. Coroutinen in WarScript nutzen Bytecode-Level Suspend und Resume. Eine Coroutine kann yield machen, um bis zum nächsten Tick zu pausieren, yield wait 2.0 für eine bestimmte Dauer, oder als Endlos-Coroutine laufen, die nach jedem Durchlauf neu startet. Der Host ruft TickCoroutines(dt) jeden Frame auf.

Eigene Bibliotheken hinzufügen folgt demselben Muster: erstelle eine statische Klasse mit einer Register(script, scope)-Methode und füge sie zur Registry hinzu. Oder nutze den [WsModule]-Source-Generator.

Integration

So nutzt dein Spiel WarScript

Erstelle eine WarScriptLanguage-Instanz mit deinem Quellcode, einem Datei-Resolver für Imports und einem optionalen Logger. Rufe Run() auf, um den Top-Level-Code zu parsen, kompilieren und auszuführen. Dann nutze GetFunction() und Call(), um Skriptfunktionen aus deiner Spielschleife aufzurufen.

Die ScriptRunner-Basisklasse handhabt das gängige Setup: Standard-Bibliotheken registrieren, das Skript ausführen und eine CallDynamic()-Methode für Aufrufe nach Funktionsname bereitstellen. Überschreibe sie, um deine spielspezifischen Native Bindings und Import-Logik hinzuzufügen.

Für Produktion: zur Build-Zeit mit SaveBytecode() kompilieren und zur Laufzeit mit LoadBytecode() laden. Das überspringt Parsing und Kompilierung komplett. Für Entwicklung: Reload() nutzen, um Quellcode auszutauschen und dabei den gesamten globalen Zustand beizubehalten.

GameSetup.cs
// Skript erstellen und ausführen
var script = new WarScriptLanguage(
    "patrol", source, ImportFile, Log);
script.InstructionBudget = 1_000_000;
script.Run();

// Funktionshandle holen (gecacht)
var tick = script.GetFunction("tick", 1);

// Spielschleife — jeden Frame aufrufen
script.Call(tick, WarValue.FromNumeric(dt));
script.TickCoroutines(dt);

// Hot Reload während der Entwicklung
script.Reload(newSource);
tick = script.GetFunction("tick", 1);

Installation

WarScript zu deinem Projekt hinzufügen

Für Unity-Projekte öffne den Package Manager und füge WarScript über eine Git-URL hinzu. Das Paket zielt auf Unity 6000.3+ ab und hat keine externen Abhängigkeiten.

Unity Package Manager
https://github.com/nine9123/WarScript.git#upm

Für eigenständige C#-Projekte klone das Repository oder füge es als Submodul hinzu. Die Runtime hat keine Unity-Abhängigkeiten und kann in jeder .NET-Anwendung verwendet werden.

Projekt unterstützen

Hilf WarScript zu wachsen

WarScript ist kostenlos und Open Source. Wenn du es nützlich findest und die Entwicklung unterstützen möchtest, werde Patron. Aber am wichtigsten — nutze es einfach. Probiere WarScript in deinem Spiel aus, melde Probleme, schlage Features vor und hilf die Community aufzubauen.

Loslegen

Erkunde die API, lies die Dokumentation und beginne mit dem Skripting.