Light Racer 2.0 - Days 61-64 - Completion

The last 4 days of work were some of the most intense. Finishing up a project always is. The majority of the work was completing all of the little things like music, sounds, preferences, menu options, layout and UI tweaks and vibration. What's really interesting about the last few days is how some of the most critical bugs tend to pop up right then. For instance, when we added a how-to-play dialog to the game just today (on the release day), it brought up a massive bug where the user couldn't input any control at all. I figured out why that was happening but am still having a few problems with that now and then. Perhaps someone could explain why a thread waiting on a synchronized block gets stuck waiting even though the mutex is bring locked and freed by another thread many times over? Even with that, we managed to get everything together and get the game onto the market!

Day 61 - What was done:

Clear Achievements menu option added
Settings menu option added to all activities
Fixed pausing

Day 62 - What was done:

Added landscape layouts for any screens that need text input.
Moved the submit score dialog into its own activity for speed trial score submission
Added splash screen with animations
Created "battery powered" voice and added to splash screen
Created new game icon - used my sloppy 3ds model and snippets from my graphic designer
Added music to the menu activities - created and used a MenuMusicPlayer
** show code for MenuMusicPlayer and give examples for cross-activity music
Added music to the main game - used an old song I wrote in 2004
Started optimizing for size - it's difficult because everything is already very small.

Day 63 - What was done:

Added music volume adjustments
Fixed music transitions for preferences screen and activity lifecycle
Fixed several bugs
Added how-to-play screen
Fixed CPU pegged / unresponsive controls problems sometimes when starting the game problem
Added vibrations
Fixed sound enable/disable in-game

Day 64 - What was done:

Added end game music
Added some tab icons for the leaderboard
Cleaned up some resources
Designed the Light Racer Elite Logo
Switched accounting over to Battery Powered Games
Created new project for Light Racer Elite
Copied all source over to LRE project
Tested a ton
Ripped out extras from LR adding buy Elite hooks in instead
Tested more
Release!

Summary

One of the things I'm really proud of is getting the inter-menu music working really well. I've been writing electronic music for years and while I haven't done any recently, I have a whole library of stuff that I've done that works really well for video games. I sifted through my collection and found a few loops that I thought would work well in the game. I wanted one loop for the menus, one for the main game and one for the end game screen. I edited them in Audacity, like always, and kept tweaking the data rates and options until I was able to get something that sounds somewhat ok into a tiny OGG.

If you've ever tried to implement music across activities that plays well with the multitasking of Android, you'll know what I mean when I say that it's a major pain. I found a good way of dealing with it and so I'm going to post the code that I use to do the music for Light Racer.

public class MusicManager {
private static final String TAG = "MusicManager";

public static final int MUSIC_PREVIOUS = -1;
public static final int MUSIC_MENU = 0;
public static final int MUSIC_GAME = 1;
public static final int MUSIC_END_GAME = 2;

private static HashMap players = new HashMap();
private static int currentMusic = -1;
private static int previousMusic = -1;

public static float getMusicVolume(Context context) {
String[] volumes = context.getResources().getStringArray(R.array.volume_values);
String volumeString = PreferenceManager.getDefaultSharedPreferences(context).getString(
context.getString(R.string.key_pref_music_volume), volumes[PREF_DEFAULT_MUSIC_VOLUME_ITEM]);
return new Float(volumeString).floatValue();
}

public static void start(Context context, int music) {
start(context, music, false);
}

public static void start(Context context, int music, boolean force) {
if (!force && currentMusic > -1) {
// already playing some music and not forced to change
return;
}
if (music == MUSIC_PREVIOUS) {
Log.d(TAG, "Using previous music [" + previousMusic + "]");
music = previousMusic;
}
if (currentMusic == music) {
// already playing this music
return;
}
if (currentMusic != -1) {
previousMusic = currentMusic;
Log.d(TAG, "Previous music was [" + previousMusic + "]");
// playing some other music, pause it and change
pause();
}
currentMusic = music;
Log.d(TAG, "Current music is now [" + currentMusic + "]");
MediaPlayer mp = players.get(music);
if (mp != null) {
if (!mp.isPlaying()) {
mp.start();
}
} else {
if (music == MUSIC_MENU) {
mp = MediaPlayer.create(context, R.raw.menu_music);
} else if (music == MUSIC_GAME) {
mp = MediaPlayer.create(context, R.raw.game_music);
} else if (music == MUSIC_END_GAME) {
mp = MediaPlayer.create(context, R.raw.end_game_music);
} else {
Log.e(TAG, "unsupported music number - " + music);
return;
}
players.put(music, mp);
float volume = getMusicVolume(context);
Log.d(TAG, "Setting music volume to " + volume);
mp.setVolume(volume, volume);
if (mp == null) {
Log.e(TAG, "player was not created successfully");
} else {
try {
mp.setLooping(true);
mp.start();
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
}

public static void pause() {
Collection mps = players.values();
for (MediaPlayer p : mps) {
if (p.isPlaying()) {
p.pause();
}
}
// previousMusic should always be something valid
if (currentMusic != -1) {
previousMusic = currentMusic;
Log.d(TAG, "Previous music was [" + previousMusic + "]");
}
currentMusic = -1;
Log.d(TAG, "Current music is now [" + currentMusic + "]");
}

public static void updateVolumeFromPrefs(Context context) {
try {
float volume = getMusicVolume(context);
Log.d(TAG, "Setting music volume to " + volume);
Collection mps = players.values();
for (MediaPlayer p : mps) {
p.setVolume(volume, volume);
}
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
}

public static void release() {
Log.d(TAG, "Releasing media players");
Collection mps = players.values();
for (MediaPlayer mp : mps) {
try {
if (mp != null) {
if (mp.isPlaying()) {
mp.stop();
}
mp.release();
}
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
}
mps.clear();
if (currentMusic != -1) {
previousMusic = currentMusic;
Log.d(TAG, "Previous music was [" + previousMusic + "]");
}
currentMusic = -1;
Log.d(TAG, "Current music is now [" + currentMusic + "]");
}
}

To use this, each activity must have a boolean field called continueMusic or whatever you want to call it. For any activity that isn't the root activity (launcher), override onKeyDown and set continueMusic to true if the key is the "back" key. Also set continueMusic to true when launching any activities in which you want to keep playing this music in with no interruption.

Then the magic is in the onPause and onResume:

@Override
protected void onPause() {
super.onPause();
if (!continueMusic) {
MusicManager.pause();
}
}

@Override
protected void onResume() {
super.onResume();
continueMusic = false;
MusicManager.start(this, MusicManager.MUSIC_MENU);
}

See how that works? You simply opt-in on keeping the music playing for when the activity will be pausing. It works really well except for a little studder on orientation changes.

I can't believe Light Racer Elite is actually done. I've been working on it for so long now! I started putting together a product page here - http://www.rbgrn.net/lightracer - but that will be moved to the Battery Powered Games site as soon as it is ready.

If you haven't tried Light Racer yet, please do. I'd love to hear your feedback! If you'd like to support my development, please purchase a copy of Light Racer Elite or Wixel. I really appreciate the support!

The next few articles will deal with getting started in 3D development. It may be a few days until the next update as I have to tie up some loose ends with Light Racer and finish transitioning Wixel to Battery Powered Games.

5 Comments

Post a comment here or discuss this and other topics in the forums

The hard slog

I am thinking of developing a game for android, all be it probably not as in-depth to start with, though some real good pointers through this, i will be referring back. Could see that multi-player coding really took it out of you. That's what separates a games designer, with a game in market from the rest, the persistence and dedication to see it through, inspiring stuff, cheers..

perfect!

thank you so much for the code and the explanations! It def solved my problem!

inspiration

Hi, just wanted to say thanks for putting this blog up, it's truly unique and worth the time to read. I've read it from start to finish and I've gotta say I admire your persistence. I'm about to start developing a game myself for the android, I hope it has half the features yours does.

Congratulations!

The game is awesome and I can already see how addicting it will be when I get to play it over WiFi with a buddy! I have not yet purchased the elite version, but I will soon. Just a thought, you may want to consider letting all versions join mulitplayer games, but only allow paid versions to host games.

However, I encountered a problem: after installing the new version, the old icon is still on my "start" menu. More problematic is it doesn't start the game when I select it: "Application is not installed on your phone". I CAN run the game by opening it from Android Market->My Downloads. I have not tried uninstalling and reinstalling it yet.

Overall, I am seriously enjoying the game. I appreciate all of the polish and extra features packed into it and the new swipe control is my preferred way of playing.

Jason

Thanks!

I appreciate the comments. I also noticed that upgrade problem. I think it has something to do with the way the old game was configured in the manifest. What I've found is that uninstalling/reinstalling works correctly as well as simply rebooting the phone. Once the main menu loads again, all is well. It's strange that it has problems and I'd really like for them not to be there but it's not something I can control. I posted a note about it on the app in the market, saying to just uninstall/reinstall. If it's not a big deal, just leave it and it'll be fixed the next time the home app restarts.

Thanks again for your support. We'll be doing some point releases for this one to iron out any wrinkles that we find and to make multiplayer work better. I have to work on some business stuff this week but will be starting on LR3D immediately. I'm rusty on 3D coding but am excited to start working in it again.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.