Stage ja Actorite süsteem

LibGDX-i Scene2D on raamistik 2D graafika, kasutajaliidese (UI), menüüde, HUD-i ja hierarhiliste mänguobjektide loomiseks. See võimaldab lihtsamalt hallata sisendit, paigutust, animatsioone ja komponentide organiseerimist võrreldes käsitsi SpriteBatch-iga töötamisega.

Scene2D võimaldab luua stseeni, kus objektid on organiseeritud hierarhias ning nende uuendamine ja joonistamine toimub tsentraalselt.

Scene2D põhikomponendid

Scene2D koosneb mitmest põhilisest ehitusblokist:

  • Stage – kogu stseeni konteiner, mis haldab Actor'eid

  • Actor – kõige baasilisem element stseenis

  • Group – Actor, mis võib sisaldada teisi Actor'eid

  • Widget – kasutajaliidese komponent (Label, Button jne)

  • Viewport – määrab kuidas stseen ekraanile sobitatakse

  • Table – paigutuselement UI komponentide joondamiseks

Kõik Scene2D elemendid põhinevad lõpuks Actor klassil.

Miks Scene2D-d kasutada?

Scene2D eelised võrreldes käsitsi renderdamisega:

  • Sisend (klikid, puudutused, hover) on lihtne Actor'itele Listener'ite abil lisada

  • Animatsioonid tehakse Actions süsteemiga

  • Paigutus on mugav Table, HorizontalGroup ja VerticalGroup abil

  • Stage haldab Actor'ite uuendamist ja joonistamist

  • Hierarhiline struktuur muudab suuremad UI süsteemid lihtsamini hallatavaks

Important

Kõik Scene2D komponendid (Stage, Actor, Table, Label, Skin jne) töötavad ainult kliendipoolses rakenduses. Serveris toimub ainult mänguloogika, füüsika ja seisundi sünkroniseerimine.

Klient saab serverilt andmed (näiteks ServerConnection kaudu) ning uuendab nende põhjal kasutajaliidest ja visuaalseid elemente.

Stage

Stage on Scene2D keskne konteiner. Ta hoiab kõiki selle külge lisatud Actor-eid ning vastutab nende uuendamise, joonistamise ja sisendi jagamise eest.

Stage'i peamised ülesanded:

  • hallata Actor'ite hierarhiat

  • uuendada Actor'eid meetodiga act(delta)

  • joonistada Actor'id meetodiga draw()

  • edastada sisendsündmusi õigetele komponentidele

  • kasutada Viewport-i, et stseen sobituks ekraaniga

Ühele Screen-ile võib panna mitu Stage'i, kuid enamasti piisab ühest hästi organiseeritud Stage'ist koos Group-idega.

Stage'i eelised:

  • Actor'id asendavad paljusid Sprite'e

  • automaatne hierarhia ja paigutus (Layout liides)

  • lihtsad animatsioonid Actions'itega

  • sisend hõlpsasti: Gdx.input.setInputProcessor(stage) + Listener'id

Stage uuendamine ja joonistamine

Scene2D kasutamisel on kaks kõige olulisemat meetodit:

  • stage.act(delta)

  • stage.draw()

Need täidavad erinevaid rolle.

stage.act(delta):

  • uuendab animatsioone

  • töötleb Actions süsteemi

  • uuendab objektide liikumist

  • uuendab ajast sõltuvat loogikat

stage.draw():

  • joonistab Actor'id

  • kuvab visuaalse tulemuse

Lihtne reegel:

  • act(delta) = uuenda olekut

  • draw() = kuva olek

Warning

Kui stage.act(delta) ei kutsuta, siis animatsioonid ja Actions ei tööta ning UI ei uuene korrektselt.

Kui stage.draw() ei kutsuta, siis UI ei kuvata ekraanile.

Actorile InputListener'i lisamise näide

actor.setBounds(0, 0, texture.getWidth(), texture.getHeight());
actor.addListener(new InputListener() {
    @Override
    public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
        System.out.println("Down");
        return true;
    }

    @Override
    public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
        System.out.println("Up");
    }
});

Mängu tüüpiline kihiline struktuur

Enamikus 2D-mängudes on kolm peamist kihti:

  1. Taust

  2. Mänguobjektid

  3. HUD / UI

Kihiline renderdamine (alt üles):

[ Kiht 3: HUD / UI ]    <-- Stage (Kõige peal)
[ Kiht 2: Maailm   ]    <-- Mänguobjektid
[ Kiht 1: Taust    ]    <-- Staatiline foon

Joonistamise järjekord:
Taust -> Maailm -> HUD

Note

LibGDX joonistab objekte selles järjekorras, kuidas neid render() meetodis kutsutakse.

Selleks, et UI ei jääks teiste objektide taha, peab see olema renderdamise lõpus.

Kihilise struktuuri loomiseks kasutatakse üldiselt kahte lähenemisviisi:

  • Üks Stage + mitu Group-i (soovitatav)

  • Eraldi Stage'id (kui Viewport'id on väga erinevad)

Soovitus jõudluseks: Üks Stage + Group'id on tavaliselt parem valik.

Camera ja Viewport Stage'iga

// Näide: maailm + UI koos
public void create() {
    worldViewport = new FitViewport(1280, 720);
    uiViewport   = new FitViewport(1280, 720);  // või ScreenViewport HUD-i jaoks

    batch = new SpriteBatch();
    stage = new Stage(uiViewport, batch);
}

public void render(float delta) {
    // Maailma render
    worldViewport.apply();
    batch.setProjectionMatrix(worldViewport.getCamera().combined);
    batch.begin();
    // ... joonista mängumaailm
    batch.end();

    // UI / HUD
    stage.getViewport().apply();
    stage.act(delta);         // animatsioonid ja UI uuendamine
    stage.draw();             // UI joonistamine
}

public void resize(int width, int height) {
    worldViewport.update(width, height, true);
    stage.getViewport().update(width, height, true);
}

public void dispose() {
    stage.dispose();
    batch.dispose();          // kui ei jaga mujal
}

Actor ja selle hierarhia

Actor on kõige baasiliseim üksus Stage'il.

Peamised omadused:

  • positsioon

  • suurus

  • skale

  • rotatsioon

  • värv ja alpha

  • z-index

  • vanem-laps suhe

  • Listener'id

Lihtsustatud hierarhia:

Actor
├── Widget
│   ├── Label
│   ├── Image
│   ├── Button → TextButton, ImageButton, CheckBox
│   ├── TextField, TextArea
│   ├── ProgressBar, Slider
│   └── Touchpad
│
└── Group / WidgetGroup
    ├── Table
    ├── Container
    ├── Window, Dialog
    ├── ScrollPane
    ├── Stack
    ├── HorizontalGroup
    └── VerticalGroup

Projektistruktuur

Kuna Scene2D on kliendipoolne tehnoloogia, asuvad need klassid ainult core moodulis.

Soovitatav struktuur:

core/src/main/java/ee/taltech/examplegame/

├── screen/
│   ├── TitleScreen.java
│   ├── GameScreen.java
│   └── overlay/
│       └── Hud.java
│
├── ui/
│   ├── widgets/
│   └── factories/
│
├── actors/
│   ├── PlayerActor.java
│   └── BulletActor.java
│
└── util/
    └── SkinManager.java

Hea praktika:

  • Screen haldab Stage'i

  • Actor on visuaalne komponent

  • mänguloogika peaks olema eraldi klassides

Näide meie mängust

Kogu kood on leitav:

TitleScreen: https://github.com/Rig14/example-game/blob/main/core/src/main/java/ee/taltech/examplegame/screen/TitleScreen.java

public class TitleScreen extends ScreenAdapter {
    private final Stage stage;

    public TitleScreen(Game game) {
        stage = new Stage();

        // NB! important line - will make the stage listen for user input
        // For example when this is not set no hover or click events will be triggered
        Gdx.input.setInputProcessor(stage);

        // menu buttons
        var startButton = getButton(20, "Start", () -> {
            // send a message to the server that the player wants to join the game
            ServerConnection.getInstance().getClient().sendTCP(new GameJoinMessage());
            game.setScreen(new GameScreen(game));
        });
        var exitButton = getButton(20, "Exit", () -> Gdx.app.exit());

        // positioning the buttons. you can think of the following as a table (or flexbox) in HTML
        var table = new Table();
        table.setFillParent(true);
        table.add(startButton).padBottom(20);
        table.row();
        table.add(exitButton);

        stage.addActor(table);
    }

    /**
     * Renders the TitleScreen, clearing it and drawing the buttons.
     *
     * @param delta time since last frame.
     */
    @Override
    public void render(float delta) {
        super.render(delta);
        // clear the screen
        Gdx.gl.glClearColor(192 / 255f, 192 / 255f, 192 / 255f, 1); // to get that win 95 look
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        // draw the buttons
        stage.act(delta);   // updates UI logic and animations
        stage.draw();       // renders UI elements
    }

    @Override
    public void resize(int width, int height) {
        super.resize(width, height);

        stage.getViewport().update(width, height, true);
    }
}

Hud: https://github.com/Rig14/example-game/blob/main/core/src/main/java/ee/taltech/examplegame/screen/overlay/Hud.java

/**
 * Info overlay that contains information about: player names, lives, game status, game time.
 *
 * @param spriteBatch used for rendering all visual elements. Using the same spritebatch as the Arena (players,
 *                    bullets etc.) helps with scaling and resizing
 */
public Hud(SpriteBatch spriteBatch) {
    localPLayerId = ServerConnection.getInstance().getClient().getID();

    // The viewport with current hardcoded width and height works decently with most screen/window sizes
    // There's no need for additional font re-scaling when adjusting the window size
    Viewport viewport = new FitViewport(640, 480, new OrthographicCamera());

    // Create a stage to render the HUD content
    stage = new Stage(viewport, spriteBatch);

    // Create a table to display fields such as lives count
    Table table = createHudTable();
    table.setDebug(false);  // true - outline all table cells, labels with a red line (makes table non-transparent)
    stage.addActor(table);
}

/**
 * Table manages where different fields such as player names and lives are displayed on the screen
 */
private Table createHudTable() {
    // For simple tables, using an empty placeholder label is usually the easiest solution to adjust alignment
    Label emptyLabel = createLabel("", Color.WHITE, LABEL_SIZE);

    Table table = new Table();
    table.setFillParent(true);  // Fill whole screen
    table.top();  // Align the table's content to the top
    table.padTop(TABLE_PADDING_TOP);

    // First row: player names and time
    table.add(localPlayerNameLabel);
    table.add(timeLabel);
    table.add(remotePlayerNameLabel);
    table.row().expandX();  // make the row fill the entire width of the screen

    // Second row: player lives
    table.add(localPlayerLivesLabel);
    table.add(emptyLabel);  // empty label as a placeholder for alignment
    table.add(remotePlayerLivesLabel);
    table.row().expandX();

    // Third row: game status message
    table.add(gameStatusLabel)
        .colspan(3)  // Make the label span across all 3 table columns
        .padTop(GAME_STATUS_LABEL_PADDING_TOP)
        .align(Align.center);  // Center-align the label within the table cell
    table.row();

    return table;
}

Parimad praktikad ja jõudlus

  • Kasuta Skin-i nuppude ja label'ite jaoks

  • Table.setDebug(true) aitab paigutuse silumisel

  • Eelista Group'e mitme Stage asemel

  • Alati kutsu dispose()

  • Kasuta InputMultiplexer-it mitme sisendi jaoks

Gdx.input.setInputProcessor(new InputMultiplexer(stage, playerInput));

Scene2D: https://libgdx.com/wiki/graphics/2d/scene2d/scene2d

Table: https://libgdx.com/wiki/graphics/2d/scene2d/table

Viewport: https://libgdx.com/wiki/graphics/2d/scene2d/viewports

StackExchange: https://gamedev.stackexchange.com/questions/128381/may-i-put-a-clicklistener-to-the-stage-or-better-if-every-actor-group-has-its-ow

GameDevDoc: https://gamedevdoc.pages.taltech.ee/menu/menu.html#ekraani-loomine