Monday, January 12, 2015

Making Games in Liblol: Part 5: The Level Chooser

The level chooser code (Chooser.java) has changed a lot over the years.  Back in the early days, I auto-generated it, and programmers had no control over how it looked.  Obviously that wasn't popular, because it meant that even the tiniest customization required students to jump off a cliff.



The next version of the level chooser code entailed me providing some code to draw the chooser, and the programmer overloading a couple dozen getters that parameterized how the chooser was drawn (things like font size, font face, font color, number of rows of buttons, number of columns of buttons, etc.).  That was better, but the amount of code was way too high for the amount of customization that most people wanted.  And, worse yet, there were still a lot of students who had to jump over the cliff to get the chooser screens they wanted, because I didn't give a getter for everything.

That brings us to the new Chooser.  It serves a few goals:
  • The cliff is gentler... in fact, I draw the chooser slightly differently from one page to the next, to illustrate an increasingly advanced programming style.
  • The total lines of code actually decreased, since the Chooser is yet another ScreenManager.
  • Total customization is now possible, without jumping into my code.
  • There is physics in the Chooser, so animations and other nice visual effects are possible.
There are still a few declarative parameters that are useful.  In MyGame.java, in the configure() function, you'll see these three lines:


        mNumLevels = 92;
        mUnlockAllLevels = true;
        mEnableChooser = true;

The first specifies the number of levels.  This is used at the end of the last level to indicate that the game should go back to the chooser, since there's no level 93.  We'll get to the other two variables later.

Drawing Chooser Screens


The chooser code has a few helper functions in it.  You can always write helper code and then use it in your Chooser (or your Splash, or Level, or Store).  My helper functions let me draw a chooser button (an image with text over it, where pressing the image takes you to a specific level) and draw navigation buttons (for moving to the next or previous chooser screen, and for going back to the splash screen).  These functions aren't very interesting, so I'm not going to bother describing them.

Let's take a look at the code for the first level of the chooser:

        // screen 1: show 1-->15
        //
        // NB: in this screen, we assume you haven't done much programming, so
        // we draw each button with its own line of code, and we don't use any
        // variables.
        if (which == 1) {
            Level.configure(48, 32);
            Physics.configure(0, 0);

            // set up background and music
            Util.drawPicture(0, 0, 48, 32, "chooser.png", 0);
            Level.setMusic("tune.ogg");

            // for each button, draw an obstacle with a touchCallback, and then
            // put text on top of it. Our buttons are 5x5, we have 1.5 meters
            // between buttons, there's an 8.5 meter border on the left and
            // right, and there's an 11 meter border on the top
            drawLevelButton(8.5f, 16, 5, 5, 1);
            drawLevelButton(15f, 16, 5, 5, 2);
            drawLevelButton(21.5f, 16, 5, 5, 3);
            drawLevelButton(28f, 16, 5, 5, 4);
            drawLevelButton(34.5f, 16, 5, 5, 5);

            drawLevelButton(8.5f, 9.5f, 5, 5, 6);
            drawLevelButton(15f, 9.5f, 5, 5, 7);
            drawLevelButton(21.5f, 9.5f, 5, 5, 8);
            drawLevelButton(28f, 9.5f, 5, 5, 9);
            drawLevelButton(34.5f, 9.5f, 5, 5, 10);

            drawLevelButton(8.5f, 3f, 5, 5, 11);
            drawLevelButton(15f, 3f, 5, 5, 12);
            drawLevelButton(21.5f, 3f, 5, 5, 13);
            drawLevelButton(28f, 3f, 5, 5, 14);
            drawLevelButton(34.5f, 3f, 5, 5, 15);

            // draw the navigation buttons
            drawNextButton(43, 9.5f, 5, 5, 2);
            drawSplashButton(0, 0, 5, 5);
        }

This is the kind of code that nobody should ever write.  We're drawing 15 buttons, and their positions are being provided manually in the code.  There are so many other ways of doing this and the other ways have two very important benefits.  First, they take much less code.  Second, when you want to change the behavior, these other techniques let you change one declaration, instead of changing a bunch of function calls.

Let's try to do a little bit better for the second chooser screen.  We'll use a loop to draw the 5 buttons in each row:


        // screen 2: show levels 16-->30
        //
        // NB: this time, we'll use three loops to create the three rows. By
        // using some variables in the loops, we get the same effect as the
        // previous screen. The code isn't simpler yet, but it's still pretty
        // easy to understand.
        else if (which == 2) {
            Level.configure(48, 32);
            Physics.configure(0, 0);

            // set up background and music
            Util.drawPicture(0, 0, 48, 32, "chooser.png", 0);
            Level.setMusic("tune.ogg");

            // let's use a loop to do each row
            float x = 8.5f;
            int level = 16;
            for (int i = 0; i < 5; ++i) {
                drawLevelButton(x, 16, 5, 5, level);
                level++;
                x += 6.5f;
            }

            x = 8.5f;
            for (int i = 0; i < 5; ++i) {
                drawLevelButton(x, 9.5f, 5, 5, level);
                level++;
                x += 6.5f;
            }

            x = 8.5f;
            for (int i = 0; i < 5; ++i) {
                drawLevelButton(x, 3, 5, 5, level);
                level++;
                x += 6.5f;
            }

            // draw the navigation buttons
            drawPrevButton(0, 9.5f, 5, 5, 1);
            drawNextButton(43, 9.5f, 5, 5, 3);
            drawSplashButton(0, 0, 5, 5);
        }

In that code, we use the variable 'x' to store the x coordinate of the next button to draw, and we reset it after each row.  If we wanted to change the spacing between buttons, we could just change 'x += 6.5f' in three places, instead of re-doing the math for 15 buttons.  Naturally, we can also use a loop to do the three rows:


        // screen 3: show levels 31-->45
        //
        // NB: now we use a nested pair of loops, and we can do three rows in
        // just a few more lines than one row.
        else if (which == 3) {
            Level.configure(48, 32);
            Physics.configure(0, 0);

            // set up background and music
            Util.drawPicture(0, 0, 48, 32, "chooser.png", 0);
            Level.setMusic("tune.ogg");

            // let's use a loop to do each row and each column
            float y = 16;
            int level = 31;
            for (int r = 0; r < 3; ++r) {
                float x = 8.5f;
                for (int i = 0; i < 5; ++i) {
                    drawLevelButton(x, y, 5, 5, level);
                    level++;
                    x += 6.5f;
                }
                y -= 6.5f;
            }

            // draw the navigation buttons
            drawPrevButton(0, 9.5f, 5, 5, 2);
            drawNextButton(43, 9.5f, 5, 5, 4);
            drawSplashButton(0, 0, 5, 5);
        }

Now there's one place where we specify the space above the top row (16), one place where we specify the space to the left of the first column (8.5f), and one place for each of the horizontal and vertical spacing of the buttons (6.5f and -6.5f, respectively).  This code is much more maintainable, and it's also much shorter.

You should look at the trick used to draw screens 4-7 in one arm of the if statement.  If all of your chooser screens look the same, you can draw them without adding much code at all.  And if you need to fine-tune specific aspects of the chooser screens, you can do that by using the more verbose code.

Pressing the Back Button


Android devices have a "back" button, and we need to do the right thing when it is pressed.  In LibLOL, pressing back from a level takes you to the appropriate chooser screen, and pressing back from the chooser takes you to the splash screen.  Pressing back from any help or store level takes you to the splash screen.  In games without a chooser, you can set the mEnableChooser field to false (MyGame.java::configure()) to indicate that pressing back from a level should go to the splash screen.

Locking Levels


In LibLOL, a level gets unlocked when you succeed in finishing the previous level.  The other option is to set mUnlockAllLevels, and then everything is unlocked, all the time (this is really helpful when debugging).  The chooser code I provided will look at the lock status and figure out if a button should be active or not.

If you want to require a certain score to unlock the next level, then you'll need to make a few changes.  First, you'll need to set up a Win Callback (see level 32 for an example) so that you can record the fact of the level being unlocked.  The Facts subsystem is used for this.  In Facts, there are "level facts", "session facts", and "game facts", corresponding to information that gets reset when the level ends, when the player quits the game, and never).  You'll need to set up a PostScene that doesn't let you move on to the next level, if that's not appropriate (you can add a callback control that covers the whole screen).  And then you'll need to edit the code for drawing chooser buttons, so that the condition for unlocking a level involves the game fact that you saved.

No comments:

Post a Comment