Upcoming Events
Southwest Gaming Expo
11/20 - 11/22 @ Dallas, TX

Workshop on Network and Systems Support for Games (NetGames 2009)
11/23 - 11/25 @ Paris, France

ICIDS 2009 Interactive Storytelling
12/9 - 12/11 @ Guimarăes, Portugal

Global Game Jam
1/29 - 1/31  

More events...


Quick Stats
7303 people currently visiting GDNet.
2341 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!



Link to us

Link to us

  Intel sponsors gamedev.net search:   

Microsoft XNA Game Studio 3.0 Unleashed
Creating a 3D Game


Creating the Game Logic (Con't)

Let’s return to the playing state code and add the following to the constructor:

playerSphere = new BoundingSphere(OurGame.Camera.Position, 1.5f);

missileManager = new MissileManager(Game);
Game.Components.Add(missileManager);
missileManager.Enabled = false;
missileManager.Visible = false;

enemyManager = new EnemyManager(Game);
Game.Components.Add(enemyManager);
enemyManager.Enabled = false;
enemyManager.Visible = false;

Levels = new List<Level>(10);
Levels.Add(new Level(50, 10, 60, 9.0f));
Levels.Add(new Level(25, 10, 60, 9.0f));
Levels.Add(new Level(15, 15, 60, 9.0f));
Levels.Add(new Level(10, 15, 60, 9.0f));
Levels.Add(new Level(5, 15, 60, 9.0f));
Levels.Add(new Level(5, 20, 60, 9.0f));
Levels.Add(new Level(5, 25, 60, 9.0f));
Levels.Add(new Level(5, 30, 60, 10.0f));
Levels.Add(new Level(5, 40, 90, 10.0f));
Levels.Add(new Level(3, 50, 90, 10.0f));

currentLevel = 0;
enemyManager.Enemies = new List<Enemy>(Levels[CurrentLevel].Enemies);
In this game, we are using a stationary camera, so we are not going to continually update the player’s bounding sphere. Instead, it is set once in the constructor. We add the missile manager and enemy manager game components and create all our levels.

Our PlayingState will start the game. The exposed method calls two private methods to prepare for the game and then to start the level. These methods are shown here:

public void StartGame()
{
   SetupGame();
   StartLevel();
}

private void SetupGame()
{
   TotalCollisions = 0;
   currentLevel = 0;
}

public void StartLevel()
{
   GamePad.SetVibration(0, 0, 0);
   enemyManager.Enemies.Clear();
   totalCreatedEnemies = 0;
   
   missileManager.Load(Levels[CurrentLevel].Missiles);
   
   GameManager.PushState(OurGame.StartLevelState.Value);
}
The only thing to point out in this code is that we push the StartLevelState onto the stack inside the StartLevel method.

Next, we can look at the Update method in our PlayingGameState. We can replace the contents of the existing Update method with the following:

float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

if (Input.WasPressed(0, Buttons.Back, Keys.Escape))
   GameManager.PushState(OurGame.StartMenuState.Value);

if (Input.WasPressed(0, Buttons.Start, Keys.Enter))
{
   // push our paused state onto the stack
   GameManager.PushState(OurGame.PausedState.Value);
}

if ((Input.WasPressed(0, Buttons.A, Keys.Space)) ||
   (Input.WasPressed(0, Buttons.RightShoulder, Keys.LeftControl)) ||
   Input.GamePads[0].Triggers.Right > 0)
{
   if (missileManager.AddMissile(new Vector3(
      OurGame.Camera.Position.X,
      OurGame.Camera.Position.Y - 1,
      OurGame.Camera.Position.Z + 1
      ), OurGame.Camera.Target - OurGame.Camera.Position,
      DateTime.Now))
   {
     //play sound
   }
}

if (enemyManager.Enabled)
{
   UpdateEnemies(elapsed);
   
   while (CheckCollisions())
   {
      //increase score if enemy was hit
   }
   
   //Are we finished with this level?
   if (TotalCollisions == Levels[CurrentLevel].Enemies)
   {
      TotalCollisions = 0;
      currentLevel++;
      
      //Are we finished with the game?
      if (CurrentLevel == Levels.Count)
      {
         //You won the game!!!
         GameManager.PushState(OurGame.WonGameState.Value);
         currentLevel--; //reset count back
      }
      else
      {
         StartLevel();
      }
   }
}

base.Update(gameTime);
We check our input and push on the start menu state or the paused state if it is appropriate. We check to see if the player has fired a missile and have a placeholder for playing a sound.

We update all the enemies that are on the screen and then check to see if any of the missiles have collided with them. We will review the CheckCollisions method next. If a collision did occur, we have a placeholder to increase the score. Now we check to see if any more enemies are left. If there aren’t, we check to see if any more levels are left. If all the levels have been finished, the game is won. Otherwise, the game moves on to the next level. The CheckCollisions method is as follows:

private bool CheckCollisions()
{
   for (int ei = 0; ei < enemyManager.Enemies.Count; ei++)
   {
      //See if an enemy is too close first
      if (enemyManager.Enemies[ei].BoundingSphere.Intersects(playerSphere))
      {
         GameManager.PushState(OurGame.LostGameState.Value);
         return (false);
      }
      
      //if not, then we can check our missiles
      if (missileManager.CheckCollision(enemyManager.Enemies[ei].BoundingSphere))
      {
         enemyManager.Enemies.RemoveAt(ei);
      
         TotalCollisions++;
      
         return (true);
      }
   }
   
   return (false);
}
First, we check to see if an enemy has collided with the camera. If that happens, the game is over. Otherwise, we check to see if any of the missiles have collided with the enemies. Our Update method also calls the UpdateEnemies method, which is shown here:
private void UpdateEnemies(float elapsed)
{
   if (totalCreatedEnemies < Levels[CurrentLevel].Enemies)
   {
      if (enemyManager.Enemies.Count < EnemyManager.MAX_ENEMIES)
      {
         enemyManager.AddEnemy(Levels[CurrentLevel].EnemySpeed);
         totalCreatedEnemies++;
      }
   }
   
   for (int ei = 0; ei < enemyManager.Enemies.Count; ei++)
   {
      enemyManager.Enemies[ei].Target = OurGame.Camera.Position;
      enemyManager.Enemies[ei].Move(elapsed);
   }
}
The UpdateEnemies method checks to see if there are still enemies to be generated. We only allow MAX_ENEMIES on the screen at one time, so if a level has more than that, we wait until an enemy is destroyed before another one is spawned. The method then loops through all the enemies and updates their target based on the camera’s position. For this game, this really isn’t needed because we have a stationary camera. We then move each enemy.

For now our Draw method is very lightweight—it is only setting the view and projection properties for the missile manager and the enemy manager. We can replace the current contents of the Draw method with the following:

missileManager.View = OurGame.Camera.View;
missileManager.Projection = OurGame.Camera.Projection;

enemyManager.View = OurGame.Camera.View;
enemyManager.Projection = OurGame.Camera.Projection;

base.Draw(gameTime
We need to clear out the contents of the LoadContent method. We will be adding code to that method a little later, but for now we just need to remove the old font we used in the previous demo.

We need to know which level we are on outside of our playing state, so we need to make a public property to expose it:

public int CurrentLevel
{
   get { return (currentLevel); }
}
We also need to modify our GameStateInterfaces code. Specifically, we need to modify the IPlayingState interface to include our StartGame method and this CurrentLevel property:
void StartGame();
int CurrentLevel { get; }
The final method we need to add to our PlayingState class is the StateChanged method. We override this method so we can turn on and off the appropriate game components:
protected override void StateChanged(object sender, EventArgs e)
{
   base.StateChanged(sender, e);
   
   if (GameManager.State != this.Value)
   {
      Visible = true;
      Enabled = false;
      missileManager.Enabled = false;
      missileManager.Visible = false;
      enemyManager.Enabled = false;
      enemyManager.Visible = false;
   }
   else
   {
      missileManager.Enabled = true;
      missileManager.Visible = true;
      enemyManager.Enabled = true;
      enemyManager.Visible = true;
   }
}
We specified an effect file for both the missile manager and the enemy manager. The enemy manager is using the AmbientTexture effect file from last chapter. The file needs to be added to our projects. The missile manager, however, is using a new effect file. The basis of the effect is the vertex deformation effect we created in Chapter 15, “Advanced HLSL.” Here’s the code for VertexDisplacement.fx:
float4x4 World : WORLD;
float4x4 View;
float4x4 Projection;

float4 AmbientColor : COLOR0;
float Timer : TIME;
float Offset = 1.0f;

texture ColorMap;
sampler ColorMapSampler = sampler_state
{
   texture = <ColorMap>;
   magfilter = LINEAR;
   minfilter = LINEAR;
   mipfilter = LINEAR;
   AddressU = Wrap;
   AddressV = Wrap;
};

struct VertexInput
{
   float4 Position : POSITION0;
   float2 TexCoord : TEXCOORD0;
};

struct VertexOutput
{
   float4 Position : POSITION0;
   float2 TexCoord : TEXCOORD0;
};

VertexOutput vertexShader(VertexInput input)
{
   VertexOutput output = (VertexOutput)0;
   float4x4 WorldViewProjection = mul(mul(World, View), Projection);
   output.TexCoord = input.TexCoord  + Timer * .005;
   
   float4 Pos = input.Position;
   float y = Pos.y * Offset + Timer;
   float x = sin(y) * Offset;
   Pos.x += x;
   
   output.Position = mul(Pos, WorldViewProjection);
   
   return( output );
}

struct PixelInput
{
   float2 TexCoord : TEXCOORD0;
};

float4 pixelShader(PixelInput input) : COLOR
{
   float4 color;
   color = tex2D(ColorMapSampler, input.TexCoord);
   return(color);
}

technique Default
{
   pass P0
   {
      VertexShader = compile vs_1_1 vertexShader();
      PixelShader = compile ps_1_4 pixelShader();
   }
}
This effect code is identical to the code we used in Chapter 15. The only difference is inside the vertex shader. Besides modifying the vertex position, we also modify the texture coordinates. When we shoot our missiles, they will wobble.

The next state we need to modify is our StartLevelState. We need to clear out the code inside the class and add the following private member fields:

private bool demoMode = true;
private bool displayedDemoDialog = false;

private DateTime levelLoadTime;
private readonly int loadSoundTime = 2500;

private string levelText = “LEVEL”;
private string currentLevel;
bool startingLevel = false;

private Vector2 levelTextPosition;
private Vector2 levelTextShadowPosition;
private Vector2 levelNumberPosition;
private Vector2 levelNumberShadowPosition;
We are going to play a sound as we are loading the level. We want the actual game play to start as soon as the sound is over. There is no way to get notified of a sound being completed, so we put in our own timer. We will add the sound later but put the code into place now to handle the timing. The start level state will also display the number of the level we are starting. We store the level text once so we can use it multiple times. We also set up two sets of vectors to hold the locations of the level text (that is, LEVEL) as well as the level number (that is, 1) and their drop-shadow locations.

The constructor did not change but is listed here for completeness:

public StartLevelState(Game game) : base(game)
{
   game.Services.AddService(typeof(IStartLevelState), this);
}
The updated StateChanged method allows us to start the logic when we enter our start level state:
protected override void StateChanged(object sender, EventArgs e)
{
   base.StateChanged(sender, e);
   
   if (GameManager.State == this.Value)
   {
      startingLevel = true;
      
      if (demoMode && !displayedDemoDialog)
      {
         //We could set properties on our YesNoDialog
         //so it could have a custom message and custom
         //Yes / No buttons ...
         //YesNoDialogState.YesCaption = “Of course!”;
         GameManager.PushState(OurGame.YesNoDialogState.Value);
         this.Visible = true;
         displayedDemoDialog = true;
         startingLevel = false;
      }
   }
   
   if (startingLevel)
   {
      //play sound
      
      levelLoadTime = DateTime.Now;
      
      currentLevel = (OurGame.PlayingState.CurrentLevel + 1).ToString();
      
      Vector2 viewport = new Vector2(GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height);
      Vector2 levelTextLength = OurGame.Font.MeasureString(levelText);
      Vector2 levelNumberLength = OurGame.Font.MeasureString(currentLevel);
      levelTextShadowPosition = (viewport – levelTextLength * 3) / 2;
      levelNumberShadowPosition = (viewport – levelNumberLength * 3) / 2;
      levelNumberShadowPosition.Y += OurGame.Font.LineSpacing * 3;
      levelTextPosition.X = levelTextShadowPosition.X + 2;
      levelTextPosition.Y = levelTextShadowPosition.Y + 2;
      levelNumberPosition.X = levelNumberShadowPosition.X + 2;
      levelNumberPosition.Y = levelNumberShadowPosition.Y + 2;
   }
}
The first part of the method is the same as it was in our previous game state demo. However, we also set the startingLevel flag with the initial value of true. We modify the demo condition to set the startingLevel flag to false. Assuming we are really starting the level, which will occur the first time if we are not in demo mode (or after the dialog box is closed if we are in demo mode), we begin playing our starting level sound. We have put a placeholder in to play that sound for now. We also initialize our level load time and set our current level variable. Finally, we initialize the vectors that store the position of the text we want to display when the level starts. The text will be centered on the screen.

The Update method for this state is as follows:

public override void Update(GameTime gameTime)
{
   if (DateTime.Now > levelLoadTime + new TimeSpan(0, 0, 0, 0, loadSoundTime))
   {
      //stop sound
      
      // change state to playing
      GameManager.ChangeState(OurGame.PlayingState.Value);
   }
   
   base.Update(gameTime);
}
Inside the Update method, we change to the PlayingState and stop the sound if enough time has passed. For now, we just have a placeholder where we will eventually stop the sound. The last method in this state is the Draw method:
public override void Draw(GameTime gameTime)
{
   if (startingLevel)
   {
      OurGame.SpriteBatch.Begin();
      OurGame.SpriteBatch.DrawString(OurGame.Font, levelText,
         levelTextShadowPosition, Color.Yellow, 0, Vector2.Zero, 3.0f,
         SpriteEffects.None, 0);
      OurGame.SpriteBatch.DrawString(OurGame.Font, levelText,
         levelTextPosition, Color.Red, 0, Vector2.Zero, 3.0f,
         SpriteEffects.None, 0);
      OurGame.SpriteBatch.DrawString(OurGame.Font, currentLevel,
         levelNumberShadowPosition, Color.Yellow, 0, Vector2.Zero, 3.0f,
         SpriteEffects.None, 0);
      OurGame.SpriteBatch.DrawString(OurGame.Font, currentLevel,
         levelNumberPosition, Color.Red, 0, Vector2.Zero, 3.0f,
         SpriteEffects.None, 0);
      OurGame.SpriteBatch.End();
   }
   
   base.Draw(gameTime);
}
The Draw method simply draws the level text and the level number in the right position, complete with a drop-shadow effect. It only draws this text if we are actually starting the level. If the YesNoDialog (Demo mode) is on the stack, we do not want to display the text.

We need to declare a font variable in our TunnelVision game class:

public SpriteFont Font;
Inside the LoadContent method we need to load our font:
Font = Content.Load<SpriteFont>(@”Fonts\Arial”);
Before we compile and run our changes, we need to modify the StartMenuState class. Inside the Update method, we need to replace the contents of the condition where we check if either Start or Enter was pressed with the following code:
if (GameManager.ContainsState(OurGame.PlayingState.Value))
   GameManager.PopState();
else
{
   //starting game, queue first level
   GameManager.ChangeState(OurGame.PlayingState.Value);
   OurGame.PlayingState.StartGame();
}
We are still popping off our start menu state if our stack contains a playing state. However, instead of changing the state to the StartLevelState like in the previous demo, we are changing the state to PlayingState and calling its StartGame method.

At this point we can compile and run the game. The game logic is in place, but it is rather rough around the edges.

Reproduced from the book Microsoft XNA Game Studio 3.0 Unleashed. Copyright? 2009. Reproduced by permission of Pearson Education, Inc., 800 East 96th Street, Indianapolis, IN 46240. Written permission from Pearson Education, Inc. is required for all other uses



Creating the Crosshair & Camera


Contents
  Creating the Tunnel Vision Game
  Creating the Game Logic Part 1
  Creating the Game Logic Part 2
  Creating the Crosshair & Camera

  Printable version
  Discuss this article