Friday, January 9, 2015

Making Games in Liblol: Part 2: Creating Levels

In LibLOL, we put the code for all of the levels into a single file called "Levels.java".  If you open that file, you'll see that there's one method, called "display()":


    /**
     * Display one of the screens
     *
     * @param which The screen to display. Your code should use an /if/ statement
     *              to decide what screen to display based on the value of /which/
     */
    void display(int which) {
        ...
    }

Inside of this method, you'll see that there's a lengthy "if" statement:


    if (whichLevel == 1) {
        ...
    }

    else if (whichLevel == 2) {
        ...
    }
    ...

The code for each level is self-contained in one of these "if" blocks.  Technically, you don't have to do things this way, and when your game gets sufficiently complex, you'll want to change things.  But for now, it's easier to just put all of your code in one place.

Everything is an Event

Writing games is different than writing the kind of code that most people learn in their introductory computer science classes.  Fundamentally, a game is a simulation.  At all times, the game has some sort of state, and some rules that apply to that state.  At fixed intervals (every 1/60th of a second), the rules are applied to the state of the game to produce a new state.  So, for example, if the state consists of a single actor at position (0, 0) with a velocity of (6, 6), then after one interval passes, the rules will be applied, the actor will be moved to (.1, .1), and the game will be re-drawn.

When the player interacts with the game, either by tilting the phone or touching the screen, it creates an event.  Similarly, when certain special collisions happen between the actors in the game, or when timers expire, we have events.  The way to change the state of the game is in response to events.

The first event that matters is the event for starting a level.  Our "display" function is precisely for this purpose.  When the player touches a button on the chooser, then display() will be called, with the selected level passed in as whichLevel.  Our code runs in response to that event, to configure the initial state of the game.  It's important to remember that your game won't start playing until you finish drawing the initial state.  So, for example, you can't draw an infinitely long level in the display() code, or you'll never get to the point where you can start playing the game.

With that said, let's look at how to draw a level.

A World, and a Camera

LibLOL only supports 2D games (LibGDX, on which LibLOL is built, allows 3D games).  It also is heavily attached to the Box2D physics simulator.  What this means is that by default, every actor in the game is going to be a physical body.  Every actor has density, friction, and elasticity.  Every actor can be involved in collisions with other actors.  Every actor is able to move, and that movement is governed by the laws of physics.  If we don't want these sorts of behaviors, we have to turn them off.

The simulation I mentioned above, then, is simulating a physics world.  But how big is that world?  How much of it can we see at any time?  The easiest way to think about this is to imagine that you have a huge sheet of paper on your desk, on which you've drawn a picture.  Then hold your phone exactly one foot above the picture, and switch to your camera app.  The camera can only see part of the picture, and as we move the camera, different parts of the picture become visible.  The piece of paper on your desk is the physics world.  The phone corresponds to what we'll think of as the "viewport", or "camera".  And the fact that we're holding the phone exactly one foot above the paper is important.  Here's a picture that might help:


In the picture, the shape with the red outline is the world.  The shape with the purple outline is the camera.  The blue dotted lines show a projection from the world onto the camera, so that we can see just a portion of the world at any time.

In LibLOL, the coordinate system starts in the bottom left corner.  So (0, 0) is bottom left, and as we move up and to the right, the y and x values get larger, respectively.

The last important aspect here is the relationship between the units of the world and the units of the camera.  We're going to measure our world in meters, and our camera in pixels.  In LibLOL, there are 20 pixels per meter.  We're also going to assume that the camera is 960 pixels wide by 640 pixels high.  LibGDX will stretch our game to look nice on any phone, but starting with a 960x640 camera (which shows 48x32 meters) gives good resolution and a good width-to-height ratio for most phones.

A Very Simple Level

Now that we've gone over those details, we can start writing the levels of a game.  Remember that the game levels that you get when you check out the LibLOL code are meant to demonstrate how to use the code, and that you're encouraged to change them as you learn, and then replace them as you create your own game.  Let's take a look at level 1 of the demos:


        /*
         * In this level, all we have is a hero (the green ball) who needs to
         * make it to the destination (a mustard colored ball). The game is
         * configured to use tilt to control the hero.
         */
        if (whichLevel == 1) {
            // set the screen to 48 meters wide by 32 meters high... this is
            // important, because Config.java says the screen is 480x320, and
            // LOL likes a 20:1 pixel to meter ratio. If we went smaller than
            // 48x32, things would get really weird. And, of course, if you make
            // your screen resolution higher in Config.java, these numbers would
            // need to get bigger.
            //
            // Level.configure MUST BE THE FIRST LINE WHEN DRAWING A LEVEL!!!
            Level.configure(48, 32);
            // there is no default gravitational force
            Physics.configure(0, 0);

            // in this level, we'll use tilt to move some things around. The
            // maximum force that tilt can exert on anything is +/- 10 in the X
            // dimension, and +/- 10 in the Y dimension
            Tilt.enable(10, 10);

            // now let's create a hero, and indicate that the hero can move by
            // tilting the phone. "greenball.png" must be registered in
            // the registerMedia() method, which is also in this file. It must
            // also be in your android game's assets folder.
            Hero h = Hero.makeAsCircle(4, 17, 3, 3, "greenball.png");
            h.setMoveByTilting();

            // draw a circular destination, and indicate that the level is won
            // when the hero reaches the destination. "mustardball.png" must be
            // registered in registerMedia()
            Destination.makeAsCircle(29, 26, 2, 2, "mustardball.png");
            Score.setVictoryDestination(1);
        }

There are only seven lines of code.  Let's work through them, one at a time.

  • Level.configure() -- This line should always be the first line of code when drawing a level.  It is responsible for saying how big the physical world is for this game.  We've made it 48 meters x 32 meters, which is as small as a level can be, given the size of our camera and our pixel-to-meter ratio.  In general, if you're making a non-scrolling game, these dimensions are good.  When this line of code finishes, there is a physical world into which we can place actors.
  • Physics.configure(0, 0) -- This line indicates that there are no default forces acting on actors in the level.  This makes sense for a game where the player is supposed to be looking down at the action.  For a Mario-style game, where the view is from the side, (0, -10) is a common setting.
  • Tilt.enable(10, 10) -- This level is going to use the tilt of the phone (note: when running on the desktop, use the arrows to simulate tilt).  The numbers passed to this function set thresholds for how much force you can produce via tilting.  In this case, no matter how steep the angle, we'll say that the phone produces no more than 10 units of force.
  • Hero.makeAsCircle(4, 17, 3, 3, "greenball.png") -- This line creates an actor who is a "hero".  The hero will be drawn such that it is 3 meters wide by 3 meters high, and its bottom left corner will be at the coordinate (4, 17).  If that didn't make sense, take a look at the following image, which would correspond to Hero.makeAsCircle(7, 10, 3, 3, "greenball.png")
  • h.setMoveByTilting() -- This indicates that the hero we just made is going to be controlled by the tilt of the phone.
  • Destination.makeAsCircle(29, 26, 2, 2, "mustardball.png") -- This draws the destination (the place the hero must reach).
  • Score.setVictoryDestination(1) -- There are many ways to win a level... surviving for long enough, defeating enough enemies, collecting enough goodies, etc.  In this case, we are saying that once one hero reaches the destination, the level should end.
You should feel free to modify these lines of code and see what happens.  Change numbers.  Delete lines.  In some cases, your game will still work.  In others, Android Studio will inform you that your code is no longer valid.  Once you get comfortable with level 1, move on to subsequent levels and start exploring.  Hopefully you'll find that LibLOL makes it easy to get very advanced effects using only a small number of lines of code.

No comments:

Post a Comment