Stage ja Actorite süsteem

Stage

Scene2D on mängu UI, mis luuakse kas SpriteBatchi ja/või Stage ja Actor-ite süsteemiga. Ühele Screen-ile võib panna mitu Stage-i. Hallates Screen-e abstraktse Game-klassi kaudu, seatakse vaid 1 Screen ja selle Stage/Stage-id korraga aktiivseks. See võimaldab Sreen-i ja ta Stage-ide haldust teha Screen-i klassis. Kui Game klassi asemel tegeleb Screen-ide vahetusega ApplicationListener või ApplicationAdapter, tuleb Screen-ide ja Stage-ide haldus teha ka nende klasside sees ning ka ise Screen-ide aktiviseerumise ja inaktiveeruse loogikat ise läbi viia.

Screen-i kasutamine ilma Stage-ita:

  • Peab ise haldama sprite-ide joonistamist, asukohta ja kustutamist.

  • Animatsioonide tegemiseks tuleb kasutada Animation-klassi.

  • Kasutaja sisendi jaoks tuleb laiendada InputProcessor-it ja ka ise sisendi halduse loogika paika panna.

Stage-i kasutamise eelised:

  • Sprite-id saab asendada Actor-itega.

  • Actor-ite positsiooni määravad vanemad WidgetGroup-is, sest WidgetGroup implementeerib Layout liidest.

  • Animatsioone saab teha Actions-klassi abil.

  • Stage-ile saab kergelt lisada InputProcessor-i (Gdx.input.setInputProcessor(stage)), ja Actor-itele lisada Listener-e, määramaks, kas sisendit haldab vanem või lapse Actor kuna Actor-itel on hierarhiline süsteem.

Actorile InputListener andmine:

actor.setBounds(0, 0, texture.getWidth(), texture.getHeight());

actor.addListener(new InputListener() {
    public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
        System.out.println("down");
        return true;
    }

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

Tüüpilisel mängul on mängimise-ekraanil 3 kihti:

  1. Taust – Staatiline või dünaamiline taust, mis pole interaktiivne.

  2. Mänguobjektid – Tegelased, objektid ja klikitavad elemendid.

  3. HUD – Ekraani servadel asuv statistika (näiteks mängija punktid) ja nupud (näiteks pausinupp).

Sellise kihilise struktuuri loomiseks kasutatakse üldiselt kahte moodust:

  1. Kasutades ühte Stage-i ja jaotades Actor-id kolme Group-i, kus Stage renderdab neid vastavalt z-indeksi järjekorras.

  2. Tehes kolm eraldi Stage-i (backgroundStage, gameStage, hudStage) ning joonistades neid eraldi (backgroundStage.draw()).

Stage-i asemel saab samu tulemusi kasutades SpriteBatch-i, BitmapFont-i ja InputProcessor-i. Samuti võib AI olla eraldi Stage-ist. Kui aga kasutada mõlemat samal Screen-il korraga ehk Stage-i koos Actor-itega kui ka SpriteBatch-iga, mis ei asu Stage-i peal, peab arvestama, et Stage-il ja Camera'l on erinev Viewport.

Camera ja Stage'i Viewport

// Camera viewport:

private Viewport viewport;
private Camera camera;

public void create() {
    camera = new PerspectiveCamera();
    viewport = new FitViewport(800, 480, camera);
}

// Stage viewport:

private Stage stage;

public void create() {
    stage = new Stage(new StretchViewport(width, height));
}

Mitme erisuuruse ViewPort-i puhul on ka Camera 'l ja Stage-il erinev strateegia:

// Camera viewport:
viewport1.apply();
viewport1.draw
viewport2.apply();
viewport2.draw()()

// Stage viewport
stage1.getViewport().apply();
stage1.draw();
stage2.getViewport().apply();
stage2.draw();

Kasutades aga Camera't ja Stage-i koos:

public void create() {
   Viewport viewport = new FitViewport(worldWidth, worldHeight);
   SpriteBatch spriteBatch = new SpriteBatch();
   Stage stage = new Stage(new FitViewport(worldWidth, worldHeight));
   stage.getViewport().getCamera().position.setZero(); // (valikuline) kaamera keskele panek
}

public void render() {
    viewport.apply();
    spriteBatch.setProjectionMatrix(viewport.getCamera().combined);
    spriteBatch.begin();
    // spriteBatch.draw() sisendparameetritega
    spriteBatch.end();
    stage.getViewport().apply();
    stage.draw();
}

Actor

Kõik Stage-i elemendid on tavaliselt esindatud Actor-itena. Stage joonistab, uuendab ja eemaldab enda peal olevad Actor-id automaatselt. Kui Actor ei panda Stage-i peale, peab Actor-it ise haldama.

Actorite omadused:

  1. Positsioon – vasaku alumise nurga kaugus oma vanemast, kui Actor kuulub Group-i. Ka animatsiooni saab selle omadusi järgi lisada (asukoha muutusele lisaks anda aja-parameeter), kasutades Action-it.

  2. Suurus – ristkülikukujuline kuju.

  3. Vanem – kui Actor kuulub Group-i, siis kes on tema vanem.

  4. Skaleerimisvõime – suuruse muutmise võime, millele saab ka aja-parameetri kaasa anda Action-it kasutades.

  5. Pööramisvõime – millele saab ka aja-parameetri kaasa anda, et sujuvust muuta, Action-it kasutades.

  6. Z-indeks – kas Actor asub teiste peal või all.

  7. Värv – värvus ja läbipaistvus, millele saab ka aja-parameetri kaasa anda Action-it kasutades.

Actor-itel on olemas erinevad Listener-id, kuid keerukamate Event-ide jaoks on vaja Actor-eid laiendada või Listener-e kohandada. Actor-i alamtüübil Widget-il on juba sisseehitatud kasutaja sisendi haldamine ning lisaks kasutab Widget Layout liidest. WidgetGroup koosneb mitmest Widget-ist ehk peale Actor-i on WidgetGroup ka Group-i alamtüüp.

Actorite hierarhia:

Actor
│
├── Layout Widgetid: määravad widgetite paigutuse Stage'il.
│   ├── WidgetGroup: kui Widgetil on lapsed
│   │   ├── ScrollPane: keritavus
│   │   ├── SplitPane
│   │   ├── Tree
│   │   ├── HorizontalGroup
│   │   ├── VerticalGroup
│   │   └── Stack: Actorid pannakse üksteise peale
│   ├── Table: Saab vanemaks panna terve Stagei (setFillParent(true))
│   ├── Container:
│   └── Window: UI-aknad
│       └── Dialog
│
├── Widgetid: kasutaja sisendi jaoks
│   ├── Label
│   ├── Button
│   │   ├── TextButton
│   │   ├── ImageButton
│   │   └── CheckBox
│   ├── ProgressBar
│   │   ├── Slider
│   │   └── ProgressBar
│   ├── List
│   ├── SelectBox: Rippmenüü
│   ├── TextField
│   ├── TextArea: Mitmerealine tekstiväli
│   └── Touchpad
│
├── Image  (`TextureRegioniga` või `Drawable`)
└── CustomActor

Widget-it ja WidgetGroup-i saab luua ka Stage-ita, rakendades mõlemale pack meetodit ning kasutades SpriteBatch-i Widget-i joonistamiseks.

Label ja Table ilma Stage'ita

private SpriteBatch batch;
private Label label;
private Skin skin;
private Table table;

public void create() {
    batch = new SpriteBatch();
    skin = new Skin(Gdx.files.internal("uiskin.json"));
    label = new Label("Hello!", skin);
    label.pack();
    label.setPosition(100, 100);
    table = new Table();
    table.add(label);
    table.pack();
}

public void render() {
    batch.begin();
    label.draw(batch, 1);
    batch.end();
}

Näide meie mängust "Stage" ja "Actor" kasutamise kohta.

Kogu kood on leitav siin -> näidismäng.

Siin kasutatakse "Stage" mängu põhimenüü kuvamiseks. Selleks kasutatakse "Table" (et hõlpsasti määrata vajalik kohanduv paigutus), tabelisse lisatakse teised Actor'id – nupud, ja seejärel lisatakse Table Stage'i.

Samuti uuendatakse meetodis "resize" Stage'i viewport vastavalt akna suurusele. Ja ei tohi renderdamisel unustada kasutada meetodit stage.draw().

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

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

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

Kogu kood on leitav siin -> näidismäng

Veel üks kasutusnäide – seekord HUD-i kuvamiseks.

/**
 * 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;
}

Table kasutamine Stage'is on väga mugav, kuna see võimaldab hõlpsasti ja visuaalselt meeldivalt paigutada teisi elemente (Label, Button). Table'isse saab lisada väliseid ja sisemisi ääremarginaale, laiendada ridu horisontaalselt, paigutada elemente keskele, vasakule või paremale jne.

Lingid: