Thinking of it, we could pick up any kinds of collectibles. We could create a different class for each bonus, but that would mean we'd have to use the instanceof operator in order to distinguish them. I think it's better to give the collectibles an internal type. This way we compare simply an enum instead of using instanceof.
We introduce the following collectibles:
- Stroyent: increase score
- Nuke: all enemies explode
- Shield: gives the player temporary invicibility
- Ammo: upgrades the bullet level
- Health: increases the player's health
So let's change the Stroyent class to a Bonus class which can have various types.
public class Bonus extends SpriteBase { public enum Type { STROYENT, NUKE, HEALTH, AMMO, SHIELD } private Type type; public Bonus(Type type, Pane layer, Image image, double x, double y, double r, double dx, double dy, double dr, double health, double damage) { super(layer, image, x, y, r, dx, dy, dr, health, damage); this.type = type; } public Type getType() { return type; } @Override public void checkRemovability() { if( Double.compare( getY(), Settings.SCENE_HEIGHT) > 0) { setRemovable(true); } } }
We load the images for the various bonuses.
Image bonusStroyentImage; Image bonusNukeImage; Image bonusHealthImage; Image bonusShieldImage; Image bonusAmmoImage; // bonuses bonusStroyentImage= new Image( getClass().getResource( "assets/bonus/stroyent.png").toExternalForm()); bonusNukeImage= new Image( getClass().getResource( "assets/bonus/nuke.png").toExternalForm()); bonusHealthImage= new Image( getClass().getResource( "assets/bonus/health.png").toExternalForm()); bonusShieldImage= new Image( getClass().getResource( "assets/bonus/shield.png").toExternalForm()); bonusAmmoImage= new Image( getClass().getResource( "assets/bonus/ammo.png").toExternalForm());
We spawn the various bonuses depending on a randomness level in our Settings class.
public static int BONUS_RANDOMNESS = 50;
We consider the randomness in the spawnBonus class. We could vary the frequency of the different bonus types by changing the rndBonus check from single integer to an interval. For now the bonus types except stroyent show up equally distributed.
private void spawnBonus( SpriteBase sprite) { Bonus.Type type; Image image; // set the bonus type randomly int rndBonus = rnd.nextInt( Settings.BONUS_RANDOMNESS); // nuke if( rndBonus == 0) { type = Bonus.Type.NUKE; image = bonusNukeImage; } // health else if( rndBonus == 1) { type = Bonus.Type.HEALTH; image = bonusHealthImage; } // shield else if( rndBonus == 2) { type = Bonus.Type.SHIELD; image = bonusShieldImage; } // ammo else if( rndBonus == 3) { type = Bonus.Type.AMMO; image = bonusAmmoImage; } // default: stroyent else { type = Bonus.Type.STROYENT; image = bonusStroyentImage; } // random speed double speed = rnd.nextDouble() * 1.0 + 1.0; // random rotation delta double dr = rnd.nextDouble() * 1.0 + 1.0; // random left/right rotation if( rnd.nextInt(2) == 0) dr = -dr; double x = sprite.getCenterX(); double y = sprite.getCenterY(); // create a sprite Bonus stroyent = new Bonus( type, playfieldLayer, image, x, y, 0, 0, speed, dr, 1, 0); // becomes visible once the ship (which is on top of the stroyent sprite) is gone stroyent.getView().toBack(); // manage sprite stroyentList.add( stroyent); }
With this change the collectibles look like this on the screen:
We need to deal with the various bonus items. We modify the collision check and add a method which processes the items.
/** * Let player ship pick up bonus items. * @param bonusList */ private void checkBonusCollisions( ListbonusList) { for( Player player: playerList) { for( Bonus bonus: bonusList) { // consider only alive sprites if( !bonus.isAlive()) continue; if( player.collidesWith( bonus)) { // stop movement of sprite bonus.stopMovement(); // collect bonus collectBonus( player, bonus); // destroy sprite, set health to 0 bonus.kill(); // remove the sprite from screen by flagging it as removable bonus.remove(); } } } } private void collectBonus( Player player, Bonus bonus) { switch( bonus.getType()) { case STROYENT: // show collection score spawnScoreTransition( bonus, Settings.SCORE_BONUS_STROYENT); break; case NUKE: System.err.println( "TODO: add nuke code"); break; case SHIELD: System.err.println( "TODO: add shield code"); break; case HEALTH: System.err.println( "TODO: add health code"); break; case AMMO: System.err.println( "TODO: add ammo code"); break; default: System.err.println("Unsupported bonus type: " + bonus.getType()); } }
Now the code is similar to what it was before when we had a dedicated Stroyent bonus class, except that we can add various bonus handlers.
A note about the code which we still need to implement: In this special case I prefer to simply output an error message for the missing code. Throwing an UnsupportedOperationException or IllegalArgumentException here would be overkill. It's not really necessary and we don't want to break the game with unnecessary exceptions.
What I'm curious though is the nuke. We already have the code to let an enemy ship explode when a bullet/missile hit's it. So the nuke is just a loop over all enemies and letting them explode. Let's add this now.
The difficulty we are facing is that we can't simply run through the enemy and bonus loops again while we're already running through them. We'd get ConcurrentModificationExceptions. The solution to this is that we do the same what we already did with the bullets and missiles: We only set a flag that the nuke got fired and we fire it in the game loop. Let's get coding.
We extend the Player class:
boolean nukeCharged = false; public void fireNuke() { this.nukeCharged = true; } public boolean isFireNuke() { return this.nukeCharged; } public void unchargeNuke() { this.nukeCharged = false; }
We modify the bonus collection and explosion mechanism:
/** * Check if a projectile (eg player bullet/missile) hits an enemy. * If a collision is detected, the enemy is damaged. If enemy is killed, an explosion is spawned. * @param projectileList */ private void checkProjectileCollisions( List projectileList) { for( SpriteBase projectile: projectileList) { for( Enemy enemy: enemyList) { // consider only alive sprites if( !enemy.isAlive()) continue; if( projectile.collidesWith(enemy)) { // inflict damage, reduce enemy's health enemy.getDamagedBy(projectile); // explosion animation if( !enemy.isAlive()) { explode( enemy); } // destroy sprite, set health to 0 projectile.kill(); // remove the sprite from screen by flagging it as removable projectile.remove(); } } } } private void collectBonus( Player player, Bonus bonus) { switch( bonus.getType()) { case STROYENT: // show collection score spawnScoreTransition( bonus, Settings.SCORE_BONUS_STROYENT); break; case NUKE: // kill all enemies player.fireNuke(); break; case SHIELD: System.err.println( "TODO: add shield code"); break; case HEALTH: System.err.println( "TODO: add health code"); break; case AMMO: System.err.println( "TODO: add ammo code"); break; default: System.err.println("Unsupported bonus type: " + bonus.getType()); } } private void nuke() { for (Enemy enemy : enemyList) { // consider only alive sprites if (!enemy.isAlive()) continue; // set health to 0 enemy.kill(); // let enemy explode, spawn bonus explode( enemy); } } private void explode( Enemy enemy) { // stop movement of sprite enemy.stopMovement(); // let enemy explode spawnExplosion( enemy); // show score spawnScoreTransition( enemy, Settings.SCORE_ENEMY); // show bonus spawnBonus( enemy); } private void checkNukeFiring() { for( Player player: playerList) { if( player.isFireNuke()) { // nuke can only be fired once player.unchargeNuke(); // nuke all enemies nuke(); } } }
And we add the nuke mechanism to the game loop:
// check if player fires nuke and if so, kill all enemies checkNukeFiring();
Looks great, works perfectly without problems when we pick up a nuke:
We'll consider the other upgrades in another blog entry.