v4.x/Stateful Animation: Unterschied zwischen den Versionen
K |
(→Fallen und Landen) |
||
Zeile 253: | Zeile 253: | ||
} | } | ||
</source> | </source> | ||
+ | |||
+ | === Player Movement === | ||
+ | |||
+ | [[Datei:TransitionDiagram vx states.png|mini|Die letzten zu implementierenden Zustandsübergänge hängen von der Spielerbewegung ab]] | ||
+ | |||
+ | Die letzten zu implementierenden Zustände sind die Bewegung des Spielers. Durch die [[v4.x/Physics|Physik-Engine]] gibt es viele Möglichkeiten, Bewegung im Spiel zu simulieren. Ein physikalische Implementierung wäre zum Beispiel die regelmäßige Anwendung von Impulsen. | ||
+ | |||
+ | Allerdings ist die physikalisch "korrekte" Implementierung von Spielerbewegung '''ungeeignet für die meisten Jump n' Run Szenarien'''. Stattdessen macht es in der Regel mehr Sinn, die folgenden Paramater zu klären und entsprechend zu '''interpolieren''': | ||
+ | |||
+ | * Maximale Geschwindigkeit des Charakters | ||
+ | * Anlaufzeit "von 0 auf 100": Zeitraum, in dem der Charakter vom Stillstand zur maximalen Geschwindigkeit beschleunigt | ||
+ | * Bremszeit: Zeitraum, in dem der Charakter von maximaler Geschwindigkeit wieder zum stehen kommt | ||
+ | |||
+ | Ich definiere diese Werte als Konstanten in der <code>StatefulPlayerCharacter</code>-Klasse: | ||
+ | |||
+ | <source lang="java"> | ||
+ | private static final Float MAX_SPEED = 20f; | ||
+ | private static final Float ACC_TIME = 1.5f; | ||
+ | private static final Float DEC_TIME = 0.8f; | ||
+ | </source> | ||
+ | |||
+ | Du kannst im Folgenden mit diesen Werten experimentieren bis du zufrieden bist mit dem Spielerlebnis. | ||
+ | |||
+ | Um möglichst wenig Aufwand bei der Interpolation der Spielergeschwindigkeit <code>v_x</code> zu haben, nutze ich die [[v4.x/Interpolation|Keyframe-Funktion der Engine]], die automatisch die Geschwindigkeit interpolieren kann. So muss ich mit KEYFRAMES nur die Zeit und den Endwert eingeben. Die folgenden Funktionen werden jeweils beim Drücken bzw. Loslassen der A/D-Tasten aufgerufen und realisieren die gesamte Bewegung: | ||
+ | |||
+ | [[Datei:StatefulAnimation Movement Base.gif|mini|Die Figur bewegt sich nun, aber die Zustände sind noch nicht umgesetzt.]] | ||
+ | |||
+ | <source lang="java"> | ||
+ | private KeyFrames currentMovement = null; | ||
+ | |||
+ | private void initiateMovement(boolean toTheRight) { | ||
+ | KeyFrames movementSpeed = new KeyFrames((speed) -> setVelocity(new Vector(speed, getVelocity().getY()))); | ||
+ | movementSpeed.addKeyframe(new KeyFrame<>(getVelocity().getX(), KeyFrame.Type.LINEAR, 0)); | ||
+ | movementSpeed.addKeyframe(new KeyFrame<>(MAX_SPEED * (toTheRight ? 1 : -1), KeyFrame.Type.SMOOTHED_SIN, ACC_TIME)); | ||
+ | movementSpeed.setInifinite(true); | ||
+ | setNewMovement(movementSpeed); | ||
+ | } | ||
+ | |||
+ | private void stopMovement() { | ||
+ | if(Game.isKeyPressed(KeyEvent.VK_A) || Game.isKeyPressed(KeyEvent.VK_D)) { | ||
+ | return; | ||
+ | } | ||
+ | KeyFrames movementSpeed = new KeyFrames((speed) -> setVelocity(new Vector(speed, getVelocity().getY()))); | ||
+ | movementSpeed.addKeyframe(new KeyFrame<>(getVelocity().getX(), KeyFrame.Type.SMOOTHED_SIN, 0)); | ||
+ | movementSpeed.addKeyframe(new KeyFrame<>(0f, KeyFrame.Type.SMOOTHED_SIN, DEC_TIME)); | ||
+ | movementSpeed.setInifinite(false); | ||
+ | setNewMovement(movementSpeed); | ||
+ | } | ||
+ | |||
+ | private void setNewMovement(KeyFrames movementInterpolation) { | ||
+ | if(currentMovement != null) { | ||
+ | removeFrameUpdateListener(currentMovement); | ||
+ | } | ||
+ | currentMovement = movementInterpolation; | ||
+ | addFrameUpdateListener(movementInterpolation); | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | Die Methode <code>initiateMovement(boolean toTheRight)</code> beginnt die Bewegung der Figur, entweder nach links (<code>initiateMovement(false)</code>) oder nach rechts (<code>initiateMovement(true)</code>). Sie interpoliert die X-Geschwindigkeit der Figur <code>v_x</code> linear von ihrem derzeitigen Wert bis zu <code>MAX_SPEED</code> innerhalb der Zeit <code>ACC_TIME</code>. Dabei ändert sie die Y-Geschwindigkeit nie. Nachdem die maximale Geschwindigkeit erreicht wird, sorgt die Interpolation bis auf Weiteres dafür, dass die Figur die maximale Geschwindigkeit beibehält. | ||
+ | |||
+ | Die Methode <code>stopMovement()</code> beendet die Bewegung der Figur, indem sie von der aktuellen X-Geschwindigkeit bis hin zu 0 "smooth" interpoliert (zunächst verliert die Figur wenig Geschwindigkeit, dann mehr, und schließlich wieder weniger). Dies passiert innerhalb der Zeit <code>DEC_TIME</code>. Nachdem die X-Geschwindigkeit auf 0 reduziert wurde, ist die Interpolation beendet. Diese Methode beginnt mit einer Abfrage, die sicherstellt, dass sich der Spieler nicht immernoch bewegen möchte (also ob mindestens eine der Tasten A oder D gedrückt ist). Ist dies der Fall, soll die Bewegung nicht gestoppt werden. Wenn du dir unsicher bist, warum die Abfrage zu beginn nötig ist, nimm sie raus, und sie was passiert. | ||
+ | |||
+ | === Die Übergänge IDLE - WALKING - RUNNING === | ||
+ | |||
+ | Nachdem sich |
Version vom 6. Januar 2020, 05:00 Uhr
Dies ist ein weiterführendes Tutorial zur Engine Version 4.x. Du findest eine Übersicht über alle Tutorials hier.
Inhaltsverzeichnis
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 | |
Jumping | |
Midair | |
Falling | |
Landing | |
Walking | |
Running |
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.
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());
}
}
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.
Implementieren der Zustände & Übergänge
Springen
Springen ist schnell umgesetzt. Auf Tastendruck (Leertaste) soll die Spielfigur springen, wenn sie auf festem Boden steht. Die Spielfigur implementiert nun zusätzlich KeyListener
und führt auf Leertastendruck die Sprungroutine aus:
private void attemptJump() {
PlayerState state = getCurrentState();
if(state == PlayerState.IDLE || state == PlayerState.WALKING || state == PlayerState.RUNNING) {
if(isGrounded()) {
applyImpulse(new Vector(0, 850));
setState(PlayerState.JUMPING);
}
}
}
Fallen und Landen
Als nächstes sorgen wir dafür, dass die Figur landen kann und schließlich zurück in den IDLE
-Zustand kommt. Dafür ist die Geschwindigkeit der Figur in Y-Richtung wichtig. Im Zustandsübergangsdiagramm haben wir dafür v_y < 0
als Fallen definiert und v_y = 0
als Stehen. Das ist im Modell in Ordnung, allerdings ist die Physik mit Fließkomma-Zahlen nicht ideal für "harte" Schwellwerte. Stattdessen definieren wir einen Grenzwert, innerhalb dessen wir auf 0 runden. Ich habe dafür private static final float THRESHOLD = 0.01f;
definiert. Es geht sicherlich noch genauer, aber das reicht für dieses Tutorial.
Unsere Spielfigur soll einfach in jedem Frame ihre eigene Y-Geschwidingkeit überprüfen. Dazu implementiert sie nun zusätzlich FrameUpdateListener
und prüft in jedem Frame entsprechend unseres Zustandsübergangsdiagrammes:
@Override
public void onFrameUpdate(float dT) {
Vector velocity = getVelocity();
PlayerState state = getCurrentState();
if(velocity.getY() < -THRESHOLD) {
switch(state) {
case JUMPING:
setState(PlayerState.MIDAIR);
break;
case IDLE:
case WALKING:
case RUNNING:
setState(PlayerState.FALLING);
break;
default:
break;
}
} else if(velocity.getY() < THRESHOLD && state==PlayerState.FALLING) {
setState(PlayerState.LANDING);
}
}
Player Movement
Die letzten zu implementierenden Zustände sind die Bewegung des Spielers. Durch die Physik-Engine gibt es viele Möglichkeiten, Bewegung im Spiel zu simulieren. Ein physikalische Implementierung wäre zum Beispiel die regelmäßige Anwendung von Impulsen.
Allerdings ist die physikalisch "korrekte" Implementierung von Spielerbewegung ungeeignet für die meisten Jump n' Run Szenarien. Stattdessen macht es in der Regel mehr Sinn, die folgenden Paramater zu klären und entsprechend zu interpolieren:
- Maximale Geschwindigkeit des Charakters
- Anlaufzeit "von 0 auf 100": Zeitraum, in dem der Charakter vom Stillstand zur maximalen Geschwindigkeit beschleunigt
- Bremszeit: Zeitraum, in dem der Charakter von maximaler Geschwindigkeit wieder zum stehen kommt
Ich definiere diese Werte als Konstanten in der StatefulPlayerCharacter
-Klasse:
private static final Float MAX_SPEED = 20f;
private static final Float ACC_TIME = 1.5f;
private static final Float DEC_TIME = 0.8f;
Du kannst im Folgenden mit diesen Werten experimentieren bis du zufrieden bist mit dem Spielerlebnis.
Um möglichst wenig Aufwand bei der Interpolation der Spielergeschwindigkeit v_x
zu haben, nutze ich die Keyframe-Funktion der Engine, die automatisch die Geschwindigkeit interpolieren kann. So muss ich mit KEYFRAMES nur die Zeit und den Endwert eingeben. Die folgenden Funktionen werden jeweils beim Drücken bzw. Loslassen der A/D-Tasten aufgerufen und realisieren die gesamte Bewegung:
private KeyFrames currentMovement = null;
private void initiateMovement(boolean toTheRight) {
KeyFrames movementSpeed = new KeyFrames((speed) -> setVelocity(new Vector(speed, getVelocity().getY())));
movementSpeed.addKeyframe(new KeyFrame<>(getVelocity().getX(), KeyFrame.Type.LINEAR, 0));
movementSpeed.addKeyframe(new KeyFrame<>(MAX_SPEED * (toTheRight ? 1 : -1), KeyFrame.Type.SMOOTHED_SIN, ACC_TIME));
movementSpeed.setInifinite(true);
setNewMovement(movementSpeed);
}
private void stopMovement() {
if(Game.isKeyPressed(KeyEvent.VK_A) || Game.isKeyPressed(KeyEvent.VK_D)) {
return;
}
KeyFrames movementSpeed = new KeyFrames((speed) -> setVelocity(new Vector(speed, getVelocity().getY())));
movementSpeed.addKeyframe(new KeyFrame<>(getVelocity().getX(), KeyFrame.Type.SMOOTHED_SIN, 0));
movementSpeed.addKeyframe(new KeyFrame<>(0f, KeyFrame.Type.SMOOTHED_SIN, DEC_TIME));
movementSpeed.setInifinite(false);
setNewMovement(movementSpeed);
}
private void setNewMovement(KeyFrames movementInterpolation) {
if(currentMovement != null) {
removeFrameUpdateListener(currentMovement);
}
currentMovement = movementInterpolation;
addFrameUpdateListener(movementInterpolation);
}
Die Methode initiateMovement(boolean toTheRight)
beginnt die Bewegung der Figur, entweder nach links (initiateMovement(false)
) oder nach rechts (initiateMovement(true)
). Sie interpoliert die X-Geschwindigkeit der Figur v_x
linear von ihrem derzeitigen Wert bis zu MAX_SPEED
innerhalb der Zeit ACC_TIME
. Dabei ändert sie die Y-Geschwindigkeit nie. Nachdem die maximale Geschwindigkeit erreicht wird, sorgt die Interpolation bis auf Weiteres dafür, dass die Figur die maximale Geschwindigkeit beibehält.
Die Methode stopMovement()
beendet die Bewegung der Figur, indem sie von der aktuellen X-Geschwindigkeit bis hin zu 0 "smooth" interpoliert (zunächst verliert die Figur wenig Geschwindigkeit, dann mehr, und schließlich wieder weniger). Dies passiert innerhalb der Zeit DEC_TIME
. Nachdem die X-Geschwindigkeit auf 0 reduziert wurde, ist die Interpolation beendet. Diese Methode beginnt mit einer Abfrage, die sicherstellt, dass sich der Spieler nicht immernoch bewegen möchte (also ob mindestens eine der Tasten A oder D gedrückt ist). Ist dies der Fall, soll die Bewegung nicht gestoppt werden. Wenn du dir unsicher bist, warum die Abfrage zu beginn nötig ist, nimm sie raus, und sie was passiert.
Die Übergänge IDLE - WALKING - RUNNING
Nachdem sich