XNA Shooter Game


It is time for some action now. You have all the 3D models, all effect files and textures, and your sound effects ready to be used, and you don’t have to worry about the landscape background anymore because it already works fine. The landscape itself is below the surface (z is below 0); this means you can easily add all your game objects at the z height 0.0, which makes adding effects, collision checking, and testing a little bit easier for the game.

You can now add your own ship in the landscape rendering method of the Mission class and control it in the Player class. Rendering just requires the following lines of code:

  Player.shipPos =   new Vector3(Player.position, AllShipsZHeight) + levelVector; AddModelToRender(   shipModels[(int)ShipModelTypes.OwnShip],   Matrix.CreateScale(ShipModelSize[(int)ShipModelTypes.OwnShip]) *   Matrix.CreateRotationZ(MathHelper.Pi) *   Matrix.CreateRotationX(Player.shipRotation.Y) *   Matrix.CreateRotationY(Player.shipRotation.X) *   Matrix.CreateTranslation(Player.shipPos)); // Add smoke effects for our ship EffectManager.AddRocketOrShipFlareAndSmoke(   Player.shipPos + new Vector3(-0.3f, -2.65f, +0.35f), 1.35f,   5 * Player.MovementSpeedPerSecond); EffectManager.AddRocketOrShipFlareAndSmoke( Player.shipPos + new Vector3(0.3f, -2.65f, +0.35f), 1.35f, 5 * Player.MovementSpeedPerSecond); 

All the scaling and rotation is just done to get the ship to the correct size and to rotate it correctly. The z rotation lets it point upwards and the x and y rotations let the ship wiggle when moving to the sides or up and down. Then two flare and smoke effects are added where your ship engine is. The EffectManager is discussed in a minute.

Game Logic

The code to control your ships is located in the Player class, where most of the other game logic is handled for you including firing your weapons, moving the ship, getting points for killing ships, and handling items:

  // From Player.HandleGameLogic: // [Show victory/defeat messages if game is over] // Increase game time gameTimeMs += BaseGame.ElapsedTimeThisFrameInMs; // Control our ship position with the keyboard or gamepad. // Use keyboard cursor keys and the left thumb stick. The // right hand is used for fireing (ctrl, space, a, b). Vector2 lastPosition = position; Vector2 lastRotation = shipRotation; float moveFactor = mouseSensibility *   MovementSpeedPerSecond * BaseGame.MoveFactorPerSecond; // Left/Right if (Input.Keyboard.IsKeyDown(moveLeftKey) ||   Input.Keyboard.IsKeyDown(Keys.Left) ||   Input.Keyboard.IsKeyDown(Keys.NumPad4) ||   Input.GamePad.DPad.Left == ButtonState.Pressed) {   position.X -= moveFactor; } // if if (Input.Keyboard.IsKeyDown(moveRightKey) ||   Input.Keyboard.IsKeyDown(Keys.Right) ||   Input.Keyboard.IsKeyDown(Keys.NumPad6) ||   Input.GamePad.DPad.Right == ButtonState.Pressed) {   position.X += moveFactor; } // if if (Input.GamePad.ThumbSticks.Left.X != 0.0f) {   position.X += Input.GamePad.ThumbSticks.Left.X;// *0.75f; } // if // Keep position in bounds if (position.X < -MaxXPosition)   position.X = -MaxXPosition; if (position.X > MaxXPosition)   position.X = MaxXPosition; // [Same for Down/Up changes position.Y, see Player.cs] // Calculate ship rotation based on the current movement if (lastPosition.X > position.X)   shipRotation.X = -0.5f; else if (lastPosition.X < position.X)   shipRotation.X = +0.5f; else   shipRotation.X = 0; // [Same for shipRotation.Y, see above] // Interpolate ship rotation to be more smooth shipRotation = lastRotation * 0.95f + shipRotation * 0.05f; 

HandleGameLogic first checks if the game is over and displays a message on the screen telling you if you have won or lost. Then the current game time is increased. After that your ship control is handled, and then the weapon firing and items are handled at the end of the method together with some code that gives you scores for killing enemy units.

You allow input from the keyboard and gamepad input devices. The mouse is not supported because tweaking it is hard for a shoot-’em-up game and I personally don’t like controlling a shoot-’em-up game with the mouse; it feels wrong. With the help of the MaxXPosition and MaxYPosition constants you make sure that your ship does not move outside the visible screen area and the shipRotation is calculated based on the movement you made. If no movement was made it will slowly get back to zero.

The following lines show how the firing is handled. Additional details can be found in the Player class.

  // Fire? if (Input.GamePadAPressed ||   Input.GamePad.Triggers.Right > 0.5f ||   Input.Keyboard.IsKeyDown(Keys.LeftControl) ||   Input.Keyboard.IsKeyDown(Keys.RightControl)) {   switch (currentWeapon)   {     case WeaponTypes.MG:       // Shooting cooldown passed?       if (gameTimeMs - lastShootTimeMs >= 150)       {         // [Shooting code goes here ...]         shootNum++;         lastShootTimeMs = gameTimeMs;         Input.GamePadRumble(0.015f, 0.125f);         Player.score += 1;       } // if       break;     case WeaponTypes.Plasma:       // [etc. all other weapons are handled here]       break;   } // switch } // if 

As soon as you press the A button or the right trigger on the gamepad or the Ctrl keys on the keyboard you enter the firing code, but the weapon will not fire until you reach the next cool-down phase (each weapon has different cool-down times). Then the weapon firing and weapon collision detection is handled and lastShootTimeMs is reset to the current game time waiting for the next cool-down phase.

After handling the weapons the only thing left is to create the winning and losing conditions. You achieve victory if you successfully reach the end of the level and you lose if you run out of hitpoints before that happens.

  if (Player.health <= 0) {   victory = false;   EffectManager.AddExplosion(shipPos, 20);   for (int num = 0; num<8; num++)     EffectManager.AddFlameExplosion(shipPos+       RandomHelper.GetRandomVector3(-12, +12));   Player.SetGameOverAndUploadHighscore(); } // if 

The rest of the game logic is part of the enemy units, the items, and the weapon projectiles, which are all handled in their own separate classes that are described at the end of this chapter.

3D Effects

Back to the 3D effects. You already learned a lot about them and thanks to the Billboard class it is not hard to render textures in 3D now. The effects are now handled at a higher level; you worry about the effect length, animation steps, and fading them in and out. All the 3D polygon generation and rendering is handled by the Billboard and Texture classes. All effects are managed through the EffectManager class (see Figure 11-15), which allows you to add new effects from anywhere in the code thanks to the many static Add methods.

image from book
Figure 11-15

You can see the many fields in the inside Effect class, which are used to allow you to create many different kinds of 3D effects. Explosions are optimized differently than light effects. For example regular effects are just blended out with an alpha value, but for light effects this does not work because if the alpha value was modified the effect would get darker and look very strange. Instead of doing light effects just get smaller at the end of their. The other enums help you to quickly identify effect types and to add them easily through the AddEffect method.

The best way to learn about the EffectManager class is to just look at the TestEffects unit test at the end of the class. Then add new effects here and implement them after changing the unit test or just write new unit tests for more effects.

  public static void TestEffects() {   TestGame.Start("TestEffects",     delegate     {       // No initialization code necessary here     },     delegate     {       // Press 1-0 for creating effects in center of the 3D scene       if (Input.Keyboard.IsKeyDown(Keys.D1) &&         BaseGame.EveryMs(200))       AddMgEffect(new Vector3(-10.0f, 0, -10),         new Vector3((BaseGame.TotalTimeMs % 3592) / 100.0f, 25, +100),         0, 1, true, true);       if (Input.Keyboard.IsKeyDown(Keys.D2))       {         AddPlasmaEffect(new Vector3(-50.0f, 0.0f, 0.0f), 0.5f, 5);         AddPlasmaEffect(new Vector3(0.0f, 0.0f, 0.0f), 1.5f, 5);         AddPlasmaEffect(new Vector3(50.0f, 0.0f, 0.0f), 0.0f, 5);       } // if (Input.Keyboard.IsKeyDown(Keys.D2])       if (Input.Keyboard.IsKeyDown(Keys.D3))       {         AddFireBallEffect(new Vector3(-50.0f, +10.0f, 0.0f), 0.0f, 10);         AddFireBallEffect(new Vector3(0.0f, +10.0f, 0.0f),           (float)Math.PI / 8, 10);         AddFireBallEffect(new Vector3(50.0f, +10.0f, 0.0f),           (float)Math.PI * 3 / 8, 10);       } // if (Input.Keyboard.IsKeyDown(Keys.D3])       if (Input.Keyboard.IsKeyDown(Keys.D4))         AddRocketOrShipFlareAndSmoke(           new Vector3((BaseGame.TotalTimeMs % 4000) / 40.0f, 0, 0),           5.0f, 150.0f);       if (Input.Keyboard.IsKeyDown(Keys.D5) &&         BaseGame.EveryMs(1000))         AddExplosion(Vector3.Zero, 9.0f);       // etc.       // Play a couple of sound effects       if (Input.Keyboard.IsKeyDown(Keys.P) &&         BaseGame.EveryMs(500))         PlaySoundEffect(EffectSoundType.PlasmaShoot);       // etc.       // We have to render the effects ourselfs because       // it is usually done in RocketCommanderForm (not in TestGame)!       // Finally render all effects before applying post screen shaders       BaseGame.effectManager.HandleAllEffects();     }); } // TestEffects() 

Unit Class

Maybe this class should be called EnemyUnit because it is only used for the enemy ships; your own ship is already handled in the Player class. Originally I wanted to unify all units (your own ship and the enemy ships) in this class, but they behave very differently and it only made the code more confusing and complicated. In more complex games you should maybe create a base class for units and then derive it once for enemy units and once for your own and friendly player ships (for example, for multiplayer code, where you want to make sure that all the other players get the same ship movement you do locally on their machines). Anyway, take a look at the class in Figure 11-16.

image from book
Figure 11-16

The class uses many fields internally to keep track of each unit’s hitpoints, shoot times, position values, and so on, but from the outside the class looks very simple; it just has one method you have to call every frame: Render. The constructor just creates the unit and assigns the unit type, position, and all the default hitpoints and damage values to it (from constant arrays inside the class - more complex games should load these values from xml files or other external data sources that can be changed easily).

As always you should check out the unit test to learn more about this class:

  public static void TestUnitAI() {   Unit testUnit = null;   Mission dummyMission = null;   TestGame.Start("TestUnitAI",     delegate     {       dummyMission = new Mission();       testUnit = new Unit(UnitTypes.Corvette, Vector2.Zero,         MovementPattern.StraightDown);       // Call dummyMission.RenderLandscape once to initialize everything        dummyMission.RenderLevelBackground(0);       // Remove the all enemy units (the start enemies) and       // all neutral objects       dummyMission.numOfModelsToRender = 2;     },     delegate     {       // [Helper texts are displayed here, press 1-0 or CSFRA, etc.]       ResetUnitDelegate ResetUnit = delegate(MovementPattern setPattern)         {           testUnit.movementPattern = setPattern;           testUnit.position = new Vector2(             RandomHelper.GetRandomFloat(-20, +20),             Mission.SegmentLength/2);           testUnit.hitpoints = testUnit.maxHitpoints;           testUnit.speed = 0;           testUnit.lifeTimeMs = 0;         };       if (Input.KeyboardKeyJustPressed(Keys.D1))         ResetUnit(MovementPattern.StraightDown);       if (Input.KeyboardKeyJustPressed(Keys.D2))         ResetUnit(MovementPattern.GetFasterAndMoveDown);       // [etc.]       if (Input.KeyboardKeyJustPressed(Keys.Space))         ResetUnit(testUnit.movementPattern);       if (Input.KeyboardKeyJustPressed(Keys.C))         testUnit.unitType = UnitTypes.Corvette;       if (Input.KeyboardKeyJustPressed(Keys.S))         testUnit.unitType = UnitTypes.SmallTransporter;       if (Input.KeyboardKeyJustPressed(Keys.F))         testUnit.unitType = UnitTypes.Firebird;       if (Input.KeyboardKeyJustPressed(Keys.R))         testUnit.unitType = UnitTypes.RocketFrigate;       if (Input.KeyboardKeyJustPressed(Keys.A))         testUnit.unitType = UnitTypes.Asteroid;       // Update and render unit       if (testUnit.Render(dummyMission))         // Restart unit if it was removed because it was too far down         ResetUnit(testUnit.movementPattern);       // Render all models the normal way       for (int num = 0; num < dummyMission.numOfModelsToRender; num++)         dummyMission.modelsToRender[num].model.Render(           dummyMission.modelsToRender[num].matrix);       BaseGame.MeshRenderManager.Render();       // Restore number of units as before.       dummyMission.numOfModelsToRender = 2;       // Show all effects (unit smoke, etc.)       BaseGame.effectManager.HandleAllEffects();     }); } // TestUnitAI() 

The unit test allows you to press C, S, F, R, or A to change the unit type to the five available units: Corvette, Small Transporter, Firebird, Rocket-Frigate, and Asteroid. By pressing 1-0 you are able to change the AI behavior of this unit. Speaking about AI here is a little bit crazy because all the AI does for the unit is to handle the movement differently. The unit just follows certain movement patterns and is in no way intelligent. For example, the GetFasterAndMoveDown movement code looks like this:

  case MovementPattern.GetFasterAndMoveDown:   // Out of visible area? Then keep speed slow and wait.   if (position.Y - Mission.LookAtPosition.Y > 30)     lifeTimeMs = 300;   if (lifeTimeMs < 3000)     speed = lifeTimeMs / 3000;   position += new Vector2(0, -1) * speed * 1.5f * maxSpeed * moveSpeed; break; 

Other movement patterns are even simpler. The name of each movement pattern should tell you enough about the behavior. In the game you randomly assign a movement pattern to each new unit. You could also create a level with predefined positions and unit values including the movement AI for a shoot-’em-up game, but I wanted to keep things simple. The rest of the unit test just renders the unit and the background landscape. It was used to create the whole Unit class and it should cover everything except the shooting and dying of units, which is tested easier directly in the game.

Projectile Class

While we are speaking about units shooting around, the code required to let the Corvette shoot is not so complicated because it immediately hits you if you are below it when it shoots (it just checks its and your x and y position and acts accordingly). Corvettes just shoot into the emptiness most of the time.

All other units do not fire instant weapons; instead projectiles like rockets, fireballs, or plasma balls are fired. These projectiles stay active for a short while and fly toward their target position. The Projectile class (see Figure 11-17) helps you manage these objects and simplifies the weapon logic because you can now just shoot a projectile and forget about it. The projectile will handle collisions with any enemy ships themselves and after it runs out of fuel or goes out of the visible area, it is removed too.

image from book
Figure 11-17

There are three different projectiles in XNA Shooter and they all behave a little differently:

  • Plasma projectiles are only fired from your own ship and you fire them only if you currently have the Plasma weapon. Plasma projectiles are fast and inflict more damage than the MG can, but the Gatling-Gun and the Rocket Launcher are more powerful, although they have their disadvantages too.

  • Fireballs are fired from enemy Firebird ships and they fly slowly toward you. They do not change direction, but the enemy Firebird’s AI shoots them a little bit ahead of you, so you always have to evade them.

  • Rockets are used by both you and the enemy Rocket Frigate ship. Your rockets are bigger and inflict more damage, but they just fly straight ahead. Enemy rockets are more intelligent and will adjust their target to your ship position, constantly making it much harder to evade them.

Compared to the Unit class Render method, the Projectile class Render method is not very complex. The most interesting part is the collision handling after updating the position and rendering the projectile in the Render method.

  public bool Render(Mission mission) {   // [Update movement ...]   // [Render projectile, either the 3D model for the rocket or just the   // effect for the Fireball or Plasma weapons]   // Own projectile?   if (ownProjectile)   {     // Hit enemy units, check all of them     for (int num = 0; num < Mission.units.Count; num++)     {       Unit enemyUnit = Mission.units[num];       // Near enough to enemy ship?       Vector2 distVec =         new Vector2(enemyUnit.position.X, enemyUnit.position.Y)          new Vector2(position.X, position.Y);       if (distVec.Length() < 7 &&         (enemyUnit.position.Y - Player.shipPos.Y) < 60)       {         // Explode and do damage!         EffectManager.AddFlameExplosion(position);         Player.score += (int)enemyUnit.hitpoints / 10;         enemyUnit.hitpoints -= damage;         return true;       } // if     } // for   } // if   // Else this is an enemy projectile?   else   {     // Near enough to our ship?     Vector2 distVec =       new Vector2(Player.shipPos.X, Player.shipPos.Y)        new Vector2(position.X, position.Y);     if (distVec.Length() < 3)     {       // Explode and do damage!       EffectManager.AddFlameExplosion(position);       Player.health -= damage / 1000.0f;       return true;     } // if   } // else   // Don't remove projectile yet   return false; } // Render() 

If this is your own projectile (plasma or rocket) you have to check if it collides with any enemy ship that is currently active. If that happens you inflict the damage this projectile does and the explosion effect is added. You also get some points for this kill (10% of the unit’s remaining health). Then true is returned to tell the caller of this method that you are done with this projectile and it can be removed from the currently active projectiles list. This also happens if the projectile goes outside of the visible landscape area.

If the projectile was shot by an enemy unit, the collision checking code is much simpler; you only have to check if it collides with the player ship and then the damage is inflicted the same way. Both your unit and the enemy unit’s death is handled by their own Render method. You already saw the defeat condition earlier that is triggered when you run out of health. The enemy unit’s death condition looks very similar, and there you also return true to allow removing this unit from the current unit list.

Item Class

Last but not least, the Item class is used to handle all the items in the game. It is very simple as you can see in Figure 11-18, but without it the game would not be half the fun. As you can see from the ItemTypes enum inside the class there are six available items. Four of them are the weapons for your ship and the other two are the health item, which completely refreshes your ship’s health, and the EMP bomb, which allows you to kill all units on the screen by pressing the space key. You can stack up to three bombs at the same time.

image from book
Figure 11-18

The Render method is similar to the one from the Projectiles class; you just don’t kill anyone that collides with this projectile. Instead the player can collect these items and their effect is immediately handled. This means you either get a new weapon or you can get back 100% of your health or you get another EMP bomb.

  /// <summary> /// Render item, returns false if we are done with it. /// </summary> /// <returns>True if done, false otherwise</returns> public bool Render(Mission mission) {   // Remove unit if it is out of visible range!   float distance = Mission.LookAtPosition.Y - position.Y;   const float MaxUnitDistance = 60;   if (distance > MaxUnitDistance)     return true;   // Render   float itemSize = Mission.ItemModelSize;   float itemRotation = 0;   Vector3 itemPos = new Vector3(position, Mission.AllShipsZHeight);   mission.AddModelToRender(mission.itemModels[(int)itemType],     Matrix.CreateScale(itemSize) *     Matrix.CreateRotationZ(itemRotation) *     Matrix.CreateTranslation(itemPos));   // Add glow effect around the item   EffectManager.AddEffect(itemPos + new Vector3(0, 0, 1.01f),     EffectManager.EffectType.LightInstant,     7.5f, 0, 0);   EffectManager.AddEffect(itemPos + new Vector3(0, 0, 1.02f),     EffectManager.EffectType.LightInstant,     5.0f, 0, 0);   // Collect item and give to player if colliding!   Vector2 distVec =     new Vector2(Player.shipPos.X, Player.shipPos.Y)      new Vector2(position.X, position.Y);   if (distVec.Length() < 5.0f)   {     if (itemType == ItemTypes.Health)     {       // Refresh health       Sound.Play(Sound.Sounds.Health);       Player.health = 1.0f;     } // if     else     {       Sound.Play(Sound.Sounds.NewWeapon);       if (itemType == ItemTypes.Mg)         Player.currentWeapon = Player.WeaponTypes.MG;       else if (itemType == ItemTypes.Plasma)         Player.currentWeapon = Player.WeaponTypes.Plasma;       else if (itemType == ItemTypes.Gattling)         Player.currentWeapon = Player.WeaponTypes.Gattling;       else if (itemType == ItemTypes.Rockets)         Player.currentWeapon = Player.WeaponTypes.Rockets;       else if (itemType == ItemTypes.Emp &&         Player.empBombs < 3)         Player.empBombs++;     } // else     Player.score += 500;     return true;   } // else   // Don't remove item yet   return false; } // Render() 

The render code just puts the item at the given screen location rotated and scaled correctly. It also adds two light effects to make the item glow a little. If you don’t look at the items you might not notice the glow effect, but when it is missing it is a lot harder to detect the items currently flying around.

If you are closer than five units to any item, you automatically collect it. Your ship has a radius of about five units too, which means you can touch the item with any part of your ship. Then the item is handled either giving you health or the new weapon, and after that you even get a little bit of extra score for your heroic act of collecting this item. The item can now be removed. If you did not collide with the item, it stays active until you do or it is no longer visible.

Final Screenshot

Yeah, you finally made it. That was everything you need for your XNA Shooter game; see Figure 11-19 for the result. I hope this chapter was more helpful than Chapter 8 where I did not want to repeat my existing Rocket Commander tutorials, which already explain how to create the original Rocket Commander game.

image from book
Figure 11-19

I hope you enjoy XNA Shooter and you find it useful if you want to create your own shoot-’em-up game. Remember that it was created in only a couple of days and you can probably make it much better, add more levels, enemy ships, or a better AI. Have fun with it.




Professional XNA Game Programming
Professional XNA Programming: Building Games for Xbox 360 and Windows with XNA Game Studio 2.0
ISBN: 0470261285
EAN: 2147483647
Year: 2007
Pages: 138

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net