How to make your own Android Game

In this article, I will describe all necessary steps required to produce and publish an Android App of your own.

I will write this article in the form of a log of what actions I do myself while I prepare a development environment and write a simple clone of an arcade game from the golden era of the late 70's.

This means that unlike most other tutorials, this one will also include all the "lessons learned", the typical pitfalls and the common mistakes. Hopefully so that you don't have to repeat them yourself. ;-)

 

My development platform is a minimal netbook (Acer Aspire One). It is actually possible to use it as it is (I did that when I wrote my first few apps) but it is really painful with the default 1 GB. I paid €200 for the netbook, and for only €30 more you get 2 GB. Note that the pre-installed Windows 7 Starter only supports 1 GB, but a change of OS is more or less mandatory anyway ;-)

The screen is also painfully small when you prepare the development environment. Some of the GUI:s put the OK and CANCEL buttons below the bottom of the screen. It is still possible to navigate with the TAB-key, though.

 

I refer to Ubuntu Linux in the first chapters of this book/article. Android development is just as easy in Windows, but I just happen to prefer Linux..

 

Happy Reading!

 

/Anders Ekstrand

Chapter 01: Upgrade the OS

My netbook was delivered with Windows 7 Starter Edition, which has a limit on max 1 GB RAM. Thus step 1 became to install another operating system on it. (If you have another flavor of Windows, that can handle 2 GB RAM or more, it should be fine to use that.) 

Historically, I have altered between different RedHat/Fedora distributions on my computers, but in this specific case I choose the latest and greatest Ubuntu distribution (Ubuntu 11.10 32-bit) due to the fact that it worked completely out-of-the-box on my netbook, while the latest Fedora release required a manually built driver to get wireless networking to work.

Therefore the chapters describing how to set up your environment will be adapted for Ubuntu 11.10, and names of installation packets and so on will likely be different if you have chosen another operating system.

Anyway, I downloaded Ubuntu 11.10 from http://www.ubuntu.com/download/ubuntu/download and put it on a USB stick with LiveUSBCreator to be able to test it properly before I installed it on my hard drive.

I verified that I got proper resolution on the internal display, that multiple displays work, that WiFi worked and that Suspend and Hibernate functioned as it should. After this I installed it "for real" but on a secondary partition with dual boot in place, so that I would be able to start up Windows 7 in case of emergency. (Such as when I need to use DVDfab.) 

Chapter 02: Install Java

Well, this is the shortest chapter of them all, as I decided to give the standard OpenJDK a shot. Install it the simple way (through the command line) with:

$ sudo apt-get install icedtea6-plugin openjdk-6-jre openjdk-6-jdk ant

If you are using another operating system than Ubuntu, please visit http://www.oracle.com/technetwork/java/javase/downloads/index.html and download the appropriate installation package from there.

Observe that you need the "JDK" package. Only the "JRE" is not sufficient.

Chapter 03: Install the Android SDK

Visit http://developer.android.com/sdk/index.html and download the Linux version (ex: android-sdk_r16-linux.tgz) to your Downloads folder.

Create a "Dev" folder and unpack the Android SDK there and update it:

$ cd
$ mkdir Dev
$ cd Dev
$ tar xvfz ~/Downloads/android-sdk*
$ cd android-sdk*
$ tools/android update sdk

You will now be shown a dialog where you will get the possibility to accept that a whole bunch of packages will be downloaded. My recommendation is to click "Cancel" here:

You will then get to a list with more specific choices. I select "Android 2.1" in my example, as the oldest Android device I have here at home is a Sony Ericsson X10 Mini Pro with Android 2.1, plus "Tools" and "Extras". If you want to integrate ads from AdMob in your app you also need to include the latest version (in this case 4.0.3), so I pick that one also.:

Then click on "Install packages" and when all downloads and unpacking are finished, you can close the window.

After this, I want to move away the Android SDK from my home directory, as I don't need it to be backed up quite as frequently as the files I usually keep there (replace "ajo" with your own user name below):

$ sudo mv android-sdk-linux/ /opt/
$ sudo chown -R ajo:ajo /opt/android-sdk*

 

Chapter 04: Install and configure Eclipse

As many other developers, I also failed to get the necessary Android tools to work with the Eclipse package that came with my Ubuntu version. Therefore I chose to install Eclipse manually. It seems like the culprit is with Eclipse 3.7 (Indigo) itself, so I went for Eclipse 3.6 (Helios) instead. Visit http://www.eclipse.org/downloads/packages/release/helios/sr2, find "Eclipse Classic" and click on the "Linux 32-bit" link.

When the download is finished, unpack the downloaded file, start Eclipse and add the Android repository to it before moving it to the /opt folder with: 

$ cd ~/Downloads
$ tar xvfz eclipse-SDK*
$ cd eclipse
$ ./eclipse

After being shown the Eclipse splash screen for a while, you will get to chose where your "Workspace" will be stored. The standard settings are allright, so just click "OK". As always, make sure that you make regularly backups of this directory as no-one else is likely to do it for you.

Now close down Eclipse, so we can move it from your Download folder, and then start it up again:

$ cd ~/Downloads
$ sudo mv eclipse /opt/
$ /opt/eclipse/eclipse

Now it's time to select "Help" -> "Install New Software" in the Eclipse menu, click on "Add..." and enter "Android Plugin" and "https://dl-ssl.google.com/android/eclipse/" in the dialog:

Click "OK" and wait while the list of available packages is updated.

Check "Developer Tools" and click "Next >":

Click "Next >" again:

Check "I accept..." and click "Finish":

Let the installation complete:

...and possible accept a few warnings:

Click "Restart Now":

When Eclipse is restarted, it is time to configure the location of the Android SDK. Select "Window" -> "Preferences" in the menu. 

Click on the "Android" line and "Browse" in the "SDK Location" field and browse your way to where you put the SDK in previous chapter:

After a final "OK" you should have a working development environment!

 

Note that this way of installing Eclipse wont give you any shortcut in the Ubuntu start menu, so in the future you will have to start Eclipse with:

$ /opt/eclipse/eclipse

Chapter 05: Define your app

It is pretty vital to have a clear picture of what your app will do before you start to dig into details, otherwise the obvious risk is that you more or less never will be really "finished". Don't fall into that trap.

I'm planning to make a clone of the ancient arcade game "Asteroids", which I assume already is so exploited that no-one will care if yet another private person copies the idea.

I'm also planning to make the game ad-financed, as I would need those few extra dollars that might find their way into my bank account in order to keep this domain and web server up and running.

Let's start with a few simple sketches (sorry for the Swedish labels in the images - there are English translations below them):


Sketch 1: The game itself (Score, Ads, Level, Ship, Missiles, Asteroids)

 

Sketch 2: The controls (Accelerate, Fire, Rotate ship)

 

Sketch 3: The different "pages" in the app

These three sketches should be enough to get a clear picture of what I will try to accomplish, but I will still write down a series of requirements so that I won't miss any embarrassing detail.

So here's a list that I can check against while the development is progressing and also test against before I publish the app for the general public.

  1. The app shall be started through a clickable icon as any other app.
  2. When the app is started, the Main Menu shall be shown, unless the app already was running in the background - in that case the last viewed page shall be shown.
  3. On the Main Menu there shall be three links to the other pages; Play, How-To and Credits.
  4. The page "Play" is the game itself.
  5. The page "How-To" shall show how the ship is controlled and what the game is all about.
  6. The page "Credits" shall contain information on who made which part of the game and under which rights it is distributed.
  7. When pressing "Back" on your Android device, you shall end up "one page up". If your already on "Main Menu", the app shall be closed.
  8. When pressing "Home", the app shall be put in the background and the Android device shall display the standard Android Home Screen.
  9. There shall be ads on all pages in the app.
  10. The game starts at Level 1.
  11. There shall be no upper limit on Level.
  12. When a level starts, the ship shall be in the center of the screen.
  13. When a level starts, the ship should have the velocity 0 and be pointing upwards.
  14. When a level starts, there shall be one "full-size" asteroid per level (1 asteroid on level 1, 2 on level 2 and so on...) on the outer parts of the screen.
  15. When a level starts, the asteroids shall have random velocity and direction.
  16. The asteroids shall rotate (just for the effect).
  17. The ship fires missiles when you "tap" on the screen.
  18. The ship shall rotate clockwise/counter clockwise when you "swipe" right/left on the screen, but not change its velocity in any way.
  19. The ship shall increase its velocity in its nose's direction when you "swipe" up on the screen.
  20. The ship's velocity shall not decrease automatically. The player will have to rotate 180° and accelerate in order to brake.
  21. Objects (asteroids, missiles, ship) shall "loop" over the screen's edges and re-appear on the other side.
  22. Missiles only "live" for a short period of time so that they can't hit the ship that fired them in the back.
  23. When a "full-size" asteroid is hit by a missile, it shall be split into two "half-size" asteroids.
  24. When a "half-size" asteroid is hit by a missile, it shall be split into two "quarter-size" asteroids.
  25. When a "quarter-size" asteroid id hit by a missile, it shall disappear.
  26. When an asteroid is split into two, both parts shall inherit the "parent's" velocity, but change their direction approximately 45° (clockwise for one of them and counter-clockwise for the other).
  27. If the ship collides with an asteroid, the game is over.
  28. When the game is over, some sort of summery shall be shown along with a link back to the Main Menu.
  29. When all asteroids are destroyed, the level is completed. 
  30. When a level is completed, a "Next Level" link/button shall be shown along with a summary displaying how much time it took to complete the level, how many missiles that were fired and how much fuel that was used. (All these resources are unlimited in this game, but it can be fun to compete with yourself).

 

I think that we have a good start here, so let's begin!

Chapter 06: Come up with a name

It might seem a trivial task, but your app should be found if people search for it on the Android Marketplace, it should be at least possible to guess what the app does if you see the name and it's bad if the name in itself is an intrusion in someone else's trademark etc.

I was planning to call my game "JustRoids" and did a quick search on that name on Google and Android Market Place: https://market.android.com/?hl=sv.

I came up with exactly zero hits, so JustRoids it is!

 

(If you are completely out of ideas, you might want to try "The Video Game Name Generator" at http://videogamena.me/. It will suggest names like "Confusing Handgun Maniac", "Mystical Hair Salon Wars" and "Toxic Fun - The Quickening" that are at best of limited use, but it might still give you some fresh ideas ;-)  )

Chapter 07: Create a new Eclipseproject

Begin with starting up Eclipse:

$ /opt/eclipse/eclipse &

When Eclipse is all started up, select File -> New -> Android Project, choose a project name and click Next:

Then select which Androidversion you want to build for (they are backwards compatible, so if you choose to build for 2.1, your app will work on newer versions also but you can not use features that arrived later than 2.1). Then click Next:

Now you will have to choose a "Package Name". It is a unique identifier that can't be changed after you app has been published. Preferably you should use your domain name in reverse order (if you have one) as prefix. In my case I should use com.benareby.<app name>, but for historical reasons I'm naming all my packages com.ajomannen.<app name>.

Do not use com.ajomannen as prefix. Come up with your own.

Then click Finish:

 

The fact is that we now should have a working "empty" app, if all the previous steps went ok. The app will not do anything at all, but this is a golden opportunity to start implementing a very good way-of-working: Make sure that your project builds without errors as often as possible. Preferably always. Also run your app as frequently as possible. So why not start up the "empty" app right away, so we know that we are on the right track before we start adding functionality that we might have to roll back otherwise?

Start with expanding your project in the "Package Explorer" on the left hand side in Eclipse, by clicking on the arrow next to its name. Then click on "Restore" in the tool tab in the bottom-right corner (if it's not already open):

Then select "Project" in the Eclipse menu and make sure that "Build Automatically" is checked:

 

Now it is time to create an Android Virtual Device that we can use to test our app on. Select "Window" -> "AVD Manager" and click "New":

You may now choose a few properties for your virtual device. As I wrote before, I'll go for Android 2.1 as target, a symbolic SD Card on 100 MB and QVGA resolution, as I want a very small screen size in order to avoid overly large screen dumps in this tutorial. Then click "Create AVD":

Now we have a brand new virtual device:

You can now close the Android Virtual Device Manager window.

Select "Run" -> "Run As" -> "Android Application", and Eclipse will do it's best to start up your virtual device. I can take quite a long time on a slow computer:

Finally we have something that looks like the display on a simple Android phone:

Now we should check some info in the main Eclipse window. If Eclipse succeeded to send your empty app to the virtual device, the three lines "Uploading...", "Installing..." and "Success!" should be visible in the Console tab:

When you also can see the "Starting..." line, it is time to go back to the virtual device window and "unlock" it, just as on a regular cell phone. Use the mouse as an virtual index finger, and keep the left mouse button pressed to simulate that you touch the screen. What you hopefully will see when the device is unlocked is the following:

Fanfare!

We have now created an empty project and dry-run the build and deployment processes, so that we are sure that all bits and pieces are in the right place before we start coding. Leave you virtual device running in the background and adapt the habit to select Run -> Run As -> Android Project (or simply CTRL-F11) every now and then in order to detect any mistakes made as early as possible.

Chapter 08: Create some graphics

This chapter will not be as detailed as the previous ones, as the graphics neither is what I'm good at nor a core topic in this tutorial. But I will try to cover each step at least briefly.

I have chosen to use Gimp to produce the different graphical elements needed for my app.

It can easily be installed with:

$ sudo apt-get install gimp

When this is complete, start Gimp through the standard Ubuntu start menu.

The objects on the Start Menu

Select File -> Create -> Logos -> Starscape in the Gimp menu.

You can now choose font, color and text size. I used 150 as size, more or less randomly.

After four rounds I have the following images:

Next step is to scale down the images, so they fit the screen of the target device. If best practice should be followed, you should actually produce three different versions of each and every image; one for low resolution devices, one for medium and one for high resolution. I intend to take a shortcut this time though, and only produce images for high resolution devices and let the other devices scale down the images themselves. If it doesn't work, I can always get back and produce the other versions afterwards.

The typical resolutions for Android devices are: QVGA (240×320, low density, small screen) WQVGA (240×400, low density, normal screen) FWQVGA (240×432, low density, normal screen) HVGA (320×480, medium density, normal screen) WVGA800 (480×800, high density, normal screen) WVGA854 (480×854 high density, normal screen).

Given this, I will make my four images 480 pixels wide, so they fill the screen width in portrait mode on WVGA devices.

Select Image -> Scale Image for the JustRoids logotype and scale it down to 480 pixels.

Do the same operation for the three buttons, but scale them to 30 percent instead.

Now all four images have the proper proportions and text sizes I wanted them to, but the three buttons are not 480 pixels wide. So we need to fill them out to that size with transparent color to prevent the device to scale them up when displaying them.

For each one of the three buttons, select Image -> Canvas Size, uncheck the chain-link-symbol that locks the Width/Height ratio, set Width to 480, click Center and then Resize:

When all three are padded to 480 pixels, it is time to open up a file browser.

Locate your project folder, for instance: ~/eclipse-workspace/JustRoids.

Open the subfolder "res" (where all project resources shall be located.)

There you will find "drawable-ldpi", "drawable-mdpi" and "drawable-hdpi" for the different resolution types. But I am taking a shortcut here, and only create a common set of graphics for all types, we need to create a new folder named just "drawable".

When this is done, all our four images shall be saved into this new "drawable" folder.

Select File -> Save and name the images:

justroids.png
play.png
howto.png
credits.png

Select "Merge Visible Layers", "Export" and "Save" for each image.

The spaceship

At this point, I decided to only make preliminary game graphics for the time being, and ask a friend of mine to add a bit more esthetic graphics at later stage. (I think I have to face it - I don't have the talent to produce any good looking graphics. I'm good at other things.)

So, I will do something really quick that will have to do as ship and missile. I want a 100x100 pixel spaceship and a 20x20 pixel missile, so I start with Gimp; File -> Create -> Logo -> Glowing Hot and write the text "V" in a large font size (100):

After that I open up the Layer toolbox, disable the "V-layer" and the "Background-layer", so that only the "glow" is left:

I crop the image, scale it to 100x100 and turn it upside-down:

Save the image as ship_100.png in your "drawable"-folder.

 

The missile

Exactly in the same manner as the spaceship, but with the text "." and the final size 20x20:

Save it as missile_20.png.

The asteroid

I happened to stumble upon a very suitable asteroid image on the web, on http://www.fysx.org/2010/12/12/how-about-some-3d-asteroid-models/, were it's creator allows anyone to use the his image as you wish.

I download his image, shift it's color slightly into the red spectra, make the background transparent and scale it down to 100x100 pixels:

Save the resulting image as asteroid_100.png.

 

This will have to do for the moment. I move on to work in Eclipse, and will perhaps get back to the graphics later on.

Chapter 09: Connect to AdMob

As I mentioned earlier, I want to show ads in JustRoids. My ad-broker of choice is AdMob.

I have opened up an account on: http://www.admob.com/ with my contact details and my bank account number.

It won't be any large amount of money pouring in from AdMob for displaying ads - you'll get a few cents for each click on an ad-banner. But if the volumes are large enough...

Anyway, when you're registered on AdMob, visit: http://www.admob.com/my_sites/create_site and click on "Android App":

Enter your apps name and the package name you choose when you created your Eclipse project. Once again - don't pick the same as I did:

Click on "Download AdMob Android SDK". It's an archive containing a file that needs to be included in your Eclipse project:

Click on "Go to Sites/Apps", and select "Manage Settings" for your app:

You will now get to see your Publisher ID. Write this down somewhere, as you will use it in you Eclipse project soon. 

Click on the "App Settings" tab:

Check "Google Ads: Use Google Ads..." and click "Save Settings":

 

Now it's time to locate the "AdMob Android SDK" archive we just downloaded and unpack it. It should contain the following:

Copy the jar file (or the whole folder) into your home folder, Dev folder or similar.

Now go back to Eclipse, mark your new project, right click on it and select "Properties":

Select "Java Build Path" and "Add External JARs...":

Find the AdMob Android SDK jar file, verify that it is included in the list and click "OK":

Now expand your project tree in the Package Explorer, double-click on "AndroidManifest.xml" and click the rightmost tab (AndroidManifest.xml) to open up the file in a text editor:

Here we must add one new "activity" and two new "uses-permission":

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.ajomannen.justroids"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="7" />
    <uses-permission android:name="android.permission.INTERNET"/>    
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".JustRoidsActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
           android:name="com.google.ads.AdActivity"
           android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
        />
    </application>
</manifest>

Actually I became stuck here for a while when I tried to add the "android:configChanges" line. All seven of the listed allowed configuration changes needs to be included, just as in the code above, but almost half of them was not valid parameters until Android 3.2. This means that my intended target Android version (2.1) does not support them...

I found my way around the problem by changing the target version for a while by right-clicking "JustRoids" in the Eclipse Package Explorer, select "Properties", "Android", "Project Build Target" and select the latest Android version (4.0.3 - API Level 15) instead of 2.1 (API Level 7):

I kept the line <uses-sdk android:minSdkVersion="7" /> in the manifest file, though, and cross my fingers that it won't cause any conflicts.

 

After saving our manifest file, the foundation for displaying AdMob ads is in place!

Chapter 10: Create the app's four pages

Now it's time to create the four different "pages" I sketched down in Chapter 05:

Let's start with declaring three new activities in the AndroidManifest.xml file. We can put the directly beneath our AdActivity:

    <activity
        android:name=".GameActivity"
        android:label="Game Activity"
        android:screenOrientation="portrait"
        android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
    />
    <activity
        android:name=".HowtoActivity"
        android:label="Howto Activity"
        android:screenOrientation="portrait"
        android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
    />
    <activity
        android:name=".CreditsActivity"
        android:label="Credits Activity"
        android:screenOrientation="portrait"
        android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
    />

While here, we can also remove the title bar from the main ".JustRoidsActivity" (our main menu) by adding a line to that block of code:

        <activity
            android:name=".JustRoidsActivity"
            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

Next step is to implement the three activities we just declared.

If we expand "src" and the package within it, we will see that only our main activity is there:

Right-click on the package and select "New" -> "Class":

Enter "GameActivity" as name and click "Browse":

Search for "Activity", select "Activity - android.app" and click "OK":

Then click "Finish":

A more or less empty class file is now created for you:

Repeat the same procedure ("New" -> "Class") and create "HowtoActivity" and "CreditsActivity" in the same manner, so that we have four java files in total in our package:

 

Next step is to create three new "layouts".

If we expand "res/layout" we'll see only main.xml there:

Mark "main.xml" and press ctrl-c to copy the file.

Then press ctrl-v and enter "credits.xml" as name. Repeat for "game.xml" and "howto.xml", so that we have four layout files in total:

Very soon we will edit the three new layouts, so that we can see the difference between them when we make our first test, but first this:

In order to make an app easy to adapt to additional languages, you should never have any text strings hard coded in your source, but rather in a separate resource file, that later on can be duplicated and edited for each supported language. We do this by opening up the file "res/values/strings.xml" and add the four lines:

    <string name="justroids">JustRoids</string>
    <string name="credits">Credits</string>
    <string name="play">Play</string>
    <string name="howto">HowTo</string>

Now we have four new text resources that we can refer to in our layouts.

Give "android:text" in credits.xml the value "@string/credits" :

 

Make the corresponding changes in game.xml and howto.xml.

 

Well, now we're finished with three layouts - time to let our three latest activities display them. 

Edit HowtoActivity.java and add an override on onCreate where we specify that the layout "howto" shall be shown:

package com.ajomannen.justroids;

import android.app.Activity;
import android.os.Bundle;

public class HowtoActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.howto);
    }
   
}

Repeat this for CreditsActivity.java (credits) and GameActivity.java (game).

 

These three were the easy ones, as they don't contain any functionality for navigating to other pages. (The handling of "Back" and "Home" is something that Android handle for us automatically.)

 

Our main menu (JustRoidsActivity.java / main.xml) is a bit more complex, though.

To begin with, main.xml should contain our four different text-images; one logo plus three buttons that links to the other pages. Furthermore it shall contain a special ad-view. (I decided to change my the idea of having ads displayed during the gameplay, to have them in the main menu instead. It is quite possible that I also decide to put ads in the Credits and HowTo pages later on.)

Open up main.xml in text mode, remove the existing content and paste the following instead:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ads="http://schemas.android.com/apk/lib/com.google.ads"
    android:id="@+id/rootLayout"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <ImageView
        android:id="@+id/imgLogo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:adjustViewBounds="true"
        android:contentDescription="@string/justroids"
        android:src="@drawable/justroids" />

    <ImageView
        android:id="@+id/btnGame"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/imgLogo"
        android:layout_gravity="center_horizontal"
        android:clickable="true"
        android:contentDescription="@string/play"
        android:onClick="onClickGame"
        android:src="@drawable/play" />

    <ImageView
        android:id="@+id/btnHowto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btnGame"
        android:layout_gravity="center_horizontal"
        android:clickable="true"
        android:contentDescription="@string/howto"
        android:onClick="onClickHowto"
        android:src="@drawable/howto" />

    <ImageView
        android:id="@+id/btnCredits"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btnHowto"
        android:layout_gravity="center_horizontal"
        android:clickable="true"
        android:contentDescription="@string/credits"
        android:onClick="onClickCredits"
        android:src="@drawable/credits" />

    <com.google.ads.AdView
        android:id="@+id/adView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerInParent="false"
        ads:adSize="BANNER"
        ads:adUnitId="XXXXXXXXXXXXXXX"
        android:gravity="bottom"
        ads:loadAdOnCreate="false"
        ads:test="true"
        android:visibility="visible" />

</RelativeLayout>

(The value of "ads.adUnitId" shall be the AdMob Publisher Id you wrote down somewhere safe earlier.)

You can double check that all objects shows up by clicking the "Graphical Layout" tab:

 

What remains now is to make JustRoidsActivity.java look like this:

package com.ajomannen.justroids;

import com.google.ads.AdRequest;
import com.google.ads.AdView;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class JustRoidsActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  AdView adView = (AdView) findViewById(R.id.adView);
  AdRequest adRequest = new AdRequest();
  adRequest.addTestDevice(AdRequest.TEST_EMULATOR);
  // adRequest.addTestDevice("E213A781A27DEE785F5CCD8A07BF21BF");
  adView.loadAd(adRequest);
}

public void onClickGame(View v) {
  Toast.makeText(this, "You clicked on Game!", Toast.LENGTH_LONG).show();
  Intent intent = new Intent(this, GameActivity.class);
  startActivity(intent);
}

public void onClickHowto(View v) {
  Toast.makeText(this, "You clicked on Howto!", Toast.LENGTH_LONG).show();
  Intent intent = new Intent(this, HowtoActivity.class);
  startActivity(intent);
}

public void onClickCredits(View v) {
  Toast.makeText(this, "You clicked on Credits!", Toast.LENGTH_LONG) .show();
  Intent intent = new Intent(this, CreditsActivity.class);
  startActivity(intent);
}

}

What we have done here is to find the ad view and load it with an ad (the orange block) at app startup and add three handlers for the differnt onClick-events we created earlier.

Observe that you should run your ad-integration in so called "Test-mode" while developing the app. This is done by 1) Setting "ads:test" to "true" in the ad view in main.xml and 2) Including the device you're testing on in the orange block (addTestDevice). Virtual devices, such as the one we created, should already be covered in the code above, and the out-commented line is an example of how a real physical device is added.

 

At this stage, our app only contains four different pages and the mechanism to navigate between them, but this is a good point to start it up and verify that we've got it right so far.

Press ctrl-F11 to install it on your virtual device and run it.

If everything's in order, you will eventually see the main menu:

Now try pressing "Play":

(I just noticed a bit of inconsistency regarding the use of the words "game" and "play", but that's not a bug. It's by design.)

Now we can verify the the rest of the page navigation works as intended; the back-button on the device should take you back to the main menu, while the home-button should put the app into the background and take you to the Android start screen. If you start the app again, it should display the page that was active when you left it.

 

This means that we are done with the basic framework, and the coming chapters will be a bit more advanced.

 

Requirements implemented in this chapter

✔ 01. The app shall be started through a clickable icon as any other app.

✔ 02. When the app is started, the Main Menu shall be shown, unless the app already was running in the background - in that case the last viewed page shall be shown.

✔ 03. On the Main Menu there shall be three links to the other pages; Play, How-To and Credits.

✔ 07. When pressing "Back" on your Android device, you shall end up "one page up". If your already on "Main Menu", the app shall be closed.

✔ 08. When pressing "Home", the app shall be put in the background and the Android device shall display the standard Android Home Screen.

Chapter 11: Implement the GameEngine

Finally it's time for some Java coding!

Start with removing the three "Toast.makeText" lines from JustRoidsActivity.java, as we already tested the page navigation and I only added them to show an example of the simplest possible tracing functionality.

I also set back "Project Build Target" to Android 2.1, so that Eclipse is able to stop me from writing code that won't work on 2.1.

Given that, I must also comment out the AdActivity from the manifest file. (I will put it back before publishing the app):

        <!--
        <activity 
            android:name="com.google.ads.AdActivity"
            android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
        />
        -->
 
Ok, let's start coding.
 
Right click on your package, select New -> Class and name it "GameEngine".
 
Paste in the following code:
 
package com.ajomannen.justroids;

import java.text.SimpleDateFormat;
import java.util.Date;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;

public class GameEngine {

  public float screenWidth;
  public float screenHeight;
  private Paint blackPaint;
  private Paint textPaint;
  private String currentTimeString;

  public void Init(Context context) {
    setSurfaceDimensions(240, 160);

    blackPaint = new Paint();
    blackPaint.setColor(Color.BLACK);
    blackPaint.setStyle(Style.FILL);

    textPaint = new Paint();
    textPaint.setColor(Color.LTGRAY);
    textPaint.setTextSize(40);
  }

  public void onDestroy() {
    try {
    } catch (Exception e) {
    }
  }

  public void setSurfaceDimensions(int width, int height) {
    screenWidth = width;
    screenHeight = height;
  }

  public void Update() {
    currentTimeString = new SimpleDateFormat("HH:mm:ss").format(new Date());
  }

  public void Draw(Canvas canvas) {
    canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), blackPaint);
    canvas.drawText(currentTimeString, 30, 100, textPaint);
  }

}
 
What we have done here is the bare minimums; 
  • An Init method, in which we handle everything that needs to be set up at program startup.
  • An onDestroy event handler, which is currently empty.
  • A setSurfaceDimensions that we need to call from our future GameSurface class.
  • The classic Update and Draw methods, that are the core in any real-time game.

(Just to have something to update and draw, I put the current time into a text string and draw it onto screen.)

 

Chapter 12: Implement the GameThread

Right click on your package, select New -> Class, name it "GameThread" and paste in the following code:

package com.ajomannen.justroids;

import android.content.Context;
import android.graphics.Canvas;
import android.os.Handler;
import android.view.SurfaceHolder;

public class GameThread extends Thread {
  private SurfaceHolder surfaceHolder;
  private Canvas canvas;
  private long delay = 1000000000L / 25;
  private long beforeTime = 0;
  private long afterTime = 0;
  private long timeDiff = 0;
  private long sleepTime;
  private long overSleepTime = 0;
  private long excess = 0;
 
  public final static int RUNNING = 1;
  public final static int PAUSED = 2;
  private static final int MAX_FRAME_SKIPS = 5;
  int state = RUNNING;
 
  GameEngine gameEngine;
 
  public GameThread(SurfaceHolder surfaceHolder, Context context,
    Handler handler, GameEngine gameEngine) {
   this.surfaceHolder = surfaceHolder;
   this.gameEngine = gameEngine;
  }
 
  @Override
  public void run() {
 
   while (state == RUNNING) {
    beforeTime = System.nanoTime();
 
    gameEngine.Update();
 
    canvas = null;
    try {
     canvas = surfaceHolder.lockCanvas(null);
     synchronized (surfaceHolder) {
      gameEngine.Draw(canvas);
     }
    } finally {
     if (canvas != null) {
      surfaceHolder.unlockCanvasAndPost(canvas);
     }
    }
 
    afterTime = System.nanoTime();
    timeDiff = afterTime - beforeTime;
    sleepTime = ((delay) - timeDiff) - overSleepTime;
 
    if (sleepTime > 0) {
     try {
      sleep(sleepTime / 1000000L);
     } catch (InterruptedException ex) {
     }
     overSleepTime = (System.nanoTime() - afterTime) - sleepTime;
    } else {
     excess -= sleepTime;
     overSleepTime = 0L;
    }
 
    int skips = 0;
    while ((excess > delay) && (skips < MAX_FRAME_SKIPS)) {
     excess -= delay;
     gameEngine.Update();
     skips++;
    }
 
   }
 
  }

}

 

An even simpler version of this thread could just call the GameEngine's Update and Draw methods 25 times per second, but since we know for a fact that there are lots of other apps running in the background on a typical device, it is more or less necessary to add timing functionality and skip drawing and just run updates until we are in sync again if necessary.

I will write this game with an update frequency of 25 frames per second. I estimate the maximum number of moving objects in JustRoids to be less than 50 (1 ship, 20 asteroids and 20 missiles) so I reckon that the CPU will be sufficient for this on any kind of Android 2.1 device. (These are of course very rough assumptions, but if it does not work it will be a pretty simple task to half the fps and double the speed of all objects afterwards to compensate.)

Chapter 13: Implement the GameSurface

The last of the three central classes to handle a real time game app is GameSurface:

package com.ajomannen.justroids;

import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class GameSurface extends SurfaceView implements SurfaceHolder.Callback {

  GameEngine gameEngine;
  SurfaceHolder surfaceHolder;
  Context context;
  private GameThread gameThread;
 
  public GameSurface(Context context, AttributeSet attrs, int defStyle) {
   super(context, attrs, defStyle);
   this.context = context;
   InitView();
  }
 
  public GameSurface(Context context, AttributeSet attrs) {
   super(context, attrs);
   this.context = context;
   InitView();
  }
 
  void InitView() {
   SurfaceHolder surfaceHolder = getHolder();
   surfaceHolder.addCallback(this);
   gameEngine = new GameEngine();
   gameEngine.Init(context);
   gameThread = new GameThread(surfaceHolder, context, new Handler(),
     gameEngine);
   setFocusable(true);
  }
 
  @Override
  public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
   boolean retry = true;
   gameThread.state = GameThread.PAUSED;
   while (retry) {
    try {
     gameThread.join();
     retry = false;
    } catch (InterruptedException e) {
    }
   }
  }
 
  @Override
  public void surfaceCreated(SurfaceHolder arg0) {
   if (gameThread.state == GameThread.PAUSED) {
    gameThread = new GameThread(getHolder(), context, new Handler(),
      gameEngine);
    gameThread.start();
   } else {
    gameThread.start();
   }
  }
 
  @Override
  public void surfaceChanged(SurfaceHolder holder, int format, int width,
    int height) {
   gameEngine.setSurfaceDimensions(width, height);
  }

}

It starts up the GameThread during app startup, shuts it down nicely when stopped and handles any event from the device.

We will add functionality for handling touch event later on, but this will do for now.

Chapter 14: Tie the knots together and watch the GameEngine work

All we need to do now to put our GameSurface of the device's (or emulator's) surface is to include it in res/layout/game.xml. It's that simple!

So throw away the existing content in game.xml and replace it with:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical" >
 
  <com.ajomannen.justroids.GameSurface
   android:id="@+id/gamesurface"
   android:layout_width="fill_parent"
   android:layout_height="fill_parent" />
 
</LinearLayout>

Now try to run your project (ctrl-F11) and click "Play" to see the first draft of our game engine work:

Voila!

There is still a long way to go before we have an actual game here, and not just a clock. But all that is handled by other requirements in our list from Chapter 5, so:

 

Requirements implemented in this chapter

✔ 04. The page "Play" is the game itself.

Chapter 15: Throw in an asteroid

In the previous chapters we have spent large amounts of time with very small amounts of visible results on the device's screen.

So let's switch mode and start implementing visible stuff!

As we will have three different types of moving graphical objects, it is wise to start with a generic class with all that is common.

Thus, create GfxObject.java and put in the following:

package com.ajomannen.justroids;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;

public class GfxObject {

  protected Bitmap bitmap;
  protected float x;
  protected float y;
  private double velocity = 0;
  private double direction = 0;
  private double dX = 0;
  private double dY = 0;
 
  public void setVelocity(float velocity) {
   this.velocity = velocity;
   calculateDXDY();
  }
 
  public void setDirection(float direction) {
   this.direction = direction;
   calculateDXDY();
  }
 
  private void calculateDXDY() {
   double radians = Math.toRadians(direction);
   dX = Math.cos(radians) * velocity;
   dY = Math.sin(radians) * velocity;
  }
 
  public void move(float screenWidth, float screenHeight) {
   float minX = 0 - bitmap.getWidth() / 2;
   float maxX = screenWidth + bitmap.getWidth() / 2;
   float minY = 0 - bitmap.getHeight() / 2;
   float maxY = screenHeight + bitmap.getHeight() / 2;
 
   x += dX;
   y += dY;
   if (x > maxX)
    x = minX;
   if (x < minX)
    x = maxX;
   if (y > maxY)
    y = minY;
   if (y < minY)
    y = maxY;
  }
 
  public void draw(Canvas canvas, float x, float y, Paint paint) {
   canvas.drawBitmap(bitmap, x - bitmap.getWidth() / 2,
     y - bitmap.getHeight() / 2, paint);
  }

}

Actually, almost everything required to move around an asteroid (or ship or missile) is here.

As you notice I choose to use polar coordinates (distance, angle - or velocity, direction) to express the object's movement, while I use Cartesian coordinates (x, y) for it's position. I found it more intuitive that way, as the asteroid's two halves will inherit their "parent's" speed later on but get a changed direction, while x,y is more suitable when it comes to "looping" objects around the screen's edges.

Anyway, it's time to create Asteroid.java:

package com.ajomannen.justroids;

import android.content.res.Resources;
import android.graphics.BitmapFactory;

public class Asteroid extends GfxObject {
 
  public Asteroid(Resources resources, float xPos, float yPos) {
   bitmap = BitmapFactory.decodeResource(resources,
     R.drawable.asteroid_100);
   x = xPos;
   y = yPos;
  }

}

See how short it became? We will have to put a bit more code later on, though, such as the handling of splitting an asteroid into two halves and so on. but for now it will do.

All we have to do now, is to create an instance of Asteroid and call it's move() and draw() methods from our GameEngine:

package com.ajomannen.justroids;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;

public class GameEngine {

  Context context;
  Resources resources;
 
  public float screenWidth;
  public float screenHeight;
  private Paint blackPaint;
  private Paint textPaint;
 
  Asteroid asteroid;
 
  public void Init(Context context) {
   this.context = context;
   resources = context.getResources();
 
   blackPaint = new Paint();
   blackPaint.setColor(Color.BLACK);
   blackPaint.setStyle(Style.FILL);
 
   textPaint = new Paint();
   textPaint.setColor(Color.LTGRAY);
   textPaint.setTextSize(40);
 
   asteroid = new Asteroid(resources, 20, 20);
   asteroid.setVelocity(2);
   asteroid.setDirection(135);
 
   setSurfaceDimensions(240, 160);
 
  }
 
  public void onDestroy() {
   try {
   } catch (Exception e) {
   }
  }
 
  public void setSurfaceDimensions(int width, int height) {
   screenWidth = width;
   screenHeight = height;
  }
 
  public void Update() {
   asteroid.move(screenWidth, screenHeight);
  }
 
  public void Draw(Canvas canvas) {
   canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), blackPaint);
   asteroid.draw(canvas, asteroid.x, asteroid.y, textPaint);
  }

}

And now to the visibility. Press ctrl-F11, click "Play" and see the asteroid fly by:

Chapter 16: Lesson learned - or How to NOT implement rotation

After a pretty friction-less journey up to here, it's easy to get a growing feeling of megalomania, and just assume that every design decision you take is the right one.

I just came to realize that it is not always the case...

Every (or almost every) resource has it's limits, and even the coolest device has it's boiling point.

 

One of the requirements in my wish list, was that the asteroids should rotate to give the game a more "living" feeling, and I had a pretty thorough idea on how I would implement that. So I just wrote the necessary code changes our the generic GfxObject .java and made a new build.

This is how it looked when I ran it on my emulator:

As you can see, there are two major issues:

  1. The asteroid is moving to slow in general - we're not keeping the 25 fps
  2. There are "hiccups" approximately once per second

My GfxObject currently looks like this:

package com.ajomannen.justroids;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.Log;

public class GfxObject {

  protected Bitmap bitmap;
  protected Bitmap rotatedAndCroppedBitmap;
  protected float x;
  protected float y;
  private double velocity = 0;
  private double direction = 0;
  private double dX = 0;
  private double dY = 0;
  private int rotation = -1;
  private int angle = 0;

  public void setVelocity(float velocity) {
   this.velocity = velocity;
   calculateDXDY();
  }

  public void setDirection(float direction) {
   this.direction = direction;
   calculateDXDY();
  }

  private void calculateDXDY() {
   double radians = Math.toRadians(direction);
   dX = Math.cos(radians) * velocity;
   dY = Math.sin(radians) * velocity;
  }

  public void move(float screenWidth, float screenHeight) {
   float minX = 0 - bitmap.getWidth() / 2;
   float maxX = screenWidth + bitmap.getWidth() / 2;
   float minY = 0 - bitmap.getHeight() / 2;
   float maxY = screenHeight + bitmap.getHeight() / 2;

   x += dX;
   y += dY;
   if (x > maxX)
    x = minX;
   if (x < minX)
    x = maxX;
   if (y > maxY)
    y = minY;
   if (y < minY)
    y = maxY;

   angle += rotation;
   if (Math.abs(angle) >= 360)
    angle = 0;
  }

  public void draw(Canvas canvas, float x, float y, Paint paint) {
   rotate(angle);
   canvas.drawBitmap(rotatedAndCroppedBitmap,
     x - rotatedAndCroppedBitmap.getWidth() / 2, y
       - rotatedAndCroppedBitmap.getHeight() / 2, paint);
  }

  private void rotate(int degrees) {

   int width = bitmap.getWidth();
   int height = bitmap.getHeight();

   float midX = (float) width / 2;
   float midY = (float) height / 2;

   Matrix matrix = new Matrix();
   matrix.setRotate(degrees, midX, midY);

   try {
    Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width,
      height, matrix, true);

    rotatedAndCroppedBitmap = Bitmap.createBitmap(rotatedBitmap,
      (rotatedBitmap.getWidth() - width) / 2,
      (rotatedBitmap.getHeight() - height) / 2, width, height);
   } catch (Exception e) {
    Log.e("JustRoids", "Failed to rotate an image " + degrees
      + " degrees:");
    e.printStackTrace();
   }
  }

}

After reading the article on: http://developer.android.com/guide/practices/design/performance.html I realize that I have made two embarrassing mistakes, which I marked in red color above:

  1. I create three expensive objects (two Bitmaps and one Matrix) 25 times per second, causing periodic Garbage Collections - the hiccups.
  2. I rotate the asteroid 25 times per second. It was too expensive. End of statement.

I will have to throw the changes above away and choose another strategy.

 

See you in next chapter!

 

Chapter 17: Start profiling your app's performance

I recently attended a seminar where I was let to know that it is better practice to base your design decision on data rather than opinion. This came of course as a shock to me - not the fact that data is​ more reliable than opinion in itself, but the thing that it is recognized as a best practice. My personal experience is that opinion overrides facts. Every time. But let's not open up that can of worms for now ;-)

As the previous chapter gave us some kind of low-water mark, this is a golden opportunity to start using the Profiling functions in Eclipse/Android SDK, so we can measure what improvements we get with a re-design of the rotation mechanism.

 

First - switch view in Eclipse with: Window -> Open Perspective -> DDMS. (You get back to the standard view with Window -> Open Perspective -> Java).

As I'm using Android 2.1, I must also allow my app write access to the SD Card, by adding:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

to my manifest file.

Redeploy the app (ctrl-F11), and wait for it to start up and click "Play" when the main menu shows.

Select your app's process and click Start Method Profiling in the toolbar:

 Let the asteroid sweep (or perhaps "stumble" in this case) a few times and then click Stop Method Profiling.

Now the profiling data will be pulled from the device into Eclipse and you will see everything you need.

 

First off is the timeline, which gives a very clear visualization of the periodic GCs that my excessive object creations causes. It is quite bad:

Then, let's expand the thread tree and find out what the CPU is busy with:

Here we see that our GameEngine's draw-method takes 70.9% av all CPU cycles available, so of course we focus on that and expand it's Children tree.

There it is obvious that GfxObject.draw is next point of focus with it's 94.4%.

We continue to drill our way down via GfxObject.rotate to the two calls to Bitmap.createBitmap.

0.709 * 0.944 * 0.956 * (0.702 + 0.250) = 0.61

My poor device is spending 61% of it's time creating bitmaps!

 

Normally, I focus on the top three bottlenecks in my development/optimization/tuning activities, but in this case we have enough material to just go ahead and stop creating bitmaps each frame.

 

(Reference material: http://developer.android.com/guide/developing/debugging/ddms.html#profiling)

Chapter 18: A better implementation of object rotation

I tried a completely different approach, and instead of creating bitmaps over and over again, I rotate the whole canvas around the center of our asteroid before drawing it. This is the new GfxObject.java:

package com.ajomannen.justroids;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;

public class GfxObject {

  protected Bitmap bitmap;
  protected float x;
  protected float y;
  private double velocity = 0;
  private double direction = 0;
  private double dX = 0;
  private double dY = 0;
  private int rotation = -1;
  private int angle = 0;

  public void setVelocity(float velocity) {
   this.velocity = velocity;
   calculateDXDY();
  }

  public void setDirection(float direction) {
   this.direction = direction;
   calculateDXDY();
  }

  private void calculateDXDY() {
   double radians = Math.toRadians(direction);
   dX = Math.cos(radians) * velocity;
   dY = Math.sin(radians) * velocity;
  }

  public void move(float screenWidth, float screenHeight) {
   float minX = 0 - bitmap.getWidth() / 2;
   float maxX = screenWidth + bitmap.getWidth() / 2;
   float minY = 0 - bitmap.getHeight() / 2;
   float maxY = screenHeight + bitmap.getHeight() / 2;

   x += dX;
   y += dY;
   if (x > maxX)
    x = minX;
   if (x < minX)
    x = maxX;
   if (y > maxY)
    y = minY;
   if (y < minY)
    y = maxY;

   angle += rotation;
   if (Math.abs(angle) >= 360)
    angle = 0;
  }

  public void draw(Canvas canvas, float x, float y, Paint paint) {
   canvas.save(Canvas.MATRIX_SAVE_FLAG);
   canvas.rotate(-angle, x, y);
   canvas.drawBitmap(bitmap, x - bitmap.getWidth() / 2,
     y - bitmap.getHeight() / 2, paint);
   canvas.restore();
  }

}

A quick profiling shows that all periodic GCs are gone:

Success! (All the hiccups are also gone, and the asteroid moves smoothly around on the screen.)

 

When looking at the CPU consumption, we can also see a significant improvement:

Now it is the Canvas.drawBitmap method that takes the most CPU, with a total of 60.7% of 64.9% of 44% = 17%, while rotating the canvas 25 times per second only costs 5% of the available cycles (on my emulator running on my €200 netbook). 

I did not expect that Canvas.rotate would be that​ cheap.

 

Anyway, I think we're done here for now. Rotating the canvas is the way forward for the moment.

 

Requirements implemented in this chapter

✔ 16. The asteroids shall rotate (just for the effect).

Chapter 19: The birth of an AsteroidHandler

After the huge success regarding the effectiveness of rotating the whole canvas lead me to a few conclusions:

  1. We do not need 1 bitmap per asteroid, scaled to the current size (whole, half, quarter) and rotated individually. It's better to create three different sizes from the beginning (in some common place) and let each asteroid just keep a pointer to one of them.
  2. The GameEngine will soon be cluttered with special code for the asteroid. And then comes the ship and the missiles also. What we need is an AsteroidHandler.
  3. I might as well create the three asteroid sizes from the beginning instead of in the app, to be able to have different images if I want to later on.

Said and done! 

I started with renaming the image "asteroid_100" to "asteroid_whole" and created two smaller versions called "asteroid_half" and "asteroid_quarter".

Then is was time to implement the AsteroidHandler: 

package com.ajomannen.justroids;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class AsteroidHandler {
  private Bitmap[] asteroidBitmaps;
  private List<Asteroid> asteroids;
  private Random random;
  private Paint paint;

  public AsteroidHandler(Resources resources, int level) {
   paint = new Paint();
   random = new Random();

   asteroidBitmaps = new Bitmap[3];
   asteroidBitmaps[Asteroid.WHOLE] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_whole);
   asteroidBitmaps[Asteroid.HALF] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_half);
   asteroidBitmaps[Asteroid.QUARTER] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_quarter);

   asteroids = new ArrayList<Asteroid>();
   float x;
   float y;
   double velocity;
   double direction;
   int rotation;

   for (int i = 0; i < level; i++) {
    x = 160 * random.nextFloat();
    y = 1;
    velocity = 0.2 + 1 * random.nextFloat();
    direction = 360 * random.nextFloat();
    rotation = -1 - random.nextInt(3);
    asteroids.add(new Asteroid(asteroidBitmaps[Asteroid.WHOLE], x, y,
      velocity, direction, rotation));
   }
  }

  public void update(float screenWidth, float screenHeight) {
   for (Asteroid asteroid : asteroids)
    asteroid.move(screenWidth, screenHeight);
  }

  public void draw(Canvas canvas) {
   for (Asteroid asteroid : asteroids)
    asteroid.draw(canvas, asteroid.x, asteroid.y, paint);
  }

}

What this handler does is simply to handle the creation of the asteroids and keep track of them in an ArrayList in order to handle the updating and drawing of them.

 

We then have to make a few rearrangements in our GameEngine, remove the handling of our initial test-asteroid and invoke the AsteroidHandler instead:

package com.ajomannen.justroids;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;

public class GameEngine {

  Context context;
  Resources resources;

  public float screenWidth;
  public float screenHeight;
  private Paint blackPaint;

  private int level;
  private AsteroidHandler asteroidHandler;

  public void Init(Context context) {
   this.context = context;
   resources = context.getResources();

   blackPaint = new Paint();
   blackPaint.setColor(Color.BLACK);
   blackPaint.setStyle(Style.FILL);

   level = 5;
   asteroidHandler = new AsteroidHandler(resources, level);

   setSurfaceDimensions(240, 160);

  }

  public void onDestroy() {
   try {
   } catch (Exception e) {
   }
  }

  public void setSurfaceDimensions(int width, int height) {
   screenWidth = width;
   screenHeight = height;
  }

  public void update() {
   asteroidHandler.update(screenWidth, screenHeight);
  }

  public void draw(Canvas canvas) {
   canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), blackPaint);
   asteroidHandler.draw(canvas);
  }

}

A few minor changes to GfxObject (like removal of the terrible rotate method):

package com.ajomannen.justroids;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;

public class GfxObject {

  protected Bitmap bitmap;
  protected float x;
  protected float y;
  private double velocity = 0;
  private double direction = 0;
  private double dX = 0;
  private double dY = 0;
  protected int rotation = 1;
  private int angle = 0;

  public void setVelocity(double velocity) {
   this.velocity = velocity;
   calculateDXDY();
  }

  public void setDirection(double direction) {
   this.direction = direction;
   calculateDXDY();
  }

  private void calculateDXDY() {
   double radians = Math.toRadians(direction);
   dX = Math.cos(radians) * velocity;
   dY = Math.sin(radians) * velocity;
  }

  public void move(float screenWidth, float screenHeight) {
   float minX = 0 - bitmap.getWidth() / 2;
   float maxX = screenWidth + bitmap.getWidth() / 2;
   float minY = 0 - bitmap.getHeight() / 2;
   float maxY = screenHeight + bitmap.getHeight() / 2;

   x += dX;
   y += dY;
   if (x > maxX)
    x = minX;
   if (x < minX)
    x = maxX;
   if (y > maxY)
    y = minY;
   if (y < minY)
    y = maxY;

   angle += rotation;
   if (Math.abs(angle) >= 360)
    angle = 0;
  }

  public void draw(Canvas canvas, float x, float y, Paint paint) {
   canvas.save(Canvas.MATRIX_SAVE_FLAG);
   canvas.rotate(angle, x, y);
   canvas.drawBitmap(bitmap, x - bitmap.getWidth() / 2,
     y - bitmap.getHeight() / 2, paint);
   canvas.restore();
  }

}

An finally, the latest version of Asteroid:

package com.ajomannen.justroids;

import android.graphics.Bitmap;

public class Asteroid extends GfxObject {
  static final int WHOLE = 2;
  static final int HALF = 1;
  static final int QUARTER = 0;

  public Asteroid(Bitmap bitmap, float x, float y, double velocity,
    double direction, int rotation) {
   this.bitmap = bitmap;
   this.x = x;
   this.y = y;
   setVelocity(velocity);
   setDirection(direction);
   this.rotation = rotation;
  }

}

The result of all this? Five beautifully rotating asteroids:

Chapter 20: Collision detection basics

Now that we have five different objects moving around on the screen, there's a golden opportunity to implement some sort of collision detection mechanism.

(I'm not sure that I want the asteroids to be able to collide in the finished game, but they do represent a good test case.)

But which method should we use? 

  1. Check if the borders of any object intersects with any other object's borders? (There is a Rect.intersects() method that could be used for that.)
  2. Check while we are drawing and see if one more pixels are occupied already? (Drawback is that objects only can collide with other objects that are already drawn - meaning that the idea of separate update() and draw() in our GameEngine doesn't really fly.)
  3. Check within a circle from each objects midpoint? (Better than within a rectangle, but not quite pixel-perfect.)

(If you want to dig deeper into the details, there's a very thorough article here: http://www.metanetsoftware.com/technique/tutorialA.html).

Method 1 isn't really applicable in our case, as our asteroids are very far from being rectangular. So I think I start elaborating around using a combination of method 2 and 3 or just method 3.

Consider two asteroids traveling in trajectories that will make them collide in the very near future:

 

As we already decided, the shapes of the asteroids are pretty close to circles, so we will use circles in our detection algorithm. The question is: How large circles?

There are two main options here:

Option 1: We use circles that completely covers the asteroids, and if they intersect - we look more closely into the rectangular area that covers the intersection:

 

Option 2: ​We use circles that aligns with the real borders as good as possible, and if they intersect - we call it a collision. (Sometimes they should have collided already before, and sometimes they shouldn't have collided at all.):

 

Option 1 is the solution that can be "pixel perfect", but it will cost us many expensive CPU-cycles. First we need to know whether the circles intersect or not, preferably by calculating the distance between the center points of asteroid A and B and see if that distance is smaller than the sum of both circles' radiuses. That's a pretty fast operation, but the second part is worse. Far worse. 

Remember how we decided to rotate the whole canvas instead of the asteroids while drawing them on the display? That choice bites back now.

Take a look at these two scenarios as an example of what I mean:

The circles are on identical positions in both scenarios, as well as the x,y positions of the asteroids. What differs is the rotation. One scenario is a collision, the other one is not. But we don't have any rotated bitmap images in our code... We never rotate anything at all in the update() method. We just increase the value of a rotation-counter.

If our assignment was to really, really make a pixel-perfect collision detection, we could have solved it by:

  1. Check for collisions in the draw() method instead
  2. Make sure that our GameEngine calls draw() every frame, and skip our nice timing functionality
  3. Draw one asteroid at a time, and between each canvas rotation and asteroid draw, loop through the whole bitmap and check if the corresponding position on the canvas is empty or if there's part of an already drawn asteroid there (the "method 2 strategy")

We could of course make all asteroids perfectly circular. Or rectangular. Or as a series of different polygons... (Real "Big-League" game engines would probably use a huge set of 2-D triangular shapes in a 3-D space. But let's not get into that now. Maybe later...)

Ok, that was about the first option. Now let's take a deeper look at Option 2.

Nah - let's just go for it. It's so much faster and easier.

So, I will try to implement the option-2-collision-detection as a nested for-loop just after each asteroid's move, looping through all previously processed asteroid. I know that it will lead to a number of calculations that corresponds to number-of-asteroids * number-of-asteroids / 2, which isn't really that optimal... (Actually it's a lot like the "Good Morning"-goldfish-scene in Monty Python's The Meaning of Life.)

But let's try it out anyway in the next chapter, as we now know how to profile our app.

Chapter 21: Lessons learned #2 and #3 - Scaling and bouncing

Eager to get going, I just started coding three new "features" into the code:

  1. The property "size" for an asteroid. I forgot that earlier. (I just included a pointer to the "whole" asteroid image when they are created.)
  2. The collision detection itself, consisting of a loop that checks if the distance between all previously handles asteroids and the current one, is larger than the sum of their radiuses.
  3. A simple bounce-effect, by reverting the direction of the current asteroid.

 

It didn't work quite the way I hoped it to:

First, asteroids collide way before they actually meet.

Secondly, they get stuck on each other.

It looks really terrible. Worse than the bad rotation.

 

This is what I did in AsteroidHandler.java. (Bold parts are to handle the concept of "size", red parts are the collision detection and bouncing.):

package com.ajomannen.justroids;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class AsteroidHandler {
  private Bitmap[] asteroidBitmaps;
  private float[] asteroidRadiuses;
  private List<Asteroid> asteroids;
  private Random random;
  private Paint paint;

  public AsteroidHandler(Resources resources, int level) {
   paint = new Paint();
   random = new Random();

   asteroidBitmaps = new Bitmap[3];
   asteroidBitmaps[Asteroid.WHOLE] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_whole);
   asteroidBitmaps[Asteroid.HALF] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_half);
   asteroidBitmaps[Asteroid.QUARTER] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_quarter);

   asteroidRadiuses = new float[3];
   asteroidRadiuses[Asteroid.WHOLE] = 50;
   asteroidRadiuses[Asteroid.HALF] = 30;
   asteroidRadiuses[Asteroid.QUARTER] = 15;

   asteroids = new ArrayList<Asteroid>();
   float x;
   float y;
   double velocity;
   double direction;
   int rotation;

   for (int i = 0; i < level; i++) {
    x = 160 * random.nextFloat();
    y = 320 * random.nextFloat();
    // No random velocity anymore. Each asteroid is faster than the
    // previous.
    velocity = 0.2 + 0.5 * i;
    direction = 360 * random.nextFloat();
    rotation = -1 - random.nextInt(3);
    asteroids.add(new Asteroid(asteroidBitmaps[Asteroid.WHOLE],
      Asteroid.WHOLE, x, y, velocity, direction, rotation));
   }
  }

  public void update(float screenWidth, float screenHeight) {
   int index = 0;
   Asteroid otherAsteroid;
   float dX;
   float dY;
   float distance;

   for (Asteroid thisAsteroid : asteroids) {
    thisAsteroid.move(screenWidth, screenHeight);
    for (int i = 0; i < index; i++) {
     otherAsteroid = asteroids.get(i);
     dX = Math.abs(thisAsteroid.x - otherAsteroid.x);
     dY = Math.abs(thisAsteroid.y - otherAsteroid.y);
     distance = (float) Math.sqrt(dX * dX + dY * dY);
     if (distance <= (asteroidRadiuses[thisAsteroid.size] + asteroidRadiuses[otherAsteroid.size])) {
      double newDirection = thisAsteroid.getDirection() + 160
        + 40 * random.nextFloat();
      if (newDirection >= 360)
       newDirection -= 360;
      thisAsteroid.setDirection(newDirection);
      break;
     }
    }
    index++;
   }
  }

  public void draw(Canvas canvas) {
   for (Asteroid asteroid : asteroids)
    asteroid.draw(canvas, asteroid.x, asteroid.y, paint);
  }

}

As we call getDirection() and getVelocity() for an asteroid above, we have to add those two methods in GfxObject.java also:

public double getVelocity() {
  return velocity;
}

public double getDirection() {
  return direction;
}

We also have to add "size" to Asteroid.java:

package com.ajomannen.justroids;

import android.graphics.Bitmap;

public class Asteroid extends GfxObject {
  static final int WHOLE = 2;
  static final int HALF = 1;
  static final int QUARTER = 0;

  int size;

  public Asteroid(Bitmap bitmap, int size, float x, float y, double velocity,
    double direction, int rotation) {
   this.bitmap = bitmap;
   this.size = size;
   this.x = x;
   this.y = y;
   setVelocity(velocity);
   setDirection(direction);
   this.rotation = rotation;
  }

}

My mistakes here are two; 

  1. Just because the asteroid-images are a certain number of pixels wide, it doesn't mean that they are that wide on the device. Android scales images at will.  What actually happens is that Android will consider my only version of asteroid_whole to be a "medium-density" (mdpi) version, and if the device we use is ldpi or hdpi it will be scaled unless we specifically ask it to not scale. Hence, we can not simply assume a certain radius in our code - we must check the width/height of the bitmap image after​ it has been loaded.
  2. Real collisions does not often lead to that the fastest object simply turn back at the same speed. And if they do, they don't turn back again and stick to the slower object if the collision is still ongoing. 

I think I will have to solve the issues one at a time, and start with disabling "bouncing" and just highlight the collided asteroids in a separate color, while I fix the scaling issue. 

Let's start with adding a "collided" property to GfxObject:

  protected boolean collided = false;

  public boolean isCollided() {
   return collided;
  }

  public void setCollided(boolean collided) {
   this.collided = collided;
  }

Then we comment out the crappy bouncing code and instead set "collided" for both asteroids in AsteroidHandler update() method:

 public void update(float screenWidth, float screenHeight) {
  int index = 0;
  Asteroid otherAsteroid;
  float dX;
  float dY;
  float distance;

  for (Asteroid thisAsteroid : asteroids) {
   thisAsteroid.move(screenWidth, screenHeight);
   thisAsteroid.setCollided(false);
   for (int i = 0; i < index; i++) {
    otherAsteroid = asteroids.get(i);
    dX = Math.abs(thisAsteroid.x - otherAsteroid.x);
    dY = Math.abs(thisAsteroid.y - otherAsteroid.y);
    distance = (float) Math.sqrt(dX * dX + dY * dY);
    if (distance <= (asteroidRadiuses[thisAsteroid.size] + asteroidRadiuses[otherAsteroid.size])) {
     thisAsteroid.setCollided(true);
     otherAsteroid.setCollided(true);
     // double newDirection = thisAsteroid.getDirection() + 160
     // + 40 * random.nextFloat();
     // if (newDirection >= 360)
     // newDirection -= 360;
     // thisAsteroid.setDirection(newDirection);
     // break;
    }
   }
   index++;
  }
}

After that, we can decide how this collision should be illustrated.

I choose to make a very red copy of all three asteroid images and call them "asteroid_xxxx_highlight.png", add a second Bitmap in GfxObject:

protected Bitmap bitmapHighlighted;

and also add it in the constructor in Asteroid:

 public Asteroid(Bitmap bitmap, Bitmap bitmapHilighted, int size, float x,
   float y, double velocity, double direction, int rotation) {
  this.bitmap = bitmap;
  this.bitmapHighlighted = bitmapHighlighted;
  this.size = size;
  this.x = x;
  this.y = y;
  setVelocity(velocity);
  setDirection(direction);
  this.rotation = rotation;
}

Time to get another Bitmap array into AsteroidHandler:

  private Bitmap[] asteroidBitmaps;
  private Bitmap[] asteroidBitmapsHighlighted;

Populate it with:

  asteroidBitmaps = new Bitmap[3];
  asteroidBitmaps[Asteroid.WHOLE] = BitmapFactory.decodeResource(
    resources, R.drawable.asteroid_whole);
  asteroidBitmaps[Asteroid.HALF] = BitmapFactory.decodeResource(
    resources, R.drawable.asteroid_half);
  asteroidBitmaps[Asteroid.QUARTER] = BitmapFactory.decodeResource(
    resources, R.drawable.asteroid_quarter);
 
  asteroidBitmapsHighlighted = new Bitmap[3];
  asteroidBitmapsHighlighted[Asteroid.WHOLE] = BitmapFactory
    .decodeResource(resources,
      R.drawable.asteroid_whole_highlighted);
  asteroidBitmapsHighlighted[Asteroid.HALF] = BitmapFactory
    .decodeResource(resources, R.drawable.asteroid_half_highlighted);
  asteroidBitmapsHighlighted[Asteroid.QUARTER] = BitmapFactory
    .decodeResource(resources,
      R.drawable.asteroid_quarter_highlighted);

And include one of them when creating the asteroids:

   asteroids.add(new Asteroid(asteroidBitmaps[Asteroid.WHOLE],
     asteroidBitmapsHighlighted[Asteroid.WHOLE], Asteroid.WHOLE,
     x, y, velocity, direction, rotation));

And finally, get the draw() method in GfxObject to choose between the two images:

 public void draw(Canvas canvas, float x, float y, Paint paint) {
  canvas.save(Canvas.MATRIX_SAVE_FLAG);
  canvas.rotate(angle, x, y);
  if (collided) {
   canvas.drawBitmap(bitmapHighlighted,
     x - bitmapHighlighted.getWidth() / 2,
     y - bitmapHighlighted.getHeight() / 2, paint);
  } else {
   canvas.drawBitmap(bitmap, x - bitmap.getWidth() / 2,
     y - bitmap.getHeight() / 2, paint);
  }
  canvas.restore();
}

Well, this worked like a charm:

 

Now we have a good trouble-shooting scenario to fix the scaling issue.

Let's do that in next chapter.

Chapter 22: How to deal with scaling

In order to really visualize the scaling, we can draw two circles around each asteroid, one as wide as we thought our image was and one as wide as Android says it is.

To do this, we can add two more Paints in the AsteroidHandler constructor:

 public AsteroidHandler(Resources resources, int level) {
  random = new Random();

  paint = new Paint();
  
  yellowPaint = new Paint();
  yellowPaint.setColor(Color.YELLOW);
  yellowPaint.setStyle(Style.STROKE);
  
  whitePaint = new Paint();
  whitePaint.setColor(Color.WHITE);
  whitePaint.setStyle(Style.STROKE);

and use them to draw two circles after drawing each asteroid in AsteroidHandler's draw() method:

 public void draw(Canvas canvas) {
  for (Asteroid asteroid : asteroids) {
   asteroid.draw(canvas, asteroid.x, asteroid.y, paint);
   canvas.drawCircle(asteroid.x, asteroid.y, asteroidRadiuses[asteroid.size], whitePaint);
   canvas.drawCircle(asteroid.x, asteroid.y, asteroid.bitmap.getWidth()/2, yellowPaint);
  }
}

The result speaks for itself:

Having an array of asteroidRadiuses was obviously not meaningful here.

We can easily verify this, by changing the detection itself in AsteroidHandler.update():

  // if (distance <= (asteroidRadiuses[thisAsteroid.size] + asteroidRadiuses[otherAsteroid.size])) {
  if (distance <= (thisAsteroid.bitmap.getWidth() / 2 + otherAsteroid.bitmap.getWidth() / 2)) {

Great! Now we can clearly see that intersections of yellow circles gives red asteroids, while white circles are harmless:

 

After this discovery, I erase all traces of asteroidRadiuses from AsteroidHandler.java. By doing that, I will also have to remove the drawing of the white circles, and while we're at it, we might as well remove the yellow circle and the two new Paints (whitePaint and yellowPaint).

As there is no real use case for bouncing objects in this game, I decided that I won't deal with the physics behind impacts any further in this tutorial. It is likely to cover a few chapters in itself and it is better to cover that in another article.

So, I'm more or less finished with the collision detection for now. (I'll leave the red highlighting of colliding asteroids in the code. It might be of use later on.)

 

Lessons learned in this and the previous chapter are quite a few:

  1. Avoid shortcuts. If I created three versions of asteroid_whole (ldpi, mdpi and hdpi) from the beginning, scaling would not have occurred.
  2. Dig deeper. There is something called BitmapFactory.Options. That one has a property called inScaled that can be set to false and then passed to BitmapFactory.decodeResource() to prevent scaling. Who would have thought...  (It would not have worked well in this game though, as I took the shortcut with only one size of the graphics.)
  3. Take small steps. Make sure that your app is "runnable" all the time. Test after each addition of functionality. No "Big Bang" tests at the end of the project.
  4. Choose your debugging tools wisely. We used "Toast" to verify slow events, such as user input. We used profiling to identify bottlenecks. And now we used additional visualization to troubleshoot frequently updated graphics.
Next chapter will introduce the ship!

Chapter 23: Add a ship among our asteroids

In this chapter we will finally add our ship!

It will not be cotrollable yet though, as handling of touch events is the topic for next chapter.

 

First, we need to make a few changes to our ship-image. 100 pixels is too big compared to the asteroid sizes.

I scaled down ship_100.png to 50 pixels, and called it "ship.png". I then made a copy, shifted it's colours to the red spectra and called it "ship_highlighted.png".

Then some coding.

Create "ship.java":

package com.ajomannen.justroids;

import java.util.List;

import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class Ship extends GfxObject {
  private Paint paint;

  public Ship(Resources resources, float x, float y, double velocity,
    double direction, int angle) {
   paint = new Paint();
   bitmap = BitmapFactory.decodeResource(resources, R.drawable.ship);
   bitmapHighlighted = BitmapFactory.decodeResource(resources,
     R.drawable.ship_highlighted);
   this.x = x;
   this.y = y;
   setVelocity(velocity);
   setDirection(direction);
   this.angle = angle;
  }

  public void update(float screenWidth, float screenHeight,
    List<Asteroid> asteroids) {
   float dX;
   float dY;
   float distance;

   move(screenWidth, screenHeight);
   setCollided(false);
   for (Asteroid asteroid : asteroids) {
    dX = Math.abs(x - asteroid.x);
    dY = Math.abs(y - asteroid.y);
    distance = (float) Math.sqrt(dX * dX + dY * dY);
    if (distance <= (bitmap.getWidth() / 2 + asteroid.bitmap.getWidth() / 2)) {
     collided = true;
    }
   }
  }

  public void draw(Canvas canvas) {
   draw(canvas, x, y, paint);
  }

}

The "ship" is similar to "asteroid", apart from that we will not use a handler for it, so a few extra methods needs to be there. I chose to enable collision detection right away, but I only highlight the ship for now. No crashes yet.

I also had to do a few minor changes to GfxObject.java:

package com.ajomannen.justroids;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;

public class GfxObject {

  protected Bitmap bitmap;
  protected Bitmap bitmapHighlighted;
  protected float x;
  protected float y;
  private double velocity = 0;
  private double direction = 0;
  private double dX = 0;
  private double dY = 0;
  protected int rotation = 0;
  protected int angle = 0;
  protected boolean collided = false;

  public boolean isCollided() {
   return collided;
  }

  public void setCollided(boolean collided) {
   this.collided = collided;
  }

  public double getVelocity() {
   return velocity;
  }

  public void setVelocity(double velocity) {
   this.velocity = velocity;
   calculateDXDY();
  }

  public double getDirection() {
   return direction;
  }

  public void setDirection(double direction) {
   this.direction = direction;
   calculateDXDY();
  }

  private void calculateDXDY() {
   double radians = Math.toRadians(direction-90);
   dX = Math.cos(radians) * velocity;
   dY = Math.sin(radians) * velocity;
  }

  public void move(float screenWidth, float screenHeight) {
   float minX = 0 - bitmap.getWidth() / 2;
   float maxX = screenWidth + bitmap.getWidth() / 2;
   float minY = 0 - bitmap.getHeight() / 2;
   float maxY = screenHeight + bitmap.getHeight() / 2;

   x += dX;
   y += dY;
   if (x > maxX)
    x = minX;
   if (x < minX)
    x = maxX;
   if (y > maxY)
    y = minY;
   if (y < minY)
    y = maxY;

   angle += rotation;
   if (Math.abs(angle) >= 360)
    angle = 0;
  }

  public void draw(Canvas canvas, float x, float y, Paint paint) {
   canvas.save(Canvas.MATRIX_SAVE_FLAG);
   canvas.rotate(angle, x, y);
   if (collided) {
    canvas.drawBitmap(bitmapHighlighted,
      x - bitmapHighlighted.getWidth() / 2,
      y - bitmapHighlighted.getHeight() / 2, paint);
   } else {
    canvas.drawBitmap(bitmap, x - bitmap.getWidth() / 2,
      y - bitmap.getHeight() / 2, paint);
   }
   canvas.restore();
  }

}

Now we have the possibility to simply add "ship" to our GameEngine:

package com.ajomannen.justroids;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;

public class GameEngine {

  Context context;
  Resources resources;

  public float screenWidth;
  public float screenHeight;
  private Paint blackPaint;

  private int level;
  private AsteroidHandler asteroidHandler;
  private Ship ship;

  public void Init(Context context) {
   this.context = context;
   resources = context.getResources();

   blackPaint = new Paint();
   blackPaint.setColor(Color.BLACK);
   blackPaint.setStyle(Style.FILL);

   level = 5;
   asteroidHandler = new AsteroidHandler(resources, level);
   ship = new Ship(resources, 120, 280, 0.1, 0, 0);

   setSurfaceDimensions(240, 320);

  }

  public void onDestroy() {
   try {
   } catch (Exception e) {
   }
  }

  public void setSurfaceDimensions(int width, int height) {
   screenWidth = width;
   screenHeight = height;
  }

  public void update() {
   asteroidHandler.update(screenWidth, screenHeight);
   ship.update(screenWidth, screenHeight, asteroidHandler.asteroids);
  }

  public void draw(Canvas canvas) {
   canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), blackPaint);
   asteroidHandler.draw(canvas);
   ship.draw(canvas);
  }

}

I then took the opportunity to completely remove the collision detection from AsteroidHandler, as  we now have it in Ship. I also had to make the asteroid list public, so that our ship can walk through it during it's collision detection. The asteroids' initial positions and direction was also changed: 

package com.ajomannen.justroids;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class AsteroidHandler {
  private Bitmap[] asteroidBitmaps;
  private Bitmap[] asteroidBitmapsHighlighted;
  public List<Asteroid> asteroids;
  private Random random;
  private Paint paint;

  public AsteroidHandler(Resources resources, int level) {
   random = new Random();

   paint = new Paint();

   asteroidBitmaps = new Bitmap[3];
   asteroidBitmaps[Asteroid.WHOLE] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_whole);
   asteroidBitmaps[Asteroid.HALF] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_half);
   asteroidBitmaps[Asteroid.QUARTER] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_quarter);

   asteroidBitmapsHighlighted = new Bitmap[3];
   asteroidBitmapsHighlighted[Asteroid.WHOLE] = BitmapFactory
     .decodeResource(resources,
       R.drawable.asteroid_whole_highlighted);
   asteroidBitmapsHighlighted[Asteroid.HALF] = BitmapFactory
     .decodeResource(resources, R.drawable.asteroid_half_highlighted);
   asteroidBitmapsHighlighted[Asteroid.QUARTER] = BitmapFactory
     .decodeResource(resources,
       R.drawable.asteroid_quarter_highlighted);

   asteroids = new ArrayList<Asteroid>();
   float x;
   float y;
   double velocity;
   double direction;
   int rotation;

   for (int i = 0; i < level; i++) {
    x = 240 * random.nextFloat();
    y = 50 * random.nextFloat();
    velocity = 0.2 + 0.5 * i;
    direction = 180 - 45 + 90 * random.nextFloat();
    rotation = -1 - random.nextInt(3);
    asteroids.add(new Asteroid(asteroidBitmaps[Asteroid.WHOLE],
      asteroidBitmapsHighlighted[Asteroid.WHOLE], Asteroid.WHOLE,
      x, y, velocity, direction, rotation));
   }
  }

  public void update(float screenWidth, float screenHeight) {

   for (Asteroid thisAsteroid : asteroids) {
    thisAsteroid.move(screenWidth, screenHeight);
   }
  }

  public void draw(Canvas canvas) {
   for (Asteroid asteroid : asteroids) {
    asteroid.draw(canvas, asteroid.x, asteroid.y, paint);
   }
  }

}

 

Well, that's about it. We now have a (very) slowly moving ship, that turns red when it collides with an asteroid.

Great!

In next chapter we will add the possibility to move it also.

Chapter 24: How to handle TouchEvents

In this chapter we will introduce the handling of user input for the first time.

In order to do this, we will add a few new member variables and one new method to our GameSurface:

private float lastTouchX;
private float lastTouchY;
private long touchDownTime = 0;
private static final int INVALID_POINTER_ID = -1;
private int activePointerId = INVALID_POINTER_ID;

and:

  @Override
  public boolean onTouchEvent(MotionEvent motionEvent) {
   final int action = motionEvent.getAction();

   switch (action & MotionEvent.ACTION_MASK) {

   case MotionEvent.ACTION_DOWN: {
    final float x = motionEvent.getX();
    final float y = motionEvent.getY();

    lastTouchX = x;
    lastTouchY = y;
    touchDownTime = System.currentTimeMillis();
    activePointerId = motionEvent.getPointerId(0);
    break;
   }

   case MotionEvent.ACTION_MOVE: {
    final int pointerIndex = motionEvent
      .findPointerIndex(activePointerId);
    final float x = motionEvent.getX(pointerIndex);
    final float y = motionEvent.getY(pointerIndex);
    final float dX = x - lastTouchX;
    final float dY = y - lastTouchY;

    lastTouchX = x;
    lastTouchY = y;

    if (dY < -20) {
     Log.d("JustRoids", "MotionEvent: Swipe up (Thrust)");
     break;
    }
    if (dX > 20) {
     Log.d("JustRoids", "MotionEvent: Swipe right");
     break;
    }
    if (dX < -20) {
     Log.d("JustRoids", "MotionEvent: Swipe left");
     break;
    }
    break;
   }

   case MotionEvent.ACTION_UP: {
    if (System.currentTimeMillis() < touchDownTime + 150) {
     Log.d("JustRoids", "MotionEvent: Tap (Fire)");
    }
    activePointerId = INVALID_POINTER_ID;
    break;
   }

   case MotionEvent.ACTION_CANCEL: {
    activePointerId = INVALID_POINTER_ID;
    break;
   }

   case MotionEvent.ACTION_POINTER_UP: {
    final int pointerIndex = (action & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT;
    final int pointerId = motionEvent.getPointerId(pointerIndex);
    if (pointerId == activePointerId) {
     final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
     lastTouchX = motionEvent.getX(newPointerIndex);
     lastTouchY = motionEvent.getY(newPointerIndex);
     activePointerId = motionEvent.getPointerId(newPointerIndex);
    }
    break;
   }
   }

   return true;

  }

These changes will not do anything with our ship, yet. They will only detect and print out the different actions we like to capture; swipe left/right/up and tap.

In order to see the debug printout, you will have to select the LogCat tab in the lower section of Eclipse:

Then you can add a filter (the green "plus" symbol in the LogCat window), in order to hide all but the messages we print out from our own app:

 

Now we have everything in place to run our app again and try a few sweeps and taps on the screen, and see the printouts:

It works! We now have the framework for event handling in place.

In the next chapter, we'll go through how we can use these events to make our ship move.

Chapter 25: Making our ship controllable

Finally! Time to really introduce some interaction in our game.

If you are building a more complex game, where each and every event is important and lagging is unacceptable, the method I describe below is not the most optimal one. Robert Green has written an excellent article on how to use Input Pipelines, that are a bit more accurate (http://www.rbgrn.net/content/342-using-input-pipelines-your-android-game).

But for a game like this, this way will be sufficient.

We will introduce changes through the whole stack, from GameSurface, through, GameEngine and Ship, all the way down to GfXObject.

Let's start with GfxObject. We need a new method in order to add velocity, and not just set it to a certain value:

public void addVelocity(double increment, int angle) {
  double radians = Math.toRadians(angle - 90);
  dX += Math.cos(radians) * increment;
  dY += Math.sin(radians) * increment;
}

Then we have modify the update method in Ship, so that it can receive the current event and rotate the ship or change it's velocity in the direction it's pointing:

public void update(float screenWidth, float screenHeight,
   List<Asteroid> asteroids, int event) {
  float dX;
  float dY;
  float distance;
 
  switch (event) {
 
  case GameEngine.EVENT_NONE: {
   break;
  }
 
  case GameEngine.EVENT_LEFT: {
   angle -= 10;
   if (Math.abs(angle) >= 360)
    angle = 0;
   break;
  }
 
  case GameEngine.EVENT_RIGHT: {
   angle += 10;
   if (Math.abs(angle) >= 360)
    angle = 0;
   break;
  }
 
  case GameEngine.EVENT_THRUST: {
   addVelocity(0.5, angle);
   break;
  }
 
  }
 
  move(screenWidth, screenHeight);
  setCollided(false);
  for (Asteroid asteroid : asteroids) {
   dX = Math.abs(x - asteroid.x);
   dY = Math.abs(y - asteroid.y);
   distance = (float) Math.sqrt(dX * dX + dY * dY);
   if (distance <= (bitmap.getWidth() / 2 + asteroid.bitmap.getWidth() / 2)) {
    collided = true;
   }
  }
}

In GameEngine we will add 5 constants and 1 variable:

public static final int EVENT_NONE = 0;
public static final int EVENT_LEFT = 1;
public static final int EVENT_RIGHT = 2;
public static final int EVENT_THRUST = 3;
public static final int EVENT_FIRE = 4;
public int pendingEvent = EVENT_NONE;

and add "pendingEvent" when we call ship.update:

public void update() {
  asteroidHandler.update(screenWidth, screenHeight);
  ship.update(screenWidth, screenHeight, asteroidHandler.asteroids, pendingEvent);
  pendingEvent = EVENT_NONE;
}

Finally, we remove three of the log-printouts (left, right and thrust) from GameSurface and send the events to GameEngine instead (I also changed the threshold from 20 to 10 for the events to trigger):

case MotionEvent.ACTION_MOVE: {
  final int pointerIndex = motionEvent
    .findPointerIndex(activePointerId);
  final float x = motionEvent.getX(pointerIndex);
  final float y = motionEvent.getY(pointerIndex);
  final float dX = x - lastTouchX;
  final float dY = y - lastTouchY;

  lastTouchX = x;
  lastTouchY = y;

  if (dY < -10) {
   gameEngine.pendingEvent = GameEngine.EVENT_THRUST;
   break;
  }
  if (dX > 10) {
   gameEngine.pendingEvent = GameEngine.EVENT_RIGHT;
   break;
  }
  if (dX < -10) {
   gameEngine.pendingEvent = GameEngine.EVENT_LEFT;
   break;
  }
  break;
}

That's it!

Keep in mind though, that this way of passing events, by directly setting a public member variable (pendingEvent) in another class without the use of "synchronized", is rather ugly. If our game was a bit more complex, we would have added a public Object in GameEngine, and wrapped all code reading or setting pendingEvent in a "synchronized(Object)" block, or used Robert Green's method.

But this will do perfectly ok for now.

Launch the app and try out a few sweeps and thrusts:

 

Next chapter will introduce the missiles!

 

Requirements implemented in this chapter

✔ 18. The ship shall rotate clockwise/counter clockwise when you "swipe" right/left on the screen, but not change its velocity in any way.

✔ 19. The ship shall increase its velocity in its nose's direction when you "swipe" up on the screen.

✔ 20. The ship's velocity shall not decrease automatically. The player will have to rotate 180° and accelerate in order to brake.

Chapter 26: Add some firepower

The last, but not least, of our moving objects is the missile (or the "missiles", as there can be quite many of them at the same time).

I choose to implement these missiles similar to the asteroids, with a Missile object, a MissileHandler containing a list of all active missiles and so on.

So let's begin with Missile.java:

package com.ajomannen.justroids;

import android.graphics.Bitmap;

public class Missile extends GfxObject {
  public long age;

  public Missile(Bitmap bitmap, float x, float y, double velocity,
    double direction) {
   age = 0;
   this.bitmap = bitmap;
   this.x = x;
   this.y = y;
   setVelocity(velocity);
   setDirection(direction);
  }

}

Notice that we included the member variable "age" here. Remember that a missile only should live a limited amount of time? That's were "age" comes in.

The MissileHandler is also pretty straight-forward:

package com.ajomannen.justroids;

import java.util.ArrayList;
import java.util.List;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class MissileHandler {
  private Bitmap bitmap;
  public List<Missile> missiles;
  private Paint paint;

  public MissileHandler(Resources resources) {
   paint = new Paint();
   bitmap = BitmapFactory.decodeResource(resources, R.drawable.missile_20);
   missiles = new ArrayList<Missile>();
  }

  public void update(float screenWidth, float screenHeight, int pendingEvent,
    float shipX, float shipY, int shipAngle) {
   if (pendingEvent == GameEngine.EVENT_FIRE) {
    Missile newMissile = new Missile(bitmap, shipX, shipY, 3, shipAngle);
    missiles.add(newMissile);
   }
   for (Missile missile : missiles) {
    missile.age++;
    missile.move(screenWidth, screenHeight);
   }
  }

  public void draw(Canvas canvas) {
   for (Missile missile : missiles) {
    missile.draw(canvas, missile.x, missile.y, paint);
   }
  }

}

One big difference compared to the AsteroidHandler is that we do not create any missiles at all when the handler is created. A second difference is that we pass on any pending event plus the position and angle of the ship during each update, so that we can create new missiles in the right place.

In the GameEngine, we simply introduce MissileHandler in the same way as AsteroidHandler:

package com.ajomannen.justroids;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;

public class GameEngine {

  Context context;
  Resources resources;

  public float screenWidth;
  public float screenHeight;
  private Paint blackPaint;

  private int level;
  private AsteroidHandler asteroidHandler;
  private MissileHandler missileHandler;
  private Ship ship;

  public static final int EVENT_NONE = 0;
  public static final int EVENT_LEFT = 1;
  public static final int EVENT_RIGHT = 2;
  public static final int EVENT_THRUST = 3;
  public static final int EVENT_FIRE = 4;
  public int pendingEvent = EVENT_NONE;

  public void Init(Context context) {
   this.context = context;
   resources = context.getResources();

   blackPaint = new Paint();
   blackPaint.setColor(Color.BLACK);
   blackPaint.setStyle(Style.FILL);

   level = 5;
   asteroidHandler = new AsteroidHandler(resources, level);
   missileHandler = new MissileHandler(resources);
   ship = new Ship(resources, 120, 280, 0.1, 0, 0);

   setSurfaceDimensions(240, 320);

  }

  public void onDestroy() {
   try {
   } catch (Exception e) {
   }
  }

  public void setSurfaceDimensions(int width, int height) {
   screenWidth = width;
   screenHeight = height;
  }

  public void update() {
   asteroidHandler.update(screenWidth, screenHeight);
   ship.update(screenWidth, screenHeight, asteroidHandler.asteroids,
     pendingEvent);
   missileHandler.update(screenWidth, screenHeight, pendingEvent, ship.x,
     ship.y, ship.angle);
   pendingEvent = EVENT_NONE;
  }

  public void draw(Canvas canvas) {
   canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), blackPaint);
   asteroidHandler.draw(canvas);
   ship.draw(canvas);
   missileHandler.draw(canvas);
  }

}

The last change we have to make, in order to be able to fire away those missile, is to open up GameSurface and replace the line:

Log.d("JustRoids", "MotionEvent: Tap (Fire)");

with:

gameEngine.pendingEvent = GameEngine.EVENT_FIRE;

There's still some work to do in order to make the missiles "die" when they're too old and to make them split the asteroids in two when they hit them, but let's run our app again and fire off a bunch of missiles in different directions too see how it looks:

 

That's all for now!

Next chapter will cover the work that's left, as mentioned above.

 

Requirements implemented in this chapter

✔ 17. The ship fires missiles when you "tap" on the screen.

✔ 21. Objects (asteroids, missiles, ship) shall "loop" over the screen's edges and re-appear on the other side.

Chapter 27: The Age of Missiles and How to Divide an Asteroid

Adding handling of a missile's "age" is not that complex. We already have the increase of the age-counter taken care of, so all we need to to is to simply call "missile.remove()" if a missile is too old (or has collided with something).

While we edit MissileHandler.update(), we also add the list of all active asteroids as parameter, so we can check for collisions between missiles and asteroids.

Here's the latest MissileHandler:

package com.ajomannen.justroids;

import java.util.ArrayList; // Changed data type to make it easier to remove "old" missiles
import java.util.List;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class MissileHandler {
  private Bitmap bitmap;
  public ArrayList<Missile> missiles;
  private Paint paint;

  public MissileHandler(Resources resources) {
   paint = new Paint();
   bitmap = BitmapFactory.decodeResource(resources, R.drawable.missile_20);
   missiles = new ArrayList<Missile>();
  }

  public void update(float screenWidth, float screenHeight, int pendingEvent,
    float shipX, float shipY, int shipAngle, List<Asteroid> asteroids) { 
   float dX;
   float dY;
   float distance;

   if (pendingEvent == GameEngine.EVENT_FIRE) {
    Missile newMissile = new Missile(bitmap, shipX, shipY, 3, shipAngle);
    missiles.add(newMissile);
   }
   int i = 0;
   for (Missile missile : missiles) {
    missile.age++;
    if (missile.age > 25 * 3 || missile.collided == true) {
     // TODO: Hardcoded max age. Works better with quadratic game
     // area
     missiles.remove(i);
     missiles.trimToSize();
    } else {
     missile.move(screenWidth, screenHeight);
     for (Asteroid asteroid : asteroids) {
      asteroid.collided = false;
      dX = Math.abs(missile.x - asteroid.x);
      dY = Math.abs(missile.y - asteroid.y);
      distance = (float) Math.sqrt(dX * dX + dY * dY);
      if (distance <= (missile.bitmap.getWidth() / 2 + asteroid.bitmap
        .getWidth() / 2)) {
       asteroid.collided = true;
       missile.collided = true;
      }
     }
     i++;
    }
   }
  }

  public void draw(Canvas canvas) {
   for (Missile missile : missiles) {
    missile.draw(canvas, missile.x, missile.y, paint);
   }
  }

}

We must also edit the GameEngine's update method to match this:

public void update() {
  asteroidHandler.update(screenWidth, screenHeight);
  ship.update(screenWidth, screenHeight, asteroidHandler.asteroids,
    pendingEvent);
  missileHandler.update(screenWidth, screenHeight, pendingEvent, ship.x,
    ship.y, ship.angle, asteroidHandler.asteroids);
  pendingEvent = EVENT_NONE;
}

Now we have a state where asteroids "know" when they are hit by a missile, and all that's left is to make them either split in two or completely disappear. We solve this in our AsteroidHandler:

package com.ajomannen.justroids;

import java.util.ArrayList;
import java.util.Random;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class AsteroidHandler {
  private Bitmap[] asteroidBitmaps;
  private Bitmap[] asteroidBitmapsHighlighted;
  public ArrayList<Asteroid> asteroids;
  private Random random;
  private Paint paint;

  public AsteroidHandler(Resources resources, int level) {
   random = new Random();

   paint = new Paint();

   asteroidBitmaps = new Bitmap[3];
   asteroidBitmaps[Asteroid.WHOLE] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_whole);
   asteroidBitmaps[Asteroid.HALF] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_half);
   asteroidBitmaps[Asteroid.QUARTER] = BitmapFactory.decodeResource(
     resources, R.drawable.asteroid_quarter);

   asteroidBitmapsHighlighted = new Bitmap[3];
   asteroidBitmapsHighlighted[Asteroid.WHOLE] = BitmapFactory
     .decodeResource(resources,
       R.drawable.asteroid_whole_highlighted);
   asteroidBitmapsHighlighted[Asteroid.HALF] = BitmapFactory
     .decodeResource(resources, R.drawable.asteroid_half_highlighted);
   asteroidBitmapsHighlighted[Asteroid.QUARTER] = BitmapFactory
     .decodeResource(resources,
       R.drawable.asteroid_quarter_highlighted);

   asteroids = new ArrayList<Asteroid>();
   float x;
   float y;
   double velocity;
   double direction;
   int rotation;

   for (int i = 0; i < level; i++) {
    x = 240 * random.nextFloat();
    y = 50 * random.nextFloat();
    velocity = 0.2 + 0.5 * i;
    direction = 180 - 45 + 90 * random.nextFloat();
    rotation = -1 - random.nextInt(3);
    asteroids.add(new Asteroid(asteroidBitmaps[Asteroid.WHOLE],
      asteroidBitmapsHighlighted[Asteroid.WHOLE], Asteroid.WHOLE,
      x, y, velocity, direction, rotation));
   }
  }

  public void update(float screenWidth, float screenHeight) {

   int i = 0;
   for (Asteroid asteroid : asteroids) {
    asteroid.move(screenWidth, screenHeight);
    if (asteroid.collided) {
     asteroids.remove(i);
     asteroids.trimToSize();
     if (asteroid.size > Asteroid.QUARTER) {
      int fragmentSize = Asteroid.HALF;
      if (asteroid.size == Asteroid.HALF)
       fragmentSize = Asteroid.QUARTER;
      asteroids.add(new Asteroid(asteroidBitmaps[fragmentSize],
        asteroidBitmapsHighlighted[fragmentSize],
        fragmentSize, asteroid.x, asteroid.y, asteroid
          .getVelocity(), asteroid.getDirection()-45, asteroid.rotation));
      asteroids.add(new Asteroid(asteroidBitmaps[fragmentSize],
        asteroidBitmapsHighlighted[fragmentSize],
        fragmentSize, asteroid.x, asteroid.y, asteroid
          .getVelocity(), asteroid.getDirection()+45, asteroid.rotation));
     }
     break;
    }
    i++;
   }
  }

  public void draw(Canvas canvas) {
   for (Asteroid asteroid : asteroids) {
    asteroid.draw(canvas, asteroid.x, asteroid.y, paint);
   }
  }

}

In the update method we create two new, smaller asteroids for collided asteroids larger than QUARTER, otherwise we remove them. The new, smaller asteroids are given a changed direction in relation to their "parent" (-45° and +45°), and to avoid angles below 0° or above 360°, we make a small modification in the GfxObject's setDirection method:

public void setDirection(double direction) {
  this.direction = direction;
  if (this.direction > 360)
   this.direction -= 360;
  if (this.direction < 0)
   this.direction += 360;
  calculateDXDY();
}

Let's launch the app, fire away a few missiles and see how the asteroids are divided in smaller pieces:

 

If I would happen to be a slightly better coder, we would have been done with these parts here, but when I tried to stress-test the app by firing missiles as fast as I could, the app crashed. Therefore, the next chapter will handle yet a lesson learned. Hang on...

 

Requirements implemented in this chapter

✔ 22. Missiles only "live" for a short period of time so that they can't hit the ship that fired them in the back.

✔ 23. When a "full-size" asteroid is hit by a missile, it shall be split into two "half-size" asteroids.

✔ 24. When a "half-size" asteroid is hit by a missile, it shall be split into two "quarter-size" asteroids.

✔ 25. When a "quarter-size" asteroid id hit by a missile, it shall disappear.

✔ 26. When an asteroid is split into two, both parts shall inherit the "parent's" velocity, but change their direction approximately 45° (clockwise for one of them and counter-clockwise for the other).

Chapter 28: Lesson learned #4 - Locating culprits and clumsy mistakes

As I wrote in the previous chapter, the app crashes sometimes if you fire away a few missiles and you get the following screen:

Fortunately, Eclipse gives us a bit more info on this. We can see the whole stack trace, and the exact line of code where the problem occurs:

Apparently, the for-statement fails when it tries to loop through the list of present missiles.

I could simply have added a try-catch here, to pick up any exception so that the app won't crash, but I would really know what happened here and solve the root cause.

I add a few extra debug printouts, so that I know how many missiles there are in the list, and which one we are processing for the moment.

This is what I did in MissileHandler.update() to add debug logging regarding the list of missiles:

public void update(float screenWidth, float screenHeight, int pendingEvent,
   float shipX, float shipY, int shipAngle, List<Asteroid> asteroids) {
  float dX;
  float dY;
  float distance;

  if (pendingEvent == GameEngine.EVENT_FIRE) {
   Missile newMissile = new Missile(bitmap, shipX, shipY, 3, shipAngle);
   missiles.add(newMissile);
  }
  Log.d("JustRoids", "We have " + missiles.size()
    + " missiles in our list");
  int i = 0;
  for (Missile missile : missiles) {
   Log.d("JustRoids", "We will now handle missile #" + i);
   missile.age++;
   if (missile.age > 25 * 3 || missile.collided == true) {
    // TODO: Hardcoded max age. Works better with quadratic game
    // area
    missiles.remove(i);
    missiles.trimToSize();
    Log.d("JustRoids", "We removed one missile and have now "
      + missiles.size() + " missiles in our list");
   } else {
    missile.move(screenWidth, screenHeight);
    for (Asteroid asteroid : asteroids) {
     asteroid.collided = false;
     dX = Math.abs(missile.x - asteroid.x);
     dY = Math.abs(missile.y - asteroid.y);
     distance = (float) Math.sqrt(dX * dX + dY * dY);
     if (distance <= (missile.bitmap.getWidth() / 2 + asteroid.bitmap
       .getWidth() / 2)) {
      asteroid.collided = true;
      missile.collided = true;
     }
    }
    Log.d("JustRoids", "We are now finished with missile #" + i);
    i++;
   }
  }
  Log.d("JustRoids", "We are finished with all missiles during this update"); 
}

Do you see the bug? I found it while adding the Log.d-lines, so I actually did not have to run it. But if you do - be aware of that this logging will make printouts 25 times per second, so you will seriously impact the performance of the app and possible also your computer. Run it just for a short period of time and remove it afterwards.

But back to the bug. Take a look at the same block of code with a bit clearer coloring:

public void update(float screenWidth, float screenHeight, int pendingEvent,
   float shipX, float shipY, int shipAngle, List<Asteroid> asteroids) {
  float dX;
  float dY;
  float distance;

  if (pendingEvent == GameEngine.EVENT_FIRE) {
   Missile newMissile = new Missile(bitmap, shipX, shipY, 3, shipAngle);
   missiles.add(newMissile);
  }
  Log.d("JustRoids", "We have " + missiles.size()
    + " missiles in our list");
  int i = 0;
  for (Missile missile : missiles) {
   Log.d("JustRoids", "We will now handle missile #" + i);
   missile.age++;
   if (missile.age > 25 * 3 || missile.collided == true) {
    // TODO: Hardcoded max age. Works better with quadratic game
    // area
    missiles.remove(i);
    missiles.trimToSize();
    Log.d("JustRoids", "We removed one missile and have now "
      + missiles.size() + " missiles in our list");
   } else {
    missile.move(screenWidth, screenHeight);
    for (Asteroid asteroid : asteroids) {
     asteroid.collided = false;
     dX = Math.abs(missile.x - asteroid.x);
     dY = Math.abs(missile.y - asteroid.y);
     distance = (float) Math.sqrt(dX * dX + dY * dY);
     if (distance <= (missile.bitmap.getWidth() / 2 + asteroid.bitmap
       .getWidth() / 2)) {
      asteroid.collided = true;
      missile.collided = true;
     }
    }
    Log.d("JustRoids", "We are now finished with missile #" + i);
    i++;
   }
  }
  Log.d("JustRoids", "We are finished with all missiles during this update"); 
}

For each active missile, either the yellow block or the green block should be executed; the yellow block for old or collided missiles and the green block for the rest. What I did was two clumsy (but rather common) mistakes.

Instead of using "for (Missile missile : missiles)", I could have chosen "for (int i = 0; i < missiles.size(); i++)", but the call to missiles.size() each loop would have been very expensive. So I introduced a separate integer "i" to keep track of which missile I am on instead. (I need the value so that the "remove" statement in the yellow block can have the correct number as parameter).

The method is pretty ok, but I have two bugs here, marked with red:

  1. I increase "i" only in the green block. It should of course be increased outside. Clumsy.
  2. I do a "trimToSize" during the parsing of the list. The for-loop will end up outside the missile-list's boundaries due to this. Also clumsy.

The correct update-function should be:

public void update(float screenWidth, float screenHeight, int pendingEvent,
   float shipX, float shipY, int shipAngle, List<Asteroid> asteroids) {
  float dX;
  float dY;
  float distance;

  if (pendingEvent == GameEngine.EVENT_FIRE) {
   Missile newMissile = new Missile(bitmap, shipX, shipY, 3, shipAngle);
   missiles.add(newMissile);
  }
  int i = 0;
  for (Missile missile : missiles) {
   missile.age++;
   if (missile.age > 25 * 3 || missile.collided == true) {
    // TODO: Hardcoded max age. Works better with quadratic game
    // area
    missiles.remove(i);
   } else {
    missile.move(screenWidth, screenHeight);
    for (Asteroid asteroid : asteroids) {
     asteroid.collided = false;
     dX = Math.abs(missile.x - asteroid.x);
     dY = Math.abs(missile.y - asteroid.y);
     distance = (float) Math.sqrt(dX * dX + dY * dY);
     if (distance <= (missile.bitmap.getWidth() / 2 + asteroid.bitmap
       .getWidth() / 2)) {
      asteroid.collided = true;
      missile.collided = true;
     }
    }
   }
   i++;
  }
  missiles.trimToSize();
}

These two simple corrections solved the issue!

But if I would have taken the lazy path and just wrapped the code in a "try - catch" block, it would have worked most of the time, but every now and then a missile would behave strangely.

Conclusion, only use try-catch if you know why. Don't use it to wrap bugs - fix them instead.

Chapter 29: Lesson learned #5 - Do's and Don'ts when fiddling with ArrayLists

My original plans for this chapter was to "play" the game for a few minutes on a real device, to get some sort of feedback regarding the speed and size of the objects in the game, the range of the missiles, the accuracy of the ship navigation and so on.

I never got to that.

I ended up with a new crash, similar to the previous but of another type - "ConcurrentModificationException":

I had an eerie feeling that the quick-and-dirty way of setting member variables without using synchronized() would bite back sooner or later, but I can not really understand how it happened in this case. The pending events are set into member variables in GameEngine. I can't figure out how it causes issues in MissileHandler.

A few well spent minutes (reading the JavaDocs for java.util.ArrayList) showed however, that this error has nothing to do with my pending event construction:

"...The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future...."

(Ref: http://docs.oracle.com/javase/6/docs/api/java/util/ArrayList.html)

As there is a call to "java.util.ArrayList$ArrayListIterator.next" in the stack trace above, it apparently means that "for (Missile missile : missiles)" implicitly creates an iterator for me, and I'm thereby not allowed to use "missiles.remove(i)" as I did...

But in order to be able to use the iterator's own remove method instead (as stated in the JavaDoc excerpt above), I will have to declare it explicitly (of course).

This gives the following changes in MissileHandler.update():

public void update(float screenWidth, float screenHeight, int pendingEvent,
   float shipX, float shipY, int shipAngle, List<Asteroid> asteroids) {
  float dX;
  float dY;
  float distance;

  if (pendingEvent == GameEngine.EVENT_FIRE) {
   Missile newMissile = new Missile(bitmap, shipX, shipY, 3, shipAngle);
   missiles.add(newMissile);
  }
  // int i = 0;
  // for (Missile missile : missiles) {
  Iterator<Missile> missilesIterator = missiles.iterator();
  Missile missile;
  while (missilesIterator.hasNext()) {
   missile = missilesIterator.next();
   missile.age++;
   if (missile.age > 25 * 3 || missile.collided == true) {
    // TODO: Hardcoded max age. Works better with quadratic game
    // area
    // missiles.remove(i);
    missilesIterator.remove();
   } else {
    missile.move(screenWidth, screenHeight);
    for (Asteroid asteroid : asteroids) {
     asteroid.collided = false;
     dX = Math.abs(missile.x - asteroid.x);
     dY = Math.abs(missile.y - asteroid.y);
     distance = (float) Math.sqrt(dX * dX + dY * dY);
     if (distance <= (missile.bitmap.getWidth() / 2 + asteroid.bitmap
       .getWidth() / 2)) {
      asteroid.collided = true;
      missile.collided = true;
     }
    }
   }
   // i++;
  }
  missiles.trimToSize();
}

This solved the issue, and there are no more crashes when I fire off some missiles!

There is one thing that is a bit fishy here: MissileHandler is built as a copy of AsteroidHandler, which means that the same, incorrect, remove(i) method is used there also. But it doesn't fail...

Anyway - I'll fix MissileHandler.update also:

public void update(float screenWidth, float screenHeight) {
  Iterator<Asteroid> asteroidsIterator = asteroids.iterator();
  Asteroid asteroid;
  while (asteroidsIterator.hasNext()) {
   asteroid = asteroidsIterator.next();
   asteroid.move(screenWidth, screenHeight);
   if (asteroid.collided) {
    asteroidsIterator.remove();
    if (asteroid.size > Asteroid.QUARTER) {
     int fragmentSize = Asteroid.HALF;
     if (asteroid.size == Asteroid.HALF)
      fragmentSize = Asteroid.QUARTER;
     asteroids.add(new Asteroid(asteroidBitmaps[fragmentSize],
       asteroidBitmapsHighlighted[fragmentSize],
       fragmentSize, asteroid.x, asteroid.y, asteroid
         .getVelocity(),
       asteroid.getDirection() - 45, asteroid.rotation));
     asteroids.add(new Asteroid(asteroidBitmaps[fragmentSize],
       asteroidBitmapsHighlighted[fragmentSize],
       fragmentSize, asteroid.x, asteroid.y, asteroid
         .getVelocity(),
       asteroid.getDirection() + 45, asteroid.rotation));
    }
    break;
   }
  }
  asteroids.trimToSize();
}

I guess that the playability part will be the topic for next chapter...

Chapter 30: Playability Review #1

I finally got to my long awaited playability test, and it went totally great!

At least according to my definition of "great" - I found loads of issues and misfeatures that would have been detected by some poor end-user otherwise ;-)

  1. The images on the Main Menu look pretty bad. They have way too distinct edges to look good.
  2. If I rotate or thrust the ship with very fast "sweeps" on the screen, the ship both moves and fires a missile. I can't really decide if it's good or bad, though.
  3. The missiles appear "on top" of the ship when they are fired. It looks a bit fishy...
  4. The missiles are also quite big, and does not really look like missiles at all.
  5. The missiles are slow. Boringly slow.
  6. The life-cycle (or range) of the missiles feels awkward.
  7. Last but not least: If I fire away a whole bunch of missiles towards an asteroid, only the very last of them seems to actually hit. All other just disappear into thin air (or maybe vacuum, as this is in space).

 

This is actually golden opportunity to get some external feedback on the existing functionality.

Here is a link to the current version of the app: JustRoids_playability_review_1.apk. (If your web browser should rename the .apk-file to .zip, you will have to rename it back to .apk before installing it on your device.)

If you have the possibility, please download it and see what you think of the maneuverability, look-and-feel etc.

But do keep in mind that the game is far from complete. All that we have in the game is:

  • 5 Asteroids, that it should be possible to shoot at.
  • A ship that should turn red if it's hit by an asteroid.
  • Possibility to rotate or accelerate the ship.

Please give it a try and post your findings as comments to this chapter.

 

Meanwhile, let's start fixing the seven issues already identified.

 

Issue 1 - The images on the Main Menu look pretty bad

I did a quick-fix for this using Gimp: Filters --> Decor -> Fuzzy Border. It was a great improvement, but still not quite 100%.

 

Issue 2 - Fast sweeps make the ship fire as well

I can't really decide whether this is good or bad, so I'll leave this issue as is for now.

 

Issue 3 - The missiles appear on top of the ship

Well. The fix for this issue is almost too easy to describe ;-) In GameEngine.draw, we simply draw the missiles before we draw the ship:

public void draw(Canvas canvas) {
  canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), blackPaint);
  asteroidHandler.draw(canvas);
  missileHandler.draw(canvas);
  ship.draw(canvas);
}

 

Issue 4 - The missiles are too big and look crappy

I really prefer to too keep the missiles circular, as they would have to be rotated when drawn otherwise. 

But a 10x10 pixels copy of missile_20.png, saved as missile_10 and the following change in MissileHandler's contructor:

public MissileHandler(Resources resources) {
  paint = new Paint();
  bitmap = BitmapFactory.decodeResource(resources, R.drawable.missile_10);
  missiles = new ArrayList<Missile>();
}

makes it look a whole lot better.

 

Issue 5 - The missile are too slow

Yes, they are. The missiles are created in MissileHandler.update, if there is a pendingEvent with the value EVENT_FIRE. The current velocity when creating new missiles is "3". A value of "6" is more like it:

public void update(float screenWidth, float screenHeight, int pendingEvent,
   float shipX, float shipY, int shipAngle, List<Asteroid> asteroids) {
  float dX;
  float dY;
  float distance;

  if (pendingEvent == GameEngine.EVENT_FIRE) {
   Missile newMissile = new Missile(bitmap, shipX, shipY, 6, shipAngle);
   missiles.add(newMissile);
  }
  Iterator<Missile> missilesIterator = missiles.iterator();
  Missile missile;
  while (missilesIterator.hasNext()) {
   missile = missilesIterator.next();
   missile.age++;
   if (missile.age > 25 * 3 || missile.collided == true) {
    // TODO: Hardcoded max age. Works better with quadratic game
    // area
    missilesIterator.remove();
   } else {
    missile.move(screenWidth, screenHeight);
    for (Asteroid asteroid : asteroids) {
     asteroid.collided = false;
     dX = Math.abs(missile.x - asteroid.x);
     dY = Math.abs(missile.y - asteroid.y);
     distance = (float) Math.sqrt(dX * dX + dY * dY);
     if (distance <= (missile.bitmap.getWidth() / 2 + asteroid.bitmap
       .getWidth() / 2)) {
      asteroid.collided = true;
      missile.collided = true;
     }
    }
   }
  }
  missiles.trimToSize();
}

 

Issue 6 - The missile's range is not quite perfect.

I guess it would feel a bit more natural if the missiles lived until they the reach the end of the screen instead of a certain amount of time. To accomplish this, we can add a new member variable and a getter function in GfxObject:

private boolean looped = false;
 
public boolean isLooped() {
  return looped;
}

and make a few changes in GfxObject.move:

public void move(float screenWidth, float screenHeight) {
  float minX = 0 - bitmap.getWidth() / 2;
  float maxX = screenWidth + bitmap.getWidth() / 2;
  float minY = 0 - bitmap.getHeight() / 2;
  float maxY = screenHeight + bitmap.getHeight() / 2;

  looped = false;
  x += dX;
  y += dY;
  if (x > maxX) {
   x = minX;
   looped = true;
  }
  if (x < minX) {
   x = maxX;
   looped = true;
  }
  if (y > maxY) {
   y = minY;
   looped = true;
  }
  if (y < minY) {
   y = maxY;
   looped = true;
  }

  angle += rotation;
  if (Math.abs(angle) >= 360)
   angle = 0;
}

The last change is in MissileHandler.update, where we remove the age control and replace it with checking whether the missile was "looped" during the last update:

public void update(float screenWidth, float screenHeight, int pendingEvent,
   float shipX, float shipY, int shipAngle, List<Asteroid> asteroids) {
  float dX;
  float dY;
  float distance;

  if (pendingEvent == GameEngine.EVENT_FIRE) {
   Missile newMissile = new Missile(bitmap, shipX, shipY, 6, shipAngle);
   missiles.add(newMissile);
  }
  Iterator<Missile> missilesIterator = missiles.iterator();
  Missile missile;
  while (missilesIterator.hasNext()) {
   missile = missilesIterator.next();
   // missile.age++;
   if (missile.isLooped() || missile.collided == true) {
    // TODO: Hardcoded max age. Works better with quadratic game
    // area
    missilesIterator.remove();
   } else {
    missile.move(screenWidth, screenHeight);
    for (Asteroid asteroid : asteroids) {
     asteroid.collided = false;
     dX = Math.abs(missile.x - asteroid.x);
     dY = Math.abs(missile.y - asteroid.y);
     distance = (float) Math.sqrt(dX * dX + dY * dY);
     if (distance <= (missile.bitmap.getWidth() / 2 + asteroid.bitmap
       .getWidth() / 2)) {
      asteroid.collided = true;
      missile.collided = true;
     }
    }
   }
  }
  missiles.trimToSize();
}

 

Issue 7 - Only the last missile hit it's target

Time to fix the most disturbing issue of them all in the playability review...

I must have had some kind of black-out when I wrote the collision detection in the MissileHandler. I specifically set collided to false for each asteroid before I check for collisions, which means that only the last missile in the list has any real impact. Just as we observed during the review. So, the very last change in this chapter is also in Missilehandler.update:

public void update(float screenWidth, float screenHeight, int pendingEvent,
   float shipX, float shipY, int shipAngle, List<Asteroid> asteroids) {
  float dX;
  float dY;
  float distance;

  if (pendingEvent == GameEngine.EVENT_FIRE) {
   Missile newMissile = new Missile(bitmap, shipX, shipY, 6, shipAngle);
   missiles.add(newMissile);
  }
  Iterator<Missile> missilesIterator = missiles.iterator();
  Missile missile;
  while (missilesIterator.hasNext()) {
   missile = missilesIterator.next();
   if (missile.isLooped() || missile.collided == true) {
    missilesIterator.remove();
   } else {
    missile.move(screenWidth, screenHeight);
    for (Asteroid asteroid : asteroids) {
     //asteroid.collided = false;
     dX = Math.abs(missile.x - asteroid.x);
     dY = Math.abs(missile.y - asteroid.y);
     distance = (float) Math.sqrt(dX * dX + dY * dY);
     if (distance <= (missile.bitmap.getWidth() / 2 + asteroid.bitmap
       .getWidth() / 2)) {
      asteroid.collided = true;
      missile.collided = true;
     }
    }
   }
  }
  missiles.trimToSize();
}

This last change concludes the actions to solve the issues identified during the first playability review, and it actually feels like we are more or less finished with the central parts of the game!

The upcoming chapters will focus on handling levels, adding status displays and so on.

 

Chapter 31: Adding a Game Status display

Now it's about time to start wrapping things together and start handling and displaying current level, time and so on.

Let's start with the basics to get some kind of game status display, by creating a new class: GameStatus.java:

package com.ajomannen.justroids;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;

public class GameStatus {
  private Paint textPaint;
  private int level;
  private int thrusts;
  private int firedMissiles;
  private long levelStartTime;

  public GameStatus() {
   textPaint = new Paint();
   textPaint.setColor(Color.LTGRAY);
   textPaint.setTextSize(25);

   level = 1;
   thrusts = 0;
   firedMissiles = 0;
   levelStartTime = System.nanoTime();
  }

  public int getLevel() {
   return level;
  }

  public void setLevel(int level) {
   this.level = level;
  }

  public void increaseThrusts() {
   thrusts++;
  }

  public void increaseFiredMissiles() {
   firedMissiles++;
  }

  public void draw(Canvas canvas, float screenHeight, float screenWidth) {
   long passedLevelTime = (System.nanoTime() - levelStartTime) / 1000000000L;
 
   textPaint.setTextAlign(Paint.Align.LEFT);
   canvas.drawText("Level: " + level, 1, 19, textPaint);
   canvas.drawText("Thrusts: " + thrusts, 1, screenHeight-2, textPaint);
 
   textPaint.setTextAlign(Paint.Align.RIGHT);
   canvas.drawText("Time: " + passedLevelTime + "s", screenWidth-1, 19, textPaint);
   canvas.drawText("Missiles: " + firedMissiles, screenWidth-1, screenHeight-2, textPaint);
  }

}

In order to use this new GameStatus, we have to modify three other classes; GameEngine, Ship and MissileHandler.

First we introduce the GameStatus, handle "level" through it, draw it and pass it on to Ship and MissileHandler during updates in GameEngine.java:

package com.ajomannen.justroids;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;

public class GameEngine {

  Context context;
  Resources resources;

  public float screenWidth;
  public float screenHeight;
  private Paint blackPaint;

  // private int level;
  private AsteroidHandler asteroidHandler;
  private MissileHandler missileHandler;
  private Ship ship;
  private GameStatus gameStatus;

  public static final int EVENT_NONE = 0;
  public static final int EVENT_LEFT = 1;
  public static final int EVENT_RIGHT = 2;
  public static final int EVENT_THRUST = 3;
  public static final int EVENT_FIRE = 4;
  public int pendingEvent = EVENT_NONE;

  public void Init(Context context) {
   this.context = context;
   resources = context.getResources();

   blackPaint = new Paint();
   blackPaint.setColor(Color.BLACK);
   blackPaint.setStyle(Style.FILL);

   // level = 5;
   gameStatus = new GameStatus();
   asteroidHandler = new AsteroidHandler(resources, gameStatus.getLevel());
   missileHandler = new MissileHandler(resources);
   ship = new Ship(resources, 120, 280, 0.1, 0, 0);

   setSurfaceDimensions(240, 320);

  }

  public void onDestroy() {
   try {
   } catch (Exception e) {
   }
  }

  public void setSurfaceDimensions(int width, int height) {
   screenWidth = width;
   screenHeight = height;
  }

  public void update() {
   asteroidHandler.update(screenWidth, screenHeight);
   ship.update(screenWidth, screenHeight, asteroidHandler.asteroids,
     pendingEvent, gameStatus);
   missileHandler.update(screenWidth, screenHeight, pendingEvent, ship.x,
     ship.y, ship.angle, asteroidHandler.asteroids, gameStatus);
   pendingEvent = EVENT_NONE;
  }

  public void draw(Canvas canvas) {
   canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), blackPaint);
   asteroidHandler.draw(canvas);
   missileHandler.draw(canvas);
   ship.draw(canvas);
   gameStatus.draw(canvas, screenHeight, screenWidth);
  }

}

Then we increase GameStatus' "thrust" counter in Ship.java:

package com.ajomannen.justroids;

import java.util.List;

import android.content.res.Resources;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class Ship extends GfxObject {
  private Paint paint;

  public Ship(Resources resources, float x, float y, double velocity,
    double direction, int angle) {
   paint = new Paint();
   bitmap = BitmapFactory.decodeResource(resources, R.drawable.ship);
   bitmapHighlighted = BitmapFactory.decodeResource(resources,
     R.drawable.ship_highlighted);
   this.x = x;
   this.y = y;
   setVelocity(velocity);
   setDirection(direction);
   this.angle = angle;
  }

  public void update(float screenWidth, float screenHeight,
    List<Asteroid> asteroids, int event, GameStatus gameStatus) {
   float dX;
   float dY;
   float distance;

   switch (event) {

   case GameEngine.EVENT_NONE: {
    break;
   }

   case GameEngine.EVENT_LEFT: {
    angle -= 10;
    if (Math.abs(angle) >= 360)
     angle = 0;
    break;
   }

   case GameEngine.EVENT_RIGHT: {
    angle += 10;
    if (Math.abs(angle) >= 360)
     angle = 0;
    break;
   }

   case GameEngine.EVENT_THRUST: {
    addVelocity(0.5, angle);
    gameStatus.increaseThrusts();
    break;
   }

   }

   move(screenWidth, screenHeight);
   setCollided(false);
   for (Asteroid asteroid : asteroids) {
    dX = Math.abs(x - asteroid.x);
    dY = Math.abs(y - asteroid.y);
    distance = (float) Math.sqrt(dX * dX + dY * dY);
    if (distance <= (bitmap.getWidth() / 2 + asteroid.bitmap.getWidth() / 2)) {
     collided = true;
    }
   }
  }

  public void draw(Canvas canvas) {
   draw(canvas, x, y, paint);
  }

}

And finally we increase GameStatus' "firedMissiles" counter in MissileHandler.java:

package com.ajomannen.justroids;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;

public class MissileHandler {
  private Bitmap bitmap;
  public ArrayList<Missile> missiles;
  private Paint paint;

  public MissileHandler(Resources resources) {
   paint = new Paint();
   bitmap = BitmapFactory.decodeResource(resources, R.drawable.missile_10);
   missiles = new ArrayList<Missile>();
  }

  public void update(float screenWidth, float screenHeight, int pendingEvent,
    float shipX, float shipY, int shipAngle, List<Asteroid> asteroids,
    GameStatus gameStatus) {
   float dX;
   float dY;
   float distance;

   if (pendingEvent == GameEngine.EVENT_FIRE) {
    Missile newMissile = new Missile(bitmap, shipX, shipY, 6, shipAngle);
    missiles.add(newMissile);
    gameStatus.increaseFiredMissiles();
   }
   Iterator<Missile> missilesIterator = missiles.iterator();
   Missile missile;
   while (missilesIterator.hasNext()) {
    missile = missilesIterator.next();
    if (missile.isLooped() || missile.collided == true) {
     missilesIterator.remove();
    } else {
     missile.move(screenWidth, screenHeight);
     for (Asteroid asteroid : asteroids) {
      dX = Math.abs(missile.x - asteroid.x);
      dY = Math.abs(missile.y - asteroid.y);
      distance = (float) Math.sqrt(dX * dX + dY * dY);
      if (distance <= (missile.bitmap.getWidth() / 2 + asteroid.bitmap
        .getWidth() / 2)) {
       asteroid.collided = true;
       missile.collided = true;
      }
     }
    }
   }
   missiles.trimToSize();
  }

  public void draw(Canvas canvas) {
   for (Missile missile : missiles) {
    missile.draw(canvas, missile.x, missile.y, paint);
   }
  }

}

Now we have a basic GameStatus in place:

 

Next chapter will focus on increasing level when all asteroids are gone and perhaps making the shop mortal again ;-)

Chapter 32: Introducing levels in the game

One key factor when making a game with the concept of "levels", is of course to make the next level a bit more difficult than the previous one.

In JustRoids, we accomplished that when we wrote the AsteroidHandler. Remember how each asteroid's velocity was not random:

 for (int i = 0; i < level; i++) {
   x = 240 * random.nextFloat();
   y = 50 * random.nextFloat();
   velocity = 0.2 + 0.5 * i;
   direction = 180 - 45 + 90 * random.nextFloat();
   rotation = -1 - random.nextInt(3);
   asteroids.add(new Asteroid(asteroidBitmaps[Asteroid.WHOLE],
     asteroidBitmapsHighlighted[Asteroid.WHOLE], Asteroid.WHOLE,
     x, y, velocity, direction, rotation));
}

We create as many asteroids as the number of the current level, and each one is faster then the previous. On level 1, there will be only one asteroid, slowly moving at 0.2 points per frame, but on level 10 the fastest asteroid will sweep across the screen at 25 times that speed. It should be sufficient to make the game a bit tricky as the level increases...

I most of this article, we've had the level hard coded to "5" in order to have enough asteroids on the screen for proper testing, but now it's time to make the game start on level 1 and level up each time all asteroids are gone.

There are only two files we have to modify for this. First we add a new "setLevel" method in GameEngine and move a few calls from Init to the new method:

package com.ajomannen.justroids;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;

public class GameEngine {

  Context context;
  Resources resources;

  public float screenWidth;
  public float screenHeight;
  private Paint blackPaint;

  private AsteroidHandler asteroidHandler;
  private MissileHandler missileHandler;
  private Ship ship;
  private GameStatus gameStatus;

  public static final int EVENT_NONE = 0;
  public static final int EVENT_LEFT = 1;
  public static final int EVENT_RIGHT = 2;
  public static final int EVENT_THRUST = 3;
  public static final int EVENT_FIRE = 4;
  public int pendingEvent = EVENT_NONE;

  public void Init(Context context) {
   this.context = context;
   resources = context.getResources();

   blackPaint = new Paint();
   blackPaint.setColor(Color.BLACK);
   blackPaint.setStyle(Style.FILL);

   gameStatus = new GameStatus();
   setLevel(1);
   // asteroidHandler = new AsteroidHandler(resources, gameStatus.getLevel());
   // missileHandler = new MissileHandler(resources);
   // ship = new Ship(resources, 120, 280, 0.1, 0, 0);

   setSurfaceDimensions(240, 320);

  }

  private void setLevel(int level) {
   gameStatus.setLevel(level);
   asteroidHandler = new AsteroidHandler(resources, level);
   missileHandler = new MissileHandler(resources);
   ship = new Ship(resources, 120, 280, 0.1, 0, 0);
  }

  public void onDestroy() {
   try {
   } catch (Exception e) {
   }
  }

  public void setSurfaceDimensions(int width, int height) {
   screenWidth = width;
   screenHeight = height;
  }

  public void update() {
   asteroidHandler.update(screenWidth, screenHeight);
   ship.update(screenWidth, screenHeight, asteroidHandler.asteroids,
     pendingEvent, gameStatus);
   missileHandler.update(screenWidth, screenHeight, pendingEvent, ship.x,
     ship.y, ship.angle, asteroidHandler.asteroids, gameStatus);
   pendingEvent = EVENT_NONE;

   if (asteroidHandler.asteroids.isEmpty())
    setLevel(gameStatus.getLevel() + 1);
  }

  public void draw(Canvas canvas) {
   canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), blackPaint);
   asteroidHandler.draw(canvas);
   missileHandler.draw(canvas);
   ship.draw(canvas);
   gameStatus.draw(canvas, screenHeight, screenWidth);
  }

}

Then we do a similar change in GameStatus:

package com.ajomannen.justroids;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;

public class GameStatus {
  private Paint textPaint;
  private int level;
  private int thrusts;
  private int firedMissiles;
  private long levelStartTime;

  public GameStatus() {
   textPaint = new Paint();
   textPaint.setColor(Color.LTGRAY);
   textPaint.setTextSize(25);

   // level = 1;
   // thrusts = 0;
   // firedMissiles = 0;
   // levelStartTime = System.nanoTime();
  }

  public int getLevel() {
   return level;
  }

  public void setLevel(int level) {
   this.level = level;
   thrusts = 0;
   firedMissiles = 0;
   levelStartTime = System.nanoTime();
  }

  public void increaseThrusts() {
   thrusts++;
  }

  public void increaseFiredMissiles() {
   firedMissiles++;
  }

  public void draw(Canvas canvas, float screenHeight, float screenWidth) {
   long passedLevelTime = (System.nanoTime() - levelStartTime) / 1000000000L;

   textPaint.setTextAlign(Paint.Align.LEFT);
   canvas.drawText("Level: " + level, 1, 19, textPaint);
   canvas.drawText("Thrusts: " + thrusts, 1, screenHeight - 2, textPaint);

   textPaint.setTextAlign(Paint.Align.RIGHT);
   canvas.drawText("Time: " + passedLevelTime + "s", screenWidth - 1, 19,
     textPaint);
   canvas.drawText("Missiles: " + firedMissiles, screenWidth - 1,
     screenHeight - 2, textPaint);
  }

}

That's all that is needed in order to make the game a bit more exiting:

The ship is still immortal, though. That's the topic for an upcoming chapter.

I also realized that the missiles' velocities are relative to the screen and not to the ship. It gives an interesting effect if the ship is moving fast... I guess that will the topic for next chapter.

Mortality or not - we managed to implement no less than 7(!) requirements by introducing the "level" concept:

 

Requirements implemented in this chapter

✔ 10. The game starts at Level 1.

✔ 11. There shall be no upper limit on Level.

✔ 12. When a level starts, the ship shall be in the center of the screen.

✔ 13. When a level starts, the ship should have the velocity 0 and be pointing upwards.

✔ 14. When a level starts, there shall be one "full-size" asteroid per level (1 asteroid on level 1, 2 on level 2 and so on...) on the outer parts of the screen.

✔ 15. When a level starts, the asteroids shall have random velocity and direction.

✔ 29. When all asteroids are destroyed, the level is completed. 

Chapter 33: Showing a status display between levels

I realized that I completely forgot about requirement #30 (showing a brief summary between levels) in the previous chapter, so let's do it now.

(I decided to skip the "Next Level" button, and instead add a countdown timer before next level starts.)

We only have to edit two files to get the summary and count down timer in place;

GameStatus.java:

package com.ajomannen.justroids;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;

public class GameStatus {
  private Paint textPaint;
  private int level;
  private int thrusts;
  private int thrustsLastLevel;
  private int firedMissiles;
  private int firedMissilesLastLevel;
  private long levelStartTime;
  private long passedLevelTime;
  private long passedLevelTimeLastLevel;

  private static final long GRACE_PERIOD = 5000000000L;

  public GameStatus() {
   textPaint = new Paint();
   textPaint.setColor(Color.LTGRAY);
   textPaint.setTextSize(25);
  }

  public int getLevel() {
   return level;
  }

  public void setLevel(int level) {
   this.level = level;

   thrustsLastLevel = thrusts;
   firedMissilesLastLevel = firedMissiles;
   passedLevelTimeLastLevel = passedLevelTime;

   thrusts = 0;
   firedMissiles = 0;
   levelStartTime = System.nanoTime() + GRACE_PERIOD;
  }

  public long getPassedLevelTime() {
   return passedLevelTime;
  }

  public void increaseThrusts() {
   thrusts++;
  }

  public void increaseFiredMissiles() {
   firedMissiles++;
  }

  public void update() {
   passedLevelTime = (System.nanoTime() - levelStartTime) / 1000000000L;
  }

  public void draw(Canvas canvas, float screenHeight, float screenWidth) {
   // long passedLevelTime = (System.nanoTime() - levelStartTime) / 1000000000L;

   if (passedLevelTime > 0) {
    textPaint.setTextAlign(Paint.Align.LEFT);
    canvas.drawText("Level: " + level, 1, 19, textPaint);
    canvas.drawText("Thrusts: " + thrusts, 1, screenHeight - 2,
      textPaint);

    textPaint.setTextAlign(Paint.Align.RIGHT);
    canvas.drawText("Time: " + passedLevelTime + "s", screenWidth - 1,
      19, textPaint);
    canvas.drawText("Missiles: " + firedMissiles, screenWidth - 1,
      screenHeight - 2, textPaint);
   } else {
    textPaint.setTextAlign(Paint.Align.CENTER);
    if (level > 1) {
     canvas.drawText("Level " + (level - 1) + " completed! ",
       screenWidth / 2, 29, textPaint);
     canvas.drawText(" Time: " + passedLevelTimeLastLevel + "s",
       screenWidth / 2, 60, textPaint);
     canvas.drawText("Thrusts: " + thrustsLastLevel,
       screenWidth / 2, 90, textPaint);
     canvas.drawText("Missiles: " + firedMissilesLastLevel,
       screenWidth / 2, 120, textPaint);
    }
    textPaint.setColor(Color.LTGRAY);
    textPaint.setTextSize(30);
    canvas.drawText("Level " + level + " starts in "
      + (-passedLevelTime) + "s", screenWidth / 2,
      screenHeight - 40, textPaint);
    textPaint.setTextSize(25);
   }
  }

}

and GameEngine.java:

package com.ajomannen.justroids;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;

public class GameEngine {

  Context context;
  Resources resources;

  public float screenWidth;
  public float screenHeight;
  private Paint blackPaint;

  private AsteroidHandler asteroidHandler;
  private MissileHandler missileHandler;
  private Ship ship;
  private GameStatus gameStatus;

  public static final int EVENT_NONE = 0;
  public static final int EVENT_LEFT = 1;
  public static final int EVENT_RIGHT = 2;
  public static final int EVENT_THRUST = 3;
  public static final int EVENT_FIRE = 4;
  public int pendingEvent = EVENT_NONE;

  public void Init(Context context) {
   this.context = context;
   resources = context.getResources();

   blackPaint = new Paint();
   blackPaint.setColor(Color.BLACK);
   blackPaint.setStyle(Style.FILL);

   gameStatus = new GameStatus();
   setLevel(1);

   setSurfaceDimensions(240, 320);
  }

  private void setLevel(int level) {
   gameStatus.setLevel(level);
   asteroidHandler = new AsteroidHandler(resources, level);
   missileHandler = new MissileHandler(resources);
   ship = new Ship(resources, 120, 280, 0.1, 0, 0);
  }

  public void onDestroy() {
   try {
   } catch (Exception e) {
   }
  }

  public void setSurfaceDimensions(int width, int height) {
   screenWidth = width;
   screenHeight = height;
  }

  public void update() {
   gameStatus.update();
   if (gameStatus.getPassedLevelTime() > 0) {
    asteroidHandler.update(screenWidth, screenHeight);
    ship.update(screenWidth, screenHeight, asteroidHandler.asteroids,
      pendingEvent, gameStatus);
    missileHandler.update(screenWidth, screenHeight, pendingEvent,
      ship.x, ship.y, ship.angle, asteroidHandler.asteroids,
      gameStatus);
    pendingEvent = EVENT_NONE;

    if (asteroidHandler.asteroids.isEmpty())
     setLevel(gameStatus.getLevel() + 1);

   }
  }

  public void draw(Canvas canvas) {
   canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), blackPaint);
   asteroidHandler.draw(canvas);
   missileHandler.draw(canvas);
   ship.draw(canvas);
   gameStatus.draw(canvas, screenHeight, screenWidth);
  }

}

That's it! Now we have a nice little summary between levels:

 

Requirements implemented in this chapter

✔ 30. When a level is completed, a "Next Level" link/button shall be shown along with a summary displaying how much time it took to complete the level, how many missiles that were fired and how much fuel that was used. (All these resources are unlimited in this game, but it can be fun to compete with yourself).

Chapter 34: Absolute...or relative?

Now when the game is beginning to be slightly playable, I noticed that it looks a bit awkward with missiles that always leave the ship at the same velocity regardless of what speed the ship has.

Remember how we create new missiles in MissileHandler if there is an "EVENT_FIRE" pending:

if (pendingEvent == GameEngine.EVENT_FIRE) {
   Missile newMissile = new Missile(bitmap, shipX, shipY, 6, shipAngle);
   missiles.add(newMissile);
   gameStatus.increaseFiredMissiles();
}

The hard coded figure "6" that we send to Missile's constructor do really look a bit alone among all variables...

The effect of having it like this, is that the ship might outrun the missiles if is has a velocity greater than 6.

In the screenshot below, the ship is moving straight upwards, while it's pointing "west-north-west". As you can see, the missiles' trajectories do indeed look a bit odd:

 

If we look at it from a "vector perspective" it is quite obvious why we get this effect.

Consider a ship moving "north-west" while pointing "north-east" and firing off a missile:

 

 

What is missed here is the momentum of the ship. The missile would have traveled along the exact same trajectory if the ship was standing still...

Instead of having the missiles' velocity hard coded to "6" at an angle that corresponds to the ship's, we should rather see it as two vectors; the ship's current trajectory and the base missile trajectory (green in the picture below). These two vectors should then be added in order to get a more appropriate velocity and angle for the missile: 

Code wise, this is not so complicated.

First of all we must edit GameEngine.update(), so that we not only pass on ship.x, ship.x and ship.angle to MissileHandler. We must also include ship.velocity and ship.direction. We might as well pass on the whole ship to make the code a bit more readable:

public void update() {
  gameStatus.update();
  if (gameStatus.getPassedLevelTime() > 0) {
   asteroidHandler.update(screenWidth, screenHeight);
   ship.update(screenWidth, screenHeight, asteroidHandler.asteroids,
     pendingEvent, gameStatus);
   missileHandler.update(screenWidth, screenHeight, pendingEvent,
     ship, asteroidHandler.asteroids, gameStatus);
   pendingEvent = EVENT_NONE;

   if (asteroidHandler.asteroids.isEmpty())
    setLevel(gameStatus.getLevel() + 1);

  }
} 

To match this change, we also need to edit the definition of MissileHandler.update():

public void update(float screenWidth, float screenHeight, int pendingEvent,
  Ship ship, List<Asteroid> asteroids, GameStatus gameStatus) {

Now to the core part, the vector addition in MissileHandler. In MissileHandler.update, we shall edit the block:

if (pendingEvent == GameEngine.EVENT_FIRE) {
  Missile newMissile = new Missile(bitmap, shipX, shipY, 6, shipAngle);
  missiles.add(newMissile);
  gameStatus.increaseFiredMissiles();
}

and replace the "new Missile"-line with a bit more appropriate code:

if (pendingEvent == GameEngine.EVENT_FIRE) {

  // Get the Cartesian (x, y) representation of the base missile vector
  double missileBaseRadians = Math.toRadians(ship.angle - 90);
  double missileBaseVelocity = 6; // Here it is - the hard coded number "6"
  double missileBaseDX = Math.cos(missileBaseRadians) * missileBaseVelocity;
  double missileBaseDY = -Math.sin(missileBaseRadians) * missileBaseVelocity;

  // Get the Cartesian (x, y) representation of the ship's current vector
  double shipRadians = Math.toRadians(ship.getDirection() - 90);
  double shipVelocity = ship.getVelocity();
  double shipDX = Math.cos(shipRadians) * shipVelocity;
  double shipDY = -Math.sin(shipRadians) * shipVelocity;

  // The actual vector addition
  double missileDX = missileBaseDX + shipDX;
  double missileDY = missileBaseDY + shipDY;

  // Convert back to polar (velocity, direction) representation
  double velocity = Math.sqrt(missileDX * missileDX + missileDY * missileDY);
  double direction = Math.toDegrees(Math.atan2(missileDX, missileDY));

  Missile newMissile = new Missile(bitmap, ship.x, ship.y, velocity, direction);

  missiles.add(newMissile);
  gameStatus.increaseFiredMissiles();
}

One final change in GfxObject is also needed. In the "addVelocity"-method, I only updated the Cartesian "dX" and "dY" and forgot about the polar "velocity" and "direction". So we have to add it now:

public void addVelocity(double increment, int angle) {
  double radians = Math.toRadians(angle - 90);
  dX += Math.cos(radians) * increment;
  dY += Math.sin(radians) * increment;
  velocity = Math.sqrt( dX * dX + dY * dY );
  direction = Math.toDegrees(Math.atan2( dX, -dY ));
}

That was the last of it!

Now the missiles are fired away in a bit more physically correct manner:

Chapter 35: Surface dimensions during game start-up

I tested JustRoids on a few different devices, and noticed that the ship's starting position isn't exactly according to what I originally wanted: in the middle at the bottom of the screen.

The reason for this is just as simple as the reason that the missiles had strange trajectories - we hard coded some values instead of using a proper calculation.

In GameEngine.setLevel() we create the ship at a specific position:

private void setLevel(int level) {
  gameStatus.setLevel(level);
  asteroidHandler = new AsteroidHandler(resources, level);
  missileHandler = new MissileHandler(resources);
  ship = new Ship(resources, 120, 280, 0.1, 0, 0);
}

The reason I did that, was due to the fact that Android does not provide any means to get the dimensions of a surface until during the layout stage, which occurs before anything is drawn but after the construction. If the getWidth() and getHeight() methods are called too early, they will just return zero. (That's also the reason I call "setSurfaceDimensions(240, 320)" in GameEngine.Init(), just so that there will some value there during the first update-phase.)

But as soon as Android knows the dimensions, the following method in GameSurface is called automatically:

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
  gameEngine.setSurfaceDimensions(width, height);
}

And in it we forward the values to GameEngine, so that the correct height and width are there for the rest of the game.

In order to have the ship at the right place from the beginning, we could simply wait until that call before we create it.

This requires minor changes in a few different places in the GameEngine:

package com.ajomannen.justroids;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;

public class GameEngine {

  Context context;
  Resources resources;

  public float screenWidth;
  public float screenHeight;
  private Paint blackPaint;

  private AsteroidHandler asteroidHandler;
  private MissileHandler missileHandler;
  private Ship ship;
  private GameStatus gameStatus;

  public static final int EVENT_NONE = 0;
  public static final int EVENT_LEFT = 1;
  public static final int EVENT_RIGHT = 2;
  public static final int EVENT_THRUST = 3;
  public static final int EVENT_FIRE = 4;
  public int pendingEvent = EVENT_NONE;

  private boolean surfaceReady = false;

  public void Init(Context context) {
   this.context = context;
   resources = context.getResources();

   blackPaint = new Paint();
   blackPaint.setColor(Color.BLACK);
   blackPaint.setStyle(Style.FILL);

   gameStatus = new GameStatus();

   // setLevel(1);
   // setSurfaceDimensions(240, 320);
  }

  private void setLevel(int level) {
   gameStatus.setLevel(level);
   asteroidHandler = new AsteroidHandler(resources, level);
   missileHandler = new MissileHandler(resources);
   // ship = new Ship(resources, 120, 280, 0.1, 0, 0);
   ship = new Ship(resources, screenWidth / 2, screenHeight - 30, 0.1, 0, 0);
  }

  public void onDestroy() {
   try {
   } catch (Exception e) {
   }
  }

  public void setSurfaceDimensions(int width, int height) {
   screenWidth = width;
   screenHeight = height;
   if (!surfaceReady) {
    setLevel(1);
    surfaceReady = true;
   }

  }

  public void update() {
   gameStatus.update();
   if (surfaceReady && gameStatus.getPassedLevelTime() > 0) {
    asteroidHandler.update(screenWidth, screenHeight);
    ship.update(screenWidth, screenHeight, asteroidHandler.asteroids,
      pendingEvent, gameStatus);
    missileHandler.update(screenWidth, screenHeight, pendingEvent,
      ship, asteroidHandler.asteroids, gameStatus);
    pendingEvent = EVENT_NONE;

    if (asteroidHandler.asteroids.isEmpty())
     setLevel(gameStatus.getLevel() + 1);

   }
  }

  public void draw(Canvas canvas) {
   if (surfaceReady) {
    canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(),
      blackPaint);
    asteroidHandler.draw(canvas);
    missileHandler.draw(canvas);
    ship.draw(canvas);
    gameStatus.draw(canvas, screenHeight, screenWidth);
   }
  }

}

Now we have the ship nicely located where we want it when each level starts:

Chapter 36: Refactoring - Proper states in GameEngine

I had the intention to finally let the ship be mortal, but realized that the structure in GameEngine didn't really allow me to introduce a "GameOver" state without making the code really ugly.

So this chapter is instead dedicated to a re-factoring of GameEngine, in which we remove the "surfaceReady" flag and introduce the four different states "WAITING_FOR_SURFACE", "COUNTDOWN", "RUNNING" and "GAMEOVER" and switch-case constructions in both update() and draw().

So, here is the new GameEngine:

package com.ajomannen.justroids;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;

public class GameEngine {

  Context context;
  Resources resources;

  public float screenWidth;
  public float screenHeight;
  private Paint blackPaint;

  private AsteroidHandler asteroidHandler;
  private MissileHandler missileHandler;
  private Ship ship;
  private GameStatus gameStatus;

  public static final int EVENT_NONE = 0;
  public static final int EVENT_LEFT = 1;
  public static final int EVENT_RIGHT = 2;
  public static final int EVENT_THRUST = 3;
  public static final int EVENT_FIRE = 4;
  public int pendingEvent = EVENT_NONE;

  // private boolean surfaceReady = false;
  private static final int WAITING_FOR_SURFACE = 0;
  private static final int COUNTDOWN = 1;
  private static final int RUNNING = 2;
  private static final int GAMEOVER = 3;

  private int mode = WAITING_FOR_SURFACE;

  public void Init(Context context) {
   this.context = context;
   resources = context.getResources();

   blackPaint = new Paint();
   blackPaint.setColor(Color.BLACK);
   blackPaint.setStyle(Style.FILL);

   gameStatus = new GameStatus();
  }

  private void setLevel(int level) {
   gameStatus.setLevel(level);
   asteroidHandler = new AsteroidHandler(resources, level);
   missileHandler = new MissileHandler(resources);
   ship = new Ship(resources, screenWidth / 2, screenHeight - 30, 0.1, 0,
     0);
   mode = COUNTDOWN;
  }

  public void onDestroy() {
   try {
   } catch (Exception e) {
   }
  }

  // Rewritten
  public void setSurfaceDimensions(int width, int height) {
   screenWidth = width;
   screenHeight = height;

   if (mode == WAITING_FOR_SURFACE) {
    setLevel(1);
   }

  }

  // Rewritten
  public void update() {
   switch (mode) {

   case WAITING_FOR_SURFACE: {
    // Don't update anything. Just wait until setSurfaceDimensions() is called.
    break;
   }

   case COUNTDOWN: {
    // Just update gameStatus so the timer can tick up to zero
    gameStatus.update();
    if (gameStatus.getPassedLevelTime() > 0) {
     pendingEvent = EVENT_NONE;
     mode = RUNNING;
    }
    break;
   }

   case RUNNING: {
    // Standard mode - update all objects
    gameStatus.update();
    asteroidHandler.update(screenWidth, screenHeight);
    ship.update(screenWidth, screenHeight, asteroidHandler.asteroids,
      pendingEvent, gameStatus);
    missileHandler.update(screenWidth, screenHeight, pendingEvent,
      ship, asteroidHandler.asteroids, gameStatus);
    pendingEvent = EVENT_NONE;

    if (asteroidHandler.asteroids.isEmpty()) {
     setLevel(gameStatus.getLevel() + 1);
    }
    break;
   }

   case GAMEOVER: {
    // Nothing here yet.
    break;
   }

   }

  }

  // Rewritten
  public void draw(Canvas canvas) {
   switch (mode) {

   case WAITING_FOR_SURFACE: {
    // Don't draw anything before the surface is ready
    break;
   }

   case COUNTDOWN: {
    // No "break" here. We want the same draw:s during COUNTDOWN as
    // during RUNNING
   }

   case RUNNING: {
    // Standard mode - draw all objects
    canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(),
      blackPaint);
    asteroidHandler.draw(canvas);
    missileHandler.draw(canvas);
    ship.draw(canvas);
    gameStatus.draw(canvas, screenHeight, screenWidth);
    break;
   }

   case GAMEOVER: {
    // Here we will add a nice little "Game Over" screen later on
    break;
   }

   }

  }

}

The effect on the game after these changes in GameEngine was absolutely none.

So, it was a successful refactoring ;-)

Chapter 37: Game Over

Well, it's finally time for the most important part in every arcade game: the words "GAME OVER" in capital letter all over the screen.

Most bits and pieces needed for this is are already in place, but we have to make some final changes in two files; GameEngine and GameStatus.

Let's start with GameStatus, where we add a new method to draw the actual "Game Over" screen. We also make two minor fixes while we have the file open:

package com.ajomannen.justroids;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;

public class GameStatus {
  private Paint textPaint;
  private int level;
  private int thrusts;
  private int thrustsLastLevel;
  private int firedMissiles;
  private int firedMissilesLastLevel;
  private long levelStartTime;
  private long passedLevelTime;
  private long passedLevelTimeLastLevel;

  private static final long GRACE_PERIOD = 5000000000L;

  public GameStatus() {
   textPaint = new Paint();
   textPaint.setColor(Color.LTGRAY);
   textPaint.setTextSize(25);
  }

  public int getLevel() {
   return level;
  }

  public void setLevel(int level) {
   this.level = level;

   thrustsLastLevel = thrusts;
   firedMissilesLastLevel = firedMissiles;
   passedLevelTimeLastLevel = passedLevelTime;

   thrusts = 0;
   firedMissiles = 0;
   levelStartTime = System.nanoTime() + GRACE_PERIOD;
  }

  public long getPassedLevelTime() {
   return passedLevelTime;
  }

  public void increaseThrusts() {
   thrusts++;
  }

  public void increaseFiredMissiles() {
   firedMissiles++;
  }

  public void update() {
   passedLevelTime = (System.nanoTime() - levelStartTime) / 1000000000L;
  }

  public void draw(Canvas canvas, float screenHeight, float screenWidth) {
   if (passedLevelTime > 0) {
    textPaint.setTextAlign(Paint.Align.LEFT);
    canvas.drawText("Level: " + level, 1, 19, textPaint);
    canvas.drawText("Thrusts: " + thrusts, 1, screenHeight - 2,
      textPaint);

    textPaint.setTextAlign(Paint.Align.RIGHT);
    canvas.drawText("Time: " + passedLevelTime + "s", screenWidth - 1,
      19, textPaint);
    canvas.drawText("Missiles: " + firedMissiles, screenWidth - 1,
      screenHeight - 2, textPaint);
   } else {
    textPaint.setTextAlign(Paint.Align.CENTER);
    if (level > 1) {
     canvas.drawText("Level " + (level - 1) + " completed! ",
       screenWidth / 2, 29, textPaint);
     canvas.drawText(" Time: " + passedLevelTimeLastLevel + "s",
       screenWidth / 2, 60, textPaint);
     canvas.drawText("Thrusts: " + thrustsLastLevel,
       screenWidth / 2, 90, textPaint);
     canvas.drawText("Missiles: " + firedMissilesLastLevel,
       screenWidth / 2, 120, textPaint);
    }
    // textPaint.setColor(Color.LTGRAY); // This line was redundant and can be removed
    textPaint.setTextSize(30);
    canvas.drawText("Level " + level + " starts in "
      + (-passedLevelTime) + "s", screenWidth / 2,
      (float) (screenHeight * 0.75), textPaint); // 75% scales better than a fixed value
    textPaint.setTextSize(25);
   }
  }

  // New method
  public void drawGameOverScreen(Canvas canvas, float screenHeight,
    float screenWidth) {
   textPaint.setTextAlign(Paint.Align.CENTER);
   textPaint.setTextSize(50);
   canvas.drawText("GAME OVER", screenWidth / 2, (float) (screenHeight * 0.50),
     textPaint);
   textPaint.setTextSize(25);
   canvas.drawText("You reached level " + level, screenWidth / 2, (float) (screenHeight * 0.60),
     textPaint);
   canvas.drawText("Press 'Back' for Main Menu", screenWidth / 2, (float) (screenHeight * 0.85),
     textPaint);
  }
}

Then it's time for GameEngine, where we add draw calls during GAMEOVER and rewrite the COUNTDOWN part also to make it look a bit better:

package com.ajomannen.justroids;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;

public class GameEngine {

  Context context;
  Resources resources;

  public float screenWidth;
  public float screenHeight;
  private Paint blackPaint;
  private Paint semiTransparentPaint;

  private AsteroidHandler asteroidHandler;
  private MissileHandler missileHandler;
  private Ship ship;
  private GameStatus gameStatus;

  public static final int EVENT_NONE = 0;
  public static final int EVENT_LEFT = 1;
  public static final int EVENT_RIGHT = 2;
  public static final int EVENT_THRUST = 3;
  public static final int EVENT_FIRE = 4;
  public int pendingEvent = EVENT_NONE;

  private static final int WAITING_FOR_SURFACE = 0;
  private static final int COUNTDOWN = 1;
  private static final int RUNNING = 2;
  private static final int GAMEOVER = 3;

  private int mode = WAITING_FOR_SURFACE;

  public void Init(Context context) {
   this.context = context;
   resources = context.getResources();

   blackPaint = new Paint();
   blackPaint.setColor(Color.BLACK);
   blackPaint.setStyle(Style.FILL);

   // New Paint for "shading effects"
   semiTransparentPaint = new Paint();
   semiTransparentPaint.setColor(Color.BLACK);
   semiTransparentPaint.setStyle(Style.FILL);
   semiTransparentPaint.setAlpha(200);

   gameStatus = new GameStatus();
  }

  private void setLevel(int level) {
   gameStatus.setLevel(level);
   asteroidHandler = new AsteroidHandler(resources, level);
   missileHandler = new MissileHandler(resources);
   ship = new Ship(resources, screenWidth / 2, screenHeight - 30, 0.1, 0,
     0);
   mode = COUNTDOWN;
  }

  public void onDestroy() {
   try {
   } catch (Exception e) {
   }
  }

  public void setSurfaceDimensions(int width, int height) {
   screenWidth = width;
   screenHeight = height;

   if (mode == WAITING_FOR_SURFACE) {
    setLevel(1);
   }

  }

  public void update() {
   switch (mode) {

   case WAITING_FOR_SURFACE: {
    // Don't update anything. Just wait until setSurfaceDimensions() is
    // called.
    break;
   }

   case COUNTDOWN: {
    // Just update gameStatus so the timer can tick up to zero
    gameStatus.update();
    if (gameStatus.getPassedLevelTime() > 0) {
     pendingEvent = EVENT_NONE;
     mode = RUNNING;
    }
    break;
   }

   case RUNNING: {
    // Standard mode - update all objects
    gameStatus.update();
    asteroidHandler.update(screenWidth, screenHeight);
    ship.update(screenWidth, screenHeight, asteroidHandler.asteroids,
      pendingEvent, gameStatus);
    missileHandler.update(screenWidth, screenHeight, pendingEvent,
      ship, asteroidHandler.asteroids, gameStatus);
    pendingEvent = EVENT_NONE;

    // Up one level if all asteroids are gone
    if (asteroidHandler.asteroids.isEmpty()) {
     setLevel(gameStatus.getLevel() + 1);
    }

    // Game Over if the ship has collided with an asteroid
    if (ship.isCollided()) {
     mode = GAMEOVER;
    }
    break;
   }

   case GAMEOVER: {
    // Nothing needed here
    break;
   }

   }

  }

  public void draw(Canvas canvas) {
   switch (mode) {

   case WAITING_FOR_SURFACE: {
    // Don't draw anything before the surface is ready
    break;
   }

   case COUNTDOWN: {
    // We want to draw all objects, but everything except the counter
    // should be shaded
    canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(),
      blackPaint);
    asteroidHandler.draw(canvas);
    missileHandler.draw(canvas);
    ship.draw(canvas);
    canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(),
      semiTransparentPaint);
    gameStatus.draw(canvas, screenHeight, screenWidth);
    break;
   }

   case RUNNING: {
    // Standard mode - draw all objects
    canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(),
      blackPaint);
    asteroidHandler.draw(canvas);
    missileHandler.draw(canvas);
    ship.draw(canvas);
    gameStatus.draw(canvas, screenHeight, screenWidth);
    break;
   }

   case GAMEOVER: {
    // Draw everything plus a game-over text
    canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(),
      blackPaint);
    asteroidHandler.draw(canvas);
    missileHandler.draw(canvas);
    ship.draw(canvas);
    gameStatus.draw(canvas, screenHeight, screenWidth);
    canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(),
      semiTransparentPaint);
    gameStatus.drawGameOverScreen(canvas, screenHeight, screenWidth);
    break;
   }

   }

  }

}

With these latest changes and additions, the game look more or less complete!

 

If I don't stumble upon any new issue, we should be ready to choose an icon for the app, put the ads back and then publish it on the Marketplace.

Exciting!

Requirements implemented in this chapter

✔ 27. If the ship collides with an asteroid, the game is over.

✔ 28. When the game is over, some sort of summery shall be shown along with a link back to the Main Menu.

Chapter 38: Scaling in GameStatus

One last touch in the code (hopefully).

I noticed that on devices with very small screens, such as Sony Ericsson X10 Mini Pro, the text "GAME OVER" does not fit.

On very large screen, on the other hand, a few text fields are squeezed together even though there are plenty of space around.

So let's change the hard coded text sizes 25, 30 and 50 and the hard coded y-positions in GameStatus.draw()  to scaled values instead.

In order to do that, we must add a call to GameStatus from GameEngine when we get the surface dimensions from Android. So, add one line in GameEngine.setSurfaceDimensions:

public void setSurfaceDimensions(int width, int height) {
    screenWidth = width;
    screenHeight = height;
        
    gameStatus.updateSurfaceDimensions(width, height);

    if (mode == WAITING_FOR_SURFACE) {
        setLevel(1);
    }

}

Unfortunately the required changes in GameStatus are spread out all through the class:

package com.ajomannen.justroids;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;

public class GameStatus {
    private Paint textPaint;
    private int level;
    private int thrusts;
    private int thrustsLastLevel;
    private int firedMissiles;
    private int firedMissilesLastLevel;
    private long levelStartTime;
    private long passedLevelTime;
    private long passedLevelTimeLastLevel;
    private float smallTextSize = 25;
    private float mediumTextSize = 30;
    private float largeTextSize = 50;

    private static final long GRACE_PERIOD = 5000000000L;

    public GameStatus() {
        textPaint = new Paint();
        textPaint.setColor(Color.LTGRAY);
        // textPaint.setTextSize(25);
    }

    public void updateSurfaceDimensions(int width, int height) {
        // 25, 30 and 50 are perfect sizes on a 320 points wide screen
        smallTextSize = width / (320 / 25);
        mediumTextSize = width / (320 / 30);
        largeTextSize = width / (320 / 50);
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;

        thrustsLastLevel = thrusts;
        firedMissilesLastLevel = firedMissiles;
        passedLevelTimeLastLevel = passedLevelTime;

        thrusts = 0;
        firedMissiles = 0;
        levelStartTime = System.nanoTime() + GRACE_PERIOD;
    }

    public long getPassedLevelTime() {
        return passedLevelTime;
    }

    public void increaseThrusts() {
        thrusts++;
    }

    public void increaseFiredMissiles() {
        firedMissiles++;
    }

    public void update() {
        passedLevelTime = (System.nanoTime() - levelStartTime) / 1000000000L;
    }

    public void draw(Canvas canvas, float screenHeight, float screenWidth) {
        textPaint.setTextSize(smallTextSize);
        if (passedLevelTime > 0) {
            textPaint.setTextAlign(Paint.Align.LEFT);
            canvas.drawText("Level: " + level, 1, smallTextSize, textPaint);
            canvas.drawText("Thrusts: " + thrusts, 1, screenHeight - 2,
                    textPaint);

            textPaint.setTextAlign(Paint.Align.RIGHT);
            canvas.drawText("Time: " + passedLevelTime + "s", screenWidth - 1,
                    19, textPaint);
            canvas.drawText("Missiles: " + firedMissiles, screenWidth - 1,
                    screenHeight - 2, textPaint);
        } else {
            textPaint.setTextAlign(Paint.Align.CENTER);
            if (level > 1) {
                canvas.drawText("Level " + (level - 1) + " completed! ",
                        screenWidth / 2, mediumTextSize, textPaint);
                canvas.drawText(" Time: " + passedLevelTimeLastLevel + "s",
                        screenWidth / 2, mediumTextSize * 2, textPaint);
                canvas.drawText("Thrusts: " + thrustsLastLevel,
                        screenWidth / 2, mediumTextSize * 3, textPaint);
                canvas.drawText("Missiles: " + firedMissilesLastLevel,
                        screenWidth / 2, mediumTextSize * 4, textPaint);
            }
            textPaint.setTextSize(mediumTextSize);
            canvas.drawText("Level " + level + " starts in "
                    + (-passedLevelTime) + "s", screenWidth / 2,
                    (float) (screenHeight * 0.75), textPaint);
            // textPaint.setTextSize(25);
        }
    }

    public void drawGameOverScreen(Canvas canvas, float screenHeight,
            float screenWidth) {
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTextSize(largeTextSize);
        canvas.drawText("GAME OVER", screenWidth / 2,
                (float) (screenHeight * 0.50), textPaint);
        textPaint.setTextSize(smallTextSize);
        canvas.drawText("You reached level " + level, screenWidth / 2,
                (float) (screenHeight * 0.60), textPaint);
        canvas.drawText("Press 'Back' for Main Menu", screenWidth / 2,
                (float) (screenHeight * 0.85), textPaint);
    }

}

Now the text also fits on really tiny screens:

(I will not include a screenshot from a very large screen, but I can assure you that the text is better aligned ;-) )

 

Note: I used an online Java syntax highllighter in this chapter, instead of performing a lot of manual steps when including source code. The look and feel is a bit different than in previous chapters, but I think it's to the better.

Chapter 39: Creating an app icon

Now it's real close! The app is almost ready to be published.

But the app's icon doesn't look that great for the moment:

The one we currently have is the default one, so let's do our own.

An app's icon should be made in three sizes; 72x72 (to be stored in res/drawable-ldpi), 48x48 (res/drawable-mdpi) and 36x36 (res/drawable-ldpi) and there are guidelines and templates for icon-buidling

But, lazy as I sometimes am, I'll just copy the existing image "asteroid_whole.png" from "res/drawable" and scale down in Gimp to the three required sizes and save one copy as "ic_launcher.png" in each of the three drawable-folders, overwriting the default versions.

Then we just mark the "res"-folder in Eclipse's Package Explorer and press "F5" to refresh the file list, so that Eclipse notices the three new files.

After this we will have to rebuild the project by selecting "Project -> Clean" in the menu, press "ctrl-F11" and see the new icon in place:

 

One step closer ;-)

 

Chapter 40: Fill in the blanks #1 - The Credits page

Now we can't wait any longer. We need to do something about the "How-to" and "Credits" pages.

I know it's a bit boring since they aren't part of the actual game, but it's still good practice ;-)

We did the very basics in chapter 10, but we only printed the word "Credits" in a TextView at that time. I want to provide a bit more info than that ;-)

I was still thinking about a static page though, with a few lines mentioning that JustRoids is part of a tutorial (with a link to this site) and a special thanks to Martin Felis for the asteroid image used in the game: 

We did a simple xml-file (res/layout/credits.xml) in chapter 10, but it's not really suitable to use that to build the whole page as I now want it. We can keep the file however, but we will have to rewrite it so that we have two different TextViews in it; one for the heading and one for the body text. We also have to add id:s to the TextViews, so that we can access them from our Java code and handle the rest there. So open up credits.xml and change it to:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/creditsHeadingView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="30dip"
        android:gravity="center_horizontal"
        android:text="@string/credits"
        android:textSize="40dip" />

    <TextView
        android:id="@+id/creditsBodyView"
        android:layout_marginLeft="30dip"
        android:layout_marginRight="30dip"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/credits" />

</LinearLayout>

 Then we will have to switch over to CreditsActivity.java and handle the actual printout there:

package com.ajomannen.justroids;

import android.app.Activity;
import android.os.Bundle;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;

public class CreditsActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.credits);

        // New block to print the body text in the Credits page
        TextView textView = (TextView) findViewById(R.id.creditsBodyView);
        textView.setMovementMethod(LinkMovementMethod.getInstance());
        String text = "JustRoids is a game written by Anders Ekstrand as a part of an Android Game "
                + "Development Tutorial published at <a href=\"http://www.benareby.com/tutorial/\">"
                + "www.benareby.com/tutorial</a>, where all source code is available.<br/><br/>"
                + "A very special thank to Martin Felis, who made the asteroid image that plays a central "
                + "part in this game.";
        textView.setText(Html.fromHtml(text));
        // End of body text block
    }
    
}

That's it:

Chapter 41: Fill in the blanks #2 - The How-To page

Since the controlling of the ship in JustRoids might seem a bit "cryptic" to anyone not following this tutorial, I think I must explain the concepts of right and left "sweeps", clockwise/counter-clockwise rotation and "thrusts" as thoroughly as possible to avoid disastrous ratings on the Android Market.

Maybe I over-did this part a bit, but I decided to create four different images to illustrate the three sweep-actions and the tap plus four more images to show how the ship responds on the inputs.

I used "Sketch & Paint!" (http://www.onemotion.com/flash/sketch-paint/) for the "free-hand" arrows and the tap-symbol and Inkscape for the images illustrating the ship's reactions:

sweep_right.png:

sweep_left.png:

sweep_up.png:

tap.png:

rotate_clockwise.png:

rotate_counter_clockwise.png:

thrust_right.png:

and fire.png:

All eight images goes into the "res/drawable" folder, so we can refer to them in our updated howto.xml:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="20dip"
            android:gravity="center_horizontal"
            android:text="-= How-To =-"
            android:textSize="40dip" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="3dip"
            android:layout_marginLeft="30dip"
            android:text="The Plot"
            android:textSize="25dip" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dip"
            android:layout_marginRight="30dip"
            android:text="
In this retro style space-shoot&apos;em-up game you control a
V-winged long-range scouting ship on a mission a long, long
way from home.\n
\n
An interplanetary highway has been planned right through the
sector your are in, but unfortunately an asteroid belt has been
detected right in the wrong place - jeopardizing the whole
project...\n
\n
Luckily your ship is equipped with tactical nukes, so can save
the situation by taking the asteroids out, one by one.\n
\n
You have an endless supply of nuclear missiles but in order to
get room for that supply, the shield generators had to be
removed. Hence your ship is completely unprotected and will be
destroyed if it comes too close to an asteroid.\n
\n
" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="3dip"
            android:layout_marginLeft="30dip"
            android:text="Controls"
            android:textSize="25dip" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dip"
            android:layout_marginRight="30dip"
            android:text="
As your ship is a V-wing Mk I (without the Momentum Eliminator
that made Mk II so popular) it&apos;s a bit tricky to control.\n
\n
There are no brakes (or retro rockets) so if you accelerate,
the ship will continue in the same direction until you turn it
around 180 degrees and accelerate in the opposite direction.\n
\n
There are four different actions you can perform in order to
control your:\n
\n
" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="3dip"
            android:layout_marginLeft="30dip"
            android:text="Right Sweep"
            android:textSize="18dip" />

        <ImageView
            android:layout_width="200dip"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:adjustViewBounds="true"
            android:contentDescription="Right Sweep"
            android:padding="3dip"
            android:src="@drawable/sweep_right" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dip"
            android:layout_marginRight="30dip"
            android:text="
\n
If you swipe your finger from the left to the right of your
screen, the ship will rotate clockwise:\n
" />

        <ImageView
            android:layout_width="200dip"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:adjustViewBounds="true"
            android:contentDescription="Rotate Clockwise"
            android:padding="3dip"
            android:src="@drawable/rotate_clockwise" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="3dip"
            android:layout_marginLeft="30dip"
            android:layout_marginTop="20dip"
            android:text="Left Sweep"
            android:textSize="18dip" />

        <ImageView
            android:layout_width="200dip"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:adjustViewBounds="true"
            android:contentDescription="Left Sweep"
            android:padding="3dip"
            android:src="@drawable/sweep_left" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dip"
            android:layout_marginRight="30dip"
            android:text="
\n
If you swipe your finger from the right to the left of your
screen, the ship will rotate counter-clockwise:\n
" />

        <ImageView
            android:layout_width="200dip"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:adjustViewBounds="true"
            android:contentDescription="Rotate Counter-Clockwise"
            android:padding="3dip"
            android:src="@drawable/rotate_counter_clockwise" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="3dip"
            android:layout_marginLeft="30dip"
            android:layout_marginTop="20dip"
            android:text="Up Sweep"
            android:textSize="18dip" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="100dip"
            android:layout_gravity="center_horizontal"
            android:adjustViewBounds="true"
            android:contentDescription="Up Sweep"
            android:padding="3dip"
            android:src="@drawable/sweep_up" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dip"
            android:layout_marginRight="30dip"
            android:text="
\n
If you swipe your finger upwards on your screen, the ship
will accelerate in the direction it&apos;s nose is pointing
for the moment:\n
" />

        <ImageView
            android:layout_width="200dip"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:adjustViewBounds="true"
            android:contentDescription="Thrust"
            android:padding="3dip"
            android:src="@drawable/thrust_right" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="3dip"
            android:layout_marginLeft="30dip"
            android:layout_marginTop="20dip"
            android:text="Tap"
            android:textSize="18dip" />

        <ImageView
            android:layout_width="200dip"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:adjustViewBounds="true"
            android:contentDescription="Tap"
            android:padding="3dip"
            android:src="@drawable/tap" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dip"
            android:layout_marginRight="30dip"
            android:text="
\n
If you tap anywhere on the screen, the ship will fire
a missile:\n
" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="100dip"
            android:layout_gravity="center_horizontal"
            android:adjustViewBounds="true"
            android:contentDescription="Fire"
            android:padding="3dip"
            android:src="@drawable/fire" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="30dip"
            android:layout_marginRight="30dip"
            android:text="
\n
That's all info you'll get for now.\n
\n
Good luck, Commander!
" />
    </LinearLayout>

</ScrollView>

The xml above will give you a fair amount of warnings from Eclipse, as neither "android:text" nor "android:contentDescription" should be given direct text values, but rather references to text resources (as we did with the Main Menu earlier). But this will do for now.

Anyway, the result became as I wanted it to:





 

That should be the last piece in our game, and in next chapter we will put the ads back in.

Chapter 42: Putting the ads back in

In chapter 9, we laid the foundation to include AdMob ads and in chapter 10 we created our Main Menu page (main.xml) and verified that it could display ads.

However, we had to temporarily disable the AdMob integration in order to get Eclipse to help us building code that is guaranteed to work on Android 2.1.

But as our coding activities are finished (for the moment at least), we can now set the Project Build Target to 4.0.3 again, by chosing Project -> Properties in the Eclipse Menu, and select 4.0.3 again:

(Remember, our wanted target platform is Android 2.1, but in order to get Eclipse to build the project including AdMob SDK, Android 3.2 or greater must be chosen in Project Properties...)

When the project properties are changed, we can enable our "AdActivity" again. Open up the AndroidManifest.xml again and change it to:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.ajomannen.justroids"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="7" />
      <uses-permission android:name="android.permission.INTERNET"/>
      <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".JustRoidsActivity"
              android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity 
            android:name="com.google.ads.AdActivity"
            android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
        />
        
        <activity 
            android:name=".GameActivity" 
            android:label="Game Activity" 
            android:screenOrientation="portrait"
            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
          />
          <activity 
            android:name=".HowtoActivity" 
            android:label="Howto Activity"
            android:screenOrientation="portrait"
            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
          />
          <activity 
            android:name=".CreditsActivity" 
            android:label="Credits Activity" 
            android:screenOrientation="portrait"
            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
          />
          
    </application>
</manifest>

Then a similar change in "res/layout/main.xml". Uncomment the AdView block:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ads="http://schemas.android.com/apk/lib/com.google.ads"
    android:id="@+id/rootLayout"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <ImageView
        android:id="@+id/imgLogo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:adjustViewBounds="true"
        android:contentDescription="@string/justroids"
        android:src="@drawable/justroids" />

    <ImageView
        android:id="@+id/btnGame"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/imgLogo"
        android:layout_gravity="center_horizontal"
        android:clickable="true"
        android:contentDescription="@string/play"
        android:onClick="onClickGame"
        android:src="@drawable/play" />

    <ImageView
        android:id="@+id/btnHowto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btnGame"
        android:layout_gravity="center_horizontal"
        android:clickable="true"
        android:contentDescription="@string/howto"
        android:onClick="onClickHowto"
        android:src="@drawable/howto" />

    <ImageView
        android:id="@+id/btnCredits"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btnHowto"
        android:layout_gravity="center_horizontal"
        android:clickable="true"
        android:contentDescription="@string/credits"
        android:onClick="onClickCredits"
        android:src="@drawable/credits" />

    <com.google.ads.AdView
        android:id="@+id/adView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerInParent="false"
        ads:adSize="BANNER"
        ads:adUnitId="<YOUR OWN ADMOB ID>"
        android:gravity="bottom"
        ads:loadAdOnCreate="false"
        ads:test="true"
        android:visibility="visible" />

</RelativeLayout>

Finally, also uncomment the ad-related block in JustRoidsActivity.java:

package com.ajomannen.justroids;

import com.google.ads.AdRequest;
import com.google.ads.AdView;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class JustRoidsActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        AdView adView = (AdView) findViewById(R.id.adView);
        AdRequest adRequest = new AdRequest();
        adRequest.addTestDevice(AdRequest.TEST_EMULATOR);
        adRequest.addTestDevice("<YOUR OWN DEVICE'S ID>");
        adView.loadAd(adRequest);
    }

    public void onClickGame(View v) {
        Intent intent = new Intent(this, GameActivity.class);
        startActivity(intent);
    }

    public void onClickHowto(View v) {
        Intent intent = new Intent(this, HowtoActivity.class);
        startActivity(intent);
    }

    public void onClickCredits(View v) {
        Intent intent = new Intent(this, CreditsActivity.class);
        startActivity(intent);
    }

}

That should be it! Now we have the ads back on the Main Menu page:

Chapter 43: Publish on Android Market

In order to publish your app on Android Market, you will have to be registered as an "Android Market developer". This registration includes paying a one-time fee of $25.

If you are not willing or able to pay this fee, you cannot publish your app on Android Market. (There are other Marketplaces/Appstores, but I will not cover them in this article.)

I have already performed my registration last year, so I will not be able to provide screenshots and full details regarding this, but I think it should be quite clear anyway ;-)

 

Step 1 - Create a Google account

It is quite possible that you already have this already. Visit https://accounts.google.com/ and either sign up for a new account, or log in with an existing account.

 

Step 2 - Register as Android Market developer

Visit http://market.android.com/publish and fill in the registration form

 

Step 3 - Prepare your app

This step is really important and it is very well described on http://developer.android.com/guide/publishing/preparing.html, so for the first time in this article series, I will not write my own version of this step. Only these few bullets that are easy to forget:

  • Remove all debug printouts
  • Set android.versionCode and android.versionName in the manifest to proper values
  • Build a signed copy of your app (In Eclipse, right click on your project folder, select Android Tools -> Export signed Application Package)
  • Make a snapshot of your source code/project, so you know what was included in the app version you just built
  • Try the signed version on a real device

 

Step 4 - Prepare text and graphic material

You need to prepare

  • At least two screenshots (320 x 480, 480 x 800, 480 x 854, 1280 x 720 or 1280 x 800)
  • One high resolution application icon (512 x 512)
  • A description

 

Step 5 - Upload

Goto the Developer Console on https://market.android.com/publish/Home and click on the Upload Application button:

 

Select the (signed) APK-file you just built and click Upload:

and then Save.

You will now have to spend somewhere between 15 and 60 minutes to fill as many details as possible and add the images you prepared.

When all detailes are filled in, click "Save" at the top of the page, and then click on the "APK files" tab:

Activate your newly uploaded APK and then click "Publish".

 

Done! Our app is published!

 

You can now send the link to your friends and ask them to try it out. The link should be in the form:

http://market.android.com/details?id=<package_name>

For example: http://market.android.com/details?id=com.ajomannen.justroids for JustRoids that I just published.

 

So, the result of this whole tutorial, all those months of work, all late nights...is the following button:

Available in Android Market