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

时间:2017-05-03 02:05:10

标签: java libgdx event-driven

我正在为LibGDX制作的视频游戏创建一个简单的事件驱动GUI。它只需要支持按钮(矩形),单击时调用单个act()函数。我会对结构化方面的一些建议表示赞赏,因为到目前为止我所想到的解决方案似乎远非理想。

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

每个游戏屏幕(例如主菜单,字符选择,暂停菜单,游戏内屏幕)都有HashMap个按钮。单击时,游戏屏幕会遍历HashMap中的所有内容,并在单击的任何按钮上调用act()。

我遇到的问题是,按钮必须从超类中重写act()以执行其操作,并且按钮不是Screen类的成员,它包含所有的游戏代码。我为游戏中的每个按钮创建了Button的子类。我的主菜单上只有一个ButtonPlay,ButtonMapDesigner,ButtonMute,ButtonQuit等。这会很快变得凌乱,但我不能想到更好的方法来保持单独的act()方法。每个按钮。

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

我认为有一个很大的if / then来检查每次点击的坐标,并在必要时调用相应的操作。例如,

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

但是,我需要能够动态添加/删除按钮。当从游戏屏幕单击一个单元时,需要显示一个移动它的按钮,然后在实际移动该单元时消失。使用HashMap,我可以在单击或移动单元时调用的代码中map.add("buttonMove", new ButtonMove())map.remove("buttonMove")。使用if / else方法,我不需要每个按钮都有一个单独的类,但是我需要跟踪用户在游戏中此时可见和可点击的每个可点击区域是否可见比起我现在拥有的更大的头痛。

2 个答案:

答案 0 :(得分:1)

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

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

您可以通过地图跟踪按钮,只需将runnable操作传递给构造函数中的每个按钮。我故意跳过一些代码,以便您可以尝试自己。如果您有任何疑问,请与我们联系。

答案 1 :(得分:0)

Sneh的回复提醒我一个相当重要的疏忽 - 而不是必须为每个按钮创建一个单独的类,每当我创建一个按钮时,我可以使用匿名内部类,每次都指定它的坐标和act()方法。我探讨了lambda语法作为一种可能的更短的方法来做到这一点,但遇到了它的限制。我最终得到了一个灵活的解决方案,但最终还是进一步减少了它以满足我的需求。两种方式如下所示。

我游戏中的每个游戏画面都是MyScreen类的子类,它扩展了LibGDX的Screen,但增加了通用功能,例如在调整大小时更新视口,使用按钮的HashMap等等。我添加了MyScreenbuttonPressed()方法,它将一个参数作为枚举。我有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()),一个带有坐标的矩形,它的纹理(用于绘制它)和它的枚举值。 / p>

这是Button类:

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类是类似的,但有了这些变化(它比它听起来简单得多):

字段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);

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

这种方法允许更多的灵活性,但有一点需要注意。 screen中的Button字段包含MyScreen,这是我的所有游戏屏幕都可以从中扩展的基本屏幕类。 Button的函数只能使用属于MyScreen类的方法(这就是我在buttonPressed()中创建MyScreen然后意识到我可以完全废弃lambda表达式的原因)。显而易见的解决方案是转换screen字段,但对我来说,当我可以使用buttonPressed()方法时,额外的代码是不值得的。

如果我的beginGame()类(扩展MainMenuScreen)中有MyScreen方法,则传递给按钮的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();
            }
        }
    }
}

并绘制它们,位于辅助类中:

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