Zeile 55: Zeile 55:
 
== Zustandsübergangsdiagramm für die Figur ==
 
== Zustandsübergangsdiagramm für die Figur ==
  
Bevor die Umsetzung beginnt, ist es sinnvoll, die Zustände und deren Übergänge zu modellieren. Hier ist ein mögliches Zustandsübergangsdiagramm für die Figur. Im Folgenden wird die Figur in der Engine implementiert.
+
Bevor die Umsetzung beginnt, ist es sinnvoll, die Zustände und deren Übergänge zu modellieren. Hier ist ein mögliches Zustandsübergangsdiagramm für die Figur.
  
 
[[Datei:Tutorial State Transition Diagram.png|800px]]
 
[[Datei:Tutorial State Transition Diagram.png|800px]]
 +
 +
 +
== Implementieren der Figur ==
 +
 +
Nachdem nun ein guter Überblick über die Figur besteht, können wir zielgerichtet die Implementierung der Figur starten.
 +
 +
=== Die Zustände als Enumeration ===
 +
 +
Hierzu beginnen wir bei den Zuständen.
 +
Zustände einer Figur werden in der Engine stets als [http://openbook.rheinwerk-verlag.de/javainsel9/javainsel_09_004.htm <code>enum</code>] implementiert.
 +
 +
Diese <code>enum</code> definiert die Spielerzustände und speichert gleichzeitig die Dateipfade der zugehörigen GIF-Dateien.
 +
 +
<source lang ="java">
 +
public enum PlayerState {
 +
    IDLE("spr_m_traveler_idle_anim.gif"),
 +
    WALKING("spr_m_traveler_walk_anim.gif"),
 +
    RUNNING("spr_m_traveler_run_anim.gif"),
 +
    JUMPING("spr_m_traveler_jump_1up_anim.gif"),
 +
    MIDAIR("spr_m_traveler_jump_2midair_anim.gif"),
 +
    FALLING("spr_m_traveler_jump_3down_anim.gif"),
 +
    LANDING("spr_m_traveler_jump_4land_anim.gif");
 +
 +
    private String gifFileName;
 +
 +
    PlayerState(String gifFileName) {
 +
        this.gifFileName = gifFileName;
 +
    }
 +
 +
    public String getGifFileLocation() {
 +
        return "eatutorials/statefulanimation/assets/" + this.gifFileName;
 +
    }
 +
}
 +
</source>
 +
 +
Damit sind alle Zustände definiert. Ist beispielsweise das GIF des Zustandes <code>JUMPING</code> gefragt, so ist es jederzeit mit <code>JUMPING.getGifFileLocation()</code> erreichbar. Dies macht den Code deutlich wartbarer.
 +
 +
 +
=== Die Klasse für den Player Character ===
 +
 +
Mit den definierten Zuständen in <code>PlayerState</code> kann nun die Implementierung der eigentlichen Spielfigur beginnen:
 +
 +
<source lang="java">
 +
import ea.actor.Animation;
 +
import ea.actor.BodyType;
 +
import ea.actor.StatefulAnimation;
 +
 +
public class StatefulPlayerCharacter
 +
extends StatefulAnimation<PlayerState> {
 +
 +
    public StatefulPlayerCharacter() {
 +
        super(3, 3); //All GIFs are 64x64 px, hence: Same width/height. In this case: 3m each
 +
       
 +
        setupPlayerStates();
 +
        setupAutomaticTransitions();
 +
        setupPhysics();
 +
    }
 +
 +
    private void setupPlayerStates() {
 +
        for(PlayerState state : PlayerState.values()) {
 +
            Animation animationOfState = Animation.createFromAnimatedGif(state.getGifFileLocation(), 3,3);
 +
            addState(state, animationOfState);
 +
        }
 +
    }
 +
 +
    private void setupAutomaticTransitions() {
 +
        setStateTransition(PlayerState.MIDAIR, PlayerState.FALLING);
 +
        setStateTransition(PlayerState.LANDING, PlayerState.IDLE);
 +
    }
 +
 +
    private void setupPhysics() {
 +
        setBodyType(BodyType.DYNAMIC);
 +
        setRestitution(0);
 +
    }
 +
}
 +
</source>
 +
 +
In <code>setupPlayerStates()</code> werden alle in <code>PlayerState</code> definierten Zustände der Spielfigur eingepflegt, inklusive des Einladens der animierten GIFs. Hier wird der Vorteil der <code>String</code>-Variable im <code>PlayerState</code> deutlich: Der Code ist angenehm zu lesen. Im Vergleich hierzu der Code ohne die Variable:
 +
 +
<source lang="java">
 +
private void setupPlayerStatesAlternative() {
 +
    addState(PlayerState.IDLE, Animation.createFromAnimatedGif("eatutorials/statefulanimation/assets/spr_m_traveler_idle_anim.gif", 3, 3);
 +
    addState(PlayerState.WALKING, Animation.createFromAnimatedGif("eatutorials/statefulanimation/assets/spr_m_traveler_walk_anim.gif", 3, 3);
 +
    addState(PlayerState.RUNNING, Animation.createFromAnimatedGif("eatutorials/statefulanimation/assets/spr_m_traveler_run_anim.gif", 3, 3);
 +
    addState(PlayerState.JUMPING, Animation.createFromAnimatedGif("eatutorials/statefulanimation/assets/spr_m_traveler_jump_1up_anim.gif", 3, 3);
 +
    addState(PlayerState.FALLING, Animation.createFromAnimatedGif("eatutorials/statefulanimation/assets/spr_m_traveler_jump_3down_anim.gif", 3, 3);
 +
    //etc.
 +
}
 +
</source>
 +
 +
Wir wissen bereits, dass zwei der Zustände nur einen Animationszyklus bestehen. Danach sollen sie in einen anderen Zustand übergehen: <code>MIDAIR</code> geht über zu <code>FALLING</code> und <code>LANDING</code> geht über zu <code>IDLE</code>. Diese Übergänge können direkt über die Methode [https://docs.engine-alpha.org/4.x/ea/actor/StatefulAnimation.html#setStateTransition-State-State- <code>setStateTransition(...)</code>] umgesetzt werden.
 +
 +
Schließlich wird in <code>setupPhysics()</code> die Figur über die [[v4.x/Physics|Engine-Physik]] noch dynamisch gesetzt und bereit gemacht, sich als Platformer-Figur der Schwerkraft auszusetzen.
 +
 +
=== Testbed ===
 +
 +
Damit die Figur getestet werden kann, schreiben wir ein schnelles Testbett für sie. In einer <code>Scene</code> bekommt sie einen Boden zum Laufen:
 +
 +
<source lang="java">
 +
import ea.Game;
 +
import ea.Scene;
 +
import ea.Vector;
 +
import ea.actor.BodyType;
 +
import ea.actor.Rectangle;
 +
 +
import java.awt.Color;
 +
 +
public class StatefulAnimationTestScene
 +
extends Scene {
 +
 +
    public StatefulAnimationTestScene() {
 +
        StatefulPlayerCharacter character = new StatefulPlayerCharacter();
 +
 +
        setupGround();
 +
        add(character);
 +
 +
        setGravity(new Vector(0, -9.81f));
 +
    }
 +
 +
    private void setupGround() {
 +
        Rectangle ground = new Rectangle(200, 0.2f);
 +
        ground.setCenter(0, -5);
 +
        ground.setColor(new Color(255, 195, 150));
 +
        ground.setBodyType(BodyType.STATIC);
 +
        ground.setRestitution(0);
 +
        add(ground);
 +
    }
 +
 +
    public static void main(String[] args) {
 +
        Game.start(1200, 820, new StatefulAnimationTestScene());
 +
    }
 +
}
 +
</source>
 +
 +
[[Datei:StatefulAnimation First Testbed.gif|mini|Der Zwischenstand: Noch passiert nicht viel.]]
 +
 +
Damit können wir das Zwischenergebnis schonmal sehen. Und sehen noch nicht viel. Die Figur bleibt im <code>IDLE</code>-Zustand hängen. Nun gilt es, die übrigen Zustandsübergänge zu implementieren.

Version vom 6. Januar 2020, 01:36 Uhr


Dies ist ein weiterführendes Tutorial zur Engine Version 4.x. Du findest eine Übersicht über alle Tutorials hier.

Inhalt

Dies ist ein Tutorial zur ea.actor.StatefulAnimation. In diesem Tutorial:

  • Konzipierst du eine komplexe Spielfigur mit Zustandsübergängen.
  • Setzt die Spielfigur in einer simplen Demo um.

Stateful Animations

Die StatefulAnimation ist eine elegante Möglichkeit, komplexe Spielfiguren mit wenig Aufwand umzusetzen.

Nehmen wir dieses Beispiel:

Zustand Animiertes GIF
Idle
spr m traveler idle anim.gif
Jumping
spr m traveler jump 1up anim.gif
Midair
spr m traveler jump 2midair anim.gif
Falling
spr m traveler jump 3down anim.gif
Landing
spr m traveler jump 4land anim.gif
Walking
spr m traveler walk anim.gif
Running
spr m traveler run anim.gif

Das sind viele zu jonglierende Zustände. Und für ein normales Platformer-Spiel ist die Anzahl an Zuständen eher gering.

Zum Nachimplementieren kannst du die animierten GIFs vom Wiki herunterladen.

Zustandsübergangsdiagramm für die Figur

Bevor die Umsetzung beginnt, ist es sinnvoll, die Zustände und deren Übergänge zu modellieren. Hier ist ein mögliches Zustandsübergangsdiagramm für die Figur.

Tutorial State Transition Diagram.png


Implementieren der Figur

Nachdem nun ein guter Überblick über die Figur besteht, können wir zielgerichtet die Implementierung der Figur starten.

Die Zustände als Enumeration

Hierzu beginnen wir bei den Zuständen. Zustände einer Figur werden in der Engine stets als enum implementiert.

Diese enum definiert die Spielerzustände und speichert gleichzeitig die Dateipfade der zugehörigen GIF-Dateien.

public enum PlayerState {
    IDLE("spr_m_traveler_idle_anim.gif"),
    WALKING("spr_m_traveler_walk_anim.gif"),
    RUNNING("spr_m_traveler_run_anim.gif"),
    JUMPING("spr_m_traveler_jump_1up_anim.gif"),
    MIDAIR("spr_m_traveler_jump_2midair_anim.gif"),
    FALLING("spr_m_traveler_jump_3down_anim.gif"),
    LANDING("spr_m_traveler_jump_4land_anim.gif");

    private String gifFileName;

    PlayerState(String gifFileName) {
        this.gifFileName = gifFileName;
    }

    public String getGifFileLocation() {
        return "eatutorials/statefulanimation/assets/" + this.gifFileName;
    }
}

Damit sind alle Zustände definiert. Ist beispielsweise das GIF des Zustandes JUMPING gefragt, so ist es jederzeit mit JUMPING.getGifFileLocation() erreichbar. Dies macht den Code deutlich wartbarer.


Die Klasse für den Player Character

Mit den definierten Zuständen in PlayerState kann nun die Implementierung der eigentlichen Spielfigur beginnen:

import ea.actor.Animation;
import ea.actor.BodyType;
import ea.actor.StatefulAnimation;

public class StatefulPlayerCharacter
extends StatefulAnimation<PlayerState> {

    public StatefulPlayerCharacter() {
        super(3, 3); //All GIFs are 64x64 px, hence: Same width/height. In this case: 3m each
        
        setupPlayerStates();
        setupAutomaticTransitions();
        setupPhysics();
    }

    private void setupPlayerStates() {
        for(PlayerState state : PlayerState.values()) {
            Animation animationOfState = Animation.createFromAnimatedGif(state.getGifFileLocation(), 3,3);
            addState(state, animationOfState);
        }
    }

    private void setupAutomaticTransitions() {
        setStateTransition(PlayerState.MIDAIR, PlayerState.FALLING);
        setStateTransition(PlayerState.LANDING, PlayerState.IDLE);
    }

    private void setupPhysics() {
        setBodyType(BodyType.DYNAMIC);
        setRestitution(0);
    }
}

In setupPlayerStates() werden alle in PlayerState definierten Zustände der Spielfigur eingepflegt, inklusive des Einladens der animierten GIFs. Hier wird der Vorteil der String-Variable im PlayerState deutlich: Der Code ist angenehm zu lesen. Im Vergleich hierzu der Code ohne die Variable:

private void setupPlayerStatesAlternative() {
    addState(PlayerState.IDLE, Animation.createFromAnimatedGif("eatutorials/statefulanimation/assets/spr_m_traveler_idle_anim.gif", 3, 3);
    addState(PlayerState.WALKING, Animation.createFromAnimatedGif("eatutorials/statefulanimation/assets/spr_m_traveler_walk_anim.gif", 3, 3);
    addState(PlayerState.RUNNING, Animation.createFromAnimatedGif("eatutorials/statefulanimation/assets/spr_m_traveler_run_anim.gif", 3, 3);
    addState(PlayerState.JUMPING, Animation.createFromAnimatedGif("eatutorials/statefulanimation/assets/spr_m_traveler_jump_1up_anim.gif", 3, 3);
    addState(PlayerState.FALLING, Animation.createFromAnimatedGif("eatutorials/statefulanimation/assets/spr_m_traveler_jump_3down_anim.gif", 3, 3);
    //etc.
}

Wir wissen bereits, dass zwei der Zustände nur einen Animationszyklus bestehen. Danach sollen sie in einen anderen Zustand übergehen: MIDAIR geht über zu FALLING und LANDING geht über zu IDLE. Diese Übergänge können direkt über die Methode setStateTransition(...) umgesetzt werden.

Schließlich wird in setupPhysics() die Figur über die Engine-Physik noch dynamisch gesetzt und bereit gemacht, sich als Platformer-Figur der Schwerkraft auszusetzen.

Testbed

Damit die Figur getestet werden kann, schreiben wir ein schnelles Testbett für sie. In einer Scene bekommt sie einen Boden zum Laufen:

import ea.Game;
import ea.Scene;
import ea.Vector;
import ea.actor.BodyType;
import ea.actor.Rectangle;

import java.awt.Color;

public class StatefulAnimationTestScene
extends Scene {

    public StatefulAnimationTestScene() {
        StatefulPlayerCharacter character = new StatefulPlayerCharacter();

        setupGround();
        add(character);

        setGravity(new Vector(0, -9.81f));
    }

    private void setupGround() {
        Rectangle ground = new Rectangle(200, 0.2f);
        ground.setCenter(0, -5);
        ground.setColor(new Color(255, 195, 150));
        ground.setBodyType(BodyType.STATIC);
        ground.setRestitution(0);
        add(ground);
    }

    public static void main(String[] args) {
        Game.start(1200, 820, new StatefulAnimationTestScene());
    }
}
Der Zwischenstand: Noch passiert nicht viel.

Damit können wir das Zwischenergebnis schonmal sehen. Und sehen noch nicht viel. Die Figur bleibt im IDLE-Zustand hängen. Nun gilt es, die übrigen Zustandsübergänge zu implementieren.