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'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.
![]()
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:

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

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