Game Mechanic 101 - Bewegung

Mit diesem Posting geht es an die erste Spielmechanik - im englischen hört sich das spannender an: game mechanic. Für mich war es zunächst wichtig was GameMechanics sind und ich habe mich auf den Weg gemacht das herauszufinden. Beim Lesen von Wikipedia, Blogartikeln und Expertenmeinungen kommt man dann schon etwas in Wanken und fragt sich ob dieser Begriff so eindeutig definiert ist. Von Spielmechanik, game mechanics, gameplay, game design, usability, game theory springen die erklärenden Seiten von Begriff zu Begriff.

Für mich definiere ich die Begriff nun wie folgt:

Game Theory

Der Begriff "Game Theory" ist für mich die akademische Annäherung an Games und untersucht die Einflüsse und Auswirkungen von Spielen oder Inhalte eines Spiels auf das Spiel, den Spieler oder den Spielproduzenten. Somit ist der Faktor von politischer Situation und wirtschaftliches Umfeld genauso einzubeziehen, wie die psychologischen, physiologischen und biologischen Eigenschaften von Spielern. Damit kann dann ein Spiel grob skizziert werden in der Planung oder bei der Bewertung des Effekts.

Game Design

Der Begriff "Game Design" ist das konkrete Planen eines Spieles. Game Designer sind die künstlerische Komponente in der Planungsphase, die mit ihrer Vorstellungskraft die technischen Möglichkeiten, (zu erstellenden) visuellen oder akustischen Effekte ausloten und verbinden mit einer Geschichte die erzählt werden möchte. Sie greifen bei der Erstellung des Konzepts auf Ergebnisse der "Game Theory" zurück und erzeugen mit dem Konzept das grundlegende Spiel von Singleplayer Jump&Run bis Multiplayer Fantasygame (das eine Story haben kann, aber nicht muss). Wenn das Spiel dann entwickelt wird, sind sie die Berater und bewerten "Soll" von "Ist" und passen notfalls die Idee und das Konzept an technische Machbarkeit an.

Game Mechanic

Der Begriff "Game Mechanic" ist ein Element in einem Spiel das dem Konzept aus dem "Game Design" folgt und unterstützt, die gewünschte Wirkung zu erreichen. Es ist der technische Aspekt in der Entwicklung, da eine Reihung von Spielmechaniken die Stimmung oder das "Gameplay" mitentwickelt. Eine Mechanik kann von Einfach bis Komplex das ganze Spiel bestimmen und reicht von nicht-interaktiven Inhalten bis hin zu interaktiven, körperlichen Einbringung.

GamePlay

GamePlay ist das Ergebnis für den Spieler, das mit GameDesign und GameMechanic erzeugt wurde. Im deutschen wäre es einfach der Spielspaß und die Bindung zu einem Spiel die man aufbauen kann.


Nachdem ich die Begriffe aufgeschrieben habe würde ich die nächsten Tätigkeiten eindeutig als "GameMechanic" sehen, denn es geht nur um einen kleinen Teil in einem Spiel. Es ist kein Design und kein Konzept, sondern nur ein Baustein in einem Puzzle: Die GameMechanic "Bewegung" - die Bewegung einer Spielfigur in einer Welt.

Direkt zum Github Repository geht es mit diesem Link und zur Online Demo mit diesem Link

In einem vorherigen Artikel habe ich meine Entwicklungsumgebung beschrieben und diese werde ich für dieses Projekt natürlich auch nutzen. Zuerst wird ein neuer Ordner erstellt und in dem Ordner lasse ich den Yeoman Generator phaser-alt seine Arbeit erledigen, sodass das Grundgerüst steht.

$ mkdir mech01
$ cd mech01
$ yo phaser-alt

Um sich vorzustellen zu können wie das Ergebnis aussehen sein soll, habe ich mit Inkscape eine grobe Zeichnung erstellt auf der ein Boden (schwarz), drei Hindernisse (blau) und die Startposition der Spielfigur (rot) zu sehen ist.

Mechanic pre drawing

Diese Blockformen werden wir auch als einfachen Grafikersatz nutzen, sprich es gibt keine künstlerisch animierten Charakter oder detaillierte Texturen. Es geht mir nur um den Aufbau und die Möglichkeiten der Mechanik "Bewegung". Die Grafiken sind drei quadratische Bilder (32x32px) mit den jeweiligen Farben:

Asset black block Asset blue block Asset red block

Nun kann man endlich mit dem coden anfangen und wir beginnen damit die Image Assets im preload State einzubinden und zu laden:

'use strict';

/**
 * Preload state
 *
 * Preloader for the game to load all kind of
 * assets and lastly start the menu state
 */
function Preload() {
}

Preload.prototype = {
  /**
   * Preload the preload state
   *
   * In the preload state all assets are loaded
   * via the `this.load` reference to Phaser.Loader.
   *
   * The visually indicate for the use the loading,
   * setup the preloadSprite system.
   */
  preload: function preload() {
    var cx = this.game.width / 2;
    var cy = this.game.height / 2;

    this.loadBackground = this.game.add.sprite(cx - 100, cy - 30, 'preload-bg');
    this.loadBar = this.game.add.sprite(cx - 100, cy - 30, 'preload-fg');

    this.load.setPreloadSprite(this.loadBar);

    // load the play button spritesheet
    this.load.spritesheet(
      'menu-btn-play',
      '/assets/images/menu-btn-play.png',
      128, 32  // width+height of each frame
    );
    // load the audio for the menu
    this.load.audio('audio-menu-tick', '/assets/audio/menu-tick.mp3');
    this.load.audio('audio-menu-select', '/assets/audio/menu-select.mp3');


    //******
    // Load the 3 quadratic 32x32 images
    //******
    this.load.image('block-black', '/assets/images/sprite_blockblack.png');
    this.load.image('block-blue', '/assets/images/sprite_blue.png');
    this.load.image('block-red', '/assets/images/sprite_red.png');
  },

  /**
   * Create the preload state
   *
   * When the `create` method is called, then the preload
   * of assets has been done and we can jump over
   * to the `play` state.
   */
  create: function create() {
    this.state.start('menu');
  }
};

module.exports = Preload;

Für die Übersicht folgen nun nochmal die 3 neuen Zeilen die entscheidend sind für das Laden der Bilder. Phaser bietet via Phaser.Loader (nutzbar mit this.load) die einfache Möglichkeit über ein Imagekey (block-xyz) und der URL zum Bild (/assets/images/sprite_xyz), das Bild vorladen und später zu nutzen über den ImageKey

  //******
  // Load the 3 quadratic 32x32 images
  // this.load.image('image-key', 'path-to-image-on-server');
  //******
  this.load.image('block-black', '/assets/images/sprite_blockblack.png');
  this.load.image('block-blue', '/assets/images/sprite_blue.png');
  this.load.image('block-red', '/assets/images/sprite_red.png');

Und damit verlassen wir auch die preload.js State Datei und begeben uns zum play State (/app/js/states/play.js).


Der Play-State wird nun der State sein in dem wir uns die nächste Zeit bewegen werden. Hierbei sind die Funktionen create() und update() die Hauptakteure und daher werde ich das Grundgerüst der play.js Datei einmal komplett aufzeigen und bei den folgenden Veränderungen nur noch die entsprechenden Änderungen dokumentieren.

'use strict';

/**
 * Play state - the actual game
 */
function Play() {
}

Play.prototype = {
  /**
   * Preload the game
  */
  preload: function preload() {
    // prevent window jumping, by binding the keys that
    // trigger the browser to scroll
    this.input.keyboard.addKeyCapture([
      Phaser.Keyboard.LEFT,
      Phaser.Keyboard.RIGHT,
      Phaser.Keyboard.UP,
      Phaser.Keyboard.DOWN,
      Phaser.Keyboard.SPACEBAR
    ]);

    // if arrow keys needed, use `createCursorKeys`
    this.cursors = this.input.keyboard.createCursorKeys();

    // enable advance (precission) timing
    this.game.time.advancedTiming = true;
  },

  /**
   * Create the game instance that makes a 
   * level playable. Constructs the entities and
   * the basic logic that is needed
   */
  create: function create() {
  },

  /**
   * Update function - called on every frame
   */
  update: function update() {
  },

  /**
   * Render function, mostly used for debugging
   */
  render: function render() {
    // example: debug a sprite object when physics are enabled
    //  this.game.debug.body(this.player);
  },
};

module.exports = Play;

Schritt 1: Laden der Bilder in den play state

Die Bilder sind im preload State bereits geladen, jedoch noch nicht dem play State bekannt gemacht. Also werden wir jetzt bei der Erstellung des Play State (function create()) die Bilder adden und weisen die Referenzen auf die hinzugefügten Bilder direkt passenden Variablen zu.

Play.prototype = {
  create: function create() {
    this.stage.setBackgroundColor(0xffffff);  // set backgroundcolor of stage (white)

    this.platform = this.game.add.sprite(0, this.game.height / 2, 'block-black');
    this.pole1 = this.game.add.sprite(200, this.game.height / 2 - 32, 'block-blue');
    this.pole2 = this.game.add.sprite(350, this.game.height / 2 - 32, 'block-blue');
    this.pole3 = this.game.add.sprite(500, this.game.height / 2 - 32, 'block-blue');
    this.player = this.game.add.sprite(64, this.game.height / 2 - 32, 'block-red');
  },
};

Alle 5 Sprites werden hinzugefügt mittels this.game.add.sprite(x-pos, y-pos, image-key) - die Syntax ist grundlegend selbsterklärend - der image-key Parameter ist der Name, den man im preload State gewählt hat. Wenn das Spiel nun im Browser geöffnet wird, dann sollte man folgendes sehen

Mechanic Step 01

Schritt 2: Plattform und Hindernisse skalieren

Die schwarze Plattform ist noch keine durchgehende Plattform und die blauen Blöcke als Hindernisse sind keine wirklichen Hindernisse, daher Skalieren wir die Elemente auf der Y-Achse.

  create: function create() {
    this.stage.setBackgroundColor(0xffffff);  // set backgroundcolor of stage (white)

    this.platform = this.game.add.sprite(0, this.game.height / 2, 'block-black');
    this.pole1 = this.game.add.sprite(200, this.game.height / 2 - 32, 'block-blue');
    this.pole2 = this.game.add.sprite(350, this.game.height / 2 - 32, 'block-blue');
    this.pole3 = this.game.add.sprite(500, this.game.height / 2 - 32, 'block-blue');
    this.player = this.game.add.sprite(64, this.game.height / 2 - 32, 'block-red');

    this.platform.scale.x = this.game.width / 32;
    this.pole1.scale.y = 4;
    this.pole2.scale.y = 6;
    this.pole3.scale.y = 4;
  },

Mechanic Step 02

Ankerpunkte (Anchors) eines Sprites

Das Ergebnis der Skalierung zeigt eine durchgehende schwarze Plattform und wachsende Hindernisse. Jedoch sind die blauen Blöcke nicht mehr richtig ausgerichtet und gehen durch den schwarzen Boden. Was ist passiert? Phaser (und vermutlich jede andere Engine auch) hat einen Referenzpunkt auf einem Sprite und bei Aktionen die die Größe, Position oder Rotation des Sprites verändern wird, abhängig von diesem Referenzpunkt, die Aktion ausgeführt. Der Referenzpunkt heißt Ankerpunkt (engl. Anchor) und ist der Punkt x:0, y:0 des Sprites. Bei Phaser befindet sich dieser Nullpunkt immer links-oben, alle Aktionen werden also ausgehend von der oberen, linken Ecke des Sprites ausgeführt.

Als einfaches Beispiel ist die Positionierung eines Sprites in unserem Spiel. Wenn wir ein Sprite an der Position x:100, y:100 setzen dann wird der Anchor (0, 0) des Sprites auf die Position gesetzt und von dieser Position das Bild gezeichnet:

Sprite Anchors Example Defaults

Das verändern des Anchorpoints in Phaser pro Sprite funktioniert über die Eigenschaft anchor

this.pole1.anchor.setTo(0, 1);

Die Zahlen 0, 1 sind zu erklären: Ausgehend vom Nullpunkt eines Sprites kann auf einer Werteskala zwischen 0 und 1 für den X und Y Ankerpunkt gewählt werden. Um sich das merken zu können eine kleine Zeichnung:

Sprite Anchors

In dem obigen Code-Beispiel setzen wir den x Ankerpunkt auf 0 (unverändert zum Defaultwert) und den y Ankerpunkt auf 1, also die maximale Breite vom Sprite. Man kann sich das auch merken mit Prozentangaben. Die Werteskala zwischen 0 und 1 entspricht 0 und 100% der Breite (x-Achse), bzw. Höhe (y-Achse) des Bildes. Das setzen des Ankerpunkts auf die Mitte des Bildes (also 50% der Breite und 50% der Höhe) würde bedeuten, den Ankerpunkt auf x:0,5 und y:0,5 zu setzen (50%/100=0,5).

Wenn wir diesen Ankerpunkt nun gesetzt haben und führen eine Aktion auf das Sprite aus, führt das natürlich zu unterschiedlichen Ergebnissen. Als Beispiel habe ich nun die Rotation eines Bildes gewählt mit unterschiedlichen Anchorpoints und der Auswirkung der unterschiedlichen Anchors:

Sprite Rotation Example

Um das nun für alle Elemente im Spiel anzuwenden, ändern wir die jeweiligen Anchors und müssen dann die initiale Positionierung des jeweiligen Element anpassen.

Play.prototype = {
  create: function create() {
    this.stage.setBackgroundColor(0xffffff);  // set backgroundcolor of stage (white)

    this.platform = this.game.add.sprite(0, this.game.height / 2, 'block-black');
    this.pole1 = this.game.add.sprite(200, this.game.height / 2, 'block-blue');
    this.pole2 = this.game.add.sprite(350, this.game.height / 2, 'block-blue');
    this.pole3 = this.game.add.sprite(500, this.game.height / 2, 'block-blue');
    this.player = this.game.add.sprite(64, this.game.height / 2 - 32, 'block-red');

    this.platform.scale.x = this.game.width / 32;
    this.pole1.scale.y = 4;
    this.pole2.scale.y = 6;
    this.pole3.scale.y = 4;

    this.pole1.anchor.setTo(0, 1);
    this.pole2.anchor.setTo(0, 1);
    this.pole3.anchor.setTo(0, 1);
  },
};

Das Ergebnis sollte dann wie folgt aussehen und der Vorstellung entsprechen:

Mechanic Step 03

Physik aktivieren

Das Layout unserer Sprites ist abgeschlossen und es geht nun darum die Interaktivität zu aktivieren. Interaktivität bedeutet in Spielen das Arbeiten gegen etwas und in diesem einfachen Beispiel arbeiten wir auch gegen etwas einfaches: Nämlich Physik.

Phaser bietet das grundlegende Feature an und lässt wählen zwischen den Physik Engines Arcade, Ninja und P2. Arcade die einfache und P2 eine komplexere Physik Engine, zudem kann man weitere Physik Engines einbinden um die Komplexität der Physikberechnung zu erhöhen. Auch hier gilt, einfaches Spiel -> einfache Physik Engine und es wird die Arcade Engine genutzt.

Im Boot State ist die Engine bereits eingestellt mittels

this.game.physics.startSystem(Phaser.Physics.ARCADE);

Jetzt geht es darum zu sagen, auf welche Elemente im Spiel die Physik Engine angewandt werden soll. Dazu überlegen wir uns was eine Physik Engine macht.

Basics Physik Engine

Eine Physik Engine macht aus einem Sprite einen Gegenstand und ein Gegenstand hat unter anderem eine Masse und die Spielwelt erhält eine Gravitation die einen Gegenstand je nach Masse nach unten zieht.

Wenn in einem Phaser Game die Physik Engine aktiviert wird, müssten somit alle Element (wie zum Beispiel der Boden oder die Hindernisse) nach unten fallen. Das passiert allerdings nicht, denn der Physik Engine wurden zwei Merkmale noch nicht bekannt gemacht: Welche Elemente haben eine Masse und müssen in der Physik Engine berechnet werden und welche Gravitation herrscht in der Spielwelt.

Standardmässig wird kein Sprite oder Gegenstand im Spiel der Physik Engine bekannt gemacht und die Gravitation ist x:0, y:0 - sprich es ist ein Raum ohne Gravitation. Das ist bei einem Jump&Run eher unvorteilhaft, denn ohne Gravitation macht das Springen wenig Sinn. Der Hintergrund warum Phaser kein Objekt, trotz aktivierter Physik, keine Masse gibt ist vermutlich dem Umstand geschuldet, das nicht jedes Element ein interaktives Element sein muss. So kann ein Sprite auch nur ein dekoratives Bild im Hintergrund sein oder das muss die Physik Engine ja nicht berechnen. Das sollte man aus dem Absatz auch mitnehmen: Physik Engine kostet Performance - je weniger die Physik Engine zu berechnen hat, desto flüssiger wird das Spiel laufen.

Also werden nun die Spielobjekte der Physik Engine bekannt gemacht und die Spielwelt erhält eine y-Gravitation und Gegenstände fallen nach unten.

Play.prototype = {
  create: function create() {
    this.stage.setBackgroundColor(0xffffff);  // set backgroundcolor of stage (white)

    this.platform = this.game.add.sprite(0, this.game.height / 2, 'block-black');
    this.pole1 = this.game.add.sprite(200, this.game.height / 2, 'block-blue');
    this.pole2 = this.game.add.sprite(350, this.game.height / 2, 'block-blue');
    this.pole3 = this.game.add.sprite(500, this.game.height / 2, 'block-blue');
    this.player = this.game.add.sprite(64, this.game.height / 2 - 32, 'block-red');

    this.platform.scale.x = this.game.width / 32;
    this.pole1.scale.y = 4;
    this.pole2.scale.y = 6;
    this.pole3.scale.y = 4;

    this.pole1.anchor.setTo(0, 1);
    this.pole2.anchor.setTo(0, 1);
    this.pole3.anchor.setTo(0, 1);


    // ****
    // enable pyhsics for these objects
    this.game.physics.arcade.enable([this.platform, this.pole1, this.pole2, this.pole3, this.player]);

    // enable gravity, so that elements "fall down"
    this.physics.arcade.gravity.y = 2000;
  },
};

Mit diesen zwei Zeilen ist das ganze schon erledigt und nun ergeben sich neue Probleme. Denn die Gravitation wirkt sich auf alle Objekte aus, die eine Masse haben (der Physik Engine bekannt sind) und beim starten/reloaden das Spieles fällt alles nach unten aus dem Bildschirm. Blöd aber logisch.

Damit das unterbunden werden kann muss der Physik Engine gesagt werden das zum Beispiel der Boden und die blauen Hindernisse zwar in der Physik Engine vorhanden sein sollen, aber nicht von der Gravitation beeinflusst werden dürfen. Ebenso wird direkt gesagt, das auch kein anderer Einfluss diese Gegenstände die Position verändern können. Ein "anderer Einfluss" könnte später die Spielfigur sein, die gegen die Hindernisse läuft und damit diese wegschiebt. Das kann für manche Arten von Spielen natürlich ein gewünschter Effekt sein (Puzzle Game, bei dem man Kisten verschieben muss), aber hier in der Erklärung der Mechanik "Bewegung einer Spielfigur" macht es kein Sinn.

Play.prototype = {
  create: function create() {
    this.stage.setBackgroundColor(0xffffff);  // set backgroundcolor of stage (white)

    this.platform = this.game.add.sprite(0, this.game.height / 2, 'block-black');
    this.pole1 = this.game.add.sprite(200, this.game.height / 2, 'block-blue');
    this.pole2 = this.game.add.sprite(350, this.game.height / 2, 'block-blue');
    this.pole3 = this.game.add.sprite(500, this.game.height / 2, 'block-blue');
    this.player = this.game.add.sprite(64, this.game.height / 2 - 32, 'block-red');

    this.platform.scale.x = this.game.width / 32;
    this.pole1.scale.y = 4;
    this.pole2.scale.y = 6;
    this.pole3.scale.y = 4;

    this.pole1.anchor.setTo(0, 1);
    this.pole2.anchor.setTo(0, 1);
    this.pole3.anchor.setTo(0, 1);

    // enable pyhsics for these objects
    this.game.physics.arcade.enable([
        this.platform, this.pole1, this.pole2, this.pole3, this.player]);

    // enable gravity, so that elements "fall down"
    this.physics.arcade.gravity.y = 2000;


    // *****
    // Set platform and poles to be static
    this.platform.body.immovable = true;
    this.platform.body.allowGravity = false;
    this.pole1.body.immovable = true;
    this.pole1.body.allowGravity = false;
    this.pole2.body.immovable = true;
    this.pole2.body.allowGravity = false;
    this.pole3.body.immovable = true;
    this.pole3.body.allowGravity = false;
  },
};

Bei einem neuen Test des Spieles werden die Plattform und Poles stehen bleiben, aber die rote Spielfigur wird weiterhin nach unten fallen. Es würde keinen Sinn machen, die Eigenschaften immovable und allowGravity wie bei der Plattform/Poles zu setzen, denn eine Spielfigur die man nicht bewegen kann macht nicht besonders viel Spaß. Das Thema wird später in der update() Funktion gelöst.

Wir belassen die create() Funktion erst einmal so wie sie ist und führen eine kleine Zusammenfassung durch. In dieser Funktion wurden die drei Sprites in den Play State hinzugefügt, positioniert und skaliert. Anschließend ist diesen Objekten die Physik und der gesamten Spielwelt eine Gravitation auf der vertikalen Achse zugewiesen. Damit die Physik auf die statischen Elemente (Boden, Hindernisse) keine Auswirkung hat, sind diese explizit von der Beeinflussung durch die Gravitation (allowGravity = false) und einem generellen verändern der Position (immovable = true) ausgenommen. Der Spieler hat weiterhin die Eigenschaft, das er den Regeln der Physik folgt und fällt durch den Boden. Der Code ist derzeit nicht schön und sehr repetetiv und muss noch aufgeräumt werden.

GameLoop update()

In Phaser wird wenig von einem GameLoop gesprochen, sondern der GameLoop besteht daraus, das bei jedem Frame die update() Funktion im State aufgerufen wird. Somit wird jede Bewegung der Spielfigur und die Auswirkung der Bewegung in update() gesteuert.

Das erste Thema das angegangen wird ist das fallen der roten Spielfigur. Der Spieler soll sich nach links/rechts/oben bewegen können und dabei mit dem Boden und den Hindernissen kollidieren - sprich er läuft auf dem Boden und gegen die Hindernisse. In der Aussage ist auch gleichzeitig die Lösung für den fallen roten Klotz gegeben: Die Spielfigur muss mit dem Boden in der Physik Engine kollidieren, sodass die Physik Engine die Bewegung (fallen) stoppt.

Play.prototype = {
  // other code

  update: function update() {
    // check physics between player and ground
    this.game.physics.arcade.collide(this.player, this.platform);
  },
};

Mit dieser einen Zeile ist die Kollision zwischen Spieler und Boden bereits definiert und die rote Spielfigur fällt nicht mehr durch den Boden durch. Das gleiche können wir auch direkt schon für die blauen Hindernisse tun und die Kollision zwischen Player und Pole definieren.

Play.prototype = {
  // other code

  update: function update() {
    // check physics between player and ground
    this.game.physics.arcade.collide(this.player, this.ptlatform);
    this.game.physics.arcade.collide(this.player, [this.pole1, this.pole2, this.pole3]);
  },
};
Die Spielfigur bewegen

Das nun entscheidene Element im Spiel und der eigentlichen GameMechanic kommt endlich: Die Bewegung der Spielfigur! Dazu ist es wichtig zu verstehen, was denn Bewegung in diesem kleinen Spiel eigentlich bedeutet: Wenn wir mit den Pfeiltasten auf der Tastatur eine Bewegung vom Spiel fordern, wird dieser Tastendruck länger andauernd als 1 Frame (16ms) und pro Frame und gedrückter Taste müssen die x/y Position der Spielfigur verändert werden. Das könnte einfach über this.player.position.setTo(x, y) erfolgen, doch da müssen wir zuerst noch die aktuelle Position des Spielers ermitteln und eine Anzahl von Pixel addieren. Also ist die Bewegung der Spielfigur im Spiel pro Frame(!) die aktuelle Position plus die vom Entwickler gewünschte Veränderung "v" pro Pixel pro Frame. Wenn wir die Variable "v" nun hoch ansetzen, bewegt sich unsere Spielfigur pro Frame schneller vorwärts und das Spiel wirkt schnell und aktionreich. Wenn dieser Wert aber gering ist, bewegt sich die Spielfigur eben langsamer.

Phaser als GameEngine hat zusammen mit der Physik Engine natürlich bereits alles an Board um das sehr schnell und einfach zu lösen. Wir brauchen die Informationen über Input vom Spieler (Tastendruck Keyboard) um damit der Physik Engine zu sagen das die Figur bewegt werden soll um einen Wert "v" (pro Frame). Die Physik Engine wird zunächst eine Kollision prüfen und wenn das erfolgreich ist, die Position der Spielfigur aktualisieren.

Die Überwachung der Pfeiltasten der Tastatur wurde bereits in Play::preload() aktiviert mit der Zeile this.cursors = this.input.keyboard.createCursorKeys(); und der Status über einen Tastendruck liegt nun in this.cursors vor. Da wir die Bewegung "links|rechts|oben" in dem Spiel simulieren wollen, prüfen wir pro Frame ob die entsprechen Pfeiltasten gedrückt werden. Wenn einer dieser Taste gedrückt wird verändern wir die "velocity" der Masse des Spielers, was bei Phaser der "body" ist. Velocity ist eigentlich die Geschwindigkeit und ich finde den Begriff nicht deutlich, daher ist für mich Velocity hier die Veränderung der aktuellen Position um einen Faktor "v" in Pixel und laut Phaser Dokumentation ist der Faktor "v" die Anzahl der Pixel pro Sekunde (nicht Frame).

Wir lassen die Spielfigur sich bewegen:

Play.prototype = {
  // other code

  update: function update() {
    // check physics between player and ground
    this.game.physics.arcade.collide(this.player, this.platform);
    this.game.physics.arcade.collide(this.player,
        [this.pole1, this.pole2, this.pole3]);

    var v = 300;

    if(this.cursors.left.isDown) {
      this.player.body.velocity.x = -v;
    } else if(this.cursors.right.isDown) {
      this.player.body.velocity.x = v;
    } else {
      this.player.body.velocity.x = 0;
    }

    if(this.cursors.up.isDown) {
      this.player.body.velocity.y = -v;
    } else {
      this.player.body.velocity.y = 0;
    }
  },
};

Wenn die Taste 'Pfeil links' oder 'Pfeil rechts' gedrückt ist, verändere die Velocity auf der x-Achse um den Wert "v" (sprich hier 300px pro Sekunde). Wenn keiner der beiden Tasten gedrückt werden, dann setzte die Velocity wieder auf 0 zurück (ansonsten rutscht die Spielfigur einfach weiter). Das Prinzip funktioniert bei der 'Pfeil oben' Abhandlung das identisch.

Dabei wird man vielleicht zwei Probleme feststellen:

  1. Die Spielfigur kann aus dem Bildschirm gehen
  2. Die Spielfigur springt nicht einmal, sie schwebt wie mit einem JetPack

Und auch das wird nun gefixt. Für Punkt 1 bieter Phaser bereits eine einfache Lösung. In create() wird der Player auf die Spielwelt limitiert mit der Zeile:


Play.prototype = {
  // other code
  create: function create() {
    // other code
    this.player.body.collideWorldBounds = true;
  },
  // other code
}

Bei Punkt 2 wird der Aufwand etwas größer, denn die Situation sieht wie folgt aus:

Während es beim laufen nach links oder rechts okay ist, wenn der Spieler durchgehen laufen kann (es entspricht ein wenig der Logik das man lange laufen kann), so sieht es beim Springen anders aus. Springen ist ein Akt, bei dem einmal Kraft aufgewandt und sich vom Boden abgestoßen wird. Ausgehend von der Kraft springt man hoch und wird dann sofort von der Schwerkraft herunter gezogen, jedoch hier kann der Spieler so lange wie er will auf die Pfeiltaste "up" drück und damit ist die Kraft unendlich.

Als Optionen gibt es zwei Wege: Das drücken der Sprungtaste hält nur für ~100ms an und alle Tastendrücke danach werden ignoriert oder man prüft beim Tastendruck ob der Spieler Kontakt mit einem Untergrund hat und lässt ihn dann springen. Der zweite Weg unterbindet damit auch einen anderen Trick den der Spieler anwenden kann: Fällt er von einem Hindernis kann er in der Luft immer noch "springen", da der Timer erst nach dem ersten Sprung anläuft. Somit wird der zweite Weg, die Prüfung ob der Spieler auf dem Grund steht, gegangen.

Hierbei ist Phaser aber auch wieder hilfreich, denn es gibt über das Physik System die Option zu prüfen, ob das Player Sprite Berührung zu einem anderen (der Physik Engine bekannten) Sprite hat. Die Implementierung in update() sieht wie folgt aus:


Play.prototype = {
  // other code
  update: function update() {
    // other code
    var isOnGround = this.player.body.touching.down;  // check if touching ground
    if(isOnGround && this.cursors.up.isDown) {
      this.player.body.velocity.y = -v * 2;
    }
  },
  // other code
}

Damit sollten die Probleme gefixt sein, jedoch wird der Sprung auf das erste Hindernis schon zum Ärgernis, denn es ist einfach zu hoch oder wie springen nicht hoch genug. Hierbei einfach mit den scale Werten der Hindernisse, Gravitation oder dem v Velocity Wert spielen, bis ein angenehmes Spielgefühl entsteht.

Zusammenfassung

Der Post war sehr lang und enthält eigentlich auch nicht viel Code und Inhalt, aber es die erste grundlegende Spielmechanik oder so wichtig für das Spielgeschehen, das es mir wichtig war jeden einzelnen Punkt genau zu erklären.

Die Beispiele die man bei der Entwicklung entdeckt hat geben viele Möglichkeiten für Spiele. Nehmen wir nur die Thematik um das durchgehende Springen beim drücken der Pfeiltaste "up". Aus einem Sprung ist ein JetPack verhalten geworden nur durch eine Eigenschaft der Physik Engine und dem verhalten wie ein simpler Code reagiert.

Weiter wird man beim einstellen der Werte für Hohe der Hindernisse, Velocity oder Gravity merken, das man eine breite an Spielen nur mit den Werten erstellen und ein anderes Spielgefühl erstellen kann. Fastpace Jump & Run oder sneaky hiding Game? Die Werte machen eine Menge aus.

Das der Code nicht schön aussieht, wartbar ist und schlecht zu erweitern ist, ist ein Umstand der gefixt werden muss. In dem Zusammenhang sollte ich auch die Erweiterung der Welt und Platzierung von Hindernissen angehen. Aber das ist ein Thema für ein anderen Blogpost.