Diese Anleitung beruht auf dem Ilios Scripting Guide von Chris Tikkos (Dionous Games) und erscheint hier mit seiner Erlaubnis. Beispielprojekte und Abbildungen stammen von Dionous Games. Die Abbildungen zeigen die englische Oberfläche, deshalb stehen die Bezeichnungen der Bedienelemente hier ebenfalls auf Englisch.

Bevor du loslegst

Visionaire Studio wurde ursprünglich für 2D Point-and-Click-Adventures entwickelt. Seit Version 5 machen die Box2D-Physik und die Skriptsprache Ilios auch andere Genres möglich, zum Beispiel physikbasierte Plattformer. Diese Anleitung führt dich durch deine ersten Ilios-Skripte und die Physik-Einrichtung eines kleinen Plattformer-Prototyps.

Nützliche Verweise:

Voraussetzungen

  • Visionaire Studio 5.1.9.2 oder neuer
  • Das Grafikpaket Animated Pixel Adventurer von rvros. Es ist kostenlos, über eine Spende freut sich der Künstler aber sehr.
  • Das Hintergrundbild für das Physik-Beispiel

Tiefe Programmierkenntnisse brauchst du nicht, aber die Grundlagen der objektorientierten Programmierung (Klassen, Funktionen, Variablen) helfen sehr. Die Anleitung erklärt nicht das Programmieren an sich, sondern zeigt, wie diese Konzepte in Visionaire funktionieren.

Code oder visuelles Skripting

Visionaire bietet zwei Wege, Ilios-Skripte zu schreiben:

  • Code: klassisch getippter Programmcode
  • Blocks: visuelles Skripting mit Bausteinen

Öffne in der oberen Leiste „Visual Scripting“, lege ein neues Skript an und wähle den gewünschten Editor:

Neues Ilios-Skript anlegen und zwischen Code und Blocks wählen
Neues Ilios-Skript anlegen und zwischen Code und Blocks wählen

Beide Arten lassen sich in einem Projekt mischen. Manche Skripte kannst du also als Code schreiben und andere mit Blöcken.

Dein erstes Ilios-Skript

Ilios bringt einen komponentenbasierten Ansatz nach Visionaire. Du schreibst Behaviours (Verhaltensklassen) und hängst sie über den Reiter „Components“ an Objekte, Charaktere, Szenen, Interfaces, Buttons oder an das Spiel selbst.

1. Skriptblock anlegen

Wechsle in den Reiter „Visual Scripting“ und lege einen neuen Eintrag an. Er nimmt deine Verhaltensklasse auf. Code und Blöcke können dasselbe, die Wahl ist Geschmackssache. Dieses Beispiel zeigt zuerst die Blöcke, die Code-Fassung folgt am Ende des Abschnitts.

Neuer Eintrag im Reiter Visual Scripting
Neuer Eintrag im Reiter Visual Scripting

2. Verhaltensklasse erstellen

Klicke auf die Arbeitsfläche, um die Blockliste zu öffnen, wähle „New Class“ und bestätige mit einem Doppelklick.

Blockliste mit dem Eintrag New Class
Blockliste mit dem Eintrag New Class

Gib deiner Klasse einen Namen und weise ihr eine Oberklasse (Superclass) zu. Eine Oberklasse enthält bereits das Grundverhalten eines Visionaire-Elements. Deine Klasse erbt es, du kannst ihre Funktionen überschreiben und deine Klasse als Komponente an das passende Element hängen.

Verfügbare Oberklassen:

  • GameBehaviour
  • ObjectBehaviour
  • CharacterBehaviour
  • SceneBehaviour
  • ButtonBehaviour
  • InterfaceBehaviour

Für dieses Beispiel erstellst du ein Objektverhalten, wähle also ObjectBehaviour:

Klassendialog mit ObjectBehaviour als Oberklasse
Klassendialog mit ObjectBehaviour als Oberklasse

Klicke auf „OK“. Der Klassenknoten erscheint auf der Arbeitsfläche:

Klassenknoten auf der Arbeitsfläche
Klassenknoten auf der Arbeitsfläche

3. Funktion anlegen

Über das + fügst du Anschlusspunkte (gelbe Punkte) für Funktionen, Variablen, Zustände und mehr hinzu. Ziehe einen Anschlusspunkt auf die Arbeitsfläche und lass ihn los. Dann siehst du die Liste der Knoten, die du anschließen kannst.

Liste der Knoten, die an die Klasse angeschlossen werden können
Liste der Knoten, die an die Klasse angeschlossen werden können

Hier kannst du auch Funktionen der Oberklasse ObjectBehaviour überschreiben, zum Beispiel FixedUpdate. Diese Funktion läuft in einem festen Takt, etwa 50-mal pro Sekunde:

FixedUpdate überschreiben
FixedUpdate überschreiben

Der Block FixedUpdate ist gelb, das kennzeichnet eine Funktion:

Der Funktionsblock FixedUpdate
Der Funktionsblock FixedUpdate

Jetzt rufst du eine Methode auf, die Text ausgibt: Debug.Print(). Klicke auf die Arbeitsfläche, suche nach debug und wähle per Doppelklick „Print(string)“ aus der Klasse Debug:

Suche nach Debug.Print in der Blockliste
Suche nach Debug.Print in der Blockliste

Der Block sieht so aus:

Der Block Debug.Print
Der Block Debug.Print

Eingänge liegen links am Block, Ausgänge rechts. Der violette Punkt ist ein Eingang vom Typ string (Text). Du kannst einen Text anschließen oder ihn direkt in das kleine Feld neben dem Punkt schreiben:

Text direkt in den String-Eingang schreiben
Text direkt in den String-Eingang schreiben

4. Ausführungsfluss verbinden

Der Print-Block tut nichts, solange er nicht Teil des Ausführungsflusses ist, der weißen Linie zwischen den Blöcken. Die Ausführung beginnt bei einer Funktion, folgt der Linie und führt alle Blöcke der Reihe nach aus.

Die weiße Linie des Ausführungsflusses
Die weiße Linie des Ausführungsflusses

Ziehe den kleinen weißen Pfeil von FixedUpdate auf den Block Debug.Print. Der Hinweis add exec erscheint. Lass los, und dein erstes Skript ist fertig:

FixedUpdate mit Debug.Print verbunden
FixedUpdate mit Debug.Print verbunden

5. Verhalten an ein Objekt hängen

Das neue Verhalten steht jetzt im Reiter „Components“ deines Objekts zur Verfügung (hier test-player):

Verhalten im Reiter Components hinzufügen
Verhalten im Reiter Components hinzufügen

Starte das Spiel und öffne die Konsole mit der Tab-Taste. Unter „Lua Execute“ siehst du die Ausgabe von Debug.Print, etwa 50-mal pro Sekunde, weil der Block in FixedUpdate steckt.

Konsolenausgabe des ersten Skripts
Konsolenausgabe des ersten Skripts

6. Funktionsargumente nutzen

Jetzt soll zusätzlich der Name des Objekts erscheinen. Der Schlüssel dazu ist das Argument this der Update-Funktion:

Das Argument this von FixedUpdate
Das Argument this von FixedUpdate

this verweist auf das Objekt, an dem das Verhalten hängt. Ziehe this auf die Arbeitsfläche:

this auf die Arbeitsfläche gezogen
this auf die Arbeitsfläche gezogen

Damit hast du Zugriff auf alle Eigenschaften des Objekts. Ziehe den blauen Ausgang auf die Arbeitsfläche und wähle „Name“ aus der Liste:

Die Eigenschaft Name auswählen
Die Eigenschaft Name auswählen

Der Knoten „Name“ hat einen violetten Ausgang, liefert also einen Text:

Der Knoten Name
Der Knoten Name

Verbinde ihn mit einem zweiten Debug.Print-Block:

Name mit einem zweiten Debug.Print-Block verbunden
Name mit einem zweiten Debug.Print-Block verbunden

Verbinde zum Schluss beide Print-Blöcke mit der Ausführungslinie, damit sie nacheinander laufen:

Das fertige Skript
Das fertige Skript

Die Konsole zeigt beide Zeilen:

Konsolenausgabe mit dem Objektnamen
Konsolenausgabe mit dem Objektnamen

Dasselbe Skript als Code

class TestClassCode : ObjectBehaviour
{
  override void FixedUpdate()
  {
    Debug.Print("Hello World! -Code Version-");
    Debug.Print(this.Name);
  }
}

Die Klasse hat einen anderen Namen, damit du in der Komponentenauswahl zwischen Block-Fassung und Code-Fassung wechseln kannst:

Beide Fassungen in der Komponentenauswahl
Beide Fassungen in der Komponentenauswahl

Beispielprojekt: quickstart.veb (Dionous Games)

Drei Arten, Ilios-Skripte zu nutzen

  • Als Komponente, die einem Objekt, einer Szene, einem Charakter, einem Button, einem Interface oder dem Spiel ein Verhalten gibt
  • Als statische Klasse mit Funktionen, die jedes andere Skript nutzen kann
  • Als Datentyp, also als Vorlage für Daten, die andere Skripte verwenden

Skript-Komponenten

Damit du ein Skript an ein Element hängen kannst, muss seine Klasse von der passenden Oberklasse erben:

ElementOberklasse
ObjektObjectBehaviour
SzeneSceneBehaviour
ButtonButtonBehaviour
InterfaceInterfaceBehaviour
CharakterCharacterBehaviour
SpielGameBehaviour

Komponenten sind Instanzen: Du kannst dasselbe Skript mehrfach hinzufügen, und jede Instanz behält ihre eigenen Daten. Um eine Instanz aus einem anderen Skript zu nutzen, brauchst du einen Verweis auf genau diese Instanz.

Bestimmte Funktionen einer Komponente ruft Visionaire automatisch auf, zum Beispiel Awake und FixedUpdate. So verbindet sich dein Code mit dem Geschehen im Spiel:

class PlayerMoveC : ObjectBehaviour
{
  // wird einmal vor dem ersten Update aufgerufen
  override void Awake()
  {
    // Code zur Initialisierung
  }

  // wird in einem festen Takt aufgerufen
  override void FixedUpdate()
  {
    // Code, der das Objekt aktualisiert
  }
}

Etwas passiert nur, wenn eine Komponente auf einen Auslöser reagiert: eine Eingabe, eine Kollision, einen Aufruf aus einem anderen Skript oder einen regelmäßigen Aufruf wie FixedUpdate oder Update.

Statische Klassen

Statische Klassen hängen an keinem Element. Sie existieren einmal im Projekt, und jedes Skript kann sie direkt über ihren Klassennamen nutzen, ohne Verweis. Das macht sie praktisch für Hilfsfunktionen, die viele Skripte brauchen.

Beispiel: eine Hilfsklasse, die den Abstand zwischen zwei Punkten berechnet:

class MathUtils
{
  // Abstand zwischen zwei Punkten
  static float CalculateDistance(vec2 pointA, vec2 pointB)
  {
    float distance = vec2.Distance(pointB, pointA);
    return distance;
  }
}

Aufrufen kannst du sie von überall so:

MathUtils.CalculateDistance(new vec2(0,0), new vec2(3,4))

Zum Testen nimmst du den Aktionsteil „Run ilios script“ und gibst das Ergebnis in der Konsole aus:

Debug.Print(MathUtils.CalculateDistance(new vec2(0,0), new vec2(3,4)).ToString());

Debug.Print() nimmt nur Text an, deshalb wandelt .ToString() das Ergebnis vom Typ float vorher um.

Eigene Datentypen

Mit Skripten kannst du auch eigene Datentypen festlegen: Structs, einfache Klassen, Enums oder Interfaces. Du nutzt sie im ganzen Spiel als Datentyp oder als Vorlage, siehe Konstruktoren weiter unten.

Variablen und Outlets

Ein einfaches Beispiel mit zwei Variablen:

class Player : ObjectBehaviour
{
  outlet float playerHealth;
  outlet bool isDead;

  void Damage(float dmg)
  {
    playerHealth -= dmg;
    if(playerHealth < 0)
    {
      // der Spieler ist besiegt
      isDead = true;
    }
  }

  void AddHealth(float health)
  {
    playerHealth += health;
    if(playerHealth > 100)
    {
      // Gesundheit ist voll
      playerHealth = 100;
    }
  }
}

Das Schlüsselwort outlet zeigt die Variable im Editor an, sodass du ihren Wert dort einstellen kannst. Füge die Komponente Player einem Objekt hinzu, um das zu sehen:

Outlet-Variablen in den Komponenten-Einstellungen
Outlet-Variablen in den Komponenten-Einstellungen

Physik des Spielers einrichten

Spieler vorbereiten

  • Lege das Spielerobjekt an, zum Beispiel test-player.
  • Erstelle eine Ruheanimation, füge ihre Einzelbilder hinzu und stelle sie auf unendliche Wiederholung:
Ruheanimation mit unendlicher Wiederholung
Ruheanimation mit unendlicher Wiederholung
  • Platziere die Animation auf der Arbeitsfläche, vorerst bei (40,90):
Animation platzieren
Animation platzieren
  • Lege die Ruheanimation als Standardanimation des Spielers fest:
Ruheanimation als Standardanimation
Ruheanimation als Standardanimation

Wenn du das Spiel startest, erscheint der Spieler mit seiner Ruheanimation. Weil die Pixelgrafik sehr klein ist, verbessern diese Einstellungen die Darstellung:

  • Stelle die Spielauflösung auf 320 × 240 Pixel.
  • Vergrößere das Fenster mit einem Skript in der Startaktion:
Skript in der Startaktion, das das Fenster vergrößert
Skript in der Startaktion, das das Fenster vergrößert
  • Aktiviere den Pixel-Effekt für scharfe Pixel:
Einstellung Pixel-Effekt
Einstellung Pixel-Effekt

Body und Fixture

Visionaire nutzt Box2D für die Physik und fügt sie Objekten über Komponenten hinzu. Ein funktionierendes Box2D-Objekt braucht einen Body (Körper) und eine Fixture (Form):

Komponenten Body und Fixture
Komponenten Body und Fixture

Der Body enthält die Eigenschaften, die man weder sieht noch anfassen kann:

  • Masse: wie schwer er ist
  • Geschwindigkeit: wie schnell und in welche Richtung er sich bewegt
  • Trägheitsmoment: wie viel Kraft nötig ist, um eine Drehung zu starten oder zu stoppen
  • Winkelgeschwindigkeit: wie schnell und in welche Richtung er sich dreht
  • Position: wo er sich befindet
  • Winkel: in welche Richtung er zeigt

Füge dem Spieler einen Body hinzu:

Body-Einstellungen des Spielers
Body-Einstellungen des Spielers

Box2D kennt drei Arten von Bodies:

  • dynamisch: wird von Kräften beeinflusst und reagiert auf Kollisionen, wie unser Spieler
  • statisch: wird nicht von Kräften beeinflusst und lässt sich nicht bewegen, wie Wände oder der Boden
  • kinematisch: bewegt sich mit vorgegebener Geschwindigkeit, ignoriert aber Kräfte, wie eine bewegliche Plattform. Als die Originalanleitung entstand, unterstützte Visionaire diese Art noch nicht.

Einstellungen des Body:

  • „Static“: nicht ankreuzen, der Spieler ist ein dynamischer Body.
  • „Fixed Rotation“: verhindert Drehungen, zum Beispiel nach einer Kollision. Der Spieler braucht das.
  • „Linear Damping“: bremst die Bewegungsgeschwindigkeit.
  • „Angular Damping“: bremst die Drehgeschwindigkeit.
  • „Gravity Scale“: schwächt oder verstärkt die Schwerkraft. 1 ist normale Schwerkraft, -1 kehrt sie um, 0 schaltet sie ab.

Ein Body allein hat keine Form und kann nicht kollidieren. Größe, Form und Material kommen von den Fixtures. Ein Body kann mehrere Fixtures haben, und ihre Anordnung beeinflusst seinen Schwerpunkt. Wenn zwei Bodies zusammenstoßen, entscheiden ihre Fixtures, wie sie reagieren. Eigenschaften einer Fixture:

  • Sensor: Ein Sensor erkennt Kollisionen, reagiert aber nicht darauf. Der Spieler ist kein Sensor.
  • Reibung (friction): wie rutschig sie ist
  • Rückprall (restitution): wie stark sie abprallt
  • Dichte (density): wie schwer sie im Verhältnis zu ihrer Fläche ist
  • Form (shape): Polygon oder Kreis

Visionaire bietet drei Formen: Rechteck, Kreis und Kante (freie Form). Für den Spieler wählst du das Rechteck:

Rechteckige Fixture des Spielers
Rechteckige Fixture des Spielers

Das rote Rechteck ist die Kollisionsgrenze des Spielers. Starte das Spiel: Der Spieler fällt wegen der Schwerkraft aus dem Bild. Füge also das Hintergrundbild und ein Bodenobjekt mit einem statischen Body und eigener Fixture hinzu:

Bodenobjekt mit statischem Body und Fixture
Bodenobjekt mit statischem Body und Fixture

Starte das Spiel erneut. Der Spieler steht jetzt auf dem Boden: Deine erste Kollision funktioniert.

Beispielprojekt: player-setup.zip (Dionous Games)

Konstruktoren

Ein Konstruktor ist eine besondere Funktion, die automatisch läuft, wenn eine neue Instanz entsteht. Er legt die Anfangswerte der neuen Instanz fest.

Lege einen neuen Block an und erstelle im Code-Reiter eine Klasse für Waffen mit Konstruktor:

class Weapon : object // erbt von der Oberklasse object
{
  string wpnName;
  int dmg;
  float fRate;

  // Konstruktor, muss genauso heißen wie die Klasse
  Weapon(string weaponName, int damage, float fireRate)
  {
    this.wpnName = weaponName;
    this.dmg = damage;
    this.fRate = fireRate;
    Debug.Print("Created ${this.wpnName} with Damage ${this.dmg} and Fire Rate ${this.fRate}");
  }
}
  • Solche Klassen erben von der internen Oberklasse object. Sie beschreiben Daten, kein Verhalten, und lassen sich deshalb nicht an Visionaire-Elemente hängen.
  • Der Konstruktor heißt genauso wie seine Klasse, hier Weapon.
  • this verweist auf die Instanz, die gerade entsteht.

Dasselbe Skript als Blöcke (zum Vergrößern anklicken):

Die Klasse Weapon mit ihrem Konstruktor als Blöcke
Die Klasse Weapon mit ihrem Konstruktor als Blöcke

Einen Konstruktor legst du im Block-Editor als normalen Funktionsblock an und wählst dort „Constructor“:

Constructor im Funktionsblock auswählen
Constructor im Funktionsblock auswählen

Den Konstruktor verwenden

In diesem Beispiel entstehen die Waffen einmal beim Laden des Skripts, also in Awake(). Das Skript hängt am Spielerobjekt, seine Oberklasse ist deshalb ObjectBehaviour:

class MainPlayer : ObjectBehaviour
{
  override void Awake()
  {
    Weapon axe = new Weapon("Axe", 7, 3);
    Weapon sword = new Weapon("Sword", 5, 4);
  }
}

Dasselbe als Blöcke:

Zwei Waffen in Awake mit Blöcken erzeugen
Zwei Waffen in Awake mit Blöcken erzeugen

Starte das Projekt und öffne die Konsole: Beide Waffen werden erzeugt.

Konsolenausgabe der beiden Konstruktoren
Konsolenausgabe der beiden Konstruktoren

Konstruktoren überladen

Eine Klasse kann mehrere Konstruktoren mit unterschiedlicher Signatur haben, also mit unterschiedlichen Parametern in den Klammern. Neben Weapon(string weaponName, int damage, float fireRate) könntest du Weapon() ohne Parameter und Weapon(string weaponName) nur mit Namen anlegen. Dann lässt sich eine Waffe auf verschiedene Arten erzeugen, zum Beispiel mit new Weapon("Spear") oder new Weapon().

Beispielprojekte (Dionous Games): constructors.veb (Blöcke) und constructors-code.veb (Code)

Nächster Schritt: Baue für diesen Spieler ein Level im Plattformer-Tutorial mit Tiled und Box2D.

Mehr zu Konstruktoren, Destruktoren und statischen Konstruktoren findest du in der Ilios Referenz.