Charakterauswahl à la Maniac Mansion

1. Vorbereitung

Zuerst brauchen wir ein paar Ressourcen:

Das neueste Bernard-Startpack gibt es hier: Github-Repository

Dieser 320x200-Screenshot enthält den nötigen Raumhintergrund, den Original-Auswahlrahmen und den Mauscursor:

Charakterauswahl

Mit einem Grafikprogramm können wir den Rahmen "ausmessen", er ist 32x34 Pixel groß. Wir werden ihn selbst zeichnen, brauchen ihn also nicht als Sprite. Wir löschen den Rahmen und speichern den Hintergrund.

2. Einbau der Raumes ins Spiel

Als erstes öffnen wir AGS 3.5 oder neuer und laden das Bernard-Starterpack. Ich verwende das aktuelle AGS 3.6.1, es sollte aber keinen großen Unterschied machen.

Dann legen wir einen neuen Raum (Nr. 22) an und wechseln das Hintergrundbild aus. Außerdem benötigt unser Raum ein "Enters room before fadein" event, dass wir im Event-Panel wie gewohnt anlegen.

Im nächsten Schritt bearbeiten wir die Hotspots des Raums und wählen dazu den ersten aus (hHotspot1). Wir fügen das "Any click on hotspot"-Event hinzu, dann ziehen wir mit dem Rechteck-Tool insgesamt sieben Rechtecke um die Köpfe, sodass diese gerade bedeckt sind.
Da unser Hotspot für alle Klicks auf die Kinder zuständig sein wird, brauchen wir noch ein paar Koordinaten: mit der Maus zielen wir genau zwischen Dave's und Syd's Kopf und können oben ablesen, dass wir von der X-Koordinate her ca. bei 60 sind. Zwischen Razor und Jeff hingegen sind wir bei ca. 260.

Jetzt brauchen wir ein bisschen Mathe, denn wir wollen die angeklickte X-Koordinate in einen Index von 0 bis 6 umrechnen, sprich den Index des AGS-Characters, der zum Kopf gehört (das funktioniert 1:1, weil in den Starterpacks praktischerweise die sieben Kinder alle in der richtigen Reihenfolge enthalten sind).

Denken wir uns also sieben Rechtecke um die Köpfe, die alle aneinanderstoßen, so müssen die mittleren fünf eine Gesamtbreite von 260-60 also 200 haben. Das bedeutet, ein Kopf-Rechteck hat eine Breite von 40 Pixel. Dave's geht hierbei von X-Koordinate 20 bis 60, Syd's von 60 bis 100, usw.
Diese Zahl in einen Index von 0-6 umzurechnen, bedeutet, dass wir den linken Rand abziehen müssen, also 20, und dann durch 40 teilen:

  int ci = (mouse.x - 20) / 40;

Bernard's Bereich z.B. geht von 180 bis 219; setzen wir das in die Formel ein, bekommen wir 4 bzw. 4,975 aber dank Integer-Abrundung eben immer eine glatte vier. Und da wir im Spiel sowieso nur den Bereich des Hotspots anklicken können, sind wir auf jeden Fall immer beim richtigen Index.

Wir testen das gleich mal im Spiel, indem wir das Raumskript aufmachen und in die hHotspot1_AnyClick-Funktion folgendes einfügen:

  int ci = (mouse.x - 20) / 40;
  Character* clicked = character[ci];
  Display("Klick auf %s", clicked.Name);

Jetzt öffnen wir cBernard aus dem Charakter-Bereich und ändern StartingRoom auf den Auswahl-Raum (22) und StartY auf -1 und starten dann einen Testlauf mit F5. Unsere Klicks funktionieren bereits einwandfrei, jetzt fehlen nur die Auswahlrahmen.

3. State management

Da wir nicht einfach nur eines der Kinder anklicken, sondern insgesamt drei auswählen sollen, brauchen wir jetzt eine Möglichkeit, den Zustand unseres Auswahlbildschirms zu speichern. Aus der Perspektive der Standard-AGS-Mechanismen könnte man sieben Rahmen-Objekte auf die Köpfe legen und diese an- und ausschalten. Wir wollen stattdessen möglichst ohne Duplizierung und zusätzliche Ressourcen auskommen, d.h. wir werden den Rahmen selbst zeichnen und auch selbst den Zustand speichern.

Hierfür brauchen wir einen Array von Bool'schen Variablen. Wir öffnen also wieder unser Raumskript, und fügen ganz oben über der room_Load-Funktion folgende Zeile ein:

bool selected[7];

Wir haben jetzt sieben Variablen zur Verfügung, die entweder true oder false sein können. Nach der Deklaration sind alle erst einmal false, d.h. wir brauchen keine Initialisierung in room_Load.

Um nun den Status zu verändern, brauchen wir nur eine einzige zusätzliche Zeile in hHotspot1_AnyClick, die wir am Ende der Funktion mit hinein nehmen (Die Character*- und die Display-Zeile können wir löschen, diese haben ihren Zweck erfüllt):

  selected[ci] = !selected[ci];

Diese Zeile schaltet die zum angeklickten Kind gehörende Variable zwischen true und false hin und her, indem der aktuelle Wert negiert wird.

4. Optisches Feedback

Damit wir auch sehen, welche Kinder ausgewählt wurden, müssen wir nun entsprechend der Variablen-Werte Rahmen zeichnen (und wieder löschen). Dazu brauchen wir ein DynamicSprite und ein bisschen Code in room_Load; wir ersetzen die leere room_Load-Funktion also mit diesem Stück Code:

DynamicSprite* bgBackup;

function room_Load()
{
  bgBackup = DynamicSprite.CreateFromBackground();
}

Zur späteren Verwendung machen wir hier beim Laden des Raumes ein Backup des Hintergrunds ohne Rahmen, das wir einfach immer wieder drübermalen werden.

Jetzt fügen wir über der hHotspot1_AnyClick-Funktion eine weitere Funktion ein:

function DrawSelection() {
  DrawingSurface* ds = Room.GetDrawingSurfaceForBackground();
  ds.DrawImage(0, 0, bgBackup.Graphic);

  ds.DrawingColor = 31; // white
  for (int i = 0; i < 7; i++) {
    if (!selected[i]) continue;
    int x1 = i * 40 + 23, x2 = x1 + 31;
    int y1 = 103, y2 = 103 + 33;
    ds.DrawLine(x1, y1, x2, y1);
    ds.DrawLine(x2, y1, x2, y2);
    ds.DrawLine(x2, y2, x1, y2);
    ds.DrawLine(x1, y2, x1, y1);
  }
  ds.Release();
}

Wir gehen die Funktion gleich kurz durch, allerdings müssen wir sie auch aufrufen. Wir fügen also wieder am Ende von hHotspot1_AnyClick eine Zeile ein, diesmal schlicht:

  DrawSelection();

Wenn wir jetzt das Spiel mit F5 testen, können wir bereits durch Anklicken der Köpfe den Rahmen setzen und wieder entfernen!
Die Funktion DrawSelection holt sich zuerst die DrawingSurface des Raumhintergrunds. Dann löscht sie erst einmal alle Rahmen, indem sie die Sicherung des ursprünglichen Hintergrundbildes wieder herstellt.
Danach wird die Zeichenfarbe auf weiß gesetzt, und eine for-Schleife schaut sich der Reihe nach die sieben Bool-Variablen an und zeichnet ggf. den Rahmen um den Kopf. Die erste Zeile in der Schleife springt zum nächsten Index, falls das Kind nicht ausgewählt ist (continue). Ist das Kind gewählt, werden die Koordinaten der linken oberen und rechten unteren Ecke berechnet bzw. angegeben, und dann vier Linien gezeichnet, unser Kasten um den Kopf.

5. Der Mechanismus wird erweitert

Theoretisch könnten wir jetzt bereits einen Start-Button einbauen, allerdings dürfen wir uns nicht darauf verlassen, dass der Spieler auch wirklich genau drei Kinder auswählt. Wir müssen also mitzählen, und die Aktionsmöglichkeiten entsprechend begrenzen.

Da wir eventuell bereits den Auswahl-Klick abfangen müssen, falls schon drei Kinder gewählt wurden, müssen wir schon vorher wissen, wie groß die Anzahl ist. Zum Abzählen bietet sich die DrawSelection-Funktion an, da sie bereits eine passende Schleife enthält. Wir brauchen nur eine zusätzliche Variable, die wir wieder ganz oben im Skript deklarieren:

int kidsCount = 0;

Int-Variablen sind nach der Deklaration immer 0, aber wir schreiben das der Form halber explizit hin. Jetzt müssen wir die DrawSelection-Funktion leicht abändern.

1. direkt über der for-Schleife fügen wir diese Zeile ein:

  kidsCount = 0;

2. in der Schleife, irgendwo nach(!) der if-Zeile kommt dieser Befehl hinein:

    kidsCount++;

Wir setzen kidsCount also auf Null zurück, und zählen es dann immer eins hoch, wenn wir in der for-Schleife auf ein ausgewähltes Kind treffen. Die Variable erst beim Zeichnen zu aktualisieren, reicht hier aus, da der allererste Klick im Spiel auf ein Kind immer erlaubt sein wird.

Jetzt müssen wir aber sicherstellen, dass kein viertes Kind ausgewählt werden kann, ohne vorher ein anderes abzuwählen. Und das erreichen wir, indem wir die Zeile selected[ci] = !selected[ci]; durch dieses Stück ersetzen:

  if (!selected[ci] && kidsCount == 3) Display("Bitte erst ein anderes Kind abwählen!");
  else selected[ci] = !selected[ci];

Falls also ein bisher nicht ausgewähltes Kind angeklickt wird, während gleichzeitig bereits drei andere Kinder ausgewählt sind, bekommen wir eine entsprechende Meldung.
(Theoretisch könnte man dem Spieler durchaus erlauben, zwischendurch mehr als drei Kinder ausgewählt zu haben, solange man ihn nur zum Spiel durchlässt, wenn es wieder drei sind. Das ist eine Design-Entscheidung, die hier nicht im Vordergrund stehen soll.)

6. Der Start-Button

Als nächstes zeichnen wir einen 2. Hotspot, wieder ein Rechteck, diesmal um den Start-Button rechts im Bild. Auch hier fügen wir das "Any click"-Event hinzu (benennt man den Hotspot vorher in z.B. "hStart" um, erhält die generierte Funktion einen entsprechenden Namen und unser Raum-Skript wird lesbarer).
In die frisch erstellte Funktion schreiben wir:

  if (kidsCount != KID_AMOUNT) {
    Display("Bitte drei Kinder auswählen!");
    return;
  }

  Character* kid[] = new Character[KID_AMOUNT];
  int ki = 0;
  for (int i = 0; i < 7; i++) {
    if (selected[i]) {
        kid[ki] = character[i];
        ki++;
    }
  }

  kid[0].ChangeRoom(5, 30, 135);
  kid[0].SetAsPlayer();

Gehen wir es der Reihe nach durch. Zuerst einmal wird der Code eine Fehlermeldung produzieren, denn ich habe eine Konstante namens KID_AMOUNT eingeführt. Damit der Fehler verschwindet, fügen wir wieder ganz oben im Skript eine neue Zeile ein:

#define KID_AMOUNT 3

Mit dieser Compiler-Definition können wir später bei Bedarf die Zahl der Kinder einfach ändern, wenn wir die aktuell hardcodierte 3 überall durch die Konstante ersetzen (konkret ist es nur eine andere Stelle, nämlich in hHotspot1_AnyClick).

Wir überprüfen also erst einmal, ob aktuell auch wirklich drei Kinder ausgewählt sind. Falls nein, zeigen wir eine entsprechende Meldung und brechen dann die Funktion ab (return).

Im nächsten Teil wird es etwas kompliziert: wir führen lokal ein dynamisches Array von Character-Pointern ein, in denen wir die ausgewählten AGS-Charaktere speichern. Dazu verwenden wir wieder eine Schleife, die unsere sieben Kinder durchzählt, und speichern dann die drei ausgewählten nacheinander in kid[0], kid[1] und kid[2]. Eine zweite Integer-Variable ki übernimmt hier das mitzählen des kid-Indexes.
(character[] ist ein Array, das AGS bereitstellt. Es erlaubt uns direkten Zugriff auf alle AGS-Charakter über deren Index.)

Zum Schluß starten wir das eigentliche Adventure, indem wir das erste der Kinder in den ersten Raum schicken und dann zum aktiven Spieler machen.

7. Fazit

Um im Spiel tatsächlich drei Kinder steuern zu können, müssen die ausgewählten Kinder dauerhaft gespeichert werden. Ein lokaler kid[]-Array, der nach Ende der Funktion wieder gelöscht wird, reicht hier natürlich nicht aus. In einem echten Projekt würde man diesen Array stattdessen global anlegen und dem Spieler dann über GUI-Buttons die Möglichkeit des Charakter-Wechselns bieten.

Hier gibt es das vollständige Skript zur Kontrolle: Raum-Skript

Anregungen, Fehler und Sonstiges gerne ins Forum: Foren-Thread bei MM Mania