Saturday, January 10, 2015

Making Games in Liblol: Part 3: Configuring the Splash Screen

The folks at Studyhall Entertainment put out a new game using LibLOL, and on the splash screen, they had animations.  My first thought was that it was a really clever effect (and they even took control of the "back" button as a way of re-generating the animation... sweet!).  But then I started to wonder how they even did it, since the LibLOL splash screen was didn't have physics support.

Long story short, I decided to make the splash screen (and the chooser, and the help screens) into a physics world.  This meant throwing out a lot of code, and then making the splash screen adopt the same interface as playable levels.  Now there's less code for me to maintain, and fewer interfaces for programmers to learn.  Win-win.

To explain the splash screen, it's going to be useful to discuss a few other concepts first:

The Heads-Up Display

There are times when we want to draw things on the camera, instead of in the world.  A great example of this is in Mario-style games.  No matter where the hero is, we want the time, number of lives, and coin count in the top-left corner.  Putting text in the world wouldn't work: as Mario moves, the text would scroll off the screen.  In a phone game, where touching the screen is a way of controlling the action, we might also want to draw buttons on the camera instead of the world.  

It is confusing to think of "drawing on the camera", so games use the term "heads-up display", or HUD, to refer to anything drawn on the camera.  In LibLOL, we can put information (images and text) onto the HUD through the "Display" feature, and we can put buttons onto the HUD through the "Control" feature.

Callbacks

Recall that a game is a simulation.  Part of what that means is that we can change the state of the simulation in response to special events, like screen touches.  But that also means we need to be able to provide code that doesn't run until the event happens.  Callbacks are a way of providing this functionality.  Consider the following code:


        Control.addCallbackControl(96, 186, 160, 80, "", new LolCallback() {
            public void onEvent() {
                Lol.doHelp(1);
            }
        });

This code says "draw a button on the screen, and when the player presses it, run the code I'm providing".

In truth, most of the code for making controls in LibLOL is much simpler than this, because I provide the code to run when the button is pressed.  You don't need to write your own callbacks for jumping, running, ducking, throwing projectiles, and so on.  Callback controls are really only for custom behaviors.  But since they're there, I wanted to demo them, and what better place than the splash screen?

Let's look at this code in more detail.  We are drawing a control with a bottom left coordinate of (96, 186), a width of 160, and a height of 80.  We are not providing an image for the control (""), and hence it is invisible.  We could give an image, which would be drawn on top of the world.  We could give a partially-transparent image, too, so that the world would be visible through the image of the control.  But that's not important right now.

What is important is the last part of the code: "new LolCallback() { ... });"  This is a lot of text, and in the end, it's just to say "run Lol.doHelp(1) when the control is pressed".  Java 1.8 provides a cleaner way of writing this code, but Android Studio doesn't support Java 1.8 yet.  When it does, this code will get a lot cleaner.

If callbacks don't make complete sense right now, it's OK.  Most Computer Science programs don't teach callbacks (technically "anonymous classes") until students have a few semesters of experience.

Drawing the Splash Screen

Now that we've covered those details, let's look at the Splash.java code:


   /**
     * There is usually only one splash screen. However, the ScreenManager
     * interface requires display() to take a parameter.  We ignore it.
     */
    public void display(int which) {
        // set up a simple level. We could make interesting things happen, since
        // we've got a physics world, but we won't.
        Level.configure(48, 32);
        Physics.configure(0, 0);

        // draw the background. Note that "Play", "Help", and "Quit" are part of
        // this background image.
        Util.drawPicture(0, 0, 48, 32, "splash.png", 0);

        // start the music
        Level.setMusic("tune.ogg");

        // This is the Play button... it switches to the first screen of the
        // level chooser. You could jump straight to the first level by using
        // "doLevel(1)", but check the configuration in MyLolGame... there's a
        // field you should change if you don't want the 'back' button to go
        // from that level to the chooser.
        Control.addCallbackControl(384, 182, 186, 104, "", new LolCallback() {
            public void onEvent() {
                Lol.doChooser(1);
            }
        });

        // This is the Help button... it switches to the first screen of the
        // help system
        Control.addCallbackControl(96, 186, 160, 80, "", new LolCallback() {
            public void onEvent() {
                Lol.doHelp(1);
            }
        });

        // This is the Quit button
        Control.addCallbackControl(726, 186, 138, 78, "", new LolCallback() {
            public void onEvent() {
                Lol.doQuit();
            }
        });

        // Mute button is a tad tricky... we'll do it as an obstacle
        Obstacle o = Obstacle.makeAsBox(45, 0, 2.5f, 2.5f, "");
        // figure out which image to use for the obstacle based on the current
        // volume state
        if (Lol.getVolume()) {
            o.setImage("audio_off.png", 0);
        } else {
            o.setImage("audio_on.png", 0);
        }
        // when the obstacle is touched, change the mute and then update the
        // picture for the obstacle
        o.setTouchCallback(0, 0, 0, 0, false, new LolCallback() {
            public void onEvent() {
                Lol.toggleMute();
                if (Lol.getVolume()) {
                    mAttachedActor.setImage("audio_off.png", 0);
                } else {
                    mAttachedActor.setImage("audio_on.png", 0);
                }
            }
        });
    }

That looks pretty complicated, but it's not.  Let's break it down:

  • The splash screen is just a level, so the first two lines set up a simple level with no default forces.
  • We don't actually draw the "Play", "Help", and "Quit" buttons... we just display this picture as the background:

  • We turn on music with one line of code.
  • Then we draw three invisible buttons over the "Help", "Play", and "Quit" text, with callbacks for switching to the help screens, switching to the chooser, or quitting the game.
  • We draw a mute button as an obstacle.  We put a callback on the obstacle to toggle the state of the speakers (muted or unmuted).  The callback also changes the obstacle's image to correspond to the speaker state.
The mute button is definitely the trickiest part.  If it doesn't make sense, don't worry about it.  If you have a mute button, you'll probably only be concerned about where to put it, its size, and what images to use.  You should be able to figure out how to change those aspects of the code pretty easily.

Taking Advantage of Debug Support

One of the most common questions students ask is "How do I know what values to use for the coordinates of my buttons?  There's a nice trick here.  In MyGame.java, the configure() function can set "mShowDebugBoxes" to true or false.  When it's true, and I play the game, every control in the game will be outlined, as will every actor.  In the splash screen, it looks like this:
This means you can see where the buttons are.  But is it necessary to guess about button placement?  Not really.  When the debug boxes are enabled, then LibLOL also prints diagnostic information on every click.  Let's take a look:
In the screenshot above, I enabled the "Run" view in the bottom left corner.  Then I started the desktop version of the game, and I clicked once on an area of the game window.  As you can see, the log shows the screen coordinates (in pixels), corresponding to where on the "camera" I touched, and it also projects these to the world coordinates (in meters) where my touch would have happened in the physics world.

Note, too, that there are a few error messages about LibLOL not finding image files.  These messages can be really helpful if you mistype a file name.  So for as annoying as it can be to have those boxes showing when you are developing your game, it's a good idea to keep them enabled until you're ready to put your game on the market.  Once you get used to them, they can save you a ton of time!

No comments:

Post a Comment