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