Hüpikud ja ülekatted

Hüpikud (popup) ja ülekatted (overlays) on mänguarenduses tavalised elemendid, mida kasutatakse lisateabe, dialoogide või menüüde kuvamiseks põhimängu ekraani peal. Käesolevas õppelehel vaatame:

  • Mis on hüpikud ja ülekatted

  • Kuidas neid LibGDX-is kujundada ja realiseerida

  • Lihtsat koodinäidet

  • Kujundusalaseid tähelepanekuid, mida tasub silmas pidada

Mis on hüpikud ja ülekatted?

Hüpik (tihti nimetatud ka “dialoog”) on kasutajaliidese element, mis ilmub ekraanile, võttes fookuse enda peale. Tavaliselt muudab see mängu taustal passiivseks või “hämardab” selle, et kasutaja ei saaks muude elementidega suhelda enne, kui hüpikaken on suletud.

Ülekate on sarnane, kuid see ei pea tingimata blokeerima suhtlust alloleva ekraaniga. Ülekatted on sageli kasutusel mitmeotstarbelistes teavitustes, abitekstides, HUD-ides (heads-up display) või poolläbipaistvates menüüdes.

Kujunduslähenemine

Hüpikute ja ülekatete integreerimisel tasub mõelda järgmistele punktidele:

  1. Blokeeriv vs mitteblokeeriv - Blokeeriv: Mäng ekraani all peatub või kasutaja peab enne edasi liikumist hüpiku sulgema. - Mitteblokeeriv: Kasutaja saab samal ajal jätkata alloleva ekraani elementidega suhtlemist.

  2. Sisendi töötlemine - Kasuta InputMultiplexer’it või eraldi sisenditöötlejaid, et hallata, kuidas sündmused hüpiku/ülekatte ja põhilava (Stage) vahel jagunevad. - Blokeeriva hüpiku puhul tarbitakse kõik sisendisündmused hüpiku tasandil, et need ei jõuaks põhilavani.

  3. Paigutus ja teemakohasus - Hoia kujundus ühtne ülejäänud kasutajaliidese elementidega. - Kasutades Scene2D liidest, on mugav kasutada ühte ja sama Skin-i nii hüpikakna stiili kui ka muude UI-elementide jaoks.

  4. Jõudluse kaalutlused - Hüpikuid ja ülekatteid joonistatakse igal kaadril. Püüa hoida need võimalikult kerged. - Kui peatad taustal mängu, kontrolli, et see oleks korralikult integreeritud sinu game loop 'is.

Realiseerimine LibGDX-is

LibGDX-is on mitu võimalust hüpikute ja ülekatete tegemiseks, kuid üks levinumaid ja paindlikumaid meetodeid on kasutada Scene2D Stage’i ja Actor’it (või Window’it). Alljärgnev on põhiskeem:

  1. Põhilava (Game Stage) Selles renderdatakse põhilised mänguelemendid või teostatakse eraldi renderdusmootoriga mängu loogikat.

  2. UI lava (UI Stage) Teine lava, mis tegeleb kasutajaliidese elementidega (menüüd, nupud, dialoogid jms).

  3. InputMultiplexer (soovituslik) InputMultiplexer suudab jagada sisendisündmusi mitme lava vahel, võimaldades sul otsustada, milline lava parajasti aktiivset sisendit saab või kuidas sündmusi jagatakse.

  4. Hüpiku (Popup) klass Laienda Window (või Table) klassi, et luua oma dialoog. Lisa sobiv sisu (sildid, nupud jms) ning defineeri, kuidas see kuvamise ja peitmise ajal käitub.

  5. Positsioneerimine ja stiil Aseta hüpikaken ekraani keskele (või sobivasse asukohta). Kui soovid, et hüpik paistaks eriti silma, kasuta poolläbipaistvat tausta.

Allpool on lihtne näide, kuidas luua blokeeriv hüpikaken LibGDX Scene2D abil.

Näide: lihtne hüpikaken

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputMultiplexer;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Window;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.utils.viewport.ScreenViewport;

public class PopupExample extends ApplicationAdapter {

    private Stage gameStage;
    private Stage uiStage;
    private Skin skin;
    private Window popupWindow;
    private boolean showPopup;

    @Override
    public void create() {
        // Loome kaks lava: üks mängu jaoks, teine UI jaoks
        gameStage = new Stage(new ScreenViewport());
        uiStage = new Stage(new ScreenViewport());

        // Laeme lihtsa skini
        skin = new Skin(Gdx.files.internal("uiskin.json"));

        // Loome InputMultiplexer'i, et hallata mõlema lava sisendeid
        InputMultiplexer multiplexer = new InputMultiplexer();
        multiplexer.addProcessor(uiStage);
        multiplexer.addProcessor(gameStage);
        Gdx.input.setInputProcessor(multiplexer);

        // Loome lihtsa hüpiku
        popupWindow = new Window("Popup dialoog", skin);
        popupWindow.setModal(true);  // Muudab selle hüpiku blokeerivaks
        popupWindow.setMovable(false);
        popupWindow.setResizable(false);

        // Lisame hüpikusse sisu
        TextButton closeButton = new TextButton("Sulge", skin);
        closeButton.addListener(event -> {
            if (event.toString().equals("touchDown")) {
                showPopup = false;
                return true;
            }
            return false;
        });

        popupWindow.add("See on blokeeriv hüpikaken!").row();
        popupWindow.add(closeButton).pad(10);

        // Kohanda suurus sisule vastavaks ja paiguta keskele
        popupWindow.pack();
        popupWindow.setPosition(
            (uiStage.getWidth() - popupWindow.getWidth()) / 2,
            (uiStage.getHeight() - popupWindow.getHeight()) / 2
        );

        // Alguses ei ole hüpik nähtav
        popupWindow.setVisible(false);

        // Lisame hüpikakna UI lavale
        uiStage.addActor(popupWindow);
    }

    @Override
    public void render() {
        // Puhastame ekraani
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        // Uuendame mõlema lava loogikat
        gameStage.act(Gdx.graphics.getDeltaTime());
        uiStage.act(Gdx.graphics.getDeltaTime());

        // Kui hüpik on aktiveeritud, siis kuvatakse see ja blokeeritakse mäng
        if (showPopup) {
            popupWindow.setVisible(true);
            // Siia lisa loogika, et vajadusel blokeerida mäng
        } else {
            popupWindow.setVisible(false);
            // Võta blokeering maha, kui see oli peatatud
        }

        // Joonista põhilava
        gameStage.draw();

        // Joonista UI-lava põhilava peale
        uiStage.draw();
    }

    // Seda meetodit võib nt kutsuda mingi nupuvajutus
    public void triggerPopup() {
        showPopup = true;
    }

    @Override
    public void dispose() {
        gameStage.dispose();
        uiStage.dispose();
        skin.dispose();
    }
}

Ülekate vs hüpikaken

Hüpik ülaltoodud näites on modaalne, mis tähendab, et see takistab interaktsiooni mängulavaga. Ülekate võib olla lihtsam UI-element (nt läbipaistev Table), mis ei tarbi kõiki sisendisündmusi. Selle saavutamiseks:

  • Kasuta mitteblokeerivat Window’it või Table’it.

  • Lisa osaliselt läbipaistev taust või värv.

  • Luba sisendisündmustel ulatuda põhilavani (ära tarbi neid täielikult).

Näide: LibGDX Dialog ja Group kasutamine

Lisaks lihtsale Window-põhisele hüpikule (popup) on LibGDX-is ka sisseehitatud Dialog (mida saab käsitleda modaalaknana) ning võimalus ehitada oma ülekate/menüü Group klassi abil. Allpool on lühike näide, kus üks nupp avab veateate (Dialog) ja teine nupp avab seadetemenüü (Group):

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ScreenAdapter;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.ui.Dialog;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.Window;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable;
import com.badlogic.gdx.utils.viewport.ScreenViewport;

public class PopupExamples extends ScreenAdapter {

    private Stage stage;
    private BitmapFont font;
    private Skin skin;
    private float soundVolume = 1.0f;     // Näidislik helitugevuse muutuja
    private boolean isSettingsVisible = false;

    @Override
    public void show() {
        stage = new Stage(new ScreenViewport());
        Gdx.input.setInputProcessor(stage);

        // Näidiskirjatüüp ja skin (pead mingi enda faili panema sinna)
        font = new BitmapFont();
        skin = new Skin(Gdx.files.internal("uiskin.json"));

        // Loome taustapildi
        Image background = new Image(new Texture(Gdx.files.internal("background.png")));
        background.setFillParent(true);
        stage.addActor(background);

        // Loome nupu, mis avab dialoogi
        TextButton errorDialogButton = new TextButton("Veateade", skin);
        errorDialogButton.setPosition(100, 300);
        errorDialogButton.addListener(new ChangeListener() {
            @Override
            public void changed(ChangeEvent event, Actor actor) {
                showErrorDialog("See on veateade!");
            }
        });
        stage.addActor(errorDialogButton);

        // Loome nupu, mis avab eraldi seadete overlay
        TextButton settingsButton = new TextButton("Seaded", skin);
        settingsButton.setPosition(100, 200);
        settingsButton.addListener(new ChangeListener() {
            @Override
            public void changed(ChangeEvent event, Actor actor) {
                if (!isSettingsVisible) {
                    showSettingsOverlay();
                }
            }
        });
        stage.addActor(settingsButton);
    }

    /**
     * Lihtne `Dialog` mis kuvatakse ekraanil
     */
    private void showErrorDialog(String message) {
        // Loome dialoogi ilma pealkirjata
        Dialog dialog = new Dialog("", new Window.WindowStyle(font, Color.WHITE, null)) {
            @Override
            protected void result(Object object) {
                this.hide();
            }
        };

        // Lisame dialoogile sisu (Label)
        Label.LabelStyle labelStyle = new Label.LabelStyle(font, Color.RED);
        Label messageLabel = new Label(message, labelStyle);
        messageLabel.setFontScale(1.2f);

        dialog.getContentTable().add(messageLabel).pad(20);

        // Näitame dialoogi laval
        dialog.show(stage);

        // Paneme dialoogi 1.5 sekundi pärast automaatselt kaduma
        dialog.addAction(Actions.sequence(
                Actions.delay(1.5f),
                Actions.run(dialog::hide)
        ));
    }

    /**
     * Näide kuidas `Group` abil eraldi seadete aken ehitada
     */
    private void showSettingsOverlay() {
        isSettingsVisible = true;

        // Grupi loome "kastina", kuhu lisame sisu
        final Group settingsGroup = new Group();
        settingsGroup.setBounds(100, 100, 400, 300);

        // Taustapilt
        Image bg = new Image(new TextureRegionDrawable(new Texture(Gdx.files.internal("dialog-background.png"))));
        bg.setSize(400, 300);
        settingsGroup.addActor(bg);

        // Lisame menüüle pealkirja
        Label title = new Label("Seaded", new Label.LabelStyle(font, Color.WHITE));
        title.setPosition(160, 250); // Liiguta pealkirja vastavalt oma soovidele
        settingsGroup.addActor(title);

        // Lisame nupu
        TextButton closeButton = new TextButton("Sulge", skin);
        closeButton.setPosition(160, 40);
        closeButton.addListener(new ChangeListener() {
            @Override
            public void changed(ChangeEvent event, Actor actor) {
                settingsGroup.remove(); // Eemaldame grupi lavalt
                isSettingsVisible = false;
            }
        });
        settingsGroup.addActor(closeButton);

        // Lisa veel muud sisu (sliderid, nupud, tekstid jms)

        // Lõpuks lisame selle grupi lavale
        stage.addActor(settingsGroup);
    }

    @Override
    public void render(float delta) {
        Gdx.gl.glClearColor(0, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        stage.act(delta);
        stage.draw();
    }

    @Override
    public void dispose() {
        stage.dispose();
        font.dispose();
        skin.dispose();
    }
}

Selgitus: - showErrorDialog(...) loob väikese Dialog akna, mis iseseisvalt kaob 1.5 sekundi pärast. - showSettingsOverlay() ehitab ise lihtsa overlay Group baasil. See ei ole vaikimisi blokeeriv (st kasutaja saab vahepeal muudele elementidele klõpsata), aga soovi korral võid sisendeid tarbida/peita, et muuta see sarnasemaks blokeerivale hüpikule.

Need kaks meetodit illustreerivad hästi, kuidas LibGDX-i Scene2D-s saab luua nii kiireid, modaalseid dialooge (Dialog) kui ka täpsemalt kohandatavaid ülekatteid (Group).

Nõuanded ja parimad tavad

  1. Kasuta eraldi lavasid Hoia mängu põhiloogika ühel laval (või omaette renderdussüsteemis) ja kasutajaliidese elemendid teisel. Nii on lihtsam hallata keerukust.

  2. Peata mängu olek (kui vaja) Blokeeriva hüpikakna avamisel võib olla mõistlik peatada mängu loogika, et vältida kummalisi olukordi.

  3. Ühtne stiil Kasuta sama Skin-i või vähemalt ühtset kujundusjoont kogu kasutajaliidese ulatuses, et tagada professionaalne ja ühtlane välimus.

  4. Hoia kood lihtne Kui sul on sagedasi teavitusi (nt overlay teadete süsteem), püüa need luua jõudlust ja ressurssi arvestades säästlikult. Väldi liigseid tekstuure või suurte objektide korduvat joonistamist.

  5. Testimine Kontrolli hüpikakende ja ülekatete toimimist erinevatel ekraanisuurustel ja kuvasuhetega. Eriti oluline, kui mäng on suunatud mitmele platvormile (lauaarvuti, mobiil, veeb).