Blog Info Screens Videos Impressum/Datenschutz

"Wie werde ich in einem Adventure reich?" – Alltagsprobleme beim Scripten

Nein nein, es geht hier nicht darum, wie ich mit einem Adventurespiel reich werde (bei uns steht nämlich der Spaß des Programmierens im Vordergrund und wenn wir später mit unseren paar Einnahmen alle unsere Freunde, die uns bei dem Projekt geholfen haben, zum Essen einladen können, werden wir mehr als glücklich sein :-) ), sondern wie man in einem Adventurespiel zu Reichtum gelangen kann – dank Scriptfehler versteht sich ;-)

Stellen wir uns einmal vor, Stone befindet sich in einem Zimmer und auf einem Tisch liegt eine Geldmünze. Jeder gute Adventurespieler steckt natürlich alles ein, was nicht niet- und nagelfest ist und deshalb wandert diese Münze auch gleich ins Inventar. Scripttechnisch sieht das ganze dann so aus:

[TAKE COIN]
    // Stone zum Tisch gehen lassen:
    STONE: GotoActor(TABLE);
    Wait();
    STONE: TurnToActor(TABLE);
    // Nimm-Animation abspielen:
    STONE: PlayTakeAnimation();
    Wait();
    // Münze vom Tisch entfernen:
    DelActor(COIN);  
    // Münze dem Inventar hinzufügen:
    STONE: AddItem(COIN, "10 Cent", "Inv_Coin.png", true); 
[END]

Starte ich das Spiel und teste, ob Stone die Münze auch tatsächlich vom Tisch nimmt, dann stelle ich fest, dass alles korrekt gescriptet ist. Ich kann also im PDC (Puzzle Dependency Chart) einen weiteren Knotenpunkt als erledigt abhaken :-)
Richtig? Nein, falsch! Ich habe nämlich eine Sache nicht beachtet, die dazu führen könnte, dass sich der Spieler eine ganze LKW-Ladung voller 10-Cent-Münzen ins Inventar steckt und damit ungeplanterweise reich wird (die Taschen in Adventurespielen sind ja bekanntlich sehr groß ;-) ). Aber wieso? Im Scriptblock wird doch die Münze vom Tisch entfernt …? Ja, das wird sie, aber wenn der Spieler den Raum verlässt und wieder hinein kommt, wird sich eine neue Münze auf dem Tisch befinden, die der Spieler wieder nehmen kann! 

Vielleicht freut sich der Spieler im ersten Moment (“Hey cool, noch eine Münze!”), aber im nächsten Moment denkt er sich zurecht, dass sie da eigentlich gar nicht mehr liegen dürfte, weil man sie ja schließlich schon genommen hat. Also kann es nur ein Bug sein. Wenn ich ein Adventure spiele, werde ich persönlich in so einem Fall beim Spielen sehr vorsichtig und beginne, den Spielstand öfter abzuspeichern. Außerdem werde ich zurückhaltender, wenn es darum geht, verrückte Sachen im Spiel auszuprobieren (z.B. Hamster in Mikrowellen stecken, usw.), weil ich ein bisschen das Vertrauen in die Stabilität des Spiels verloren habe und befürchte, dass eine unbedachte Aktion eine dramatische Auswirkung auf das weitere Spielgeschehen haben könnte. Im schlimmsten Fall könnte sie dazu führen, dass das Spiel in einer Sackgasse landet. Solche Sackgassen sind der Motivationskiller schlechthin und sollten natürlich unbedingt vermieden werden!

Und wie löst man das Münzenproblem nun? Im Prinzip ganz einfach: wir erstellen eine Boolesche Variable, in der abgespeichert ist, dass wir die Münze genommen haben. Der Scriptblock wird somit nur um eine Zeile ergänzt (blau hinterlegt):

[TAKE COIN]
    // Stone zum Tisch gehen lassen:
    STONE: GotoActor(TABLE);
    Wait();
    STONE: TurnToActor(TABLE);
    // Nimm-Animation abspielen:
    STONE: PlayTakeAnimation();
    Wait();
    // Münze vom Tisch entfernen:
    DelActor(COIN);  
    // Münze dem Inventar hinzufügen:
    STONE: AddItem(COIN, "10 Cent", "Inv_Coin.png", true); 
    pickedUpCoin := true;
[END]

Damit die Münze beim erneuten Betreten des Raums nicht wieder erscheint, wird der Zustand dieser Variablen im [INIT]-Block der Scriptdatei des Raumes abgefragt:

[INIT]
    ...
    // Münze nur darstellen, wenn sie noch nicht genommen wurde:
    if (pickedUpCoin == false) {
        AddActor(COIN, "Münze", true, "Coin.png", "", "");
    }
    ...
[END]

Fertig. :-)

Mal ein komplexeres Beispiel:
In einem geschlossenen Schrank befindet sich eine Tüte Chips, die man nehmen kann. Der einfachste Fall ist, dass der Spieler den Schrank öffnet (wir gehen der Einfachheit halber davon aus, dass er nicht abgeschlossen ist) und die Tüte nimmt. Die Umsetzung ist mit zwei Scriptblöcken und wenigen Befehlen innerhalb von wenigen Minuten gelöst:

[USE CUPBOARD]
    // Stone zum Schrank gehen lassen
    STONE: GotoActor(CUPBOARD);
    Wait();
    STONE: TurnToActor(CUPBOARD);
    // Nimm-Animation abspielen:
    STONE: PlayTakeAnimation();
    Wait();
    // Schrank öffnen und Tüte Chips im Inneren darstellen:
    CUPBOARD: SetCostume("Cupboard_open.png");
    AddActor(CHIPS, "Tüte Chips", true, "Chips.png", "", "");
[END]

[TAKE CHIPS]
    // sicherstellen, dass Stone am Schrank steht, 
    // wenn er die Nimm-Animation abspielt:
    STONE: GotoActor(CUPBOARD);
    Wait();
    STONE: TurnToActor(CUPBOARD);
    STONE: PlayTakeAnimation();
    Wait();
    // Chips aus dem Schrank entfernen:
    DelActor(CHIPS);
    // Chips dem Inventar hinzufügen und abspeichern, 
    // dass die Chips genommen wurden:
    STONE: AddItem(CHIPS, "Tüte Chips", "Inv_Chips.png", true);
    pickedUpChips := true;
[END]

[INIT]
    ...
    if (pickedUpChips == false) {
        AddActor(CHIPS, "Tüte Chips", true, "Chips.png", "", "");
    }
    ...
[END]

Ihr seht, dass ich hier bereits daran gedacht habe, die Zustandsvariable pickedUpChips korrekt zu setzen und im [INIT]-Block abzufragen, ob die Chips schon genommen wurden. Aber das reicht in diesem Fall leider nicht mehr aus! Der Code führt sogar dazu, dass sich die Szene komplett falsch verhält:
Zum Beispiel kann Stone die Chipstüte nehmen und anschließend den Schrank erneut öffnen (obwohl er ja schon offen ist). Das führt dazu, dass wieder die Chipstüte dargestellt wird, die man trotz der korrekt gesetzten Zustandsvariablen wieder nehmen kann!

Außerdem führt die Tatsache, dass sich die Chips in einem Schrank befinden, dazu, dass der Spieler viel mehr Möglichkeiten hat, mit dieser Situation umzugehen. Er könnte also zum Beispiel auf folgende Ideen kommen:

  1. Schrank öffnen, Chipstüte nicht nehmen, Raum verlassen und nach einiger Zeit den Raum erneut betreten
    –> Der Schrank muss beim erneuten Betreten offen sein. Die Chipstüte muss angezeigt werden!

  2. Schrank öffnen, Chipstüte nehmen, Raum verlassen und den Raum erneut betreten
    –> Der Schrank muss beim erneuten Betreten offen sein. Die Chipstüte darf nicht angezeigt werden!

  3. Schrank öffnen, Chipstüte (nicht) nehmen, Schrank schließen, Raum verlassen und erneut betreten
    –> Der Schrank muss beim erneuten Betreten geschlossen sein. Die Chipstüte darf nicht angezeigt werden.
  4. Schrank öffnen, Chipstüte _nicht_ nehmen, Schrank schließen
    –> Der Schrank muss geschlossen werden. Die Chipstüte darf nicht mehr angezeigt werden.
  5. Schrank immer abwechselnd öffnen und schließen, ohne dass die Chipstüte genommen wurde
    –> Bei geöffnetem Schrank muss die Chipstüte dargestellt werden.
  6. Schrank immer abwechselnd öffnen und schließen, wenn die Chipstüte bereits genommen wurde
    –> Bei geöffnetem Schrank darf die Chipstüte nicht dargestellt werden.
Es sind also viele Sonderfälle abzufangen, damit der Szenenaufbau nicht unlogisch ist. Zur Lösung reichen aber zum Glück zwei Zustandsvariablen aus: pickedUpChips und cupboardIsOpen.
Damit ergeben sich dann schließlich folgende Scriptblöcke:

[INIT]
    ...
  // korrekte Grafiken des Schranks beim Betreten 
    // des Raums setzen:
  if (cupboardIsOpen == false) {
  CUPBOARD: SetCostume("Cupboard_closed.png");
  }
  else {
  CUPBOARD: SetCostume("Cupboard_open.png");
  // Chipstüte bei geöffnetem Schrank darstellen,
        // falls sie noch nicht genommen wurde:
  if (pickedUpChips == false) {
  AddActor(CHIPS, "Tüte Chips", true, "Chips.png", "", "");
  }
  }
    ...
[END]

[USE CUPBOARD]
  // zum Schrank gehen und Nimm-Animation abspielen:
  STONE: GotoActor(CUPBOARD);
  Wait();
  STONE: TurnToActor(CUPBOARD);
  STONE: PlayTakeAnimation();
  Wait();
  // wenn der Schrank geschlossen ist, dann öffnen:
  if (cupboardIsOpen == false) {
  CUPBOARD: SetCostume("Cupboard_open.png");
  cupboardIsOpen := true;
  // Chipstüte darstellen, falls sie noch nicht 
        // genommen wurde:
  if (pickedUpChips == false) {
  AddActor(CHIPS, "Tüte Chips", true, "Chips.png", "", "");
  }
    }
    // ansonsten Schrank schließen:
  else {
  CUPBOARD: SetCostume("Cupboard_closed.png");
  cupboardIsOpen := false;
  // Chipstüte aus dem Schrank entfernen, 
        // falls sie noch nicht genommen wurde:
  if (pickedUpChips == false) {
  DelActor(CHIPS);
  }
  }
[END]

[TAKE CHIPS]
  // zum Schrank gehen und Nimm-Animation abspielen:
  STONE: GotoActor(CUPBOARD);
  Wait();
  STONE: TurnToActor(CUPBOARD);
  STONE: PlayTakeAnimation();
  Wait();
  // Chipstüte aus dem Schrank entfernen:
  DelActor(CHIPS);
  // Chipstüte dem Inventar hinzufügen:
  STONE: AddItem(CHIPS, "Tüte Chips", "Inv_Chips.png", true);
  pickedUpChips := true;
[END]

Ihr seht hoffentlich, dass selbst hinter einfachen Aktionen eine Menge Sonderfälle stecken können, die alle abgesichert sein müssen, damit das Spiel nicht unlogisch wird. Ich muss also zu jeder Zeit präzise und konzentriert arbeiten, um alle diese Fälle zu erkennen und abzufangen. Die Liste an Zustandsvariablen, die wir bereits verwenden, ist also dementsprechend lang und ich hoffe, dass ich beim Scripten keinen Sonderfall übersehen habe ;-) Was den zeitlichen Rahmen betrifft sitze ich also bei der Umsetzung jedes einzelnen Knotenpunktes im PDC mindestens eine bis zwei Stunden (inkl. Testen). Robin findet dann in der Regel immer noch mindestens einen weiteren Sonderfall. Trotzdem macht es Spaß und man entwickelt mit der Zeit Strategien, wie man solche Fälle erkennt und effektiv abfängt :-)

Roland

Blog-Archiv

Labels