The Singleton Pattern, Java HashMap, Storing Bitmaps Efficiently, and Designing Levels
If you have landed on this page from a search engine you should first view part 1 of this platform game tutorial. This is going to be a very busy and varied tutorial. We will learn the theory of the Singleton design pattern. We will then be introduced to another of the classes of the Java Collections, HashMap in which we will see how we can more efficiently store and make available the wide variety of bitmaps that are required for this project. We will also get started on our new and improved Transform class, code the first of the component-based classes, code the all-new Camera class, and make a significant start on some of the more familiar classes, GameState, PhysicsEngine, Renderer, GameEngine, and more besides.
Here is a list of what to expect and the order to expect it.
- The Singleton pattern
- The Java HashMap class
- The memory problem and the BitmapStore
- Coding a basic Transform
- Coding the block-based component classes
- Creating the game levels
- Coding a stripped-down GameObjectFactory
- Coding the GameState
- Coding the SoundEngine
- Coding the PhysicsEngine
- Coding the Renderer
- Explaining and coding the Camera class
- Coding the HUD
- Coding the UIController
- Coding the Activity
- Coding the GameEngine
- Coding a stripped-down LevelManger
- Running the game for the first time
By the end of this chapter, we will see the game in its first runnable state!
Throughout the course of this chapter, there will be loads of errors in Android Studio because so many of the classes are interconnected and we can’t code them all simultaneously. Note also that when there is an error in an interface file (and there will be) attempting to implement that interface will also cause an error in the implementing class. Stick with it until the end of this chapter as it all comes together, and we will see the first results on our screens. Be sure to read the information box at the top of chapter 22 as well as the one below for the quicker copy-and-paste approach you might like to take.
If you want to copy and paste the code read this next information box.
Feel free to copy and paste these classes if you prefer. All classes go in the usual folder except where I specifically point it out (for Level based classes). Be aware that my domain name is used in all the package names in the download bundle. The first package declaration in all the files will probably auto-update to your package name when you paste them into your project. However, when the import code refers to the packages that we have created …GOSpec (in the previous chapter) and …Level (later in this chapter) you will probably need to change the domain name and possibly the project name manually in some or maybe all of the files that import …GOSpec and …Level.
If you want to type everything then you will just need to add a new class of the correct name by right-clicking the appropriate folder and selecting New | Java Class.
Let’s talk about another pattern.
The Singleton pattern
The Singleton pattern as the name suggests is a pattern that is used when we want only one of something. And more importantly, we need to absolutely guarantee we only have one of something. The something I refer to is an instance of a class. Furthermore, the Singleton pattern is also used when we want to allow global access to some of its data or methods. The class can then be used from anywhere within a project and yet is also guaranteeing that all parts/classes of the project that access the class are using the exact same instance.
Part of the Singleton conundrum is simple. To make parts of it available to any other class you simply make the methods public and static.
But how do we guarantee that only one instance can ever be created? We will look at the code next but as a look-ahead what we will do is create a class that has a private constructor. Remember that a private method can only be called from within the class itself. And a public and static method that creates an instance then returns a reference- provided an instance has not already been created- and if it has then a reference to the existing instance is returned. This implies that the class will hold a reference to its own instance. Let’s look at this with some sample code.
The Singleton code
Here is the class declaration and it contains just one variable. A private and static instance of an object that is the same type as the class itself.
1 2 3 4 5 |
class SingletonClass { private static SingletonClass mOurInstance; } |
The name of the class is not relevant just that the name of the class is the same as the type being declared.
Next, we can look at the constructor method.
1 2 3 4 5 |
private SingletonClass() { // This is only here to prevent instantiation } |
It has the same name as the class as do all constructor methods but note it is private. When a method is private it can only be called by code within the class. Note that depending upon the needs of the project, it is perfectly acceptable to have some code in the constructor, just as long as the constructor is private.
This next method is default access and returns an object of type SingletonClass. Take a look over the code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Calling this method is the only way to get a BitmapStore static SingletonClass getInstance(Context context) { if(mOurInstance == null) { mOurInstance = new SingletonClass(); } return mOurInstance; } |
As the class is default access, any code in the same package can call the method. In addition, because it is static, it can be called without an instance of the class. Now look at the code in the body. The first line checks whether the private member, mOurInstance has been initialized with if(mOurInstance == null). If the object has not been initialized (is null) then the private constructor is called, and the instance is initialized. The final line of code returns a reference to the private instance. Also note as will be the case in the platform game, it might not even be necessary to return an instance.
Now we add a method that actually does something. After all, a class must have a purpose.
1 2 3 4 5 |
Static void someUsefulMethod() { // Do something useful } |
Note the previous method, someUsefulMethod is static so it too, like getInstance can be called without a reference to the class. This is why I said the getInstance method doesn’t necessarily have to return an instance of the class.
Next, we add another method. Note that it is not static.
1 2 3 4 5 |
void someOtherUsefulMethod() { // Do something else useful } |
As someOtherUsefulMethod is not static, an instance of the class would be needed in order to call it. So, in this specific case getInstance would need to return a reference to the object.
Now let’s see how we could use our new class in some other class of our project. First, we declare an instance of SingletonClass but as we cannot call the constructor we initialize it by calling the static getInstance method.
1 |
SingletonClass sc = SingletonClass.getInstance(); |
The getInstance method will check whether the member SingletonClass object needs to be initialized and if required calls the constructor. Either way, getInstance returns a reference to the private member instance of SingletonClass which initializes sc.
Now we can use the two methods that actually do something useful.
1 2 3 4 5 6 7 8 9 |
// Call the static method SingletonClass.someUsefulMethod(); // Call the other method sc.someOtherUsefulMethod(); |
The first method, someUsefulMethod is called without using a reference because it is static. The second method is called using the sc reference we initialized using getInstance.
It might surprise you to learn that the Singleton pattern is controversial. Its very use is discouraged and even banned in some situations. There are a number of reasons for this but in one-person or small-team projects, many of the objections are either not relevant or much less relevant. Android Studio even has a way to auto-generate a singleton. If you are interested in a discussion about the use of the Singleton pattern then a quick Google will bring up strong condemnations, spirited defenses, and even heated arguments about its use. I suggest you read this article because it gives a fairly balanced view of Singleton and from a game development perspective. http://gameprogrammingpatterns.com/singleton.html. Note that the article discusses Singleton in the context of a different programming language, C++ but the discussion is still useful.
If you are going for an interview for a programming job very soon you need to read the previous paragragh. Otherwise, enjoy your Singletons.
Next, we will learn about the Java HashMap class and then we will get to code a Singleton for real.
More Java Collections – Meet Java Hashmap
Java HashMaps are neat. They are part of the Java Collections and a kind of cousin to ArrayList. They basically encapsulate really useful data storage techniques that would otherwise be quite technical for us to code successfully for ourselves.
We will get practical with HashMap in the next section when we discuss a problem regarding storing Bitmap instances in our GameObject instances. HashMap will be the second part (Singletons are the first part) of the solution to this problem.
I thought it would be worth taking a first look at HashMap on its own.
Suppose, we want to store the data of lots of characters from an RPG-type game, and each different character is represented by an object of type Character.
We could use some of the Java tools we already know about like arrays or ArrayList. However, Java HashMap is also similar to these things but with HashMap we can give a unique key/identifier to each Character object and access any such object using that key/identifier.
The term hash comes from the process of turning our chosen key/identifier into something used internally by the HashMap class. The process is called hashing.
Any of our Character instances can then be accessed with our chosen key/identifier. A good candidate for a key/identifier in the Character class scenario would be the character’s name.
Each key/identifier has a corresponding object, in this case, it is of type Character. This is known as a key-value pair.
We just give HashMap a key and it gives us the corresponding object. No need to worry about which index we stored our characters, perhaps Geralt, Ciri, or Triss at, just pass the name to HashMap and it will do the work for us.
Let’s look at some examples. You don’t need to type any of this code just get familiar with how it works.
We can declare a new HashMap to hold keys and Character instances like this code:
1 |
Map<String, Character> characterMap; |
The previous code assumes we have coded a class called Character.
We can initialize the HashMap like this:
1 |
characterMap = new HashMap(); |
We can add a new key and its associated object like this.
1 |
characterMap.put(“Geralt”, new Character()); |
And this:
1 |
characterMap.put(“Ciri”, new Character()); |
And this:
1 |
characterMap.put(“Triss”, new Character()); |
All the example code assumes that we can somehow give the Character instances their unique properties to reflect their internal differences elsewhere.
We can then retrieve an entry from the HashMap like this:
1 |
Character ciri = characterMap.get(“Ciri”); |
Or perhaps use the Character class’s methods directly like this:
1 2 3 4 5 |
characterMap.get(“Geralt”).drawSilverSword(); // Or maybe call some other hypothetical method characterMap.get(“Triss”).openFastTravelPortal(“Kaer Morhen”); |
The previous code calls the hypothetical methods drawSilverSword and openFastTravelPortal on the Character class.
The HashMap class also has lots of useful methods like ArrayList. See the official Java page for HashMap here: https://docs.oracle.com/javase/tutorial/collections/interfaces/map.html.
Now we can use HashMap for real. Before we do let’s look ahead in this project and we will see that we have a bit of a memory problem.
The memory problem and the BitmapStore
In all the projects on this website so far, the class representing the object in the game also held a copy of the Bitmap.
When we have just one or a few of each object as we did in previous projects this is not a problem but, in this project, we will have more than a hundred of some of the Bitmaps representing the tiles that make the platforms. At best this is inefficient and will waste memory, and device power and make the game run more slowly and at worst (especially on older devices) the game will crash because it has run out of memory.
We need a way to share Bitmap instances between objects. This suggests a central store. Hey; what about that HashMap and that Singleton thing we just learned about? How convenient.
Coding the BitmapStore class
All our game objects already have a specification that holds the name of the required Bitmap in a String. We have also just learned how we can store objects in a HashMap and retrieve them using a key. In addition, we know how to make a class available to all other classes using the Singleton pattern.
What we will do is code a Singleton class called BitmapStore which will hold all the required Bitmap instances using the bitmapName String (from the graphics-based component classes). We will however only store one of each Bitmap.
When the initialize method of the graphics-related component class executes instead of initializing its own Bitmap it will call the addBitmap method of the BitmapStore class. If the Bitmap in question has not already been added to the HashMap then it will do so. When the Bitmap needs to be drawn (during the draw method) each object will simply call the getBitmap method of the BitmapStore class. The BitmapStore, as we will see, will have two HashMaps, one for the regular Bitmap instances and one for reversed instances. The BitmapStore class will also have a getReversedBitmap method.
Create a new class called BitmapStore, and add the following members and the constructor as shown next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.graphics.PointF; import java.util.HashMap; import java.util.Map; class BitmapStore { private static Map<String, Bitmap> mBitmapsMap; private static Map<String, Bitmap> mBitmapsReversedMap; private static BitmapStore mOurInstance; // Calling this method is the only way to get a BitmapStore static BitmapStore getInstance(Context context) { mOurInstance = new BitmapStore(context); return mOurInstance; } // Can't be called using new BitmapStore() private BitmapStore(Context c) { mBitmapsMap = new HashMap(); mBitmapsReversedMap = new HashMap(); // Put a default bitmap in each of the maps // to return in case a bitmap doesn't exist addBitmap(c, "death_visible", new PointF(1, 1), 128, true); } } |
There are three private members. Two HashMaps called mBitmapsMap and mBitmapsReversedMap. They will hold all the Bitmaps and reversed Bitmaps (when an object requires it) respectively. Notice that they use a String as the key and of course a Bitmap as its pair.
The third and final private member should be as expected. It is an instance of BitmapStore itself as discussed in the Singleton pattern. And, also as should be expected the constructor is private. It cannot be called from outside of the BitmapStore class.
As expected with the Singleton pattern we have a static getInstance method that calls the BitmapStore constructor if it hasn’t done so already and then returns a reference to the one and only instance of the class.
Inside this constructor, the two HashMap instances are initialized and the addBitmap method is called. We will code the addBitmap method shortly so there will be an error because the method doesn’t exist yet. It is worth explaining the details of this call now, however.
If you look at the arguments in the call to addBitmap you will see, we pass in the Context (c), a PointF containing a size (which will allow addBitmap to scale the Bitmap) a somewhat arbitrary value of 128 and a boolean with a value of true.
The value of 128 is a value that has been predetermined as the number of pixels on the device which represents one meter of the game world. If the game world is to be scaled by our soon-to-be-coded Camera class, then it needs to know what to scale it to. When you see the Camera class later this chapter this will be clearer.
The final boolean indicates whether a reversed copy of the Bitmap is required as well.
We mentioned in the previous chapter while exploring the graphical assets that the death_visible graphic is for creating an inescapable wall around the level which will kill the player if touched. This helps avoid bugs like the player falling out of the level and then tumbling for eternity through space. The graphic is noticeable, so the level tester can clearly see that he has sufficiently wrapped the level during the development phase and then before the game is released it can be replaced with a blank graphic so that only the background is visible, but the player is still contained within where the level designer wants him.
All the other calls to addBitmap will be made by the initialize method within the graphics-related component classes.
Now add the getBitmap and getBitmapReversed methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
static Bitmap getBitmap(String bitmapName) { if (mBitmapsMap.containsKey(bitmapName)) { return mBitmapsMap.get(bitmapName); } else { return mBitmapsMap.get("death_visible"); } } static Bitmap getBitmapReversed(String bitmapName) { if (mBitmapsReversedMap.containsKey(bitmapName)) { return mBitmapsReversedMap.get(bitmapName); } else { return mBitmapsReversedMap.get("death_visible"); } } |
All that these methods do is receive a String and then return the Bitmap that matches the key. The return statements are wrapped in an if-else block so that when a Bitmap does not exist the death_visible Bitmap is returned. This might at first seem odd. When we see how to lay out level designs we will see that we specifically have a way of adding these death_visible objects. And we will specifically request the death_visible Bitmap when it is required. By returning the death_visible Bitmap as a default, when we neglect to add a Bitmap the death_visible Bitmap will appear within the level when the game is tested, and we will know we have a Bitmap missing. In addition, it will prevent the game from crashing as it would if an uninitialized Bitmap was returned.
Add the addBitmap method shown next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
static void addBitmap(Context c, String bitmapName, PointF objectSize, int pixelsPerMetre, boolean needReversed) { Bitmap bitmap; Bitmap bitmapReversed; // Make a resource id out of the string of the file name int resID = c.getResources().getIdentifier(bitmapName, "drawable", c.getPackageName()); // Load the bitmap using the id bitmap = BitmapFactory .decodeResource(c.getResources(), resID); // Resize the bitmap bitmap = Bitmap.createScaledBitmap(bitmap, (int) objectSize.x * pixelsPerMetre, (int) objectSize.y * pixelsPerMetre, false); mBitmapsMap.put(bitmapName, bitmap); if (needReversed) { // Create a mirror image of the bitmap Matrix matrix = new Matrix(); matrix.setScale(-1, 1); bitmapReversed = Bitmap.createBitmap( bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); mBitmapsReversedMap.put(bitmapName, bitmapReversed); } } |
The addBitmap method should look quite familiar by now. The code loads the Bitmap from the String representing the Bitmap’s name. The Bitmap is scaled in the usual way using createScaledBitmap. The only difference to what we saw in the graphics-based component classes of the previous project is that the size of the object and the value representing the pixels-per-metre is used to determine the size.
The Bitmap is added to the HashMap and if a reversed copy of the Bitmap has been requested then the Matrix class is used to create a reversed copy and the Bitmap is added to the HashMap for reversed Bitmaps.
Add the clearStore method shown next.
1 2 3 4 5 6 7 |
static void clearStore() { mBitmapsMap.clear(); mBitmapsReversedMap.clear(); } |
The clearStore method does two things. It first uses the clear method of the HashMap class to empty the mBitmapsMap HashMap and then does the same thing for the mBitmapsReversedMap HashMap. This is needed because typically each level will use a different selection of graphics files and if you don’t clear them then every time the player attempts a different level they will be left with unnecessary bitmaps in the store.
To be realistic about this, it really wouldn’t matter even for a very basic device to have every single bitmap loaded at once- it’s still way more efficient than we have been up until now as we used to load a bitmap for every instance of every game object. But if you added more levels and more object specifications then eventually your game would perform poorly so it is good practice to clear the store before loading the required bitmaps for the current level. This is the purpose of this method.
Coding the basic transform
In this project, we will do a better job of the Transform class. In the Scrolling Shooter project, the Transform was packed full of variables and methods that many of the objects didn’t need. It served its purpose to prevent the project from getting even bigger and we got away with it because there was a limited number of objects in the game.
What we will code now is a very simple version of the Transform and it will contain only the members and methods needed by all game objects. When we need a Transform that does more specific things we can then extend this class and add the required extra members and methods.
Add a new class called Transform. Code the member variables and the constructor as shown next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
import android.graphics.PointF; import android.graphics.RectF; public class Transform { RectF mCollider; private PointF mLocation; private float mSpeed; private float mObjectHeight; private float mObjectWidth; private PointF mStartingPosition; private boolean mHeadingUp = false; private boolean mHeadingDown = false; private boolean mFacingRight = true; private boolean mHeadingLeft = false; private boolean mHeadingRight = false; Transform(float speed, float objectWidth, float objectHeight, PointF startingLocation) { mCollider = new RectF(); mSpeed = speed; mObjectHeight = objectHeight; mObjectWidth = objectWidth; mLocation = startingLocation; // This tells movable blocks their starting position mStartingPosition = new PointF( mLocation.x, mLocation.y); } } |
The members should look quite familiar and self-explanatory. We have a RectF to represent the collider (mCollider), PointF instances for the current location and the starting location (mLocation and mStartingPosition), float variables for speed, height, and width (mSpeed, mObjectHeight, and mObjectWidth), and boolean variables for each direction the object could be heading.
In the constructor, all the variables representing position, size, and speed are initialized. All the values come from the method’s parameters. We will see later in the chapter that the GameObjectFactory class will have access to all the specifications needed for a level and will instantiate all the Transform instances by passing the data we have just seen to the Transform constructor. Just as it was in the previous project except this time GameObjectFactory will have more than one type of Transform to choose from.
Code updateCollider and getCollider methods are shown next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public void updateCollider() { mCollider.top = mLocation.y; mCollider.left = mLocation.x ; mCollider.bottom = (mCollider.top + mObjectHeight); mCollider.right = (mCollider.left + mObjectWidth); } public RectF getCollider() { return mCollider; } |
The updateCollider method uses the position of the object along with its width and height to make sure the collider is up-to-date so that collision detection can be done on its precise position. The getCollider method makes the collider available to the PhysicsEngine class so collision detection can be performed.
Add this bunch of getters and setters shown next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
void headUp() { mHeadingUp = true; mHeadingDown = false; } void headDown() { mHeadingDown = true; mHeadingUp = false; } boolean headingUp() { return mHeadingUp; } boolean headingDown() { return mHeadingDown; } float getSpeed() { return mSpeed; } PointF getLocation() { return mLocation; } PointF getSize() { return new PointF( (int) mObjectWidth, (int) mObjectHeight); } |
Although quite a long list the methods we just added are quite simple. They allow the instances of the Transform class to share and set the values of some of its private member variables.
Add these getters and setters next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
void headRight() { mHeadingRight = true; mHeadingLeft = false; mFacingRight = true; } void headLeft() { mHeadingLeft = true; mHeadingRight = false; mFacingRight = false; } boolean headingRight() { return mHeadingRight; } boolean headingLeft() { return mHeadingLeft; } void stopHorizontal() { mHeadingLeft = false; mHeadingRight = false; } void stopMovingLeft() { mHeadingLeft = false; } void stopMovingRight() { mHeadingRight = false; } boolean getFacingRight() { return mFacingRight; } PointF getStartingPosition(){ return mStartingPosition; } |
These are the final methods for the Transform class. They make available to get and set, more of the members of the Transform class. Familiarize yourself with the method names and the variables they work with.
Coding the inanimate and decorative components
In the previous tutorial, we coded all the interfaces for our component classes. We coded GraphicsComponent, UpdateComponent, and InputComponent. Now we will code the three simplest component classes that will be enough to complete quite a few of our specifications.
Let’s start with the DecorativeBlockUpdateComponent.
DecorativeBlockUpdateComponent
The update-related components all implement just one method, update. Add a new class called DecorativeBlockUpdateComponent, implement UpdateComponent and add the empty update method as shown next.
class DecorativeBlockUpdateComponent implements UpdateComponent {
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Override public void update(long fps, Transform t, Transform playerTransform) { // Do nothing // Not even set a collider } } |
Yes, the update method is meant to be empty as the decorative objects don’t move or collide.
InanmiateBlockGraphicsComponent
Add a new class called InanimateBlockGraphicsComponent and add the code shown next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.Rect; import com.gamecodeschool. platformer.GOSpec.GameObjectSpec; class InanimateBlockGraphicsComponent implements GraphicsComponent { private String mBitmapName; @Override public void initialize(Context context, GameObjectSpec spec, PointF objectSize, int pixelsPerMetre) { mBitmapName = spec.getBitmapName(); BitmapStore.addBitmap(context, mBitmapName, objectSize, pixelsPerMetre, false); } } |
Note the highlighted import might need correcting if you are copying and pasting the class files.
Notice that despite this being a graphics-based component there is no Bitmap object, just a String to hold the name. The class implements the GraphicsComponent interface and therefore must provide the implementation for the initialize and draw methods.
In the code you just added you can see that it receives the required data in the parameters of the initialize method then retrieves the name of the Bitmap from the GameObjectSpec reference.
All the method needs to do is pass all the data on to the addBitmap method of the BitmapStore class and all the work of initializing a Bitmap and storing it for later use (if it hasn’t already) is taken care of by BitmapStore. Just note that the name of the Bitmap is retained in the mBitmapName member variable so InanimateBlockGraphicsComponent can later access the Bitmap again using getBitmap.
Add the draw method to the InanimateBlockGraphicsComponent as shown next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
@Override public void draw(Canvas canvas, Paint paint, Transform t, Camera cam) { Bitmap bitmap = BitmapStore.getBitmap(mBitmapName); // Use the camera to translate the real world // coordinates relative to the player- // into screen coordinates Rect screenCoordinates = cam.worldToScreen( t.getLocation().x, t.getLocation().y, t.getSize().x, t.getSize().y); canvas.drawBitmap( bitmap, screenCoordinates.left, screenCoordinates.top, paint); } |
Inside the draw method, the getBitmap method of the BitmapStore class is used to initialize a local Bitmap object. The Bitmap is drawn to the screen in quite a standard way using drawBitmap but before it is drawn the Camera reference cam is used to get the coordinates at which to draw this game object.
Remember that all game objects have world coordinates. The game objects don’t know or care anything about the device’s screen resolution. The Camera class (in the worldToScreen method) does a calculation based on the position of the camera to translate the object’s world coordinates into appropriate screen coordinates. As we will see later in the chapter, the camera follows the player. So as the player moves through the game world the camera adjusts where on the screen (if anywhere) the game objects get drawn.
InanimateBlockUpdateComponent
Now add a new class called InanimateBlockUpdateComponent and code it as follows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class InanimateBlockUpdateComponent implements UpdateComponent { private boolean mColliderNotSet = true; @Override public void update(long fps, Transform t, Transform playerTransform) { // An alternative would be to update // the collider just once when it spawns. // But this would require spawn components // - More code but a bit faster if(mColliderNotSet) { // Only need to set the collider // once because it will never move t.updateCollider(); mColliderNotSet = false; } } } |
The class implements UpdateComponent and therefore has an update method. It has one member variable, a boolean called mColliderNotSet. In the update method, the mColliderNotSet boolean is checked to test whether the updateCollider method on the Transform needs to be called. The boolean is then set to false to save a few CPU cycles for the rest of the game. As this object will never move in the game world (although it will move on the screen relative to the camera) this single call to updateCollider is sufficient.
Now we have completed three components we can make some levels that have objects that use these components.
Creating the levels
Before we make some levels let’s create another new package to put them in. As a reminder from the previous chapter, when you create a new package, it is important to do so in the correct folder. The correct folder is the same one that we have been putting all the Java files in throughout this book. If you called this project Platformer it will be the app/java/yourdomain.platformer folder.
In the Android Studio project explorer, right-click the app/java/yourdomain.platformer (NOT yourdomain.platformer(androidTest) or yourdomain.platformer(test)), select New | Package and name it Levels.
The step of creating a package is essential before you can copy and paste the class files.
We will now add some classes that will define all the layout of the levels of the game starting with a generic Level class which all the others can extend.
Don’t add this code yet, just glance at the different letters and numbers in the layouts that will represent different game objects.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// Backgrounds 1, 2, 3(City, Underground, Mountain...) // p = Player // g = Grass tile // o = Objective // m = Movable platform // b = Brick tile // c = mine Cart // s = Stone pile // l = coaL // n = coNcrete // a = lAmpost // r = scoRched tile // w = snoW tile // t = stalacTite // i = stalagmIte // d = Dead tree // e = snowy trEe // x = Collectable // z = Fire // y = invisible death_invisible |
Now let’s see the actual levels comprising the letters and numbers starting with the base class.
Level
This class is really short. It will just be used as a polymorphic type, so we can use the extended classes in common code.
Create a new class called Level and add the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package com.gamecodeschool.c22platformer.Levels; import java.util.ArrayList; public abstract class Level { // If you want to build a new level then extend this class ArrayList<String> tiles; public ArrayList<String> getTiles(){ return tiles; } } |
Change the highlighted package statement if copying & pasting
That’s it. It just has an ArrayList called tiles to hold all the game objects and a getter called getTiles which unsurprisingly returns a reference to the ArrayList.
LevelCity
We have already seen these next couple of images in the previous chapter. They are shown again here for completeness. Copy the LevelCity file from the Chapter 23/Levels folder of the download bundle. It is not recommended that you try and manually add the code. Paste the file into the Levels folder/package of your project in the Android Studio project explorer window.
This is what the level looks like at the end of the project while the camera is zoomed-out.
Next, add the Montains level.
LevelMountains
Copy the LevelMountains file from the Chapter 23/Levels folder of the download bundle. It is not recommended that you try and manually add the code. Paste the file into the Levels folder/package of your project in the Android Studio project explorer window.
This is what the level looks like at the end of the project while the camera is zoomed-out.
Now for the final level.
LevelUnderground
Copy the LevelUnderground file from the Chapter 23/Levels folder of the download bundle. It is not recommended that you try and manually add the code. Paste the file into the Levels folder/package of your project in the Android Studio project explorer window.
This is what the level looks like at the end of the project while the camera is zoomed-out.
I spent quite a bit of time designing and testing the City level. It should be quite challenging and possible to keep replaying and improving on your best time. The Mountains and Underground levels I quickly threw together for the sake of showing how the level structure works and how easy it is to have multiple levels. They are intended as a project for the reader to improve or perhaps start again from scratch.
Coding a stripped-down GameObjectFactory
The next step is to code the GameObjectFactory class. This class will look very similar to the class of the same name in the previous project but there will be a few differences that I will point out. We will code just enough in order to build the game objects that are ready to be built and we will revisit this class in the next chapter once we have finished coding all the component classes.
In this and other upcoming classes remember to check that when you import your own classes (everything from the GOSpec package) make sure/change to the correct package name.
Add a new class called GameObjectFactory and add the following member variables and constructor method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import android.content.Context; import android.graphics.PointF; import com.gamecodeschool.platformer.GOSpec.GameObjectSpec; class GameObjectFactory { private Context mContext; private GameEngine mGameEngineReference; private int mPixelsPerMetre; GameObjectFactory(Context context, GameEngine gameEngine, int pixelsPerMetre) { mContext = context; mGameEngineReference = gameEngine; mPixelsPerMetre = pixelsPerMetre; } } |
The GameObjectFactory needs three members. A Context and an int with the number of pixels for every meter of the game world so it can pass them to the graphics-related component classes and a GameEngine reference, so it can pass it to any of the input-related component classes, so they can register as observers. These three members (mContext, mGameEnginrReference and mPixelsPerMetre) are initialized in the constructor.
Next, add the create method. We will come back to this method in the next chapter and add more code to the switch block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
GameObject create(GameObjectSpec spec, PointF location) { GameObject object = new GameObject(); int mNumComponents = spec.getComponents().length; object.setTag(spec.getTag()); // First give the game object the // right kind of transform switch(object.getTag()){ case "Background": // Code coming soon break; case "Player": // Code coming soon break; default:// normal transform object.setTransform(new Transform( spec.getSpeed(), spec.getSize().x, spec.getSize().y, location)); break; } // Loop through and add/initialize all the components for (int i = 0; i < mNumComponents; i++) { switch (spec.getComponents()[i]) { case "PlayerInputComponent": // Code coming soon break; case "AnimatedGraphicsComponent": // Code coming soon break; case "PlayerUpdateComponent": // Code coming soon break; case "InanimateBlockGraphicsComponent": object.setGraphics(new InanimateBlockGraphicsComponent(), mContext, spec, spec.getSize(), mPixelsPerMetre); break; case "InanimateBlockUpdateComponent": object.setMovement(new InanimateBlockUpdateComponent()); break; case "MovableBlockUpdateComponent": // Code coming soon break; case "DecorativeBlockUpdateComponent": object.setMovement(new DecorativeBlockUpdateComponent()); break; case "BackgroundGraphicsComponent": // Code coming soon break; case "BackgroundUpdateComponent": // Code coming soon break; default: // Error unidentified component break; } } // Return the completed GameObject // to the LevelManager class return object; } |
First, in the create method a new instance of GameObject is declared and initialized. Just as we did in the previous project we capture the number of components in the current specification and then call the setTag method on the GameObject instance. The GameObject can now be properly identified by a tag.
Next, we see something new compared to the previous GameObjectFactory from the Scrolling Shooter project. We switch based on the tag of the specification. There are three possible case statements that can be executed. One for “Background”, one for “Player” and a default as well.
For now, we just add code to the default option that calls the setTransform method on the GameObject instance and passes in a new Transform reference. In the next chapter we will extend Transform twice to make special versions for the player and the backgrounds. This is how we make sure that every object gets the Transform it needs.
Next, we loop around a for loop once for each component in the specification. And just as we did in the previous project we initialize the appropriate component at each case statement by calling either setGraphics or setMovement on the GameObject instance and passing in a new component of the appropriate type according to the specification.
Also note I have added all the case statements to handle all the other components although most of them are currently empty. This will make it easy to show where the new code goes in the next chapter.
Coding a slightly commented-out game object
The game is coming together nicely, and we can now turn our attention to the GameObject class. Much will look familiar to the previous project. Create a class called GameObject and add the following member variables.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PointF; import com.gamecodeschool.platformer.GOSpec.GameObjectSpec; class GameObject { private Transform mTransform; private boolean mActive = true; private String mTag; private GraphicsComponent mGraphicsComponent; private UpdateComponent mUpdateComponent; } |
Check the highlighted import statement is correct for your project.
The members are almost the same as the previous project except we have an instance of an UpdateComponent replacing an instance of a MovementComponent.
Next, add the following methods which are used to initialize the component-based and Transform instances that we just declared.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
void setGraphics(GraphicsComponent g, Context c, GameObjectSpec spec, PointF objectSize, int pixelsPerMetre) { mGraphicsComponent = g; g.initialize(c, spec, objectSize, pixelsPerMetre); } void setMovement(UpdateComponent m) { mUpdateComponent = m; } /* Uncomment this code soon void setPlayerInputTransform(PlayerInputComponent s) { s.setTransform(mTransform); } */ void setTransform(Transform t) { mTransform = t; } |
The methods we just coded are called from GameObjectFactory and pass in the specific instances of the GraphicsComponent, UpdateComponent and Transform.
Notice I have commented out setPlayerInputTransform as we haven’t coded this Transform extended class yet. Commenting it out will enable us to run the project at an earlier opportunity.
Add the rest of the methods to the GameObject class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
void draw(Canvas canvas, Paint paint, Camera cam) { mGraphicsComponent.draw(canvas, paint, mTransform, cam); } void update(long fps, Transform playerTransform) { mUpdateComponent.update(fps, mTransform, playerTransform); } boolean checkActive() { return mActive; } String getTag() { return mTag; } void setInactive() { mActive = false; } Transform getTransform() { return mTransform; } void setTag(String tag) { mTag = tag; } |
The code we just added includes the key update and draw methods which are called each frame and the familiar bunch of getter methods for getting data from the GameObject instance.
Coding the GameState
The GameState class holds multiple fastest times, and which level is being played. The GameState class also takes care of knowing (and sharing) the current state of paused, playing, drawing, thread running, etc.
Create a new class called GameState and add the member variables and constructor as shown next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
import android.content.Context; import android.content.SharedPreferences; final class GameState { private static volatile boolean mThreadRunning = false; private static volatile boolean mPaused = true; private static volatile boolean mGameOver = true; private static volatile boolean mDrawing = false; private EngineController engineController; private int mFastestUnderground; private int mFastestMountains; private int mFastestCity; private long startTimeInMillis; private int mCoinsAvailable; private int coinsCollected; private SharedPreferences.Editor editor; private String currentLevel; GameState(EngineController gs, Context context) { engineController = gs; SharedPreferences prefs = context .getSharedPreferences("HiScore", Context.MODE_PRIVATE); editor = prefs.edit(); mFastestUnderground = prefs.getInt( "fastest_underground_time", 1000); mFastestMountains = prefs.getInt( "fastest_mountains_time", 1000); mFastestCity = prefs.getInt( "fastest_city_time", 1000); } } |
We have Boolean variables to represent whether the game thread is running, the player has paused, the game is over or currently drawing. We also have an EngineController reference so GameState can reinitialize a game/level directly.
Next up we have three int variables to hold the fastest time on each of the three levels. The startTimeInMillis variable will be initialized each time a level is attempted to record the time the level was started so it is possible to calculate how long the level took.
There are two more int members to hold the number of coins it is possible to collect in a level and the number of coins actually collected. They are mCoinsAvailable and coinsCollected.
The final two members in the previous code is an instance of SharedPrefferences.Editor for writing new high scores and a String which will represent the current level to be played, City, Underground or Mountains.
Now add this quite a long list of getters and setters to the GameState class.
v
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
oid coinCollected() { coinsCollected++; } int getCoinsRemaining() { return mCoinsAvailable - coinsCollected; } void coinAddedToLevel() { mCoinsAvailable++; } void resetCoins() { mCoinsAvailable = 0; coinsCollected = 0; } void setCurrentLevel(String level) { currentLevel = level; } String getCurrentLevel() { return currentLevel; } void objectiveReached() { endGame(); } int getFastestUnderground() { return mFastestUnderground; } int getFastestMountains() { return mFastestMountains; } int getFastestCity() { return mFastestCity; } |
A detailed description of each of the methods we just added would be somewhat laborious because they each do just one thing.
- Set a value(s)
- Return a value
- Call another method
It is, however, well worth closely inspecting each method’s name to aid understanding as we proceed.
Next, add three more methods to the GameState class for starting a new game, getting the current time, and taking action when the player dies.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
void startNewGame() { // Don't want to be handling objects while // clearing ArrayList and filling it up again stopEverything(); engineController.startNewLevel(); startEverything(); startTimeInMillis = System.currentTimeMillis(); } int getCurrentTime() { long MILLIS_IN_SECOND = 1000; return (int) ((System.currentTimeMillis() - startTimeInMillis) / MILLIS_IN_SECOND); } void death() { stopEverything(); SoundEngine.playPlayerBurn(); } |
The startNewGame method calls the stopEverything method. We will code the stopEverything method soon. The next line of code uses the GameController reference to call the startNewLevel method on the GameEngine class. Once the startNewLevel method has done its work we call the startEverything method (which we will code soon) to get things going again. The reason we do these three steps is that otherwise, we will be trying to update and draw objects at the same time as the GameEngine is also deleting and reinitializing them. This would be bound to cause a crash. The last thing we do in startNewGame is initialize the startTimeInMillis variable with the current time.
The getCurrentTime method shares the current time. Note that it takes the start time from the current time and divides the result by one thousand. This is because we want the player to see their time in seconds not milliseconds.
The death method simply calls stopEverything and then uses the SoundEngine to play a death sound.
Now code the endGame method and then we will discuss it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
private void endGame() { stopEverything(); int totalTime = ((mCoinsAvailable - coinsCollected) * 10) + getCurrentTime(); switch (currentLevel) { case "underground": if (totalTime < mFastestUnderground) { mFastestUnderground = totalTime; // Save new time editor.putInt("fastest_underground_time", mFastestUnderground); editor.commit(); } break; case "city": if (totalTime < mFastestCity) { mFastestCity = totalTime; // Save new time editor.putInt("fastest_city_time", mFastestCity); editor.commit(); } break; case "mountains": if (totalTime < mFastestMountains) { mFastestMountains = totalTime; // Save new time editor.putInt("fastest_mountains_time", mFastestMountains); editor.commit(); } break; } } |
The first line of code in endGame calls stopEverything so the game engine will halt updating and drawing.
Next, a local variable, totalTime is declared and initialized. The total time (as you might remember from Chapter 22: Platform Game: Bob was in a hurry section) is calculated by the total time the player took added to a penalty for each coin that the player failed to collect.
Next in the endGame method we enter a switch block where the condition is the curremtLevel String. The first case is when the underground level has been played. The code uses an if statement to check if totalTime is less than mFastestUnderground. If it is then the player has a new fastest time. The editor.putInt and editor.commit methods of the SharedPrefferences.Editor instance are then used to save the new record for posterity.
The next two case blocks do the same thing for the city then the mountains levels.
Now code the final methods for the GameState class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
void stopEverything() {// Except the thread mPaused = true; mGameOver = true; mDrawing = false; } private void startEverything() { mPaused = false; mGameOver = false; mDrawing = true; } void stopThread() { mThreadRunning = false; } boolean getThreadRunning() { return mThreadRunning; } void startThread() { mThreadRunning = true; } boolean getDrawing() { return mDrawing; } boolean getPaused() { return mPaused; } boolean getGameOver() { return mGameOver; } |
The final getters and setters control when the game engine does certain tasks like start/stop the thread and call update and/or draw. Familiarize yourself with their names and which members they interact with.
Let’s make some noise.
Code the SoundEngine
Fortunately, the SoundEngine class holds no surprises. This is essentially the exact same class as we coded in the Scrolling Shooter project. The only exceptions are that we load different sound effects and the methods which play the sound effects have different names and play different sounds. We have also made it a Singleton to avoid the long parameter lists that we had in the previous project when we passed a SoundEngine reference into so many other classes/methods.
Add the following code for the member variables and the getInstance method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import android.media.AudioAttributes; import android.media.AudioManager; import android.media.SoundPool; import android.os.Build; import java.io.IOException; class SoundEngine { // for playing sound effects private static SoundPool mSP; private static int mJump_ID = -1; private static int mReach_Objective_ID = -1; private static int mCoin_Pickup_ID = -1; private static int mPlayer_Burn_ID = -1; private static SoundEngine ourInstance; public static SoundEngine getInstance(Context context) { ourInstance = new SoundEngine(context); return ourInstance; } } |
The getInstance method provides access to the full functionality of the class as well as calling the constructor method to initialize all the sound effects into a SoundPool. Add the constructor method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
public SoundEngine(Context c){ // Initialize the SoundPool if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { AudioAttributes audioAttributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes .CONTENT_TYPE_SONIFICATION) .build(); mSP = new SoundPool.Builder() .setMaxStreams(5) .setAudioAttributes(audioAttributes) .build(); } else { mSP = new SoundPool(5, AudioManager.STREAM_MUSIC, 0); } try { AssetManager assetManager = c.getAssets(); AssetFileDescriptor descriptor; // Prepare the sounds in memory descriptor = assetManager.openFd("jump.ogg"); mJump_ID = mSP.load(descriptor, 0); descriptor = assetManager.openFd("reach_objective.ogg"); mReach_Objective_ID = mSP.load(descriptor, 0); descriptor = assetManager.openFd("coin_pickup.ogg"); mCoin_Pickup_ID = mSP.load(descriptor, 0); descriptor = assetManager.openFd("player_burn.ogg"); mPlayer_Burn_ID = mSP.load(descriptor, 0); } catch (IOException e) { // Error } } Now the sound files are loaded in to memory ready to play. Add the methods that play the sounds, as follows. public static void playJump(){ mSP.play(mJump_ID,1, 1, 0, 0, 1); } public static void playReachObjective(){ mSP.play(mReach_Objective_ID,1, 1, 0, 0, 1); } public static void playCoinPickup(){ mSP.play(mCoin_Pickup_ID,1, 1, 0, 0, 1); } public static void playPlayerBurn(){ mSP.play(mPlayer_Burn_ID,1, 1, 0, 0, 1); } |
These methods are public and static so are accessible from anywhere without an instance of SoundEngine.
Any class can now play any of the sound effects.
Coding the physics engine (without collision)
The PhysicsEngine class is responsible, first, for updating all the game objects and secondly for detecting and responding to collisions. This next code handles updating the game objects in the update method and we will code an empty detectCollisions method ready to add more code in Chapter 25.
Add the PhysicsEngine class and then we will discuss the code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import android.graphics.PointF; import android.graphics.RectF; import java.util.ArrayList; class PhysicsEngine { void update(long fps, ArrayList<GameObject> objects, GameState gs) { for (GameObject object : objects) { object.update(fps, objects.get(LevelManager.PLAYER_INDEX) .getTransform()); } detectCollisions(gs, objects); } private void detectCollisions(GameState gs, ArrayList<GameObject> objects) { // More code here soon } } |
The update method receives the current frames per second, an ArrayList full of all the GameObject references, and a reference to the current GameState. It then loops through all the GameObject instances calling each and every update method after first checking that the GameObject is active. Finally, the detectCollisions method is called although for now, we have left this method empty.
Coding a Renderer
As with many of the classes in this chapter. Renderer will be very similar to the earlier project, so we can zip through it and move on.
Create a new class called Renderer then add the following members and constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class Renderer { private Canvas mCanvas; private SurfaceHolder mSurfaceHolder; private Paint mPaint; // Here is our new camera private Camera mCamera; Renderer(SurfaceView sh, Point screenSize){ mSurfaceHolder = sh.getHolder(); mPaint = new Paint(); // Initialize the camera mCamera = new Camera(screenSize.x, screenSize.y); } } |
The Renderer has the Canvas, SurfaceHolder and Paint instances as we have come to expect. It also has a Camera instance called mCamera. We at last get to code the Camera class when we are done with Renderer.
In the constructor, the SurfaceHolder, Paint, and Camera instances are initialized.
Now add the getPixelsPerMetre and draw methods.
i
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
nt getPixelsPerMetre(){ return mCamera.getPixelsPerMetre(); } void draw(ArrayList<GameObject> objects, GameState gs, HUD hud) { if (mSurfaceHolder.getSurface().isValid()) { mCanvas = mSurfaceHolder.lockCanvas(); mCanvas.drawColor(Color.argb(255, 0, 0, 0)); if(gs.getDrawing()) { // Set the player as the center of the camera mCamera.setWorldCentre( objects.get(LevelManager .PLAYER_INDEX) .getTransform().getLocation()); for (GameObject object : objects) { if (object.checkActive()) { object.draw(mCanvas, mPaint, mCamera); } } } hud.draw(mCanvas, mPaint, gs); mSurfaceHolder.unlockCanvasAndPost(mCanvas); } } |
The getPixelsPerMetre method uses the instance of the Camera class to return the number of pixels that represent a virtual meter in the game world. We will code the Camera class including this method next.
The code in the draw method prepares the surface, checks it is currently OK to draw then sets the camera to whatever the player’s location is using the setWorldCentre method. We will code the Camera class including this method next. Then the code loops through all the objects checking which ones are active and drawing them. Then the HUD is drawn and the now familiar unlockCanvasAndPost method is called.
Coding the camera
This class is completely new to this project. The key to understanding it is to remember that all our game objects have sizes and positions in virtual meters. All the movement and collision detection will be done using these virtual meters. Here is the key-key point:
These virtual meters currently bare no relation to the pixels on the screen. The Camera class will know the screen’s resolution and translate these virtual meters into pixel coordinates. Not all game objects will be on the screen every frame. In fact, most game objects will not be visible, most frames.
Add a new class called Camera and add the following member variables and the constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
import android.graphics.PointF; import android.graphics.Rect; class Camera { private PointF mCurrentCameraWorldCentre; private Rect mConvertedRect; private int mPixelsPerMetre; private int mScreenCentreX; private int mScreenCentreY; Camera(int screenXResolution, int screenYResolution){ // Locate the centre of the screen mScreenCentreX = screenXResolution / 2; mScreenCentreY = screenYResolution / 2; // How many metres of world space does // the screen width show // Change this value to zoom in and out final int pixelsPerMetreToResolutionRatio = 48; mPixelsPerMetre = screenXResolution / pixelsPerMetreToResolutionRatio; mConvertedRect = new Rect(); mCurrentCameraWorldCentre = new PointF(); } |
There are five, member variables as follows:
- The PointF mCurrentCameraWorldCentre instance will hold the central position (in virtual meters) the camera is centered on. This will be updated each frame and is the same as the position of the player.
- The Rect mConvertedRect instance will hold the four converted points that have been translated from virtual meters to pixel coordinates. This is the Rect that will be used by all the graphics-related component classes in their draw methods when they call drawBitmap. Look back to the InanimateBlockGraphicsComponent section if you want to confirm this.
- The int mPixelsPerMetre member is the number of pixels on the screen that represent one virtual metre.
- The int mScreenCentreX member is the pixel coordinate of the central pixel, horizontally on the screen.
- The int mScreenCentreY member is the pixel coordinate of the central pixel, horizontally on the screen.
It is the way we calculate and combine these five values that makes the class do its magic.
Next, add the following getters and setters to the class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
int getPixelsPerMetreY(){ return mPixelsPerMetre; } int getyCentre(){ return mScreenCentreY; } float getCameraWorldCentreY(){ return mCurrentCameraWorldCentre.y; } // Set the camera to the player. Called each frame void setWorldCentre(PointF worldCentre){ mCurrentCameraWorldCentre.x = worldCentre.x; mCurrentCameraWorldCentre.y = worldCentre.y; } int getPixelsPerMetre(){ return mPixelsPerMetre; } |
Four out of five of those methods simply return a value/reference to one of the members. We will see where they are used as we proceed. The setWorldCentre method however is more interesting. It receives a PointF reference as a parameter. The setWorldCentre method is called after the update method but before the draw method by GameEngine on every frame. The PointF that is passed in is the position of the Bob in the game world (in virtual metres).
This next method is the meat of the whole class and it uses all the members and the position of Bob to translate virtual metres into pixels. Notice that it receives the object’s horizontal and vertical positions (in virtual metres) as well as the objects width and height (also in virtual metres). Also notice it returns a Rect so that each game object’s graphics-related component class can use it in the call to drawBitmap. Add the worldToScreen method and then we will examine its body.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// Return a Rect of the screen coordinates // relative to a world location Rect worldToScreen(float objectX, float objectY, float objectWidth, float objectHeight){ int left = (int) (mScreenCentreX - ((mCurrentCameraWorldCentre.x - objectX) * mPixelsPerMetre)); int top =Â (int) (mScreenCentreY - ((mCurrentCameraWorldCentre.y - objectY) * mPixelsPerMetre)); int right = (int) (left + (objectWidth * mPixelsPerMetre)); int bottom = (int) (top + (objectHeight * mPixelsPerMetre)); mConvertedRect.set(left, top, right, bottom); return mConvertedRect; } |
Let’s go through the body a line at a time. The first line of code declares and initializes a local int variable called left.
1 2 3 4 5 |
int left = (int) (mScreenCentreX - ((mCurrentCameraWorldCentre.x - objectX) * mPixelsPerMetre)); |
The initialization code uses a cast, (int), to convert the float result to int. The calculation subtracts the objects horizontal position from the current focus of the camera which in turn is subtracted from the central horizontal pixel position. By multiplying the result by mPixelsPerMetre the world (virtual meter) positions are now a pixel coordinate. Note that at this stage we have only calculated the horizontal pixel position of one of four points.
This next line of code does the same thing except using the vertical values of the screen center, world center and object position.
1 2 3 4 5 |
int top =Â (int) (mScreenCentreY - ((mCurrentCameraWorldCentre.y - objectY) * mPixelsPerMetre)); |
The next two lines of code use left then top in conjunction with the width and height (respectively) to calculate the right and bottom pixel coordinates (respectively).
1 2 3 4 5 6 7 8 9 |
int right = (int) (left + (objectWidth * mPixelsPerMetre)); int bottom = (int) (top + (objectHeight * mPixelsPerMetre)); |
Let’s invent some hypothetical positions for the player and an example object so we can see those calculations in action.
Suppose that the player is at (top left corner) the world location of 120 virtual meters horizontally and 90 meters vertically. The player is 1 meter wide and 2 meters tall. There is a platform at the world location of 116 meters horizontally and 89 meters vertically. The platform is 1 meter by 1 meter in size. If our calculations are to do their job, then they need to convert these sizes and positions into screen coordinates where the platform is to the left of the player by four times as much as it is above the player. This exercise will assume the screen is 1920 pixels wide by 1080 pixels high. First let’s calculate the player’s pixel coordinates.
Testing the formulas with hypothetical coordinates
The formula for the player’s left (in English not in code) is as follows:
Remember that the world camera center coordinates are the same as the player’s top-left coordinates and are updated each frame after update but before draw is called.
We can see that the left of the player is drawn in the horizontal center of the screen, 960 pixels.
Now for the top variable using the player’s world coordinates.
We can now see that the player’s top-left coordinates on the screen is 960, 540. This is the center of a 1920 x 1080 screen. Wherever in the game world the player moves, because mCameraCurrentWorldCentre is updated with each frame, the player stays in the center of the screen.
Let’s look quickly at how the right and bottom values are calculated and then we can run the hypothetical platform coordinates through the same formulas.
Here is the formula for the right variable.
The bottom calculation uses the following formula:
The rectangle representing where Bob will be drawn will be at the following pixel coordinates:
Left: 960, Top: 540, Bottom: 1048, Right: 636.
The formula for the block’s left (in English not in code) is as follows:
We can see that the left of the block is drawn in the horizontal center of the screen, 960 pixels.
Now for the top variable using the block’s world coordinates.
Let’s look quickly at how the right and bottom values are calculated. Here is the formula for the right variable.
Left pixel coordinate + (width in meters * pixels per meter)
The bottom calculation uses the following formula:
The rectangle representing where the block will be drawn will be at the following pixel coordinates:
Left: 960, Top: 540, Bottom: 1048, Right: 636.
Look closely at the results for converting the player’s and the other object’s world coordinates and you can see that they are within the scope of being able to accept them as correct. When we see hundreds of objects all neatly drawn in exactly the right place it will be certain the formulas work.
Finally, mConvertedRect is initialized with the four values using the set method and then returned to the calling code.
1 2 3 |
mConvertedRect.set(left, top, right, bottom); return mConvertedRect; |
Try making up some more hypothetical object positions and calculating their pixel positions. Try some values that are some distance away (perhaps 50 meters) and notice how they are at a position that will not be seen on screen.
Now we have a system for converting all the floating-point world positions into integer pixel positions we can turn our attention to the HUD class.
Coding the Hud
The HUD in this game is no more complex than the previous game. We will define some Rect instances to draw the controls on the screen, we will rely on GameState to provide the time and fastest times for each level and we will make the button Rect ArrayList available so that GameEngine can pass then them to our two classes that require them to handle the player’s input.
Get started by adding a new class called HUD and add the following members and constructor method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import java.util.ArrayList; class HUD { private Bitmap mMenuBitmap; private int mTextFormatting; private int mScreenHeight; private int mScreenWidth; final float ONE_THIRD = .33f; final float TWO_THIRDS = .66f; private ArrayList<Rect> mControls; static int LEFT = 0; static int RIGHT = 1; static int JUMP = 2; HUD(Context context, Point size){ mScreenHeight = size.y; mScreenWidth = size.x; mTextFormatting = size.x / 25; prepareControls(); // Create and scale the bitmaps mMenuBitmap = BitmapFactory .decodeResource(context.getResources(), R.drawable.menu); mMenuBitmap = Bitmap .createScaledBitmap(mMenuBitmap, size.x, size.y, false); } } |
The class starts off with five members that we will use to control position and formatting of the parts of the HUD. Next there is an ArrayList for our Rect buttons and next there is the static variables LEFT, RIGHT and JUMP which the UIController (that we code next) and PlayerInputComponent (that we code next chapter) can use to identify what the player is trying to do.
In the constructor we initialize some of our formatting variables using the passed in screen resolution, call the prepareControls method and load and scale the Bitmap that is used for the menu background.
Now we can code the prepareControls method that we just called.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
private void prepareControls(){ int buttonWidth = mScreenWidth / 14; int buttonHeight = mScreenHeight / 12; int buttonPadding = mScreenWidth / 90; Rect left = new Rect( buttonPadding, mScreenHeight - buttonHeight - buttonPadding, buttonWidth + buttonPadding, mScreenHeight - buttonPadding); Rect right = new Rect( (buttonPadding * 2) + buttonWidth, mScreenHeight - buttonHeight - buttonPadding, (buttonPadding * 2) + (buttonWidth * 2), mScreenHeight - buttonPadding); Rect jump = new Rect(mScreenWidth - buttonPadding - buttonWidth, mScreenHeight - buttonHeight - buttonPadding, mScreenWidth - buttonPadding, mScreenHeight - buttonPadding); mControls = new ArrayList<>(); mControls.add(LEFT,left); mControls.add(RIGHT,right); mControls.add(JUMP, jump); } |
In the previous code, we initialize our remaining formatting members relative to the screen resolution in pixels. We then use them to position our three Rect objects that represent the buttons and then add them to the mControls ArrayList.
Next, add the draw method which just like the HUD in the previous project will be called each frame of the game to draw the HUD. Notice the usual suspects are passed in as parameters to enable the method to do its job.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
void draw(Canvas c, Paint p, GameState gs){ if(gs.getGameOver()){ // Draw the mMenuBitmap screen c.drawBitmap(mMenuBitmap, 0,0, p); // draw a rectangle to highlight the text p.setColor(Color.argb (100, 26, 128, 182)); c.drawRect(0,0, mScreenWidth, mTextFormatting * 4, p); // Draw the level names p.setColor(Color.argb(255, 255, 255, 255)); p.setTextSize(mTextFormatting); c.drawText("Underground", mTextFormatting, mTextFormatting * 2, p); c.drawText("Mountains", mScreenWidth * ONE_THIRD + (mTextFormatting), mTextFormatting * 2, p); c.drawText("City", mScreenWidth * TWO_THIRDS + (mTextFormatting), mTextFormatting * 2, p); // Draw the fastest times p.setTextSize(mTextFormatting/1.8f); c.drawText("BEST:" + gs.getFastestUnderground() +" seconds", mTextFormatting, mTextFormatting*3, p); c.drawText("BEST:" + gs.getFastestMountains() +" seconds", mScreenWidth * ONE_THIRD + mTextFormatting, mTextFormatting * 3, p); c.drawText("BEST:" + gs.getFastestCity() + " seconds", mScreenWidth * TWO_THIRDS + mTextFormatting, mTextFormatting * 3, p); // draw a rectangle to highlight the large text p.setColor(Color.argb (100, 26, 128, 182)); c.drawRect(0,mScreenHeight - mTextFormatting * 2, mScreenWidth, mScreenHeight, p); p.setColor(Color.argb(255, 255, 255, 255)); p.setTextSize(mTextFormatting * 1.5f); c.drawText("DOUBLE TAP A LEVEL TO PLAY", ONE_THIRD + mTextFormatting * 2, mScreenHeight - mTextFormatting/2, p); } // else block follows next } |
The method is long and might seem complicated at first glance but there is nothing we haven’t seen before. There is one thing to note, however. All the code is wrapped in an if block with a condition of gs.getGameOver. So, all the code we just added runs when the game is over. We will add the else block which follows this if block in a moment.
The code inside the if block draws the background, level names, and fastest times as well as the message to tell the player how to start the game. Clearly, we don’t want these things on the screen while the game is being played.
Still, inside the draw method add the else block that follows the if block which will execute while the game is being played.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
... // else block follows next else { // draw a rectangle to highlight the text p.setColor(Color.argb (100, 0, 0, 0)); c.drawRect(0,0, mScreenWidth, mTextFormatting, p); // Draw the HUD text p.setTextSize(mTextFormatting/1.5f); p.setColor(Color.argb(255, 255, 255, 255)); c.drawText("Time:" + gs.getCurrentTime() + "+" + gs.getCoinsRemaining() * 10, mTextFormatting / 4, mTextFormatting / 1.5f, p); drawControls(c, p); } |
In the else block, we draw a transparent rectangle across the top of the screen which has the effect of highlighting the text that is drawn on top of it. Then we draw the current time. The slightly convoluted formula (gs.getCoinsRemaining() * 10) has the effect of calculating (and displaying) the time penalty based on how many coins the player still needs to collect. The final line of code in the draw method (but still inside the else block) calls the drawControls method. This is separated out purely to stop the method getting any longer than it already is.
Here are the final two methods of the HUD class. Add the drawControls and getControls method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
private void drawControls(Canvas c, Paint p){ p.setColor(Color.argb(100,255,255,255)); for(Rect r : mControls){ c.drawRect(r.left, r.top, r.right, r.bottom, p); } // Set the colors back p.setColor(Color.argb(255,255,255,255)); } ArrayList<Rect> getControls(){ return mControls; } |
The drawControls method loops through the mControls ArrayList and draws each button as a transparent rectangle. The getControls method simply returns a reference to mControls. GameEngine will use this method to pass mControls to the other classes that need it.
Coding the UIController class
This class will have the same purpose as the class of the same name did in the previous project. It will also look very similar too.
Add a new class called UIController and add the member variables, constructor and addObserver method.
i
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
mport android.graphics.Point; import android.graphics.Rect; import android.view.MotionEvent; import java.util.ArrayList; class UIController implements InputObserver { private float mThird; private boolean initialPress = false; UIController(GameEngineBroadcaster b, Point size) { // Add as an observer addObserver(b); mThird = size.x / 3; } void addObserver(GameEngineBroadcaster b) { b.addObserver(this); } } |
The float mThird variable will help us to divide the screen up vertically into thirds. The player will then be able to tap a portion of the screen to choose the level that they want to play. The initialPress Boolean is used in a workaround to avoid a bug/glitch. Sometimes when the game ends the menu will immediately register the player’s touch causing a new level to start instantly rather than allowing them to view the menu screen at their leisure. By ignoring the very first touch we avoid this problem.
This is a hack and not a good solution to go into a finished game. Unfortunately, the solution would require at least another chapter and I am running out of space. In chapter 26 I suggest some resources to continue your learning. Be sure to look into the State pattern to improve all the projects and as a solution for this hack.
In the constructor, we call the addObserver method and initialize the mThird variable to a third of the screen’s width. In the addObserver method the code uses the GameEngineBroadcaster reference to add an observer to GameEngine.
We need to add an observer each time a new game is started, and the game objects are rebuilt because the observer list is cleared. The addObserver method allows us to re-add an observer rather than just add it in the constructor.
Now add the handleInput method which receives the MotionEvent, the GameState and the ArrayList which contains the button positions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
@Override public void handleInput(MotionEvent event, GameState gameState, ArrayList<Rect> buttons) { int i = event.getActionIndex(); int x = (int) event.getX(i); int eventType = event.getAction() & MotionEvent.ACTION_MASK; if (eventType == MotionEvent.ACTION_UP || eventType == MotionEvent.ACTION_POINTER_UP) { // If game is over start a new game if (gameState.getGameOver() && initialPress) { if (x < mThird) { gameState.setCurrentLevel("underground"); gameState.startNewGame(); } else if (x >= mThird && x < mThird * 2) { gameState.setCurrentLevel("mountains"); gameState.startNewGame(); } else if (x >= mThird * 2) { gameState.setCurrentLevel("city"); gameState.startNewGame(); } } initialPress = !initialPress; } } |
As we did in the previous project we handle input by accessing the event at the index which triggered the current action. There is an if – else if – else if structure where the code detects which level was pressed. These are the only touches that need to be handled. The buttons will be handled by the PlayerController component class. After an ACTION_UP has been detected, notice also that the rest of the code is wrapped in an if condition that tests whether the game is over and initialPress is false. Outside this structure, all ACTION_UP events will switch the value of initialPress. This means that the last stray touch of game-play won’t dismiss the menu screen as soon as it starts.
Code the Activity
This class is just like the one we have been coding for virtually the entire book. Take a look at the code and then add this class to your project.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
import android.app.Activity; import android.graphics.Point; import android.os.Bundle; import android.view.Display; public class GameActivity extends Activity { GameEngine mGameEngine; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Display display = getWindowManager() .getDefaultDisplay(); Point size = new Point(); display.getSize(size); mGameEngine = new GameEngine(this, size); setContentView(mGameEngine); } @Override protected void onResume() { super.onResume(); mGameEngine.startThread(); } @Override protected void onPause() { super.onPause(); mGameEngine.stopThread(); } } |
Everything is very familiar except you can see that I have renamed the resume and pause methods of the GameEngine class to startThread and stopThread respectively.
Now we can code the GameEngine class (including startThread and stopThread) and we will then be one small step away from running our hard work from the last two chapters.
How to Code the GameEngine
Now we get to the class that glues all the others together. In the code that follows we will declare and use an instance of LevelManager that we will code immediately after GameEngine. All the rest of the code will, at last, be error-free and nearly ready to execute.
Add the GameEngine class, its members, and the constructor as shown next.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
import android.content.Context; import android.graphics.Point; import android.util.Log; import android.view.MotionEvent; import android.view.SurfaceView; import java.util.concurrent.CopyOnWriteArrayList; class GameEngine extends SurfaceView implements Runnable, GameEngineBroadcaster, EngineController { private Thread mThread = null; private long mFPS; private GameState mGameState; UIController mUIController; // This ArrayList can be accessed from either thread private CopyOnWriteArrayList<InputObserver> inputObservers = new CopyOnWriteArrayList(); HUD mHUD; LevelManager mLevelManager; PhysicsEngine mPhysicsEngine; Renderer mRenderer; public GameEngine(Context context, Point size) { super(context); // Prepare the bitmap store and sound engine BitmapStore bs = BitmapStore.getInstance(context); SoundEngine se = SoundEngine.getInstance(context); // Initialize all the significant classes // that make the engine work mHUD = new HUD(context, size); mGameState = new GameState(this, context); mUIController = new UIController(this, size); mPhysicsEngine = new PhysicsEngine(); mRenderer = new Renderer(this, size); mLevelManager = new LevelManager(context, this, mRenderer.getPixelsPerMetre()); } } |
There are a few new things happening in the code you just saw. First notice that we need to import the CopyOnWriteArrayList class. This is a version of the ArrayList class that works simultaneously on multiple threads without crashing the game. This structure will be used to hold the InputObservers. CopyOnWriteArrayList is slower than the regular ArrayList class but as it is holding just two items that are accessed infrequently (compared to the GameObject instances) it won’t cause a performance issue. The GameObject instances will be held in a regular ArrayList.
Notice as usual we have the members such as mThread and mFPS for our main game loop and measuring the frames per second. We also declare instances for many of the classes we have been slaving over for the past two chapters.
In the constructor, we initialize all the instances. Look at the way we initialize our two Singletons (BitmapStore and SoundEngine) using the getInstance method. The other initializations are just as we have seen before and should expect based on the classes we have been coding. The exception is the instance of LevelManager. Look at the parameters we pass in, a Context, this, and the number of pixels per meter (supplied by the Renderer class). We will see how we put all these things into action shortly. Let’s finish the GameEngine class.
Code the startNewLevel method and the addObserver method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public void startNewLevel() { // Clear the bitmap store BitmapStore.clearStore(); // Clear all the observers and add the UI observer back // When we call buildGameObjects the // player's observer will be added too inputObservers.clear(); mUIController.addObserver(this); mLevelManager.setCurrentLevel(mGameState.getCurrentLevel()); mLevelManager.buildGameObjects(mGameState); } // For the game engine broadcaster interface public void addObserver(InputObserver o) { inputObservers.add(o); } |
First, the static method Bitmap.clearStore is used to make sure there are no Bitmap instances already in the store. Then, the inputObservers ArrayList is cleared. The reason we need to do this each new game/level attempt is because each call to buildGameObjects deletes any existing game objects and rebuilds new ones to suit the current level. This includes the player that will be composed of a PlayerInputController component class that will have registered as an input observer. Therefore, if we leave the old input observers in the ArrayList then there will be a reference to an object which doesn’t exist. Calling it will cause an instant crash.
After we have cleared the observers for the reason just stated we then need to add the UIController instance’s observer by calling addObserver. The UIController instance remains the same throughout the life of the application but we must add it to each level of course because we have just cleared it.
To end the method, we call the setCurrentLevel and buildGameObjects methods of the soon-to-be-coded LevelManager class.
The addObserver method allows the InputObservers to register themselves and be added to the inputObservers ArrayList.
Next, add the run method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
@Override public void run() { while (mGameState.getThreadRunning()) { long frameStartTime = System.currentTimeMillis(); if (!mGameState.getPaused()) { mPhysicsEngine.update(mFPS, mLevelManager.getGameObjects(), mGameState); } mRenderer.draw(mLevelManager.getGameObjects(), mGameState, mHUD); long timeThisFrame = System.currentTimeMillis() - frameStartTime; if (timeThisFrame >= 1) { final int MILLIS_IN_SECOND = 1000; mFPS = MILLIS_IN_SECOND / timeThisFrame; } } } |
Most of this we have seen before, but this is what happens in run, step by step:
- The current time is stored in frameStartTime.
- If the game is not currently paused, then the PhysicsEngine instance’s update method is called.
- The Renderer instance’s draw method is called
- The amount of time that the previous two steps took is calculated and stored in timeThisFrame
- The current frame rate is calculated and stored in mFPS ready for use during the next frame of the game loop.
Add the onTouchEvent method along with the stopThread and startThread methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
@Override public boolean onTouchEvent(MotionEvent motionEvent) { for (InputObserver o : inputObservers) { o.handleInput(motionEvent, mGameState, mHUD.getControls()); } return true; } public void stopThread() { mGameState.stopEverything(); mGameState.stopThread(); try { mThread.join(); } catch (InterruptedException e) { Log.e("Exception", "stopThread()" + e.getMessage()); } } public void startThread() { mGameState.startThread(); mThread = new Thread(this); mThread.start(); } |
The onTouchEvent method loops through all the registered observers calling their handleInput method.
The stopThread method stops the thread (and everything else) via the GameState instance.
The startThread method starts the thread using the GameState instance.
Now we can move on to the final class for this chapter.
Coding the LevelManager class
We are nearly there. Coding this class will leave us with an executable project!
Create a new class called LevelManager and add the following members and the constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import android.content.Context; import android.graphics.PointF; import android.util.Log; import com.gamecodeschool.platformer.GOSpec.*; import com.gamecodeschool.platformer.Levels.*; import java.util.ArrayList; final class LevelManager { static int PLAYER_INDEX = 0; private ArrayList<GameObject> objects; private Level currentLevel; private GameObjectFactory factory; LevelManager(Context context, GameEngine ge, int pixelsPerMetre){ objects = new ArrayList<>(); factory = new GameObjectFactory(context, ge, pixelsPerMetre); } |
First, be aware of the two highlighted import statements that will need checking/correcting to your full package name.
We can tell we are getting close because we have declared the ArrayList that will hold all the GameObject instances (objects) and an instance of GameObjectFactory that will build them all.
We also have an instance of Level called currentLevel ready to be filled up with all the alpha-numeric characters that represent our level designs. The other member, PLAYER_INDEX is static and will help us insure that we keep a check on the position of the player in the objects ArrayList. In the constructor, objects and factory are initialized.
Add the setCurrentLevel method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
void setCurrentLevel(String level){ switch (level) { case "underground": currentLevel = new LevelUnderground(); break; case "city": currentLevel = new LevelCity(); break; case "mountains": currentLevel = new LevelMountains(); break; } } } |
This method allows the UIController class to change the current level by passing in a String which the code in the method body uses to create a new Level reference of one of the extended classes (LevelUnderground, LevelCity or LevelMountains).
The next code is long but not difficult. We glimpsed some of it (the comments) before in the earlier section Create the levels. Very similar to previous project it just adds a case to create the appropriate type of object based on the alpha-numeric character that is returned by the nested for loops which go through the level design a character at a time. Objects that aren’t available yet are commented out for later convenience.
Add the buildGameObjects method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
void buildGameObjects(GameState gs){ // Backgrounds 1, 2, 3(City, Underground, Mountain...) // p = Player // g = Grass tile // o = Objective // m = Movable platform // b = Brick tile // c = mine Cart // s = Stone pile // l = coaL // n = coNcrete // a = lAmpost // r = scoRched tile // w = snoW tile // t = stalacTite // i = stalagmIte // d = Dead tree // e = snowy trEe // x = Collectable // z = Fire // y = invisible death_invisible gs.resetCoins(); objects.clear(); ArrayList<String> levelToLoad = currentLevel.getTiles(); for(int row = 0; row < levelToLoad.size(); row++ ) { for(int column = 0; column < levelToLoad.get(row).length(); column++){ PointF coords = new PointF(column, row); switch (levelToLoad.get(row) .charAt(column)){ case '1': //objects.add(factory.create( // new BackgroundCitySpec(), // coords)); break; case '2': //objects.add(factory.create( // new BackgroundUndergroundSpec(), // coords)); break; case '3': //objects.add(factory.create( // new BackgroundMountainSpec(), // coords)); break; case 'p': //objects.add(factory.create(new // PlayerSpec(), // coords)); // Remember the location of // the player //PLAYER_INDEX = objects.size()-1; break; case 'g': objects.add(factory.create( new GrassTileSpec(), coords)); break; case 'o': objects.add(factory.create( new ObjectiveTileSpec(), coords)); break; case 'm': //objects.add(factory.create( // new MoveablePlatformSpec(), // coords)); break; case 'b': objects.add(factory.create( new BrickTileSpec(), coords)); break; case 'c': objects.add(factory.create( new CartTileSpec(), coords)); break; case 's': objects.add(factory.create( new StonePileTileSpec(), |