设计一个简单的事件驱动的 GUI

Designing a simple event-driven GUI

我正在为我用 LibGDX 制作的视频游戏创建一个简单的事件驱动 GUI。它只需要支持按钮(矩形),并在单击按钮时调用一个 act() 函数。我将不胜感激关于结构的一些建议,因为到目前为止我想到的解决方案似乎远非理想。

我当前的实现涉及扩展 Button class 的所有按钮。每个按钮都有一个 Rectangle 及其边界和一个抽象的 act() 方法。

每个游戏屏幕(例如主菜单、角色 select、暂停菜单、游戏内屏幕)都有一个 HashMap 个按钮。单击时,游戏屏幕遍历 HashMap 中的所有内容,并在单击的任何按钮上调用 act()。

我遇到的问题是 Button 必须从它们的 superclass 中覆盖它们的 act() 才能执行它们的操作,并且 Button 不是屏幕的成员 class里面包含了所有的游戏代码。我正在为游戏中的每个按钮设置 subclassing Button。仅我的主菜单就有 ButtonPlay、ButtonMapDesigner、ButtonMute、ButtonQuit 等。这很快就会变得混乱,但我想不出更好的方法来为每个按钮保留一个单独的 act() 方法.

由于我的静音按钮不是主菜单屏幕的一部分并且无法访问游戏逻辑,因此 act() 无非是 mainMenuScreen.mute();。如此有效,对于我游戏中的每个按钮,我必须创建一个 class class,它只做 <currentGameScreen>.doThisAction();,因为实际执行操作的代码必须在游戏屏幕中 class.

我考虑过用一个大 if/then 来检查每次点击的坐标,并在必要时调用适当的操作。例如,

if (clickWithinTheseCoords)
   beginGame();
else if(clickWithinTheseOtherCoords)
   muteGame();
...

但是,我需要能够即时 add/remove 按钮。当从游戏屏幕上点击一个单位时,需要出现一个移动它的按钮,然后在实际移动该单位时消失。使用 HashMap,我可以在单击或移动单元时调用的代码中仅 map.add("buttonMove", new ButtonMove())map.remove("buttonMove")。使用 if/else 方法,我不需要为每个按钮单独设置 class,但我需要跟踪测试的每个可点击区域此时是否可见并可被用户点击游戏,这似乎比我现在更头疼。

我会为您将在 act 方法中 运行 的所有按钮提供一个 运行nable。给你一个简单的例子。

private final Map<String, Button> buttons = new HashMap<>();

public void initialiseSomeExampleButtons() {
    buttons.put("changeScreenBytton", new Button(new Runnable() {
        @Override
        public void run() {
            //Put a change screen action here.
        }
    }));

    buttons.put("muteButton", new Button(new Runnable() {
        @Override
        public void run() {
            //Do a mute Action here
        }
    }));
}

public class Button {

    //Your other stuff like rectangle

    private final Runnable runnable;

    protected Button(Runnable runnable) {
        this.runnable = runnable;
    }

    public void act() {
        runnable.run();
    }
}

您通过地图跟踪您的按钮,只需将 运行nable 操作传递给构造函数中的每个按钮。我故意跳过了一些代码,以便您可以自己尝试。如果您有任何问题,请告诉我。

Sneh 的回复让我想起了一个相当重大的疏忽 - 我不必为每个按钮创建一个单独的 class,我可以在创建按钮时使用匿名内部 classes,指定它的坐标和 act() 方法每次。我探索了 lambda 语法作为一种可能的更短方法来执行此操作,但 运行 对其有限制。我最终得到了一个灵活的解决方案,但最终进一步减少了它以满足我的需求。下面介绍两种方式。

我游戏中的每个游戏屏幕都是从 MyScreen class 子class 编辑的,它扩展了 LibGDX 的 Screen,但增加了通用功能比如在调整大小时更新视口,拥有按钮的 HashMap 等。我在 MyScreen class 中添加了一个 buttonPressed() 方法,该方法将枚举作为其一个参数。我有 ButtonValues 枚举,其中包含所有可能的按钮(例如 MAINMENU_PLAY、MAINMENU_MAPDESIGNER 等)。在每个游戏屏幕中,buttonPressed() 被覆盖,并使用一个开关来执行正确的操作:

public void buttonPressed(ButtonValues b) {
    switch(b) {
        case MAINMENU_PLAY:
            beginGame();
        case MAINMENU_MAPDESIGNER:
            switchToMapDesigner();
    }
}

另一种解决方案让按钮存储一个 lambda 表达式,这样它就可以自行执行操作,而不需要 buttonPressed() 作为中介根据按下的按钮执行正确的操作。

要添加一个按钮,使用其坐标和类型(枚举)创建它,并将其添加到按钮的 HashMap 中:

    Button b = new Button(this,
            new Rectangle(300 - t.getRegionWidth() / 2, 1.9f * 60, t.getRegionWidth(), t.getRegionHeight()),
            tex, ButtonValues.MAINMENU_PLAY);
    buttons.put("buttonPlay", b);

要删除它,只需 buttons.remove("buttonPlay"). 它就会从屏幕上消失并被游戏遗忘。

参数是拥有它的游戏屏幕(因此按钮可以在游戏屏幕上调用 buttonPressed())、一个矩形及其坐标、它的纹理(用于绘制它)和它的枚举值.

这是按钮 class:

public class Button {

    public Rectangle r;
    public TextureRegion image;

    private MyScreen screen;
    private ButtonValues b;

    public Button(MyScreen screen, Rectangle r, TextureRegion image, ButtonValues b) {
        this.screen = screen;
        this.r = r;
        this.image = image;
        this.b = b;
    }

    public void act() {
        screen.buttonPressed(b);
    }

    public boolean isClicked(float x, float y) {
        return x > r.x && y > r.y && x < r.x + r.width && y < r.y + r.height;
    }
}

isClicked() 只接受一个 (x, y) 并检查该点是否包含在按钮内。单击鼠标时,我遍历所有按钮并调用 act() 如果按钮 isClicked.

第二种方法类似,但使用 lambda 表达式而不是 ButtonValues 枚举。 Button class 类似,但有以下更改(比听起来简单得多):

字段 ButtonValues b 被替换为 Runnable r,并从构造函数中删除。添加了一个 setAction() 方法,它接受一个 Runnable 并将 r 设置为传递给它的 Runnable。 act() 方法就是 r.run()。示例:

public class Button {

    [Rectangle, Texture, Screen]
    Runnable r;

    public Button(screen, rectangle, texture) {...}

    public void setAction(Runnable r) { this.r = r; }

    public void act() { r.run(); }
}

要创建按钮,我执行以下操作:

    Button b = new Button(this,
            new Rectangle(300 - t.getRegionWidth() / 2, 1.9f * 60, t.getRegionWidth(), t.getRegionHeight()),
            tex);
    b.setAction(() -> b.screen.doSomething());
    buttons.put("buttonPlay", b);

首先,创建一个按钮及其包含的游戏屏幕 class、边界框和纹理。然后,在第二个命令中,我设置了它的动作——在本例中为 b.screen.doSomething();。这不能传递给构造函数,因为此时 b 和 b.screen 不存在。 setAction() 接受一个 Runnable 并将其设置为 Button 的 Runnable,它在 act() 被调用时被调用。但是,Runnables 可以使用 lambda 语法创建,因此您无需创建匿名 Runnable class,只需传入它执行的函数即可。

此方法具有更大的灵活性,但有一个警告。 Button 中的 screen 字段包含一个 MyScreen,基本屏幕 class,我的所有游戏屏幕都从该屏幕扩展。 Button 的函数只能使用属于 MyScreen class 的方法(这就是为什么我在 MyScreen 中制作 buttonPressed() 然后意识到我可以完全废弃 lambda 表达式).显而易见的解决方案是转换 screen 字段,但对我来说,当我只能使用 buttonPressed() 方法时,不值得额外的代码。

如果我的 MainMenuScreen class(扩展 MyScreen)中有一个 beginGame() 方法,则传递给按钮的 lambda 表达式将需要涉及强制转换到 MainMenuScreen:

    b.setAction(() -> ((MainMenuScreen) b.screen).beginGame());

不幸的是,即使是通配符语法也无济于事。

最后,为了完整起见,游戏循环中用于操作按钮的代码:

public abstract class MyScreen implements Screen {

    protected HashMap<String, Button> buttons; // initialize this in the constructor

    // this is called in every game screen's game loop
    protected void handleInput() {
        if (Gdx.input.justTouched()) {
            Vector2 touchCoords = new Vector2(Gdx.input.getX(), Gdx.input.getY());
            g.viewport.unproject(touchCoords);
            for (HashMap.Entry<String, Button> b : buttons.entrySet()) {
                if (b.getValue().isClicked(touchCoords.x, touchCoords.y))
                    b.getValue().act();
            }
        }
    }
}

并绘制它们,位于助手 class:

public void drawButtons(HashMap<String, Button> buttons) {
    for (HashMap.Entry<String, Button> b : buttons.entrySet()) {
        sb.draw(b.getValue().image, b.getValue().r.x, b.getValue().r.y);
    }
}