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();
    }
}

Liigutatav ring

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'i. 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õndimisanimatsiooni.

Spritesheet'e saab nii ise teha kui ka internetist leida. Kui lood animatsiooni ise, siis paljud pixel art programmid, näiteks Aseprite või LibreSprite, võimaldavad eksportida animatsiooni otse spritesheet'ina. See tähendab, et eraldi kaadreid ei pea käsitsi üheks suureks pildiks kokku panema.

Samuti on võimalik kasutada internetist leitud spritesheet'e. Sellisel juhul tuleks kindlasti kontrollida nende litsentsi, sest kõik internetist leitud materjalid ei ole vabalt kasutatavad.

Selle näite proovimiseks võib kasutada allolevat spritesheet'i. Pildi saab alla laadida või lohistada oma projekti.

spritesheet

NB! Spritesheet ise 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 klasse.

LibGDX loeb tekstuure vaikimisi projekti assets kaustast. Kui fail asub näiteks assets/spritesheet.png, siis piisab selle laadimiseks järgmisest käsust:

Texture sheet = new Texture("spritesheet.png");

Kui tekstuur asub mõnes alamkaustas, tuleb seda täpsustada faili teekonnas, näiteks:

Texture sheet = new Texture("images/spritesheet.png");

Järgmiseks jaotame spritesheet'i üksikuteks kaadriteks:

// 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 animatsiooni kaadreid. Seejärel jagatakse pilt väiksemateks osadeks kasutades TextureRegion.split() meetodit.

Meetod jagab spritesheet'i etteantud ridade ja veergude järgi väiksemateks TextureRegion objektideks. Tulemuseks on kahe-mõõtmeline massiiv (TextureRegion[][]), kus iga element esindab ühte animatsiooni kaadrit.

FRAME_COLS ja FRAME_ROWS määravad, kuidas spritesheet jagatakse. Kui kasutad projektis sama suurusega spritesheet'e, võib need väärtused defineerida näiteks klassi konstruktoris või konstantidena.

NB! TextureRegion.split() töötab kõige paremini siis, kui kõik kaadrid on täpselt ühesuurused ja paiknevad spritesheet'il korrapärase ruudustikuna.

Kui spritesheet'il on kaadrite vahel tühjad vahed (padding) või ümber pildi on lisaservad, siis ei pruugi automaatne jaotamine õigesti töötada. Sellisel juhul tuleb kaadrite asukohad määrata käsitsi või kasutada mõnda sprite tööriista, mis ekspordib animatsiooni täpselt ühtlase grid'ina.

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 sellise animatsiooni:

animation

Animatsiooniga mängu näide

Nüüd, kui oleme läbi vaadanud 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 png directory põhjal

        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();
    }
}

Liigutatav animatsioon

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 ja stateTime-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.

Animatsiooni muutmine mängija liikumissuuna põhjal

Lisaks võib mängija animatsiooni juures arvestada ka tegelase liikumissuunda. Sageli ei ole vaja teha eraldi vasakule ja paremale suunatud spritesheet'e. Kui olemas on ainult üks suund, saab sama animatsiooni kasutada ka teises suunas, peegeldades kaadri horisontaalselt ümber.

LibGDX-is saab TextureRegion objekti peegeldada meetodiga flip(). Näiteks flip(true, false) peegeldab kaadri horisontaalselt, kuid jätab vertikaalsuuna muutmata.

See tähendab, et kui tegelane liigub vasakule, võib sama animatsiooni lihtsalt ümber pöörata. Nii piisab sageli ühest spritesheet'ist, mitte kahest eraldi variandist.

Näiteks võib render-meetodis jälgida, kas mängija liigub vasakule või paremale, ning vajadusel kaadri õigesse suunda pöörata:

boolean facingRight = true;

@Override
public void render() {
    stateTime += Gdx.graphics.getDeltaTime();

    TextureRegion currentFrame = animation.getKeyFrame(stateTime, true);

    if (Gdx.input.isKeyPressed(Input.Keys.A)) {
        circleX--;
        facingRight = false;
    }
    else if (Gdx.input.isKeyPressed(Input.Keys.D)) {
        circleX++;
        facingRight = true;
    }

    if (currentFrame.isFlipX() != !facingRight) {
        currentFrame.flip(true, false);
    }

    batch.begin();
    batch.draw(currentFrame, circleX, circleY);
    batch.end();
}

Ülalolevas näites vaadatakse, kas tegelane liigub vasakule või paremale. Kui tegelane liigub vasakule, peegeldatakse kaader horisontaalselt. Kui tegelane liigub uuesti paremale, pööratakse kaader tagasi.

NB! Kui kasutada flip() meetodit, tasub kontrollida kaadri praegust suunda enne peegeldamist. Vastasel juhul võidakse sama kaadrit igal kaadril uuesti ümber pöörata ning animatsioon hakkab visuaalselt vilkuma.

Lõpuks saame olekud ja liikumissuuna 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.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
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;
    boolean facingRight = true;

    public enum State { IDLING, WALKING }
    private State currentState;


    @Override
    public void create() {
        stateTime = 0f;
        currentState = State.IDLING;
        batch = new SpriteBatch();

        sheet = new Texture("spritesheet.png"); // NB! Muuda failinimi vastavalt enda projektile

        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();

        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--;
            facingRight = false;
        } else if (Gdx.input.isKeyPressed(Input.Keys.D)) {
            circleX++;
            facingRight = true;
        }

        Gdx.gl.glClearColor(.25f, .25f, .25f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        TextureRegion currentFrame = animation.getKeyFrame(0f, false);

        if (currentState == State.WALKING) {
            currentFrame = animation.getKeyFrame(stateTime, true);
        }

        if (currentFrame.isFlipX() != !facingRight) {
            currentFrame.flip(true, false);
        }

        batch.begin();
        batch.draw(currentFrame, circleX, circleY);
        batch.end();
    }

    @Override
    public void dispose() {
        batch.dispose();
        sheet.dispose();
    }
}

Liigutatav animatsioon olekutega