Building the scene for the menu screen

We will now begin with the actual implementation of the scene for the menu screen. First, take a look at the following diagram that shows the hierarchy of the UI scene graph that we are going to build step-by-step:

Building the scene for the menu screen

The scene graph starts with an empty Stage. Then, the first child actor added to the stage is a Stack widget. The Stack widget allows you to add actors that can overlay other actors. We will make use of this ability to create several layers. Each layer uses a Table widget as its parent actor. Using stacked tables enables us to lay out actors in an easy and logical way.

In the first step, we will add the basic structure of our stacked layers and some skeleton methods, which we are going to fill in the subsequent steps.

Add the following import lines to MenuScreen:

import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.Button;
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle;
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Slider;
import com.badlogic.gdx.scenes.scene2d.ui.Stack;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.Window;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.packtpub.libgdx.canyonbunny.game.Assets;
import com.packtpub.libgdx.canyonbunny.util.Constants;

After this, add the following lines of code to the same class:

  private Stage stage;
  private Skin skinCanyonBunny;

  // menu
  private Image imgBackground;
  private Image imgLogo;
  private Image imgInfo;
  private Image imgCoins;
  private Image imgBunny;
  private Button btnMenuPlay;
  private Button btnMenuOptions;

  // options
  private Window winOptions;
  private TextButton btnWinOptSave;
  private TextButton btnWinOptCancel;
  private CheckBox chkSound;
  private Slider sldSound;
  private CheckBox chkMusic;
  private Slider sldMusic;
  private SelectBox<CharacterSkin> selCharSkin;
  private Image imgCharSkin;
  private CheckBox chkShowFpsCounter;

  // debug
  private final float DEBUG_REBUILD_INTERVAL = 5.0f;
  private boolean debugEnabled = false;
  private float debugRebuildStage;

We added new variables to store an instance of Stage called stage, an instance of Skin called skinCanyonBunny, and some more variables for the widgets of the menu screen and the Options window.

Next, add the following code to the same class:

  private void rebuildStage () {
    skinCanyonBunny = new Skin(
        Gdx.files.internal(Constants.SKIN_CANYONBUNNY_UI),
        new TextureAtlas(Constants.TEXTURE_ATLAS_UI));

    // build all layers
    Table layerBackground = buildBackgroundLayer();
    Table layerObjects = buildObjectsLayer();
    Table layerLogos = buildLogosLayer();
    Table layerControls = buildControlsLayer();
    Table layerOptionsWindow = buildOptionsWindowLayer();

    // assemble stage for menu screen
    stage.clear();
    Stack stack = new Stack();
    stage.addActor(stack);
    stack.setSize(Constants.VIEWPORT_GUI_WIDTH, Constants.VIEWPORT_GUI_HEIGHT);
    stack.add(layerBackground);
    stack.add(layerObjects);
    stack.add(layerLogos);
    stack.add(layerControls);
    stage.addActor(layerOptionsWindow);
  }

In rebuildStage(), we build everything that will make up the final scene of our menu screen. This method is implemented in a way so that it can be called in a repeated manner, hence the name rebuildStage. While we are implementing each of the layers, you might want to try and modify the code in each step to get a better understanding of how TableLayout behaves in certain situations.

Next, add the following code to the same class:

  private Table buildBackgroundLayer () {
    Table layer = new Table();
    return layer;
  }
  private Table buildObjectsLayer () {
    Table layer = new Table();
    return layer;
  }
  private Table buildLogosLayer () {
    Table layer = new Table();
    return layer;
  }
  private Table buildControlsLayer () {
    Table layer = new Table();
    return layer;
  }
  private Table buildOptionsWindowLayer () {
    Table layer = new Table();
    return layer;
  }

We have added five new methods that contain dummy implementations for now. These will be used to build each layer of the menu.

Next, make the following changes to the same class:

  @Override
  public void resize (int width, int height) {
	stage.getViewport().update(width, height, true);
  }

  @Override
  public void hide () {
    stage.dispose();
    skinCanyonBunny.dispose();
  }

  @Override
  public void show () {
stage = new Stage(new StretchViewport(Constants.VIEWPORT_GUI_WIDTH, Constants.VIEWPORT_GUI_HEIGHT));
   Gdx.input.setInputProcessor(stage);
    rebuildStage();
  }

The show() method is called when the screen is shown. It initializes the stage, sets it as LibGDX's current input processor so that the stage will receive all the future inputs, and finally, the stage is rebuilt by calling rebuildStage(). The hide() method will free the allocated resources when the screen is hidden. The resize() method sets the viewport size of the stage.

Lastly, make the following changes to the render() method in the same class:

  @Override
  public void render (float deltaTime) {
    Gdx.gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
   Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

    if (debugEnabled) {
      debugRebuildStage -= deltaTime;
      if (debugRebuildStage <= 0) {
        debugRebuildStage = DEBUG_REBUILD_INTERVAL;
        rebuildStage();
      }
    }
    stage.act(deltaTime);
    stage.draw();
    Table.drawDebug(stage);
  }

The code in render() that would switch to the game screen when the screen is touched was replaced by calls to update and render the stage. The statement Table.drawDebug() is a debugging feature of TableLayout, which enables you to draw debug visuals in a scene. Additionally, you need to specify which Table widgets should draw debug lines by calling their debug() method.

Note

This hint might sound trivial, but this is such a big time saver that it is definitely worth pointing it out.

The code you just added to MenuScreen contains some additional debug code that will call rebuildStage() in periodic intervals defined in seconds by DEBUG_REBUILD_INTERVAL. You can enable periodic refreshing by simply setting debugEnabled to true. This will give you live updates at runtime, assuming that you are running the game in debug mode on the desktop, and will allow you to take advantage of JVM's awesome Code Hot Swapping feature.

Adding the background layer

Make the following changes in MenuScreen to add the background layer:

  private Table buildBackgroundLayer () {
    Table layer = new Table();
    // + Background
    imgBackground = new Image(skinCanyonBunny, "background");
    layer.add(imgBackground);
    return layer;
  }

There will now be a background image drawn to the scene of the menu screen. The image is referenced using the background name that we defined earlier in our skin file (canyonbunny-ui.json). If you change the size of the screen, the stage will adjust accordingly along with the background layer and its Image widget.

Adding the objects layer

Make the following changes in MenuScreen to add the objects layer:

  private Table buildObjectsLayer () {
    Table layer = new Table();
    // + Coins
    imgCoins = new Image(skinCanyonBunny, "coins");
    layer.addActor(imgCoins);
    imgCoins.setPosition(135, 80);
    // + Bunny
    imgBunny = new Image(skinCanyonBunny, "bunny");
    layer.addActor(imgBunny);
    imgBunny.setPosition(355, 40);
    return layer;
  }

There will now be an image of some coins and another image of a huge bunny head, which are both drawn on top of the background layer. The positions of each actor are explicitly set to certain coordinates by calling setPosition() on each actor.

Adding the logos layer

Make the following changes in MenuScreen to add the logos layer:

private Table buildLogosLayer () {
  Table layer = new Table();
  layer.left().top();
  // + Game Logo
  imgLogo = new Image(skinCanyonBunny, "logo");
  layer.add(imgLogo);
  layer.row().expandY();
  // + Info Logos
  imgInfo = new Image(skinCanyonBunny, "info");
  layer.add(imgInfo).bottom();
  if (debugEnabled) layer.debug();
  return layer;
}

The logos layer is anchored in the top-left corner of the screen. After this, an image logo is added to the table followed by a call of the row() and expandY() methods. Every time you call add() on a Table widget, TableLayout will add a new column, which means the widget grows in a horizontal direction. So, if you want to start a new row, you can tell TableLayout about this by calling row(). The expandY() method expands the empty space in a vertical direction. The expansion is done by shifting the widgets to the bounds of the cell.

After this, more image information is added to the table, which is literally pushed down to the bottom edge due to the call of expandY().

Lastly, there is a call to layer.debug(), which is the way to tell TableLayout the object it should draw debug visuals for.

Adding the controls layer

Make the following changes in MenuScreen to add the controls layer:

private Table buildControlsLayer () {
  Table layer = new Table();
  layer.right().bottom();
  // + Play Button
  btnMenuPlay = new Button(skinCanyonBunny, "play");
  layer.add(btnMenuPlay);
  btnMenuPlay.addListener(new ChangeListener() {
    @Override
    public void changed (ChangeEvent event, Actor actor) {
      onPlayClicked();
    }
  });
  layer.row();
  // + Options Button
  btnMenuOptions = new Button(skinCanyonBunny, "options");
  layer.add(btnMenuOptions);
  btnMenuOptions.addListener(new ChangeListener() {
    @Override
    public void changed (ChangeEvent event, Actor actor) {
      onOptionsClicked();
    }
  });
  if (debugEnabled) layer.debug();
  return layer;
}

After this, add the following lines of code to the same class:

private void onPlayClicked () {
  game.setScreen(new GameScreen(game));
}

private void onOptionsClicked () { }

The controls layer is anchored in the bottom-right corner of the screen. A new button widget is added using the Play style. Next, a new ChangeListener is added to this button to define the action to be executed when the button is clicked on.

Note

We are using ChangeListener to register new handlers for our button widgets. This is the recommended way of implementing handlers for widgets as most of them will fire ChangeEvent when changes occur. We could also use ClickListener to accomplish the detection of clicks on button widgets, but doing so has a major drawback. The ClickListener method reacts on the input events received by a widget, but does not know anything about widgets and their properties. Therefore, if a widget is set to be disabled, clicking on events will still be detected and handled by the listener.

After this, a new row is started in which the second button widget is added using the Options style. Each event handler calls a separate method to make it easier for us to maintain the code of the layer and the code to handle events. The onPlayClicked() method will switch to the game screen, while the onOptionsClicked() method is intentionally left empty for the moment.

Adding the Options window layer

The Options window layer is going to be a lot more complex in comparison to all the other layers that we have implemented so far. There are also some further preparations required before we can continue to implement this layer.

Here is a screenshot to give you a better idea of how the finished Options window will look:

Adding the Options window layer

The Options window will be a small box that shows a title bar with text on it and has some empty space to hold additional widgets. It can be dragged on the title bar to move it around in the scene. There will be a checkbox to enable and disable the Sound and Music effects as well as a slider to adjust the volume, respectively. A character skin can be chosen from a drop-down list. The current selection is shown next to it in a small preview image. The preview image is updated whenever a new selection in the drop-down list has been made to reflect the change. Lastly, there is another checkbox that toggles to check whether the FPS counter will be displayed on the game screen.

The menu controls, that is, the Play and Options buttons, will disappear when the Options window is shown. To close the Options window, the player has to choose between saving and canceling any changes that have been made in the window. When the Options window is hidden, the menu controls will appear again.

Usually, we would have to draw all of these textures that you see in the preceding screenshot to use all of the shown widgets. Luckily, we can take a shortcut here by taking a texture atlas, a suitable skin file, and a font definition file for the text to be displayed in these widgets from LibGDX's test repository. It contains much more than we need, so you might also want to take a closer look at the skin file to find out how certain widgets need to be defined that are not covered here.

You will need the following files from LibGDX's test repository to put them into CanyonBunny-android/assets/images/:

  • uiskin.png
  • uiskin.atlas
  • uiskin.json
  • default.fnt

Alternatively, you can download these files from the code bundle of Chapter 7, Menus and Options, and place them at CanyonBunny-android/assets/images/.

This is how the image of the file uiskin.png should look:

Adding the Options window layer

There are two more actions that we will do in advance before we go back to implementing the actual layer for the Options menu.

The first action is to create a new class that abstracts the process of loading and saving all of our game settings.

Create a new file called GamePreferences and add the following lines of code:

package com.packtpub.libgdx.canyonbunny.util;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Preferences;
import com.badlogic.gdx.math.MathUtils;

public class GamePreferences {
  public static final String TAG = GamePreferences.class.getName();

  public static final GamePreferences instance = new GamePreferences();

  public boolean sound;
  public boolean music;
  public float volSound;
  public float volMusic;
  public int charSkin;
  public boolean showFpsCounter;

  private Preferences prefs;

  // singleton: prevent instantiation from other classes
  private GamePreferences () {
    prefs = Gdx.app.getPreferences(Constants.PREFERENCES);
  }

  public void load () { }
  public void save () { }
}

This class is implemented as a singleton so we can call its load() and save() methods from virtually anywhere inside our project. The settings will be loaded from and saved to a preferences file defined in Constants.PREFERENCES.

Next, add the following code to the load() method of the same class:

public void load () {
  sound = prefs.getBoolean("sound", true);
  music = prefs.getBoolean("music", true);
  volSound = MathUtils.clamp(prefs.getFloat("volSound", 0.5f),0.0f, 1.0f);
  volMusic = MathUtils.clamp(prefs.getFloat("volMusic", 0.5f),0.0f, 1.0f);
  charSkin = MathUtils.clamp(prefs.getInteger("charSkin", 0),0, 2);
  showFpsCounter = prefs.getBoolean("showFpsCounter", false);
}

The load() method will always try its best to find a suitable and, more importantly, valid value. This is achieved by supplying default values to the getter methods of the Preferences class. For example, the call getFloat("volSound", 0.5f) will return a value of 0.5f if there is no value found for the key named volSound. Before the value of the sound volume is finally stored, it is also passed to the clamp() utility method to ensure that the value is within the allowed range of values, which is 0.0f and 1.0f here.

Next, add the following code to the save() method of the same class:

public void save () {
  prefs.putBoolean("sound", sound);
  prefs.putBoolean("music", music);
  prefs.putFloat("volSound", volSound);
  prefs.putFloat("volMusic", volMusic);
  prefs.putInteger("charSkin", charSkin);
  prefs.putBoolean("showFpsCounter", showFpsCounter);
  prefs.flush();
}

The save() method is pretty straightforward as it just takes the current values of its public variables and puts them into the map of the preferences file. Finally, flush() is called on the preferences file to actually write the changed values into the file.

The second action is to create another class that abstracts all selectable character skins. Create a new file called CharacterSkin and add the following lines of code:

package com.packtpub.libgdx.canyonbunny.util;

import com.badlogic.gdx.graphics.Color;

public enum CharacterSkin {
  WHITE("White", 1.0f, 1.0f, 1.0f),
  GRAY("Gray", 0.7f, 0.7f, 0.7f),
  BROWN("Brown", 0.7f, 0.5f, 0.3f);

  private String name;
  private Color color = new Color();
  private CharacterSkin (String name, float r, float g, float b) {
    this.name = name;
    color.set(r, g, b, 1.0f);
  }

  @Override
  public String toString () {
    return name;
  }

  public Color getColor () {
    return color;
  }
}

This class contains three distinct character skins, namely, White, Gray, and Brown. All character skins are defined using a name that is used for display and RGB color values to describe a color that will be used to tint the image of the character player.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset