Разработка простого графического интерфейса, управляемого событиями

Я создаю простой графический интерфейс, управляемый событиями, для видеоигры, которую я делаю с помощью LibGDX. Ему нужно только поддерживать кнопки (прямоугольные) с одной функцией 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 мне не потребуется отдельный класс для каждой кнопки, но мне нужно будет отслеживать, видна ли каждая тестируемая область, на которую можно щелкнуть, и может ли пользователь щелкнуть ее на этом этапе игры, что выглядит как еще большая головная боль, чем та, что у меня сейчас.


person the_pwner224    schedule 03.05.2017    source источник
comment
Посмотрите на Scene2D, он предназначен для создания графического интерфейса. Используйте его или скопируйте концепцию для своей игры.   -  person TomGrill Games    schedule 03.05.2017


Ответы (2)


Я бы предоставил возможность запуска для всех кнопок, которые вы будете запускать в методе действия. Чтобы дать вам простой пример.

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

Вы отслеживаете свои кнопки через карту, и вам просто нужно передать выполняемое действие каждой кнопке в конструкторе. Я намеренно пропустил часть кода, чтобы вы могли попробовать сами. Если у вас есть какие-либо вопросы, дайте мне знать.

person Sneh    schedule 03.05.2017

Ответ Снеха напомнил мне о довольно серьезном упущении — вместо того, чтобы создавать отдельный класс для каждой кнопки, я мог использовать анонимные внутренние классы всякий раз, когда создавал кнопку, каждый раз указывая ее координаты и метод act(). Я исследовал лямбда-синтаксис как возможный более короткий способ сделать это, но столкнулся с ограничениями. В итоге я получил гибкое решение, но в итоге немного сократил его, чтобы оно соответствовало моим потребностям. Оба способа представлены ниже.

Каждый игровой экран в моей игре является подклассом класса MyScreen, который расширяет класс Screen LibGDX, но добавляет универсальные функции, такие как обновление области просмотра при изменении размера, наличие HashMap кнопок и т. д. Я добавил в класс MyScreen метод buttonPressed(), который принимает как его один параметр - перечисление. У меня есть перечисление ButtonValues, которое содержит все возможные кнопки (например, MAINMENU_PLAY, MAINMENU_MAPDESIGNER и т. д.). На каждом игровом экране buttonPressed() переопределяется, и для выполнения правильного действия используется переключатель:

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

В другом решении кнопка хранит лямбда-выражение, чтобы она могла выполнять действия самостоятельно, вместо того, чтобы требовать, чтобы buttonPressed() выступал в качестве посредника, выполняющего правильное действие в зависимости от того, какая кнопка была нажата.

Чтобы добавить кнопку, она создается со своими координатами и типом (enum) и добавляется в 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() на игровом экране), прямоугольник с его координатами, его текстура (используемая для его рисования) и его значение перечисления.

А вот и класс 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.

Второй способ, который я сделал, был похож, но с лямбда-выражением вместо перечисления 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 и устанавливает его как Runnable Button, который вызывается при вызове act(). Однако Runnables можно создать с помощью лямбда-синтаксиса, поэтому вам не нужно создавать анонимный класс Runnable, и вы можете просто передать функцию, которую он выполняет.

Этот метод обеспечивает гораздо большую гибкость, но с одной оговоркой. Поле screen в Button содержит MyScreen, базовый класс экрана, из которого расширены все мои игровые экраны. Функция Button может использовать только методы, которые являются частью класса MyScreen (именно поэтому я сделал buttonPressed() в MyScreen, а затем понял, что могу просто полностью отказаться от лямбда-выражений). Очевидным решением является преобразование поля screen, но для меня это не стоило дополнительного кода, когда я мог просто использовать метод buttonPressed().

Если бы у меня был метод beginGame() в моем классе MainMenuScreen (который расширяет MyScreen), лямбда-выражение, передаваемое кнопке, должно было бы включать приведение к 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);
    }
}
person the_pwner224    schedule 04.05.2017