Making your own WP7 Angry Birds – Part 6 – Impact!

Today we will implement a physics effect: when a box gets hit with sufficient force, it is destroyed and removed from our world.

In the next post, I will explain how to animate the destruction of our body.


Before we can destroy a body we need to keep track of its health (= hit points). Each collision, or contact as it is known in Farseer, between our projectile and a body, or a collision between two regular bodies will result in the loss of a certain amount of points. Once it reaches zero, the body is destroyed.

1. New PrefabUserData properties

Add the following properties to PrefabUserData:

public int HitPoints { get; set; }
public BodyStatus Status { get; set; }
public bool IsProjectile { get; set; }

Here’s what the BodyStatus enum looks like:

public enum BodyStatus
{
	Active,
	ToBeDestroyed,
	Destroyed
}

Notice that we now keep track of the status of a body: is it ‘alive’ or has it been destroyed?
Rather than dereferencing a destroyed body, we will marked it as destroyed. Why? Allocating and de-allocating objects feeds the garbage collector and we don’t want that. If the GC decides to kick in, it will cause frames to be dropped while it’s doing its business. This is a common trick applied in XNA games.

The last property we’ve added is the boolean IsProjectile; we need to know if a body is a regular body or a projectile. Why? In our game we don’t want our projectile to break upon impact. We will make it invincible and have it self-destruct a couple of seconds after impact (more on this in the future).

2. Initializing the new PrefabUserData properties

We will initialize the HitPoints in the PrefabBodyFactory. Add an additional parameter to CreateRectangle and CreateCircle which holds the amount of hit points a body has.

This is what my PrefabBodyFactory looks like now. Notice the last parameter, which are the hitpoints for my bodies. After some trial-and-error, it turns out 7 is a good value for our game as it is right now.

static PrefabBodyFactory()
{
	Library.Add(PrefabType.GoldBox, (world, position) => CreateRectangle(world, position, "box1", 0.4f, 0.4f, 7));
	Library.Add(PrefabType.GrayBox, (world, position) => CreateRectangle(world, position, "box2", 0.4f, 0.4f, 7));
	Library.Add(PrefabType.Pig, (world, position) => CreateRectangle(world, position, "midipig", 0.5f, 0.59f, 7));
}

The IsProjectile property will be set from the Projectile class; the wrapper class that upgrades our regular Body into a Body we can catapult.

public Projectile(Body body)
{
	Body = body;
	((PrefabUserData) Body.UserData).IsProjectile = true;
}

3. Deducting hitpoints upon impact

Now that we have setup all the data, it’s time to work on the logic. When the Farseer engine simulation detects a collision between 2 bodies, it can notify us by calling a delegate and providing us with all the collision data grouped into a Contact object.
We assign a method to this delegate in our LoadContent:

_world.ContactManager.BeginContact = BeginContact;

Here is our implementation of BeginContact:

public bool BeginContact(Contact contact)
{
	Body bodyA = contact.FixtureA.Body;
	Body bodyB = contact.FixtureB.Body;

	// get the speed of impact between the two bodies
	Manifold worldManifold;
	contact.GetManifold(out worldManifold);
	ManifoldPoint p = worldManifold.Points[0];
	Vector2 vA = bodyA.GetLinearVelocityFromLocalPoint(p.LocalPoint);
	Vector2 vB = bodyB.GetLinearVelocityFromLocalPoint(p.LocalPoint);
	float approachVelocity = Math.Abs(Vector2.Dot(vB - vA, worldManifold.LocalNormal));

	//deduct hitpoints from both bodies
	ProcessContact(contact, bodyA, approachVelocity);
	ProcessContact(contact, bodyB, approachVelocity);

	return true;
}

private void ProcessContact(Contact contact, Body body, float approachVelocity)
{
	PrefabUserData userData = body.UserData as PrefabUserData;

	// only deduct hitpoints if there is userdata (word edges have no userdata)
	// and
	// if the body is not our projectile (projectiles are invincible)
	if (userData != null && !userData.IsProjectile)
	{
		var hitPoints = (int)Math.Round(approachVelocity);
		userData.HitPoints -= hitPoints;

		if (userData.HitPoints <= 0)
		{
			// let Farseer know this contact and body are processed and
			// will have no further impact (pun intended)
			contact.Enabled = false;
			body.IsSensor = true;
			// mark this status as ToBeDestroyed
			userData.Status = BodyStatus.ToBeDestroyed;
		}
	}
}

4. Drawing and *not* drawing bodies when appropriate

Our next step is doing some refactoring of existing code: we will move code to draw a Body from the main Game class to PrefabUserData. This way a Body knows how to Draw itself, rather than letting someone else (=Game) do its drawing. This is a cleaner design and you will see the pay off in later on in the series.

Here’s the loop to draw our bodies right now:

foreach (var body in _bodies)
{
	var prefabUserData = (PrefabUserData) body.UserData;

	spriteBatch.Draw(
		_sheet.Texture,					// Texture
		body.Position * Constants.Scale,		// Position
		_sheet.SourceRectangle(prefabUserData.SpriteName),
		Color.White,					// Neutral
		body.Rotation,					// Rotation
		prefabUserData.Origin * Constants.Scale,	// Origin, relative to source rectangle size
		1f,								// Scale
		SpriteEffects.None,				// No effects
		0f);							// Z-layer
}

Change it into this:

foreach (var body in _bodies)
{
	var prefabUserData = (PrefabUserData) body.UserData;

	prefabUserData.Draw(gameTime, spriteBatch, _sheet, body);
}

And move the code into a new Draw method on PrefabUserData:

public void Draw(GameTime gameTime, SpriteBatch spriteBatch, SpriteSheet sheet, Body body)
{
	spriteBatch.Draw(
		sheet.Texture, // Texture
		body.Position*Constants.Scale, // Position
		sheet.SourceRectangle(SpriteName),
		Color.White, // Neutral
		body.Rotation, // Rotation
		Origin*Constants.Scale, // Origin, relative to source rectangle size
		1f, // Scale
		SpriteEffects.None, // No effects
		0f);
	}
}

Now we have done this refactoring, we will immediately make a change to the PrefabUserData.Draw method.

I made this two distinctive steps to make the difference clear between the refactoring, which doesn’t change behaviour, and the addition of a new feature, which does affect behaviour.

The change is simple: only draw the body if it is still in an Active state:

public void Draw(GameTime gameTime, SpriteBatch spriteBatch, SpriteSheet sheet, Body body)
{
	switch(Status)
	{
		case BodyStatus.Active:
			spriteBatch.Draw(
				sheet.Texture, // Texture
				body.Position*Constants.Scale, // Position
				sheet.SourceRectangle(SpriteName),
				Color.White, // Neutral
				body.Rotation, // Rotation
				Origin*Constants.Scale, // Origin, relative to source rectangle size
				1f, // Scale
				SpriteEffects.None, // No effects
				0f);
			break;
	}
}

Run the code as-is and catapult the pig into our tower of boxes. Unlike before, you should see some boxes diappear immediatly upon impact:


Next time, we will animate the explosions instead of just having them disappear. Meanwhile you can follow me on Twitter: http://www.twitter.com/jodegreef.

Advertisements

6 Responses to Making your own WP7 Angry Birds – Part 6 – Impact!

  1. Ian Ormesher says:

    Don’t forget to add the following usings to the top of Game1.cs for the collision stuff:

    using FarseerPhysics.Dynamics.Contacts;
    using FarseerPhysics.Collision;

    Great articles! Thanks Jo.

    Ian

  2. Pingback: Dew Drop – May 17, 2011 | Alvin Ashcraft's Morning Dew

  3. JoshJosh says:

    I seem to have lost my boxes after implementing the switch statement. If i remove the switch then the boxes do show but drop through the screen. In part 5 everything was working fine and I just dont see where this is going wrong.
    Could anyone help me locate the issue?

    http://dl.dropbox.com/u/13889774/cats_n_dogs.zip

    great series btw. 😀

    • Josh says:

      Ive naarowed it down by commenting out.
      ProcessContact(contact, bodyA, approachVelocity);
      ProcessContact(contact, bodyB, approachVelocity);
      So somewhere in that method, something is making the blocks fall right through the floor.

      • Ed says:

        It’s the HitPoints. Change CreateRectangle to this:

        private static Body CreateRectangle(World world, Vector2 position, string spriteName, float width, float height, int hitpoints)
        {
        Body body = BodyFactory.CreateRectangle(world, width, height, 1f, position, hitpoints);
        body.UserData = new PrefabUserData
        {
        Origin = new Vector2(width / 2f, height / 2f),
        SpriteName = spriteName,
        HitPoints = hitpoints
        };
        body.BodyType = BodyType.Dynamic;
        return body;
        }

        and change CreateCircle to match

  4. Pingback: ??? ?? ????? ????? 29-?? ????! ?? ??????? «????????» ????! - MSDN Blogs

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: