Blog Info Screens Videos Impressum/Datenschutz

Programmierung der "Rodelberg-Engine"

Ich denke, es ist an der Zeit, den Programmierern unter euch wieder ein bisschen was von unserem Quelltext zu zeigen. Anknüpfen möchte ich dabei an den Post, den ich vor über einem Jahr geschrieben habe, in dem es um die Arbeitsweise unserer “Rodelberg-Engine” ging.
Anhand des dargestellten Diagramms könnt ihr sehen, dass die Engine mit Befehlen aus der Script-Datei gefüttert wird:


Damit die Engine diese Befehle korrekt interpretieren und schließlich ausführen kann, leitet sie jeden Befehl Zeile für Zeile an den Parser weiter. Dieser liefert der Engine als "Dankeschön" immer brav die Art des Befehls zurück.

Programmiertechnisch geschieht das alles wie folgt:
Ein Script ist nichts anderes ist als eine gewöhnliche Textdatei. Robin und ich haben in der Planungsphase vor über 3 Jahren (au Mann, so lange sitzen hier hier schon dran …) festgelegt, dass eine Scriptzeile nur maximal einen einzigen ausführbaren Befehl beinhalten darf:

[SCRIPTBLOCK_ID]
    Befehl1;
    Befehl2;
    Befehl3;
    ...
[END]

Die Engine holt sich also zunächst den kompletten Textblock zwischen [SCRIPTBLOCK_ID] und [END] und speichert jede Befehlszeile in einem NSArray ab. Anschließend wird ein NSTimer erstellt, um die Einträge dieses Arrays in fest definierten Zeitabständen nach und nach abzuarbeiten:

NSTimer *scriptBlockTimer = [NSTimer scheduledTimerWithTimeInterval: 0.01 target: self selector: @selector(scriptBlockTimerTick:) userInfo: nil repeats: YES];
[scriptBlockTimer fire];

Durch den Timer wird also alle 0,01 Sekunden die Methode scriptBlockTimerTick: aufgerufen, in der die Übermittlung der aktuellen Scriptblock-Zeile an den Parser stattfindet:

- (void)scriptBlockTimerTick:(NSTimer*)timer {
    // aktuelle Scriptzeile aus dem Array holen:
    NSString *currentScriptLine = [arrayScriptBlock objectAtIndex: currentLineNumber];

    // (1) Scriptzeile dem Parser übermitteln und Instruktion zurück erhalten:
    Instruction currentInstruction = [parser instructionFromScriptLine: currentScriptLine];

    // (2) ScriptLine-Befehl auswerten:
    if (currentInstruction == IT_AddActor) {
        [self addActorWithScriptLine: currentScriptLine]; }
    if (currentInstruction == IT_DelActor) {
        [self deleteActorWithScriptLine: currentScriptLine]; }
    if (currentInstruction == IT_Say) {
        [self sayWithScriptLine: currentScriptLine]; }
    if (currentInstruction == IT_SetTextColor) {
        [self setTextColorWithScriptLine: currentScriptLine]; }
    if (currentInstruction == IT_SetName) { 
        [self setNameWithScriptLine: currentScriptLine]; }
    if (currentInstruction == IT_SetAnimationFile) { 
        [self setAnimationFileWithScriptLine: currentScriptLine]; }
    if (currentInstruction == IT_SetCostume) { 
        [self setCostumeWithScriptLine: currentScriptLine]; }
    if (currentInstruction == IT_GotoXY) { 
        [self gotoXYWithScriptLine: currentScriptLine]; }

    … und etwa 100 weitere Befehle …
}

Nachdem die Engine durch den Aufruf [parser instructionFromScriptLine: currentScriptLine] die Art des Befehls vom Parser erhalten hat (1), wird im Bereich (2) die entsprechende Methode zur Umsetzung dieses Befehls aufgerufen. Lautete die Scriptzeile beispielsweise

STONE: Say("Hallo, liebe Adventure-Fans!", "Blabla.m4a", 1.0);

so erhält die Engine den Befehl IT_Say vom Parser zurück und ruft daraufhin die nachfolgend abgedruckte Methode sayWithScriptLine: auf. Hier werden schließlich noch die Parameter speechTextspeechFilename und volume aus der Scriptzeile gelesen und dem Actor-Objekt currentActor übergeben, der schließlich den eigentlichen Befehl ausführt:

- (void)sayWithScriptLine:(NSString*)scriptLine {
    // aktuelle ActorID, Text, Sprachdatei und Lautstärke aus der Scriptzeile ermitteln:
    NSString *currentActorIdentifier = [parser actorIdentifierFromScriptLine: scriptLine];
    NSString *speechText     = [parser parameter:  1 fromScriptLine: scriptLine];
    NSString *speechFilename = [parser parameter:  2 fromScriptLine: scriptLine];
    float    volume          = [[parser parameter: 3 fromScriptLine: scriptLine] floatValue];
    
    // Actor-Objekt anhand der ID holen:
    OSActor *currentActor = [dictionaryActorsOnStage valueForKey: currentActorIdentifier];
    if (currentActor != nil) {
        // Actor den Text sprechen lassen:
        [currentActor say: speechText withSpeechFileName: speechFilename andVolume: volume];
    }
    else {
        NSLog(@"--- ERROR: Unknown actor: %@", currentActorIdentifier);
        NSLog(@"    scriptLine: %@", scriptLine);
    }
}

Analog funktioniert das ganze z.B. für den Befehl HANK: GotoXY(200, 130);, indem auch hier der Parser IT_GotoXY als Befehl zurück liefert und die Engine die Methode gotoXYWithScriptLine: aufruft:

- (void)gotoXYWithScriptLine:(NSString*)scriptLine {
    // ActorID und seine zukünftige Position aus der scriptLine lesen:
    NSString *currentActorIdentifier = [parser actorIdentifierFromScriptLine: scriptLine];
    CGPoint newPos = CGPointMake([[parser parameter: 1 fromScriptLine: scriptLine] floatValue], [[parser parameter: 2 fromScriptLine: scriptLine] floatValue]);

    // Actor ansprechen und zu den neuen Koordinaten verschieben:
    OSActor *currentActor = [dictionaryActorsOnStage valueForKey: currentActorIdentifier];
    if (currentActor != nil) {
        [currentActor moveToPosition2D: newPos];
    }
    else {
        NSLog(@"--- ERROR: Unknown actor: %@", currentActorIdentifier);
        NSLog(@"    scriptLine: %@", scriptLine);
    }
}

Die restlichen Befehle sind je nach Komplexität ähnlich programmiert.

Die Methode des Parsers, welche die aktuelle Scriptzeile entgegen nimmt und den Befehl an die Engine zurück liefert, sieht abschließend übrigens so aus:

- (Instruction)instructionFromScriptLine:(NSString*)scriptLine {
    // Befehl zunächst auf einen definierten Wert setzen (IT_ERROR)
    // und in der Scriptline Großbuchstaben in Kleinbuchstaben wandeln:
    Instruction currentInstruction = IT_Error;
    NSString* lowercaseScriptLine = [scriptLine lowercaseString];

    // Überprüfen, ob es eine Leerzeile ist:
    if ([[lowercaseScriptLine stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]] isEqualToString: @""]) {
        currentInstruction = IT_EmptyLine;
    }

    // Befehl aus der Scriptline herausfinden:
    if ([lowercaseScriptLine rangeOfString: @"addactor("].location     != NSNotFound) {
        currentInstruction = IT_AddActor; }
    if ([lowercaseScriptLine rangeOfString: @"delactor("].location     != NSNotFound) {
        currentInstruction = IT_DelActor; }
    if ([lowercaseScriptLine rangeOfString: @"say("].location          != NSNotFound) {
        currentInstruction = IT_Say; }
    if ([lowercaseScriptLine rangeOfString: @"settextcolor("].location != NSNotFound) {
        currentInstruction = IT_SetTextColor; }
    if ([lowercaseScriptLine rangeOfString: @"setname("].location      != NSNotFound) {
        currentInstruction = IT_SetName; }
    … analog für die restlichen Befehle …

    return currentInstruction;
}

Viele Grüße,
Roland

Blog-Archiv

Labels