设计一个简单的事件驱动的GUI [英] Designing a simple event-driven GUI

查看:61
本文介绍了设计一个简单的事件驱动的GUI的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

我遇到的问题是,Button必须从其超类中覆盖其act()才能执行其动作,并且Buttons不是包含所有游戏代码的Screen类的成员.我将游戏中的每个按钮归为Button.仅我的主菜单就有一个ButtonPlay,ButtonMapDesigner,ButtonMute,ButtonQuit等.这将很快变得混乱,但是我想不出任何更好的方法来实现这一点,同时又为每个按钮保留了单独的act()方法. /p>

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

我认为如果/然后检查一次单击的坐标较大,并在必要时调用适当的操作即可.例如

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

但是,我需要能够即时添加/删除按钮.从游戏屏幕上单击一个单位时,需要出现一个移动单位的按钮,然后在实际移动单位时消失.使用HashMap时,只要单击或移动一个单元,我就可以在调用的代码中使用map.add("buttonMove", new ButtonMove())map.remove("buttonMove").使用if/else方法,我不需要为每个按钮设置单独的类,但是我需要跟踪游戏中用户在这一点上是否可以看到每个可点击区域是否可见和可点击.比我现在更头痛的事情.

解决方案

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()),带有其坐标,其纹理(用于绘制它)及其枚举值的Rectangle.

这是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)并检查按钮中是否包含该点.单击鼠标时,我将遍历所有按钮,如果按钮为isClicked,则调用act().

我做的第二种方法是相似的,但是使用了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并将其设置为在调用act()时调用的Button的Runnable.但是,可以使用lambda语法创建Runnable,因此您无需创建匿名的Runnable类,只需传入其执行的功能即可.

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

如果我的MainMenuScreen类中有一个beginGame()方法(扩展了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);
    }
}

I am creating a simple event-driven GUI for a video game I am making with LibGDX. It only needs to support buttons (rectangular) with a single act() function called when they are clicked. I would appreciate some advice on structuring because the solution I've thought of so far seems to be far from ideal.

My current implementation involves all buttons extending a Button class. Each button has a Rectangle with its bounds and an abstract act() method.

Each game screen (e.g. main menu, character select, pause menu, the in-game screen) has a HashMap of Buttons. When clicked, the game screen iterates through everything in the HashMap, and calls act() on any button that was clicked.

The problem I'm having is that Buttons have to have their act() overridden from their superclass in order to perform their action, and that the Buttons aren't a member of the Screen class which contains all the game code. I am subclassing Button for each button in the game. My main menu alone has a ButtonPlay, ButtonMapDesigner, ButtonMute, ButtonQuit, etc. This is going to get messy fast, but I can't think of any better way to do it while keeping a separate act() method for each button.

Since my mute button isn't a part of the main menu screen and can't access game logic, it's act() is nothing more than mainMenuScreen.mute();. So effectively, for every button in my game, I have to create a class class that does nothing more than <currentGameScreen>.doThisAction();, since the code to actually do stuff must be in the game screen class.

I considered having a big if/then to check the coordinates of each click and call the appropriate action if necessary. For example,

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

However, I need to be able to add/remove buttons on the fly. When a unit is clicked from the game screen, a button to move it needs to appear, and then disappear when the unit is actually moved. With a HashMap, I can just map.add("buttonMove", new ButtonMove()) and map.remove("buttonMove") in the code called when a unit is clicked or moved. With the if/else method, I won't need a separate class for every button, but I would need to keep track of whether each clickable area tested is visible and clickable by the user at this point in the game, which seems like an even bigger headache that what I have right now.

解决方案

Sneh's response reminded me of a fairly major oversight - instead of having to create a separate class for every button, I could use anonymous inner classes whenever I created a button, specifying its coordinates and act() method every time. I explored lambda syntax as a possible shorter method to do this, but ran into limitations with it. I ended up with a flexible solution, but ended up reducing it a bit further to fit my needs. Both ways are presented below.

Each game screen in my game is subclassed from a MyScreen class, which extends LibGDX's Screen but adds universal features like updating the viewport on resize, having a HashMap of Buttons, etc. I added to the MyScreen class a buttonPressed() method, which takes in as its one parameter an enum. I have ButtonValues enum which contains all the possible buttons (such as MAINMENU_PLAY, MAINMENU_MAPDESIGNER, etc.). In each game screen, buttonPressed() is overriden and a switch is used to perform the correct action:

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

The other solution has the button store a lambda expression so that it can perform actions on its own, instead of requiring buttonPressed() to act as an intermediary that performs the correct action based on what button was pressed.

To add a button, it is created with its coordinates and type (enum), and added to the HashMap of buttons:

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

To remove it, just buttons.remove("buttonPlay"). and it'll disappear from the screen and be forgotten by the game.

The arguments are the game screen which owns it (so the button can call buttonPressed() on the game screen), a Rectangle with its coordinates, its texture (used to draw it), and its enum value.

And here's the Button 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() just takes in an (x, y) and checks whether that point is contained within the button. On mouse click, I iterate through all the buttons and call act() if a button isClicked.

The second way I did it was similar, but with a lambda expression instead of the ButtonValues enum. The Button class is similar, but with these changes (it's a lot simpler than it sounds):

The field ButtonValues b is replaced with Runnable r, and this is removed from the constructor. Added is a setAction() method which takes in a Runnable and sets r to the Runnable passed to it. The act() method is just r.run(). Example:

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

To create a button, I do the following:

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

First, a button is created with its containing game screen class, its bounding box, and its texture. Then, in the second command, I set its action - in this case, b.screen.doSomething();. This can't be passed to the constructor, because b and b.screen don't exist at that point. setAction() takes a Runnable and sets it as that Button's Runnable that is called when act() is called. However, Runnables can be created with lambda syntax, so you don't need to create an anonymous Runnable class and can just pass in the function it performs.

This method allows much more flexibility, but with one caveat. The screen field in Button holds a MyScreen, the base screen class from which all of my game screens are extended. The Button's function can only use methods that are part of the MyScreen class (which is why I made buttonPressed() in MyScreen and then realized I could just scrap the lambda expressions completely). The obvious solution is to cast the screen field, but for me it wasn't worth the extra code when I could just use the buttonPressed() method.

If I had a beginGame() method in my MainMenuScreen class (which extends MyScreen), the lambda expression passed to the button would need to involve a cast to MainMenuScreen:

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

Unfortunately, even wildcard syntax doesn't help here.

And finally, for completeness, the code in the game loop to operate the buttons:

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

And to draw them, located in a helper 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);
    }
}

这篇关于设计一个简单的事件驱动的GUI的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆