S

Figure 3-24. A simple background, and Bob, master of the universe

To draw Bob's world, we'd first draw the background image to the framebuffer, followed by Bob over the background image in the framebuffer. This process is called compositing, as we compose different images into a final image. The order in which we draw images is relevant, as any new draw call will overwrite the current contents in the framebuffer. So, what would be the final output of our compositing? Figure 3-25 shows it to you.

Figure 3-25. Compositing the background and Bob into the framebuffer (not what we wanted)

Ouch, that's not what we wanted. In Figure 3-24, notice that Bob is surrounded by white pixels. When we draw Bob on top of the background to the framebuffer, those white pixels also get drawn, effectively overwriting the background. How can we draw Bob's image so that only Bob's pixels are drawn, and the white background pixels are ignored?

Enter alpha blending. Well, in Bob's case it's technically called alpha masking, but that's just a subset of alpha blending. Graphics software usually lets us not only specify the RGB values of a pixel, but also its translucency. Think of it as yet another component of a pixel's color. We can encode it just like we encoded the red, green, and blue components.

a pixel's color. We can encode it just like we encoded the red, green, and blue components.

I hinted earlier that we could store a 24-bit RGB triplet in a 32-bit integer. There are 8 unused bits in that 32-bit integer that we can grab and store our alpha value in. We can then specify the translucency of a pixel from 0 to 255, where 0 is fully transparent and 255 is opaque. This encoding is known as ARGB8888 or BGRA8888 depending on the order of the components. There are also RGBA8888 and ABGR8888 formats, of course.

In the case of 16-bit encoding, we have a little problem: all bits of our 16-bit short are taken up by the color components. Let's instead imitate the ARGB8888 format and define an ARGB4444 format analogously. That leaves 12 bits for our RGB values in total—4 bits per color component.

We can easily imagine how a rendering method for pixels that's fully translucent or opaque would work. In the first case, we'd just ignore pixels with an alpha component of zero. In the second case, we'd simply overwrite the destination pixel. When a pixel has neither a fully translucent nor fully opaque alpha component, however, things get a tiny bit more complicated.

When talking about blending in a formal way, we have to define a few things:

■ Blending has two inputs and one output, each represented as an RGB triplet (C) plus an alpha value (a).

■ The two inputs are called source and destination. The source is the pixel from the image we want to draw over the destination image (e.g., the framebuffer). The destination is the pixel we are going to (partially) overdraw with our source pixel.

■ The output is again a color expressed as an RGB triplet and an alpha value. Usually we just ignore the alpha value, though. For simplicity we'll do that in this chapter.

■ To simplify our math a little bit, we'll represent RGB and alpha values as floats in the range of 0.0 to 1.0.

Equipped with those definitions, we can create so-called blending equations. The simplest equation looks like this:

red = src.red * src.alpha + dst.red * (1 - src.alpha) blue = src.green * src.alpha + dst.green * (1 - src.alpha) green = src.blue * src.alpha + dst.blue * (1 - src.alpha)

src and dst are the pixels of the source and destination we want to blend with each other. We blend the two colors component-wise. Note the absence of the destination alpha value in these blending equations. Let's try an example and see what it does:

src = (1, 0.5, 0.5), src.alpha = 0.5, dst = (0, 1, 0) red = 1 * 0.5 + 0 * (1 - 0.5) = 0.5 blue = 0.5 * 0.5 + 1 * (1 - 0.5) = 0.75 red = 0.5 * 0.5 + 0 * (1 - 0.5) = 0.25

Figure 3-26 illustrates the preceding equation. Our source color is a shade of pink, and the destination color is a shade of green. Both colors contribute equally to the final output color, resulting in a somewhat dirty shade of green or olive.

Figure 3-26 illustrates the preceding equation. Our source color is a shade of pink, and the destination color is a shade of green. Both colors contribute equally to the final output color, resulting in a somewhat dirty shade of green or olive.

Figure 3-26. Blending two pixels

Two fine gentlemen called Porter and Duff came up with a slew of blending equations. We will stick with the preceding equation, though, as it covers most of our use cases. Try experimenting with it on paper or in your graphics software of choice to get a feeling for what blending will do to your composition.

NOTE: Blending is a wide field. If you want to exploit it to its fullest potential, I suggest searching the Web for Porter and Duff's original work on the subject. For the games we will write, though, the preceding equation is sufficient.

Notice that there are a lot of multiplications involved in the preceding equations (six, to be precise). Multiplications are costly, and we should try to avoid them where possible. In the case of blending, we can get rid of three of those multiplications by premultiplying the RGB values of the source pixel color with the source alpha value. Most graphics software supports premultiplication of an image's RGB values with the respective alphas. If that is not supported, you can do it at load time in memory. However, when we use a graphics API to draw our image with blending, we have to make sure that we use the correct blending equation. Our image will still contain the alpha values, so the preceding equation would output incorrect results. The source alpha must not be multiplied with the source color. Luckily, all Android graphics APIs allow us to fully specify how we want to blend our images.

In Bob's case, we just set all the white pixels' alpha values to zero in our graphics software of choice, load the image in ARGB8888 or ARGB4444 format, maybe premultiply the alpha, and use a drawing method that does the actual alpha blending with the correct blending equation. The result would look like Figure 3-27.

Figure 3-27. On the left is Bob blended; on the right is Bob in Paint.NET. The checkerboard illustrates that the alpha of the white background pixels is zero, so the background checkerboard shines through.

NOTE: The JPEG format does not support storing alpha values per pixel. Use the PNG format in that case.

In Practice

With all this information, we can finally start to design the interfaces for our graphics module. Let's define the functionality of those interfaces. Note that when I refer to the framebuffer, I actually mean the virtual framebuffer of the UI component we draw to. We just pretend we directly draw to the real framebuffer. We'll need to be able to perform the following operations:

■ Load images from disk and store them in memory for drawing them later on.

■ Clear the framebuffer with a color so we can erase what's still there from the last frame.

■ Set a pixel in the framebuffer at a specific location to a specific color.

■ Draw lines and rectangles to the framebuffer.

■ Draw previously loaded images to the framebuffer. We'd like to be able to either draw the complete image or portions of it. We also need to be able to draw images with and without blending.

■ Get the dimensions of the framebuffer.

I propose two simple interfaces: Graphics and Pixmap. Let's start with the Graphics interface, shown in Listing 3-6.

Listing 3-6. The Graphics Interface package com.badlogic.androidgames.framework;

public interface Graphics {

public static enum PixmapFormat { ARGB8888, ARGB4444, RGB565

public Pixmap newPixmap(String fileName, PixmapFormat format);

public void clear(int color);

public void drawPixel(int x, int y, int color);

public void drawLine(int x, int y, int x2, int y2, int color);

public void drawRect(int x, int y, int width, int height, int color);

public void drawPixmap(Pixmap pixmap, int x, int y, int srcX, int srcY, int srcWidth, int srcHeight);

public void drawPixmap(Pixmap pixmap, int x, int y);

public int getWidth();

public int getHeight();

We start with a public static enum called PixmapFormat. It encodes the different pixel formats we will support. Next we have the different methods of our Graphics interface:

■ The Graphics.newPixmap() method will load an image given in either JPEG or PNG format. We specify a desired format for the resulting Pixmap, which is a hint for the loading mechanism. The resulting Pixmap might have a different format. We do this so we can somewhat control the memory footprint of our loaded images (e.g., by loading RGB888 or ARGB8888 images as RGB565 or ARGB4444 images). The filename specifies an asset in our application's APK file.

■ The Graphics.clear() method clears the complete framebuffer with the given color. All colors in our little framework will be specified as 32-bit ARGB8888 values (Pixmaps might of course have a different format).

■ The Graphics.drawPixel() method will set the pixel at (x,y) in the framebuffer to the given color. Coordinates outside the screen will be ignored. This is called clipping.

■ The Graphics.drawLine() method is analogous to the Graphics.drawPixel() method. We specify the start point and endpoint of the line, along with a color. Any portion of the line that is outside the framebuffer's raster will be ignored.

■ The Graphics.drawRect() method draws a rectangle to the framebuffer. The (x,y) specifies the position of the rectangle's top-left corner in the framebuffer. The arguments width and height specify the number of pixels in x and y, and the rectangle will fill starting from (x,y). We fill downward in y. The color argument is the color that is used to fill the rectangle.

■ The Graphics.drawPixmap() method draws rectangular portions of a Pixmap to the framebuffer. The (x,y) coordinates specify the top-left corner's position of the Pixmap's target location in the framebuffer. The arguments srcX and srcY specify the corresponding top-left corner of the rectangular region that is used from the Pixmap, given in the Pixmap's own coordinate system. Finally, srcWidth and srcHeight specify the size of the portion that we take from the Pixmap.

■ Finally, the Graphics.getWidth() and Graphics.getHeight() methods return the width and height of the framebuffer in pixels.

All the drawing methods except Graphics.clear() will automatically perform blending for each pixel they touch, as outlined in the previous section. We could disable blending on a case-by-case basis to speed up the drawing a little bit, but that would complicate our implementation. Usually we can get away with having blending enabled all the time for simple games like Mr. Nom.

The Pixmap interface is given in Listing 3-7.

Listing 3-7. The Pixmap Interface package com.badlogic.androidgames.framework;

import com.badlogic.androidgames.framework.Graphics.PixmapFormat;

public interface Pixmap {

public int getWidth();

public int getHeight();

public PixmapFormat getFormat();

public void dispose();

We keep it very simple and immutable, as the compositing is done in the framebuffer.

■ The Pixmap.getWidth() and Pixmap.getHeight() methods return the width and the height of the Pixmap in pixels.

■ The Pixmap.getFormat() method returns the PixelFormat that the Pixmap is stored with in RAM.

■ Finally, there's the Pixmap.dispose() method. Pixmap instances use up memory and potentially other system resources. If we no longer need them, we should dispose of them with this method.

With this simple graphics module, we can implement Mr. Nom easily later on. Let's finish this chapter with a discussion of the game framework itself.

The Game Framework

After all the groundwork we've done, we can finally talk about how to actually implement the game itself. For that, let's identify what tasks have to be performed by our game:

■ The game is split up into different screens that each perform the same tasks: evaluating user input, applying the input to the state of the screen, and rendering the scene. Some screens might not need any user input, but transition to another screen after some time has passed (e.g., a splash screen).

■ The screens need to be managed somehow (e.g., we need to keep track of the current screen and have a way to transition to a new screen, which boils down to destroying the old screen and setting the new screen as the current screen).

■ The game needs to grant the screens access to the different modules (for graphics, audio, input, etc.) so they can load resources, fetch user input, play sounds, render to the framebuffer, and so on.

■ As our games will be in real time (that means things will be moving and updating constantly), we have to make the current screen update its state and render itself as often as possible. We'd normally do that inside a loop called the main loop. The loop will terminate when the user quits the game. A single iteration of this loop is called a frame. The number of frames per second (FPS) that we can compute is called the frame rate.

■ Speaking of time, we also need to keep track of the time span that has passed since our last frame. This is used for frame-independent movement, which we'll discuss in a minute.

■ The game needs to keep track of the window state (e.g., whether it got paused or resumed), and inform the current screen of these events.

■ The game framework will deal with setting up the window and creating the UI component we render to and receive input from.

Let's boil this down to some pseudocode, ignoring the window management events like pause and resume for a moment:

createWindowAndUIComponent();

Graphics graphics = new Graphics();

Screen currentScreen = new MainMenu();

Float lastFrameTime = currentTime();

float deltaTime = currentTime() - lastFrameTime; lastFrameTime = currentTime();

currentScreen.updateState(input, deltaTime); currentScreen.present(graphics, audio, deltaTime);

cleanupResources();

We start off by creating our game's window and the UI component we render to and receive input from. Next we instantiate all our modules necessary to do the low-level work. We instantiate our starting screen and make it the current screen, and record the current time. Then we enter the main loop, which will terminate if the user indicates that he wants to quit the game.

Within the game loop, we calculate the so-called delta time. This is the time that has passed since the beginning of the last frame. We then record the time of the beginning of the current frame. The delta time and the current time are usually given in seconds. For the screen, the delta time indicates how much time has passed since it was last updated — information that is needed if we want to do frame-independent movement (which we'll come back to in a minute).

Finally, we simply update the current screen's state and present it to the user. The update depends on the delta time as well as the input state, hence we provide those to the screen. The presentation consists of rendering the screen's state to the framebuffer, as well as playing back any audio the screen's state demands (e.g., due to a shot that got fired in the last update). The presentation method might also need to know how much time has passed since it was last invoked.

When the main loop is terminated, we can clean up and release all resources and close the window.

And that is how virtually every game works at a high level. Process the user input, update the state, present the state to the user, and repeat ad infinitum (or until the user is fed up with our game).

UI applications on modern operating systems do not usually work in real time. They work with an event-based paradigm, where the operating system informs the application of input events, as well as when to render itself. This is achieved by callbacks that the application registers with the operating system on startup; these are then responsible for processing received event notifications. All this happens in the so-called UI thread—the main thread of a UI application. It is generally a good idea to return from the callbacks as fast as possible, so we would not want to implement our main loop in one of these.

Instead, we host our game's main loop in a separate thread that we'll span when our game is firing up. This means that we have to take some precautions when we want to receive UI thread events, such as input events or window events. But those are details we'll deal with later on when we implement our game framework for Android. Just remember that we need to synchronize the UI thread and the game's main loop thread at certain points.

The Game and Screen Interfaces

With all that said, let's try to design a game interface. Here's what an implementation of this interface has to do:

■ Set up the window and UI component and hook into callbacks so we can receive window and input events.

■ Start the main loop thread.

■ Keep track of the current screen and tell it to update and present itself in each main loop iteration (aka frame).

■ Transfer any window events (e.g., pause and resume events) from the UI thread to the main loop thread and pass them on to the current screen so it can change its state accordingly.

■ Grant access to all the modules we developed earlier: Input, FilelO, Graphics, and Audio.

As game developers, we want to be agnostic about what thread our main loop is running on and whether we need to synchronize with a UI thread or not. We'd like to just implement the different game screens with a little help from the low-level modules and some notifications of window events. We will therefore create a very simple Game interface that hides all this complexity from us, as well as an abstract Screen class that we'll use to implement all our screens. Listing 3-8 shows the Game interface.

Listing 3-8. The Game Interface package com.badlogic.androidgames.framework;

public interface Game {

public Input getInput();

public FilelO getFileIO();

public Graphics getGraphics();

public Audio getAudio();

public void setScreen(Screen screen);

public Screen getCurrentScreen();

public Screen getStartScreen();

As expected, there are a couple of getter methods that return the instances of our low-level modules, which the Game implementation will instantiate and keep track off.

The Game.setScreen() method allows us to set the current Screen of the Game. These methods will be implemented once, along with all the internal thread creation, window management, and main loop logic that will constantly ask the current screen to present and update itself.

The Game.getCurrentScreen() method returns the currently active Screen.

We'll use an abstract class called AndroidGame later on to implement the Game interface, which will implement all methods except the Game.getStartScreen() method. This method will be an abstract method. If we create the AndroidGame instance for our actual game, we'll derive from it and override the Game.getStartScreen() method, returning an instance to the first screen of our game.

To give you an impression of how easy it will be to set up our game, here's an example (assuming we have already implemented the AndroidGame class):

public class MyAwesomeGame extends AndroidGame { public Screen getStartScreen () {

return new MySuperAwesomeStartScreen(this);

That is pretty awesome, isn't it? All we have to do is implement the screen we want our game to start with, and the AndroidGame class we'll derive from will do the rest for us. From that point onward, we'll have our MySuperAwesomeStartScreen be asked to update and render itself by the AndroidGame instance in the main loop thread. Note that we pass the MyAwesomeGame instance itself to the constructor of our Screen implementation.

NOTE: If you're wondering what actually instantiates our MyAwesomeGame class, I'll give you a hint: AndroidGame will be derived from Activity, which will be automatically instantiated by the Android operating system when a user starts our game.

The last piece in the puzzle is the abstract class Screen. We make it an abstract class instead of an interface so we can already implement some bookkeeping. This way we have to write less boilerplate code in the actual implementations of the abstract Screen class. Listing 3-9 shows the abstract Screen class.

Listing 3-9. The Screen Class package com.badlogic.androidgames.framework;

public abstract class Screen { protected final Game game;

public Screen(Game game) { this.game = game;

public Screen(Game game) { this.game = game;

public

abstract

void

update(float deltaTime);

public

abstract

void

present(float deltaTime);

public

abstract

void

pause();

public

abstract

void

resume();

public

abstract

void

dispose();

It turns out that the bookkeeping isn't so bad after all. The constructor receives the Game instance and stores it in a final member that's accessible to all subclasses. Via this mechanism we can achieve two things:

We can get access to the low-level modules of the Game to play back audio, draw to the screen, get user input, and read and write files.

We can set a new current Screen by invoking Game.setScreen() when appropriate (e.g., when a button is pressed that triggers a transition to a new screen).

The first point is pretty much obvious: our Screen implementation needs access to these modules so that it can actually do something meaningful, like rendering huge amounts of unicorns with rabies.

The second point allows us to implement our screen transitions easily within the Screen instances themselves. Each Screen can decide when to transition to which other Screen based on its state (e.g., when a menu button was pressed).

The methods Screen.update() and Screen.present() should be self-explanatory by now: they will update the screen state and present it accordingly. The Game instance will call them once in each iteration of the the main loop.

The methods Screen.pause() and Screen.resume() will be called when the game is paused or resumed. This is again done by the Game instance and applied to the currently active Screen.

The method Screen.dispose() will be called by the Game instance in case Game.setScreen() is called. The Game instance will dispose of the current Screen via this method and thereby give the Screen an opportunity to release all its system resources (e.g., graphical assets stored in Pixmaps) to make room for the new screen's resources in memory. The call to the Screen.dispose() method is also the last opportunity for a screen to make sure that any information that needs persistence is saved.

A Simple Example

Continuing with our MySuperAwesomeGame example, here is a very simple implementation of the MySuperAwesomeStartScreen class:

public class MySuperAwesomeStartScreen extends Screen { Pixmap awesomePic; int x;

public MySuperAwesomeStartScreen(Game game) { super(game);

awesomePic = game.getGraphics().newPixmap("data/pic.png", PixmapFormat.RGB565);

^Override public void update(float deltaTime) { x += 1;

^Override public void present(float deltaTime) { game.getGraphics().clear(0);

game.getGraphics().drawPixmap(awesomePic, x, 0, 0, 0, awesomePic.getWidth(), awesomePic.getHeight());

^Override public void pause() { // nothing to do here

^Override public void resume() { // nothing to do here

^Override public void dispose() { awesomePic.dispose();

Let's see what this class in combination with the MySuperAwesomeGame class will do:

1. When the MySuperAwesomeGame class is created, it will set up the window, the UI component we render to and receive events from, the callbacks to receive window and input events, and the main loop thread. Finally, it will call its own MySuperAwesomeGame.getStartScreen() method, which will return an instance of the MySuperAwesomeStartScreen() class.

2. In the MySuperAwesomeStartScreen constructor, we load a bitmap from disk and store it in a member variable. This completes our screen setup, and the control is handed back to the MySuperAwesomeGame class.

3. The main loop thread will now constantly call the MySuperAwesomeStartScreen.update() and

MySuperAwesomeStartScreen.render() methods of the instance we just created.

4. In the MySuperAwesomeStartScreen.update() method, we increase a member called x by one each frame. This member holds the x-coordinate of the image we want to render. When the x-coordinate is bigger than 100, we reset it to 0.

5. In the MySuperAwesomeStartScreen.render() method,we clear the framebuffer with the color black (0x00000000 = 0) and render our Pixmap at position (x,0).

6. The main loop thread will repeat steps 3 to 5 until the user quits the game by pressing the back button on his device. The Game instance will call then call the MySuperAwesomeStartScreen.dispose() method, which will dispose of the Pixmap.

And that's our first (not so) exciting game! All a user will see is that an image is moving from left to right on the screen. Not exactly a pleasant user experience, but we'll work on that later. Note that on Android, the game can be paused and resumed at any point in time. Our MyAwesomeGame implementation will then call the

MySuperAwesomeStartScreen.pause() and MySuperAwesomeStartScreen.resume() methods. The main loop thread will be paused for as long as the application itself is paused.

There's one last problem we have to talk about: frame-rate independent movement.

Frame Rate-Independent Movement

Let's assume that the user's device can run our game from the last section at 60 FPS. Our Pixmap will advance 100 pixels in 100 frames as we increment the MySuperAwesomeStartScreen.x member by 1 pixel each frame. At a frame rate of 60 FPS, it will take roughly 1.66 seconds to reach position (100,0).

Now let's assume that a second user plays our game on a different device. That device is capable of running our game at 30 FPS. Each second, our Pixmap advances by 30 pixels, so it takes 3.33 seconds to reach position (100,0).

This is bad. It may not have an impact on the user experience our simple game generates. But replace the Pixmap with Super Mario and think about what it would mean to move him in a frame-dependent manner. Say we hold down the right D-pad button so that Mario runs to the right. In each frame, we advance him by 1 pixel, as we do in case of our Pixmap. On a device that can run the game at 60 FPS, Mario would run twice as fast as on a device that runs the game at 30 FPS! This would totally change the user experience depending on the performance of the device. We need to fix this.

The solution to this problem is called frame-independent movement. Instead of moving our Pixmap (or Mario) by a fixed amount each frame, we specify the movement speed in units per second. Say we want our Pixmap to advance 50 pixels per second. In addition to the 50-pixels-per-second value, we also need information on how much time has passed since we last moved the Pixmap. And this is where this strange delta time comes into play. It tells us exactly how much time has passed since the last update. So our MySuperAwesomeStartScreen.update() method should look like this:

^Override public void update(float deltaTime) { x += 50 * deltaTime; if(x > 100) x = 0;

If our game runs at a constant 60 FPS, the delta time passed to the method will always be 1 / 60 ~ 0.016 seconds. In each frame we therefore advance by 50 x 0.016 ~ 0.83 pixels. At 60 FPS we advance 60 x 0.85 ~ 100 pixels! Let's test this with 30 FPS: 50 x 1 / 30 ~ 1.66. Multiplied by 30 FPS, we again move 100 pixels total each second. So, no matter how fast the device our game is running on can execute our game, our animation and movement will always be consistent with actual wall clock time.

If we actually tried this with our preceding code, our Pixmap wouldn't move at all at 60 FPS, though. This is because of a bug in our code. I'll give you some time to spot it. It's rather subtle, but a common pitfall in game development. The x member we increase each frame is actually an integer. Adding 0.83 to an integer will have no effect. To fix this we simply have to store x as a float instead of an int. This also means that we have to add a cast to int when we call Graphics.drawPixmap().

NOTE: While floating-point calculations are usually slower on Android than integer operations, the impact is mostly negligible, so we can get away with using more costly floating-point arithmetic.

And that is all there is to our game framework. We can directly translate the screens of our Mr. Nom design to our classes and interface of the framework. Of course, there are still some implementation details to tend to, but we'll leave that for a later chapter. For now you can be mighty proud of yourself that you kept on reading this chapter to the end: you are now ready to become a game developer for Android (and other platforms)!

0 0

Post a comment