Book Image

Libgdx Cross-platform Game Development Cookbook

Book Image

Libgdx Cross-platform Game Development Cookbook

Overview of this book

Table of Contents (20 chapters)
Libgdx Cross-platform Game Development Cookbook
Credits
About the Authors
About the Reviewers
www.PacktPub.com
Preface
Index

Understanding the project structure and application life cycle


Throughout this recipe, we will examine the typical project architecture of Libgdx and how it makes cross-platform development a much less cumbersome quest. We will also learn how to configure platform-specific launchers in order to tweak parameters such as resolution, colors, the OpenGL version, and so on. More importantly, we will go through the Libgdx application life cycle. This is the heart of any game you will ever make using our beloved framework, and therefore, one can imagine it is something worth getting acquainted with.

Getting ready

With the goal of illustrating the contents of this recipe, we will use the same environment test application we used in the Setting up a cross-platform development environment recipe to test that our Libgdx installation is working as expected. Fire up Eclipse and make sure you select your libgdx-cookbook workspace. Now, check you have the projects that compose the test application already available. If not, import the projects under [cookbook]/environment through Gradle, as shown in the previous recipe.

How to do it…

As we already mentioned before, Libgdx applications are typically split in several projects: core, desktop, Android, iOS, and HTML. The platform-specific projects serve as the application's entry points on each platform; their duty basically boils down to invoking the core project's main class and passing in the basic configuration parameters for the game to run.

Note

Imagine you were to target Android exclusively, you could probably get away with one single project containing both the platform-agnostic and Android-specific code. However, this is a bad practice and should be avoided. What happens if you decide to port your game to a different platform later on? No one would like to refactor the project structure to accommodate it to the new circumstances. Regardless of the platform and devices you work with, it is always preferable to keep the two categories as isolated as possible.

Using logging to get to know the application life cycle

Every Libgdx application has a very well-defined lifecycle controlling the states it can be in at a given time. These states are: creation, pausing, resuming, rendering, and disposing. The lifecycle is modeled by the ApplicationListener interface, which we are required to implement as it will serve as the entrance to our game logic. In our recipe's example, the EnvironmentTest class in the core project carries out such roles.

Meet the ApplicationListener interface:

public interface ApplicationListener {
   public void create ();
   public void resize (int width, int height);
   public void render ();
   public void pause ();
   public void resume ();
   public void dispose ();
}

Your ApplicationListener interface implementation can handle each one of these events in the way it deems convenient. Here are the typical usages:

  • create(): This is used to initialize subsystems and load resources.

  • resize(): This is used to handle setting a new screen size, which can be used to reposition UI elements or reconfigure camera objects.

  • render(): This is used to update and render the game elements. Note that there is no update() method as render() is supposed to carry out both tasks.

  • pause(): This is the save game state when it loses focus, which does not involve the actual gameplay being paused unless the developer wants it to.

  • resume(): This is used to handle the game coming back from being paused and restores the game state.

  • dispose(): This is used to free resources and clean up.

When do each of these methods get called? Well, that's a really good question! Before we start looking at cryptic diagrams, it is much better to investigate and find out for ourselves. Shall we? We will simply add some logging to know exactly how the flow works.

Take a look at the EnvironmentTest.java file:

public class EnvironmentTest implements ApplicationListener {
   private Logger logger;
   private boolean renderInterrupted = true;
   
   @Override
   public void create() {
      logger = new Logger("Application lifecycle", Logger.INFO);
      logger.info("create");
   }

   @Override
   public void dispose() {
      logger.info("dispose");
   }

   @Override
   public void render() {
      if (renderInterrupted) {
         logger.info("render");
         renderInterrupted = false;
      }
   }

   @Override
   public void resize(int width, int height) {
      logger.info("resize");
      renderInterrupted = true;
   }

   @Override
   public void pause() {
      logger.info("pause");
      renderInterrupted = true;
   }

   @Override
   public void resume() {
      logger.info("resume");
      renderInterrupted = true;
   }
}

The renderInterrupted member variable avoids printing render for every game loop iteration.

Note

Whenever Eclipse complains about missing imports, hit Ctrl + Shift + O to automatically add the necessary modules.

The Logger class helps us show useful debug information and errors on the console. Not only does it work on desktops but also on external devices, as long as they are connected to Eclipse. Remember this little new friend as it will be truly useful for as long as you work with Libgdx. The constructor receives a string that will be useful to identify the messages in the log as well as on a logging level.

In order of increasing severity, these are the available logging levels: Logger.INFO, Logger.DEBUG, Logger.ERROR, and Logger.NONE. Several methods can be used to log messages:

  • info(String message)

  • info(String message, Exception exception)

  • debug(String message)

  • debug(String message, Exception exception)

  • error(String message)

  • error(String message, Throwable exception)

Logging levels can be retrieved and set with the getLevel() and setLevel() methods, respectively. Both the level and the method used to log a message will determine whether they will actually be printed on the console. For example, if the level is set to Logger.INFO, only messages sent through info() and error() will appear, and those sent through debug() will be ignored.

Now, run the application on all the platforms and pay attention to the console. Depending on how you play with the focus, the output will vary, but it should be similar to this:

Application lifecycle: create
Application lifecycle: resize
Application lifecycle: render
Application lifecycle: pause
Application lifecycle: render
Application lifecycle: resume
Application lifecycle: render
Application lifecycle: dispose

This should give you a pretty decent handle of how the application lifecycle works.

Placing breakpoints on each ApplicationListener overridden method is also a good way of discovering what is going on. Instruction breakpoints allow you to debug an application and stop the execution flow that reaches the said instruction. At this point, you can run the code instruction by instruction and examine the current state of the active variables. To set a breakpoint, double-click next to the corresponding line; a blue dot will confirm that the breakpoint is set. Once you are done, you can debug the application by right-clicking on the desired project and selecting the Debug As menu.

The Eclipse Debug view will then enter the stage with all its shiny panels. The Debug tab shows the current execution callstack, the Variables tab contains the current state of the variables within scope, and in the following screenshot, you can see the code with the current line highlighted. The arrow buttons in the upper toolbar can be used to step over the next instruction (F6) or move on to the next method (F5), where applicable, or out of the current method (F7), as shown in the following screenshot:

Starter classes and configuration

Every platform project consists of a starter class (or entry point). This class is responsible for constructing the platform-specific application backend. Each backend implements the Application interface. The starter class also passes a new instance of our ApplicationListener implementation to the application backend. This implementation typically lives in the core project and serves as an entry point to our cross-platform game code. Finally, it also submits a configuration object, and by doing so, it provides a mechanism to customize general parameters, as we will see later.

Desktop starter

The entry point of the desktop project is the static main method of the DesktopLauncher starter class:

public class DesktopLauncher {
   public static void main (String[] arg) {
      LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
      new LwjglApplication(new EnvironmentTest(), config);
   }
}

As you can see, this creates LwjglApplicationConfiguration. Then, it instantiates a LwjglApplication object passing in a new EnvironmentTest instance along the recently created config object. Some of the most useful attributes of the configuration class are listed as follows:

  • r, g, b, and a: This is the number of bits to be used per color channel, which are red, green, blue, and alpha, respectively.

  • disableAudio: This is to set whether audio should be used. If it should, requesting the audio subsystem will return null.

  • width and height: This is the size of the application window in pixels.

  • fullScreen: This is to set whether the application should start in the full screen or windowed mode.

  • vSyncEnabled: This is to set whether vertical synchronization should be enabled. This ensures that the render operations are in sync with the monitor refresh rate, avoiding potential partial frames.

  • title: This is the string with the desired title of the window.

  • resizable: This is to set whether the user should be able to resize the application window.

  • foregroundFPS and backgroundFPS: This is the number of desired frames per second when the application is active and inactive, respectively.

Android starter

The Android starter can be found in AndroidLauncher.java:

public class AndroidLauncher extends AndroidApplication {
   @Override
   protected void onCreate (Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
      initialize(new EnvironmentTest(), config);
   }

Android starters use the Android SDK Activity framework, which those who have developed for this platform before will be familiar with. In this case, an AndroidApplicationConfiguration instance is used. Some of the most useful attributes are listed as follows:

  • r, g, b, and a: Just as with the desktop project, these refer to the number of bits used per color channel.

  • hideStatusBar: This is to set whether the application should hide the typical Android status bar that shows up right at the top of the screen.

  • maxSimultaneousSounds: This is the number of maximum sound instances that can play at a given time. As you can see in Chapter 5, Audio and File I/O, dedicated to audio, they only refer to short sound effects as opposed to long streams of audio.

  • useAccelerometer: This is to set whether the application should care about the accelerometer; it defaults to true.

  • useCompass: This is to set whether the Android application should update the compass values; it also defaults to true.

HTML starter

The HTML5 starter resides inside the GwtLauncher.java file and follows the pattern we already know:

public class GwtLauncher extends GwtApplication {
   @Override
   public GwtApplicationConfiguration getConfig () {
      GwtApplicationConfiguration cfg = new GwtApplicationConfiguration(480, 320);
      return cfg;
   }

   @Override
   public ApplicationListener getApplicationListener () {
      return new EnvironmentTest();
   }
}

A GwtApplicationConfiguration object is used to configure the HTML5 backend. Its most important parameters are as follows:

  • antialiasing: This is to set whether to enable antialiasing, which is computationally expensive, but helps to avoid rough edges when rendering.

  • canvasId: This is the identifier for the HTML element to embed the game canvas in. When not specified, the system will create a canvas element inside the <body> element.

  • fps: This is the target frames per second at which we desire to run the game.

  • width and height: These are the dimensions of the drawing area in pixels.

iOS starter

Finally, the iOS starter is hosted by the IOSLauncher.java file:

public class IOSLauncher extends IOSApplication.Delegate {
    @Override
    protected IOSApplication createApplication() {
        IOSApplicationConfiguration config = new IOSApplicationConfiguration();
        return new IOSApplication(new EnvironmentTest(), config);
    }

    public static void main(String[] argv) {
        NSAutoreleasePool pool = new NSAutoreleasePool();
        UIApplication.main(argv, null, IOSLauncher.class);
        pool.drain();
    }
}

The configuration object for this backend belongs to the IOSApplicationConfiguration class and here are its main parameters:

  • accelerometerUpdate: This is the update interval to update the accelerometer values in seconds

  • orientationLandscape and orientationPortrait: This is to set whether the application supports the landscape or portrait orientation, respectively

  • preferredFramesPerSecond: This is the number of frames per second we try to reach while running the application

  • useAccelerometer: Just as on Android, this sets whether to update the accelerometer values

  • useCompass: This is to set whether to update the compass sensor values

How it works…

So far, you autonomously experienced how a Libgdx application is organized and the mechanism it uses to run across platforms. Also, you tested how the application lifecycle works and which events are triggered as a consequence of an event. Now, it is time to get a higher-level overview of all these systems and see how they fit together.

Here is an UML class diagram showing every piece of the puzzle that is involved in any way with game startups on specific platforms and in the application lifecycle. After a quick glance, we can observe how EnvironmentTest, our ApplicationListener implementation, is used by every launcher class along the various configuration classes:

The next diagram depicts the mighty Libgdx application lifecycle. Every time the game starts, the create() method is called. Immediately after, there is a call to resize() so as to accommodate the current screen dimensions. Next, the application enters its main loop, where it calls render() continuously, while processing the input and other events, as required.

When the application loses focus (for example, the user receives a call on Android), pause() is invoked. Once the focus is recovered, resume() is called, and we enter the main loop again.

The resize() method is called every time the application surface dimensions change (for example, the user resizes the window).

Finally, it's called when the player gets bored of our game. Sorry, this will never happen! When the player runs out of time to play our game and exits, pause() will be called, followed by dispose().

There's more…

After looking at the basic concepts behind a simple Libgdx project, let's move on to a couple of tricks to improve your quality of life.

Living comfortably with ApplicationAdapter

As you already know, every Libgdx game needs to have an ApplicationListener interface implementation in its core project for the launchers to use. We also saw how the developer is forced to implement the create(), dispose(), render(), resize(), pause(), and resume() methods of such an interface. However, these overridden methods might end up completely empty. What a waste of digital ink, and more importantly, our precious time!

Luckily enough, Libgdx provides a useful ApplicationAdapter class that already contains an empty implementation for each ApplicationListener interface method. This means that you can simply inherit from ApplicationAdapter and only override the methods you really need. This comes particularly in handy when writing small tests rather than big games. These small adapter classes are quite common within the API, and they are really comfortable to use as long as we do not need to inherit from anything else. Remember that Java does not support multiple inheritance.

The following will be perfectly valid if we want a completely empty application:

public class MyGame extends ApplicationAdapter {}

Managing a multiscreen application with Game

Most games are made out of several screens the player can navigate through. The main menu, level selection settings, or levels are some of the most common examples. Though this completely depends on the nature of each project, most of them definitely share the structure. Libgdx comes with an utterly minimalistic screen system built-in, which might just be enough for your requirements, so why not use it? Reinventing the wheel is rarely a good idea.

The two main components of this system are the Game abstract class and Screen interface. Game implements the well-known ApplicationListener interface, so you will only need your main class to inherit from Game.

The Game class holds a reference to the current Screen and provides the getter and setter methods for it. Game requires you to implement the create() method, but already provides implementations for the rest of the application lifecycle methods. Be aware that if you override any of the other methods, you will need to call the parent version so as to maintain screen behavior correctness. The helpful bit comes with the render() method, which will automatically update and render the active Screen reference, as long as it is not null.

What follows is an UML class diagram illustrating a sample game architecture based on the Game/Screen model. The user implemented MyGame as a Game derived class, and the SettingsScreen, GameScreen, LevelSelectionScreen, and MainMenuScreen classes were derived from Screen:

The Game public API looks like this. Note that method implementation has been omitted for space reasons:

public abstract class Game implements ApplicationListener {
   public void dispose ();
   public void pause ();
   public void resume ();
   public void render ();
   public void resize (int width, int height);
   public void setScreen (Screen screen);
   public Screen getScreen ();
}

The Screen interface is quite similar to the ApplicationListener interface, but its equivalent methods will only be called when it is the active screen. It also adds the hide() and show() methods that will be called when changed to and from a screen, respectively. In the following code, you will find an overview of the interface:

public interface Screen {
   public void render (float delta);
   public void resize (int width, int height);
   public void show ();
   public void hide ();
   public void pause ();
   public void resume ();
   public void dispose ();
}

Note

Just like we saw before with ApplicationListener and ApplicationAdapter, the Screen interface has a convenient implementation, unsurprisingly called ScreenAdapter. You can just inherit from it and override the methods that you need only.

See also

  • Jump to Chapter 2, Working with 2D Graphics, to start rendering textures onscreen or carry on with the Updating and managing project dependencies recipe to learn more about Libgdx project configuration.