8
\$\begingroup\$

I created a side bar of buttons for my game, the idea being that if you click on the "main" button of the group, the rest of the buttons in the group would pop out from the side of the screen.

Later, I wanted to make it so that if you open one of the side menus the other ones would close. I thought about implementing some booleans and methods that would iterate through the tables that comprised the side bar, closing them if they were open and were not the table associated with the button that the player clicked. Instead I made something a little more object oriented.

One complication here was that I needed some special logic to be called when only one of these buttons was clicked, specifically the Overlay button. When closed, I wanted any open color overlays to be automatically switched off, so that the player doesn't need to open up the menu again to switch it off.

Here are the buttons in action: enter image description here

SideButtonSlider.java

public class SideButtonSlider {

    private Table table;
    //private float stageWidth;
    private float stageHeight;
    private GameScreen screen;
    private boolean isOpen;

    public SideButtonSlider(ArrayList<TextButton>buttons, GameScreen screen, Skin skin, String name, Color color, float stageWidth, float stageHeight) {
        this.table = new Table(skin);
        //this.stageWidth = stageWidth;
        this.stageHeight = stageHeight;
        this.screen = screen;

        Table buttonTable = new Table();
        buttonTable.defaults().pad(5).width(stageHeight/9).height(stageHeight/18).fill();
        buttonTable.setFillParent(true);
        for (Button button : buttons) {
            buttonTable.add(button);
            buttonTable.row();
        }
        this.table.add(buttonTable);

        TextButton toggleButton = new TextButton(name, skin);
        toggleButton.setColor(color);
        toggleButton.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                SideButtonSlider.this.toggle();
            }
        });

        Table toggleButtonTable = new Table(skin);
        toggleButtonTable.defaults().pad(5).width(stageHeight/9).height(stageHeight/18).fill();
        toggleButtonTable.setFillParent(true);
        toggleButtonTable.add(toggleButton);
        this.table.add(toggleButtonTable);

        this.close();
    }

    public boolean isOpen() {
        return this.isOpen;
    }

    public void toggle() {
        if (this.isOpen) {
            this.close();
            this.screen.sliderClosed(this);
        } else {
            this.open();
            this.screen.closeOpenSlidersExcept(this);
        }
    }

    public void show() {
        this.table.setVisible(true);
    }

    public void hide() {
        this.table.setVisible(false);
    }

    public Table getTable() {
        return this.table;
    }

    private void open() {
        this.isOpen = true;
        this.table.addAction(Actions.moveBy(this.stageHeight/7.8f, 0, 0.4f));
    }

    private void close() {
        this.isOpen = false;
        this.table.addAction(Actions.moveBy(-this.stageHeight/7.8f, 0, 0.4f));
    }
}

Here is the usage in the GameScene class:

public void createSideButtons() {
    TextButton desireOverlayButton = new TextButton("Desire", this.skin);
    desireOverlayButton.setColor(Color.BLUE);
    desireOverlayButton.addListener(new ClickListener() {
        @Override
        public void clicked(InputEvent event, float x, float y) {
            GameScreen.this.toggleDesireOverlay();
        }
    });
    TextButton populationOverlayButton = new TextButton("Pop", this.skin);
    populationOverlayButton.setColor(Color.BLUE);
    populationOverlayButton.addListener(new ClickListener() {
        @Override
        public void clicked(InputEvent event, float x, float y) {
            GameScreen.this.togglePopulationOverlay();
        }
    });
    TextButton waterOverlayButton = new TextButton("Water", this.skin);
    waterOverlayButton.setColor(Color.BLUE);
    waterOverlayButton.addListener(new ClickListener() {
        @Override
        public void clicked(InputEvent event, float x, float y) {
            GameScreen.this.toggleWaterOverlay();
        }
    });
    ArrayList<TextButton>overlayButtons = new ArrayList<TextButton>();
    overlayButtons.add(waterOverlayButton);
    overlayButtons.add(populationOverlayButton);
    overlayButtons.add(desireOverlayButton);

    TextButton servicesButton = new TextButton("Service", this.skin);
    servicesButton.setColor(Color.GREEN);
    servicesButton.addListener(new ClickListener() {
        @Override
        public void clicked(InputEvent event, float x, float y) {
            GameScreen.this.buildServicesMenu();
        }
    });
    TextButton budgetButton = new TextButton("Budget", this.skin);
    budgetButton.setColor(Color.GREEN);
    budgetButton.addListener(new ClickListener() {
        @Override
        public void clicked(InputEvent event, float x, float y) {
            GameScreen.this.buildBudgetMenu();
        }
    });
    TextButton mayorButton = new TextButton("Mayor", skin);
    mayorButton.setColor(Color.GREEN);
    mayorButton.addListener(new ClickListener() {
        @Override
        public void clicked(InputEvent event, float x, float y) {
            GameScreen.this.clickedMayorButton();
        }
    });    
    ArrayList<TextButton>moneyButtons = new ArrayList<TextButton>();
    moneyButtons.add(budgetButton);
    moneyButtons.add(servicesButton);
    moneyButtons.add(mayorButton);

    TextButton clippingButton = new TextButton("Clip", this.skin);
    clippingButton.setColor(Color.YELLOW);
    clippingButton.addListener(new ClickListener() {
        @Override
        public void clicked(InputEvent event, float x, float y) {
            GameScreen.this.toggleClipping();
        }
    });
    TextButton zoomButton = new TextButton("Zoom", this.skin);
    zoomButton.setColor(Color.YELLOW);
    zoomButton.addListener(new ClickListener() {
        @Override
        public void clicked(InputEvent event, float x, float y) {
            GameScreen.this.toggleZoom();
        }
    });
    ArrayList<TextButton>cameraButtons = new ArrayList<TextButton>();
    cameraButtons.add(clippingButton);
    cameraButtons.add(zoomButton);

    TextButton pauseButton = new TextButton("Pause", this.skin);
    pauseButton.setColor(Color.RED);
    pauseButton.addListener(new ClickListener() {
        @Override
        public void clicked(InputEvent event, float x, float y) {
            GameScreen.this.togglePause();
        }
    });
    TextButton randomizeButton = new TextButton("Random", this.skin);
    randomizeButton.setColor(Color.RED);
    randomizeButton.addListener(new ClickListener() {
        @Override
        public void clicked(InputEvent event, float x, float y) {
            GameScreen.this.game.world.randomlyUpdateWorld();
        }
    });
    TextButton menuButton = new TextButton("Menu", this.skin);
    menuButton.setColor(Color.RED);
    menuButton.addListener(new ClickListener() {
        @Override
        public void clicked(InputEvent event, float x, float y) {
            GameScreen.this.buildInGameMenu();
        }
    });
    ArrayList<TextButton>gameButtons = new ArrayList<TextButton>();
    gameButtons.add(pauseButton);
    gameButtons.add(menuButton);
    gameButtons.add(randomizeButton);

    this.buttonTable = new Table(this.skin);
    this.buttonTable.setPosition(-LibGDXGame.STAGE_HEIGHT/14, 0);
    this.buttonTable.setFillParent(true);
    this.buttonTable.left();

    this.overlaySlider = new SideButtonSlider(overlayButtons, this, this.skin, "Overlay", Color.BLUE, LibGDXGame.STAGE_WIDTH, LibGDXGame.STAGE_HEIGHT);
    this.buttonTable.add(this.overlaySlider.getTable());
    this.buttonTable.row();

    this.moneySlider = new SideButtonSlider(moneyButtons, this, this.skin, "Money", Color.GREEN, LibGDXGame.STAGE_WIDTH, LibGDXGame.STAGE_HEIGHT);
    this.buttonTable.add(this.moneySlider.getTable());
    this.buttonTable.row();

    this.cameraSlider = new SideButtonSlider(cameraButtons, this, this.skin, "Camera", Color.YELLOW, LibGDXGame.STAGE_WIDTH, LibGDXGame.STAGE_HEIGHT);
    this.buttonTable.add(this.cameraSlider.getTable());
    this.buttonTable.row();

    this.gameSlider = new SideButtonSlider(gameButtons, this, this.skin, "Game", Color.RED, LibGDXGame.STAGE_WIDTH, LibGDXGame.STAGE_HEIGHT);
    this.buttonTable.add(this.gameSlider.getTable());

    this.libGDXGame.hudStage.addActor(this.buttonTable);
}

And finally, the methods that control the special logic:

public void closeOpenSlidersExcept(SideButtonSlider slider) {
    if (slider != this.overlaySlider) {
        if (this.overlaySlider.isOpen()) {
            this.overlaySlider.toggle();
        }
    }
    if (slider != this.moneySlider) {
        if (this.moneySlider.isOpen()) {
            this.moneySlider.toggle();
        }
    }
    if (slider != this.cameraSlider) {
        if (this.cameraSlider.isOpen()) {
            this.cameraSlider.toggle();
        }
    }
    if (slider != this.gameSlider) {
        if (this.gameSlider.isOpen()) {
            this.gameSlider.toggle();
        }
    }
}

public void sliderClosed(SideButtonSlider slider) {
    if (slider == this.overlaySlider) {
        this.libGDXGame.disableOverlays();
        this.uncolorAllTileActors();
    }
}

I know that I could separate all of this logic out into another class, a SideBar class perhaps, but all of these buttons call methods in the GameScreen class so that will require a lot of work. Not sure if it is worth it.

I don't like that all of the buttons need to be created before this whole structure is put together, but again, all of the callbacks are pointing to private methods in the GameScreen class so this seemed like a reasonable way to do things.

All feedback is appreciated, as always.

\$\endgroup\$
  • \$\begingroup\$ Is Java 8 an option here, or is there a specific version (6/7) that you are targeting? \$\endgroup\$ – h.j.k. Jul 10 '15 at 16:16
  • 2
    \$\begingroup\$ Unfortunately this is cross platform stuff so I am limited to Java 6. \$\endgroup\$ – bazola Jul 10 '15 at 16:20
5
\$\begingroup\$

You can literally use the builder pattern for your Builder game here. :)

See, you have group of buttons that must be assigned a same color. These buttons have a corresponding pair of button label and a ClickListener implementation. Therefore, it's not hard to imagine a Builder implementation that lets you chain these operations:

public final class SideButtonSliderBuilder {

    private final GameScreen screen;
    private final Skin skin;
    private final String groupLabel;
    private final Color color;
    private final List<String> labels = new ArrayList<String>();
    private final List<ClickListener> listeners = new ArrayList<String>();

    private SideButtonSliderBuilder(GameScreen screen, Skin skin, 
                    String groupLabel, Color color) {
        this.screen = screen;
        this.skin = skin;
        this.groupLabel = groupLabel;
        this.color = color;
    }

    public SideButtonSliderBuilder addButton(String label, ClickListener listener) {
        // remember null checks too, just in case
        labels.add(label);
        listeners.add(listener);
        return this;
    }

    private TextButton toButton(String label, ClickListener listener) {
        // since this is a private method, null checks are probably optional
        TextButton button = new TextButton(label, skin);
        button.setColor(color);
        button.addListener(listener);
        return button;
    }

    private List<TextButton> toList() {
        List<TextButton> list = new ArrayList<TextButton>(labels.size());
        for (Iterator<String> labelIterator = labels.iterator(),
                    Iterator<ClickListener> listenerIterator = listeners.iterator();
                labelIterator.hasNext() && listenerIterator().hasNext();) {
            list.add(toButton(labelIterator.next(), listenrIterator.next()));
        }
        return list;
    }

    public SideButtonSlider build() {
        // take note of List vs ArrayList, see below
        return new SideButtonSlider(toList(), screen, skin, groupLabel, color, 
                        LibGDXGame.STAGE_WIDTH, LibGDXGame.STAGE_HEIGHT);
    }

    public static SideButtonSliderBuilder of(GameScreen screen, Skin skin,
                    String groupLabel, Color color) {
        return new SideButtonSliderBuilder(screen, skin, groupLabel, color);
    }
}

In my opinion, the benefits of the builder pattern is that it lets you specify any common parameters once to minimize code repetition (and therefore bugs), and enforces the requirements for creating the necessary instances consistently too. In the long run, it also lets you introduce new features or fix bugs all in one go. Furthermore, builder classes tend to adopt the 'fluent' programming approach by using more descriptive sounding method names and return-ing itself, thereby allowing for daisy-chaining.

An example usage will be:

// inside your GameScene class
SideButtonSliderBuilder cameraBuilder = SideButtonSliderBuilder.of(this, this.skin, 
                                                            "Camera", Color.YELLOW);
cameraBuilder.addButton("Clip", /* clip listener */)
                .addButton("Zoom", /* zoom listener */);
this.cameraSlider = cameraBuilder.build();

You can even opt to daisy-chain the entire thing, from of() to calling build().

Just two more things to highlight from my code above:

// since this is a private method, null checks are probably optional

My personal opinion is that private methods can be less... particular about null inputs, as you should be trusting enough of yourself/your team that you aren't introducing them in the first place. This is more true, I hope, when the method calling the private method is right next to it, such that there is even lesser concern that nulls will be introduced.

// take note of List vs ArrayList, see below

Declarations should be done with the interfaces, rather than the concrete implementation. This lets users of your variables simply use them as they are defined by the interface methods, rather than the exact type of implementing class. Therefore, I will suggest List<String> list = new ArrayList<String>().

\$\endgroup\$
3
\$\begingroup\$

A rather general advice....

public void closeOpenSlidersExcept(SideButtonSlider slider) {
    if (slider != this.overlaySlider) {
        if (this.overlaySlider.isOpen()) {
            this.overlaySlider.toggle();
        }
    }
    if (slider != this.moneySlider) {
        if (this.moneySlider.isOpen()) {
            this.moneySlider.toggle();
        }
    }
    ...

STOP! Whenever you repeat a few lines twice and see it's going to get more, don't continue writing and start thinking instead.

So you want to close things, can you write a close method? If so, go for it. If no, why not?

Indeed you can't as you already did it. It's just missing a test like

private void close() {
    if (this.isOpen) {
        this.isOpen = false;
        this.table.addAction(Actions.moveBy(-this.stageHeight/7.8f, 0, 0.4f));
    }
}

Now we have

public void closeOpenSlidersExcept(SideButtonSlider slider) {
    if (slider != this.overlaySlider) {
        this.overlaySlider.close();
    }
    if (slider != this.moneySlider) {
        this.moneySlider.close();
    }
    ...

Repeating this two more times could be acceptable, but we can do better. All the code blocks differ by a single thing, so a trivial loop would do.

public void closeOpenSlidersExcept(SideButtonSlider slider) {
    SideButtonSlider[] sliders = {overlaySlider, moneySlider, cameraSlider, gameSlider};
    for (SideButtonSlider s : sliders) {
        if (s != slider) {
            s.close();
        }
    }
}

You may need the sliders array in other places as well, so define it as a private final member and try to reuse it. For example, instead of repeating

this.buttonTable.add(this.overlaySlider.getTable());
this.buttonTable.row();

four times, you can use a loop.


You surely know, you don't have to use the this qualifier everywhere, it's implied.


// remember null checks too, just in case

Just add them. Use Guava's Preconditions or write your own and use a static import, so all you need is to write

    labels.add(checkNotNull(label));
    listeners.add(checkNotNull(listener));

instead of

    // remember null checks too, just in case
    labels.add(label);
    listeners.add(listener);

The reason for null checks is not to make the code null-tolerant. On the opposite, you want to get an exception ASAP. Otherwise, your evil nulls may get stored and used later and you spend time hunting bugs. In case you must tolerate nulls, replace them ASAP by a proper default value, e.g., using

    labels.add(firstNonNull(label, "no label"));

but I can't recall when I used it last time. Simply banning nulls from (nearly) everywhere makes them to a non-problem.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.