Running with Code Like with scissors, only more dangerous

22May/080

Breakout in Model-View-Presenter

Posted by Rob

I sat down tonight and in about three hours or so cranked out the first vestiges of something that kind of maybe resembles Breakout, only using graphics that are possibly worse than anything ever shown on Atari 2600 because well, I made them.

Thanks to Joel Neubeck for the video player, and thanks to digital blasphemy for the background (yes I know it's not a free image, I'll be sure to remove it before it goes anywhere).

I should also point out: I used some sounds from the Spacewar demo in XNA Game Studio 2.0.  Those sound AWESOME.

This app is built with a model in a separate assembly; the model portion of the project contains the mathematics, collision detection, and all the game objects on the screen.  We've got tiles, a ball, and a paddle.  I should also point out that there's a game board (that's the big black empty space on the screen).

The XNA game application, then, instantiates a GameBoard (that's the containing class that owns all of the rest of the objects) and then instantiates a GameBoardPresenter.  GameBoardPresenter is part of the game application itself, and it owns a copy of the GameBoard because it's intimately tied to its inner workings.

To keep things relatively efficient, the GameBoard class exposes all of its object lists as properties that return the lists.  This way, object references aren't duplicated (except for the references to the Lists), and we're not wasting unnecessary memory and copy operations.  (If you're not clear what I mean, here are the properties:)

   1: public Paddle Player
   2: {
   3:     get { return m_player; }
   4: }
   5:  
   6: public List<Ball> Balls
   7: {
   8:     get { return m_balls; }
   9: }
  10:  
  11: public List<Tile> Tiles
  12: {
  13:     get { return m_tiles; }
  14: }
  15:  
  16: public List<Projectile> Projectiles
  17: {
  18:     get { return m_projectiles; }
  19: }

Since the balls are the only pieces of the game that move, each call to Update() on the GameBoard instance iterates through each Ball object, moving it accordingly and handling any collisions.  When a collision is detected, it fires an event corresponding to the type of collision, which is consumed by the GameBoardPresenter.

One other advantage of separating out the presentation and the actual game logic is that I can scale the graphics size to anything I want; for instance, the video above was recorded and 960x720, but I was also able to run it at 1920x1200 without any apparent differences in visualization.  The internal game units actually default to a game board size of 4800x4800, and scaling is simply done within the GameBoardPresenter's Draw() method:

   1: public void Draw(SpriteBatch target)
   2: {
   3:     Texture2D currentTexture;
   4:     Vector2 scale = new Vector2(m_scale);
   5:     Vector2 offset = new Vector2(m_xOffset, 0);
   6:  
   7:     currentTexture = m_textures[BoardItemType.GameBoard];
   8:     target.Draw(currentTexture, offset,  null, Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 1f);
   9:  
  10:     currentTexture = m_textures[BoardItemType.BasicTile];
  11:     foreach (Tile t in m_tiles)
  12:     {
  13:         target.Draw(currentTexture, t.Position * scale + offset, null, Color.White, 0f, Vector2.Zero, m_scale, SpriteEffects.None, 0f);
  14:     }
  15:  
  16:     currentTexture = m_textures[BoardItemType.Paddle];
  17:     target.Draw(currentTexture, m_paddle.Position * scale + offset, null, Color.White, 0f, Vector2.Zero, m_scale, SpriteEffects.None, 0f);
  18:  
  19:     currentTexture = m_textures[BoardItemType.Ball];
  20:     foreach (Ball b in m_balls)
  21:     {
  22:         target.Draw(currentTexture, b.Position * scale + offset, null, Color.White, 0f, Vector2.Zero, m_scale, SpriteEffects.None, 1f);
  23:     }
  24: }

The scale is calculated when the backbuffer is sized.

Source code for this isn't quite ready yet; there are some things that I want to refactor (for instance, I want the Model part to define input "actions" and that View/Presenter part to keymap inputs to actions). 

Tagged as: , , , No Comments
15May/080

Adding Collision Detection

Posted by Rob

There's already rudimentary collision detection in the app, but only for the sides.  What if I wanted the balls to detect when they collide?

In the demo video you can already see the balls colliding (and, incidentally, the gravity changing from down, then left, up, right, and down again).  But how did I get there?  Wouldn't you know that XNA has a built-in structure called BoundingSphere that can handle that for you?  I defined the bounding sphere as centered around the ball's center point (which was defined by the texture's dimensions), and used the z-coordinate as 0.  Intrinsically, then, the bounding sphere acts like a bounding circle as long as everything is on the same Z-plane.

Next, I created a couple interfaces that are going to help me out.  Interfaces such as IPhysical and IPhysicalSphere -- they provide necessary information for collidable objects.  Here they are:

   1: public interface IPhysical
   2: {
   3:     Vector3 CenterOfMass
   4:     {
   5:         get;
   6:     }
   7:  
   8:     Vector3 Speed
   9:     {
  10:         get;
  11:         set;
  12:     }
  13:  
  14:     float Mass
  15:     {
  16:         get;
  17:     }
  18: }
  19:  
  20: public interface IPhysicalSphere : IPhysical
  21: {
  22:     BoundingSphere Bounds
  23:     {
  24:         get;
  25:     }
  26: }

By implementing these properties, a separate class can be constructed to perform the necessary math for bouncing balls.  When first implemented, colliding balls would simply switch speed vectors.  However, that only works if balls collide straight on (so that movement vectors would be inverse of each other).  When balls collide like this:

Amazing MS-Paint Art!

(Awesome MS-Paint artwork, huh?)

When balls collide like that, they don't simply exchange vectors.  What actually happens is that the speed vectors are changed according to the normal line between the two circles (or the normal plane between the two spheres).

I created a static class to do this work for me:

   1: public static class Collision
   2: {
   3:     public static void ApplyCollision(IPhysicalSphere sphereA, IPhysicalSphere sphereB)
   4:     {
   5:         Vector3 x = sphereB.CenterOfMass - sphereA.CenterOfMass;
   6:         x.Normalize();
   7:  
   8:         Vector3 v1 = sphereA.Speed;
   9:         float x1 = Vector3.Dot(x, v1);
  10:  
  11:         Vector3 v1x = x * x1;
  12:         Vector3 v1y = v1 - v1x;
  13:  
  14:         float m1 = sphereA.Mass;
  15:  
  16:         x = -x;
  17:         Vector3 v2 = sphereB.Speed;
  18:         float x2 = Vector3.Dot(x, v2);
  19:  
  20:         Vector3 v2x = x * x2;
  21:         Vector3 v2y = v2 - v2x;
  22:  
  23:         float m2 = sphereB.Mass;
  24:  
  25:         float combinedMass = m1 + m2;
  26:  
  27:         Vector3 newVelA = (v1x * ((m1 - m2) / combinedMass)) + (v2x * ((2f * m2) / combinedMass)) + v1y;
  28:         Vector3 newVelB = (v1x * ((2f * m1) / combinedMass)) + (v2x * ((m2 - m1) / combinedMass)) + v2y;
  29:  
  30:         sphereA.Speed = newVelA;
  31:         sphereB.Speed = newVelB;
  32:     }
  33: }

This was inspired by a blog post I found that covered it exceptionally.  I wish I could name the offer but I couldn't find the author's name!

Calling this method to adjust the speed vectors is done whenever collisions are detected.  That part is handled in the Game class during the Update method:

   1: for (int i = 0; i < m_balls.Count; i++)
   2: {
   3:     Ball b = m_balls[i];
   4:     for (int j = i + 1; j < m_balls.Count; j++)
   5:     {
   6:         Ball test = m_balls[j];
   7:         if (b.Bounds.Intersects(test.Bounds))
   8:         {
   9:             Collision.ApplyCollision(b, test);
  10:             b.Update(gameTime, GraphicsDevice.Viewport);
  11:             test.Update(gameTime, GraphicsDevice.Viewport);
  12:             b = null;
  13:             break;
  14:         }
  15:     }
  16:  
  17:     if (b != null)
  18:         b.Update(gameTime, GraphicsDevice.Viewport);
  19: }

And that's about all there is to it!  New source code is up!  Here's the next demo:

One other thing -- as a note, the red ball with the orange gradient has a mass of 8.0f; the others have a mass of 4.0f.

15May/080

My First XNA Application: The Bouncing Ball

Posted by Rob

My first application in XNA!  It's a... well, it's a ball that bounces.

It's bouncing!!

I created a new Windows XNA 2.0 application project.  I decided to abstract away a Ball object, as well as a GravitySource object.  There's a lot of cross-talk -- I'm not sure if this is good or not -- but it's happy enough for me. :-)

Gravity seems to be pretty straightforward to implement; the applied speed is going to be a vector direction with a constant speed as the magnitude.  Here's the code for the gravity source class:

   1: public class GravitySource
   2: {
   3:     private const float GRAVITY = 9.8f;
   4:  
   5:     private Vector2 m_gravitySpeed;
   6:  
   7:     public void ApplyGravity(ref Vector2 currentSpeed)
   8:     {
   9:         currentSpeed += m_gravitySpeed;
  10:     }
  11:  
  12:     public void ResetToDirection(Direction target)
  13:     {
  14:         switch (target)
  15:         {
  16:             case Direction.Up:
  17:                 m_gravitySpeed = new Vector2(0f, -GRAVITY);
  18:                 break;
  19:             case Direction.Left:
  20:                 m_gravitySpeed = new Vector2(-GRAVITY, 0f);
  21:                 break;
  22:             case Direction.Right:
  23:                 m_gravitySpeed = new Vector2(GRAVITY, 0f);
  24:                 break;
  25:             case Direction.Down:
  26:                 m_gravitySpeed = new Vector2(0f, GRAVITY);
  27:                 break;
  28:             case Direction.DownLeft:
  29:                 m_gravitySpeed = Vector2.Normalize(new Vector2(-1, 1)) * GRAVITY;
  30:                 break;
  31:             case Direction.DownRight:
  32:                 m_gravitySpeed = Vector2.Normalize(new Vector2(1, 1)) * GRAVITY;
  33:                 break;
  34:             case Direction.UpLeft:
  35:                 m_gravitySpeed = Vector2.Normalize(new Vector2(-1, -1)) * GRAVITY;
  36:                 break;
  37:             case Direction.UpRight:
  38:                 m_gravitySpeed = Vector2.Normalize(new Vector2(1, -1)) * GRAVITY;
  39:                 break;
  40:         }
  41:     }
  42: }

Note that I added a Direction enumeration to make it nice to read.

The game class has a reference to the ball object, and it handles the keyboard input to change the gravity direction all within the Update method:

   1: protected override void Update(GameTime gameTime)
   2: {
   3:     // Allows the game to exit
   4:     if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
   5:         this.Exit();
   6:  
   7:     KeyboardState keybstate = Keyboard.GetState();
   8:     if (keybstate.IsKeyDown(Keys.Down))
   9:     {
  10:         if (keybstate.IsKeyDown(Keys.Left))
  11:         {
  12:             m_grav.ResetToDirection(Direction.DownLeft);
  13:         }
  14:         else if (keybstate.IsKeyDown(Keys.Right))
  15:         {
  16:             m_grav.ResetToDirection(Direction.DownRight);
  17:         }
  18:         else
  19:         {
  20:             m_grav.ResetToDirection(Direction.Down);
  21:         }
  22:     }
  23:     else if (keybstate.IsKeyDown(Keys.Up))
  24:     {
  25:         if (keybstate.IsKeyDown(Keys.Left))
  26:         {
  27:             m_grav.ResetToDirection(Direction.UpLeft);
  28:         }
  29:         else if (keybstate.IsKeyDown(Keys.Right))
  30:         {
  31:             m_grav.ResetToDirection(Direction.UpRight);
  32:         }
  33:         else
  34:         {
  35:             m_grav.ResetToDirection(Direction.Up);
  36:         }
  37:     }
  38:     else if (keybstate.IsKeyDown(Keys.Left))
  39:     {
  40:         m_grav.ResetToDirection(Direction.Left);
  41:     }
  42:     else if (keybstate.IsKeyDown(Keys.Right))
  43:     {
  44:         m_grav.ResetToDirection(Direction.Right);
  45:     }
  46:  
  47:     m_ball.Update(gameTime, graphics.GraphicsDevice.Viewport);
  48:  
  49:     base.Update(gameTime);
  50: }

The game's drawing method is actually quite simple:

   1: protected override void Draw(GameTime gameTime)
   2: {
   3:     graphics.GraphicsDevice.Clear(Color.Black);
   4:  
   5:     spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
   6:     m_ball.Render(gameTime, spriteBatch);
   7:     spriteBatch.End();
   8:  
   9:     base.Draw(gameTime);
  10: }

And that gets implemented in the Ball class; here are the meat and potatoes:

   1: public void Render(GameTime time, SpriteBatch spriteBatch)
   2: {
   3:     spriteBatch.Draw(m_texture, m_position, Color.White);
   4: }
   5:  
   6: public void Update(GameTime time, Viewport bounds)
   7: {
   8:     m_position += m_speed * (float)time.ElapsedGameTime.TotalSeconds;
   9:  
  10:     int maxX = bounds.Width - m_texture.Width;
  11:     int maxY = bounds.Height - m_texture.Height;
  12:  
  13:     if (m_position.X > maxX)
  14:     {
  15:         m_speed.X *= -1.0f;
  16:         m_position.X = maxX;
  17:     }
  18:     else if (m_position.X < 0)
  19:     {
  20:         m_speed.X *= -1.0f;
  21:         m_position.X = 0;
  22:     }
  23:     else
  24:     {
  25:         // else we're in a freefall to the right!
  26:         m_gravity.ApplyGravity(ref m_speed);
  27:     }
  28:  
  29:     if (m_position.Y > maxY)
  30:     {
  31:         m_speed.Y *= -1.0f;
  32:         m_position.Y = maxY;
  33:     }
  34:     else if (m_position.Y < 0)
  35:     {
  36:         m_speed.Y *= -1.0f;
  37:         m_position.Y = 0;
  38:     }
  39:     else
  40:     {
  41:         // else we're in a freefall
  42:         // v(t) = at + k
  43:         // in this case a = -9.8
  44:         // Inverted because we're going down which is positive Y ;-)
  45:         m_gravity.ApplyGravity(ref m_speed);
  46:     }
  47: }

One kind of odd part is that it's designed to be simply a perfectly elastic ball (m_speed.Y *= -1.0f) but it actually bounces higher over time.

Source code is downloadable here.  Thanks to Betsy Aoki's samples online - they were incredibly helpful!

Tagged as: , , No Comments