Animatsioonid kasutades Spritesheet (LibGDX projekt)¶
Animatsioonide kasutamine LibGDX-is hõlmab mõningaid olulisi samme, sealhulgas tekstuuride jaotamist, animatsiooni loomist ja kuvamist. Siin osas proovime läbi teha lihtsa näite, kus lisame oma liigutavale tegelasele animatsiooni.
Võtame algkoodiks mängu näite varasemast materjalist. Järgige selle materjali juhendeid kuniks saate tehtud Pollimine peatüki. Alljärgneva koodiga oleme endale teinud lihtsa mängu, kus saame juhtida ekraanil olevat ringi.
package ee.taltech.iti0301.libgdxdemo;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
public class libgdxDemo extends ApplicationAdapter {
ShapeRenderer shapeRenderer;
float circleX = 200;
float circleY = 100;
@Override
public void create() {
shapeRenderer = new ShapeRenderer();
}
@Override
public void render() {
if (Gdx.input.isTouched()) {
circleX = Gdx.input.getX();
circleY = Gdx.graphics.getHeight() - Gdx.input.getY();
}
if(Gdx.input.isKeyPressed(Input.Keys.W)){
circleY++;
}
else if(Gdx.input.isKeyPressed(Input.Keys.S)){
circleY--;
}
if(Gdx.input.isKeyPressed(Input.Keys.A)){
circleX--;
}
else if(Gdx.input.isKeyPressed(Input.Keys.D)){
circleX++;
}
Gdx.gl.glClearColor(.25f, .25f, .25f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
shapeRenderer.begin(ShapeRenderer.ShapeType.Filled);
shapeRenderer.setColor(0, 1, 0, 1);
shapeRenderer.circle(circleX, circleY, 75);
shapeRenderer.end();
}
@Override
public void dispose() {
shapeRenderer.dispose();
}
}
Siit edasi proovime ringi välja vahetada koera tekstuuriga.
Animatsiooni kuvamine:¶
1. Tekstuuride ettevalmistamine Kõigepealt pead oma animatsiooni jaoks tekstuurid ette valmistama. Tavaliselt kasutatakse selleks spritesheet. Spritesheet on üks suur pilt, mis sisaldab kõiki animatsiooni kaadreid. Erinevate animatsioonide jaoks (seismine, kõndimine, jooksmine jne) oleks mõistlik teha erinevad spritesheet'id, praegu teeme enda näitele ainult kõndimis animatsiooni.
NB! Spritesheet tehes tehke kindlaks, et raamid oleksid ühtlaselt jaotatud.
2. Kaadrite jaotamine Nüüd tuleb spritesheet jaotada üksikuteks kaadriteks. Selleks võib kasutada LibGDX Texture ja TextureRegion klassi.
// Laadime spritesheedi
Texture sheet = new Texture("path/to/spritesheet.png");
// Jaotame spritesheedi kaadriteks
int FRAME_COLS = 4; // Veergude arv
int FRAME_ROWS = 2; // Ridade arv
TextureRegion[][] tmp = TextureRegion.split(sheet,
sheet.getWidth() / FRAME_COLS,
sheet.getHeight() / FRAME_ROWS);
// Koondame kõik kaadrid ühte massiivi
TextureRegion[] frames = new TextureRegion[FRAME_COLS * FRAME_ROWS];
int index = 0;
for (int i = 0; i < FRAME_ROWS; i++) {
for (int j = 0; j < FRAME_COLS; j++) {
frames[index++] = tmp[i][j];
}
}
Alustuseks loetakse sisse spritesheet, mis sisaldab erinevaid raame (kujutades tegelase erinevaid olekuid või
liikumise etappe). Ärge unustage täita oma Texture'i ``innerPath`` parameeter õigesti.
Seejärel jagatakse spritesheet individuaalseteks raamideks, kasutades TextureRegion.split()
meetodit. Meetod
võtab argumendiks sprite'i lehe ning jagab selle väiksemateks osadeks vastavalt etteantud ridadele ja veergudele.
Seejärel salvestatakse tulemused kaheulmelise massiivina (TextureRegion[][] tmp
), kus iga element esindab ühte
raami.
FRAME_COLS
ja FRAME_ROWS
on muutujad, mis määravad, kuidas sprite'i leht jagatakse raamideks. Kui igal
teie kasutataval spritesheet'il on sama arv ridu ja veerge, siis võib need välja tuua klassi konstruktoris. Meie
näite puhul on need arvud 4 ja 2.
3. Animatsiooni loomine Loome Animation objekti, mis kasutab meie kaadreid ja määrab iga kaadri kestuse.
// Kaadri kestus sekundites (1/24f = 24 kaadrit sekundis)
float frameDuration = 1/24f;
Animation<TextureRegion> animation = new Animation<TextureRegion>(frameDuration, frames);
4. Animatsiooni kuvamine Animatsiooni kuvamiseks peame uuendama ja joonistama iga kaadri, sõltuvalt ajast. Näiteks, mängu ekraani klassis võib animatsioonide kuvamise lahendus välja näha järgmiselt:
private float stateTime; // Aeg, mis on möödunud animatsiooni algusest
@Override
public void create() {
stateTime = 0f;
}
@Override
public void render() {
// Uuendame ajakulu
stateTime += Gdx.graphics.getDeltaTime();
// Saame praeguse kaadri
// Teine argument "true" tähendab, et animatsioon mängitakse tsükliliselt. Kui väärtuseks oleks "false", siis animatsioon mängitakse ainult üks kord läbi
TextureRegion currentFrame = animation.getKeyFrame(stateTime, true);
// Joonistame praeguse kaadri ekraanile
batch.begin();
batch.draw(currentFrame, circleX, circleY);
batch.end();
}
Niimoodi saame sellist animatsiooni:
Animatsiooniga mängu näide¶
Nüüd, kui oleme läbi vaatanud animatsiooni loomise sammud ning koodi, vahetame välja ringi animeeritud tekstuuriga ning proovime selle implementeerida meie mängu näitesse .
package ee.taltech.iti0301.libgdxdemo;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
public class libgdxDemo extends ApplicationAdapter {
SpriteBatch batch;
Texture sheet;
float circleX = 200;
float circleY = 100;
int FRAME_COLS = 4;
int FRAME_ROWS = 2;
TextureRegion[][] tmp;
TextureRegion[] frames;
float frameDuration;
Animation<TextureRegion> animation;
float stateTime;
@Override
public void create() {
stateTime = 0f;
batch = new SpriteBatch();
sheet = new Texture("path/to/spritesheet"); // NB! Muuta endal õigeks
tmp = TextureRegion.split(sheet,
sheet.getWidth() / FRAME_COLS,
sheet.getHeight() / FRAME_ROWS);
frames = new TextureRegion[FRAME_COLS * FRAME_ROWS];
int index = 0;
for (int i = 0; i < FRAME_ROWS; i++) {
for (int j = 0; j < FRAME_COLS; j++) {
frames[index++] = tmp[i][j];
}
}
frameDuration = 1/24f;
animation = new Animation<TextureRegion>(frameDuration, frames);
}
@Override
public void render() {
stateTime += Gdx.graphics.getDeltaTime();
TextureRegion currentFrame = animation.getKeyFrame(0f, false);
if (Gdx.input.isTouched()) {
circleX = Gdx.input.getX();
circleY = Gdx.graphics.getHeight() - Gdx.input.getY();
}
if(Gdx.input.isKeyPressed(Input.Keys.W)){
circleY++;
}
else if(Gdx.input.isKeyPressed(Input.Keys.S)){
circleY--;
}
if(Gdx.input.isKeyPressed(Input.Keys.A)){
circleX--;
}
else if(Gdx.input.isKeyPressed(Input.Keys.D)){
circleX++;
}
Gdx.gl.glClearColor(.25f, .25f, .25f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
currentFrame = animation.getKeyFrame(stateTime, true);
batch.begin();
batch.draw(currentFrame, circleX, circleY);
batch.end();
}
@Override
public void dispose() {
batch.dispose();
}
}
Animatsiooni juhtimine mängija oleku põhjal¶
Praegu on meil olemas tekstuur, mis mängib korduvalt ühte ja sama animatsiooni iga kord kui render funktsioon välja kutsutakse. Proovime anda tekstuurile erinevad 'olekud', et koera animatsioon paigal seismise ajal ei mängiks.
Animatsioonide juhtimine mängija oleku (State) põhjal muudab animatsioonide haldamise ja kuvamise lihtsamaks ja selgemaks. Kasutades olekuid, saame hõlpsasti määrata, millist animatsiooni mängija peaks näitama, ja hoida animatsioonid sünkroonis. Mängija oleku saab realiseerida kas Stringi või enumi abil. Praeguses näites kasutame enumit.
public enum State { IDLING, WALKING}
Üldjuhul võiks tegelase erinevate olekute ja nende animatsioonide objektid asuda selle tegelase omaenda klassis:
public class Player {
// mängija olek
private State currentState;
private float stateTime;
// erinevad mängija animatsioonid
private Animation<TextureRegion> idleAnimation;
private Animation<TextureRegion> walkAnimation;
public Player() {
this.currentState = State.IDLING;
this.stateTime = 0f;
}
public void setState(State newState) {
if (this.currentState != newState) {
this.currentState = newState;
this.stateTime = 0f; // kui olek muutub, stateTime läheb nulliks
}
}
public void update(float deltaTime) {
stateTime += deltaTime;
}
public TextureRegion getCurrentFrame() {
switch (currentState) {
case WALKING:
return walkAnimation.getKeyFrame(stateTime, true);
case IDLING:
default:
return idleAnimation.getKeyFrame(stateTime, true);
}
}
}
- Kasutades
State
-i jastateTime
-i lihtsustatakse ka teiste mängijate animatsioonide kuvamist. Nii saab mängija oleku ja ajakulu lihtsalt saata serverile, mis omakorda saadab need andmed teisele mängijale. Teine mängija saab lihtsalt kasutada saadud State-i ja stateTime-i, et valida õige animatsioon ja kuvada vastav kaader.
Siiski hoiame enda praegust mängu näidet lihtsana ning jätame praegu kõik erinevad mängu objektid ühte klassi. Samuti
pole meil IDLING oleku jaoks omaenda animatsiooni olemas, seega jätame enda näites seistes tekstuuri lihtsalt
esimese animatsiooni kaadri peale kinni: currentFrame = animation.getKeyFrame(0f, false);
. 0f
hoiab animatsiooni
esimese kaadri peal ning false
võtab animatsiooni kordumise maha.
Lõpuks saame olekud implementeerida mängu näitesse ning saame tulemuseks midagi sellist:
package ee.taltech.iti0301.libgdxdemo;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
public class libgdxDemo extends ApplicationAdapter {
SpriteBatch batch;
Texture sheet;
float circleX = 200;
float circleY = 100;
int FRAME_COLS = 4;
int FRAME_ROWS = 2;
TextureRegion[][] tmp;
TextureRegion[] frames;
float frameDuration;
Animation<TextureRegion> animation;
float stateTime;
public enum State { IDLING, WALKING }
private State currentState;
@Override
public void create() {
stateTime = 0f;
batch = new SpriteBatch();
sheet = new Texture("path/to/spritesheet"); // NB! Muuta endal õigeks
tmp = TextureRegion.split(sheet,
sheet.getWidth() / FRAME_COLS,
sheet.getHeight() / FRAME_ROWS);
frames = new TextureRegion[FRAME_COLS * FRAME_ROWS];
int index = 0;
for (int i = 0; i < FRAME_ROWS; i++) {
for (int j = 0; j < FRAME_COLS; j++) {
frames[index++] = tmp[i][j];
}
}
frameDuration = 1/24f;
animation = new Animation<TextureRegion>(frameDuration, frames);
}
@Override
public void render() {
stateTime += Gdx.graphics.getDeltaTime();
TextureRegion currentFrame = animation.getKeyFrame(0f, false);
if (Gdx.input.isTouched()) {
circleX = Gdx.input.getX();
circleY = Gdx.graphics.getHeight() - Gdx.input.getY();
}
if(Gdx.input.isKeyPressed(Input.Keys.W) || Gdx.input.isKeyPressed(Input.Keys.A)
|| Gdx.input.isKeyPressed(Input.Keys.S) || Gdx.input.isKeyPressed(Input.Keys.D)) {
currentState = State.WALKING;
} else {
currentState = State.IDLING;
}
if(Gdx.input.isKeyPressed(Input.Keys.W)){
circleY++;
}
else if(Gdx.input.isKeyPressed(Input.Keys.S)){
circleY--;
}
if(Gdx.input.isKeyPressed(Input.Keys.A)){
circleX--;
}
else if(Gdx.input.isKeyPressed(Input.Keys.D)){
circleX++;
}
Gdx.gl.glClearColor(.25f, .25f, .25f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
if(currentState == State.WALKING) {
currentFrame = animation.getKeyFrame(stateTime, true);
}
batch.begin();
batch.draw(currentFrame, circleX, circleY);
batch.end();
}
@Override
public void dispose() {
batch.dispose();
}
}