May 5, 2011

Part 4a & 4b were merely preparation for the big bang: catapulting our pig into the pile of boxes!

Our approach will be to wrap the pig Body in a Projectile object. The Projectile class will handle user input and launch controls.

To get information about user input, we need to call the the following method from within the Game.Update method:

TouchCollection touches = TouchPanel.GetState();

TouchPanel.GetState will return a collection of TouchLocation instances. Remember, every WP7 device is multi-touch capable, hence the collection.

TouchLocation has three properties: a Position, a State and an Id.

The Position property is a Vector2 which holds the coordinates where the touch on screen happened. On a typical device it can range from (0,0) to (800,480).

The State property is an enum which can be either Pressed, Moved or Released.

The Id property can be used to identify the same touch as it goes from one state to another.

Remember that the Update method is called 30 times per second. Every time it is called you will get a new collection with the current registered touches. The Id of a touch will remain the same for the lifetime of the touch.

Imagine the following scenario:

Pressing your finger on the screen, then dragging it and releasing it again. This will result in a TouchLocation with a fixed Id and a State that goes from Pressed to Moved to Released. In a multi-touch scenario the Id is invaluable to keep track of the different touches.

Our approach will be to capture a touch near the center of our pig, keeping track of that touch so we can draw the Pig being dragged until the touch is released which is translated into us launching the pig in the opposite direction of it being pulled.

The red cross is the center of our Pig body. Because we use our imprecise fingers as input device, we want to register any touch within a certain radius of this center. This is the yellow circle.

Finally, the green cross symbolizes the location of a touch we registered. It will almost always be off-center.

It is critical to keep track of the delta between the touch location and the pig’s center, since we will be animating the touch movement. If we were to draw the pig at the touch location, it would jump from its original position to the off-center location. By keeping track of the delta we can draw the pig at the corrected touch location. Hopefully the following image makes this clear:

Time to turn theory into code.

Below is the starting point for our Projectile class.

public class Projectile { private const float TouchRadius = 0.5f; private Vector2 _touchOffsetFromCenter; private int _touchId; public Body Body { get; private set; } /// <summary> /// Shortcut to Body.Position /// </summary> public Vector2 Position { get { return Body.Position; } } /// <summary> /// User is dragging the projectile in order to launch it /// Where is the current drag position, taking offset correction into account. /// </summary> public Vector2 DragPosition { get; private set; } /// <summary> /// Is user dragging the projectile in order to launch it? /// </summary> public bool IsBeingDragged { get; private set; } public Projectile(Body body) { Body = body; } /// <summary> /// Handle user input /// </summary> /// <param name="gameTime">current gameTime</param> /// <param name="touches">TouchCollection</param> /// <returns>True if projectile handled input, otherwise false</returns> public bool HandleInput(GameTime gameTime, TouchCollection touches) { ... } }

First of all, we define how big the radius within we will accept a touch as a touch of our projectile (the yellow circle from before). 0,5f in simulation units equals to a circle with a radius 50 pixels around the center of our projectile.

Secondly ,we have two private fields, one to keep track of the off-center delta and another to keep track of the Id of the touch that hit our projectile.

Next, we define a couple of public properties we need to expose in order to have enough information in our Game.Draw method.

Finally, there’s the constructor that takes a Body as parameter. In our example this will be the pig body.

As said before, launching a pig is a three step process: touching the pig, pulling it back and releasing it.

We will implement HandleInput step-by-step:

foreach (TouchLocation touchLocation in touches) { Vector2 touchPositionSim = touchLocation.Position / Constants.Scale; if (touchLocation.State == TouchLocationState.Pressed) { if ((Position - touchPositionSim).Length() < TouchRadius && !IsBeingDragged) { //start dragging the projectile, ready to shoot IsBeingDragged = true; //user never clicks dead-center on the projectile, this is the offset of the click compared to the center _touchOffsetFromCenter = Position - touchPositionSim; DragPosition = Position; _touchId = touchLocation.Id; return true; } break; } // handle Moved and Released }

The above code should be fairly self-explanatory. If we find a touch within the touch radius and we’re not dragging an object yet, we start tracking the projectile and it’s off-center information.

Next, we implement the Moved status.

This should be very simple: set the DragPosition to the current touch location + the off-center vector.

There is a catch however! In our game we want to limit the distance our object can be dragged. There is actually a second, bigger radius around our pig’s center, beyond which it cannot be dragged.

We will add a field which holds this radius value.

private const float MaxDragRadius = 1.5f;

When we drag the pig around, we are actually creating a vector from the original Position to the current DragPosition.

This vector has a length and if we notice its length is greater than the MaxDragRadius, we will normalize it and multiply it by this radius.

Normalizing the vector means that we keep the direction of the vector, but reduce its length to 1. Multiplying it by MaxDragRadius moves it to the outermost position it can be.

if (touchLocation.State == TouchLocationState.Moved && IsBeingDragged && touchLocation.Id == _touchId) { Vector2 pullPosition = touchPositionSim + _touchOffsetFromCenter; Vector2 dragVector = Position - pullPosition; if (dragVector.Length() < MaxDragRadius) { DragPosition = pullPosition; } else { // if draggingbeyond max radius, limit to max radius. dragVector.Normalize(); DragPosition = Position - Vector2.Multiply(dragVector, MaxDragRadius); } return true; } // handle Released

All that is left to do now is launching our projectile when the player lifts his finger.

We can do this by calling the ApplyLinearImpulse method on the Body. This method has two Vector2 parameters: impulse and point.

– we will use our dragVector as impulse, but we will multiply it by a factor that makes the impulse stronger in order to launch our pig far enough.

– the point parameter is a location relative to the origin of the World (not the object we’re applying the impulse to). We can use Body.WorldCenter to get the center point of our Body relative the World origin. To give our pig some backspin we will apply the impulse a bit lower than its center point (this is the CenterOffset you see in the code below).

private const float ImpulseModifier = 1.25f; private static readonly Vector2 CenterOffset = new Vector2(0, 0.025f);

if (touchLocation.State == TouchLocationState.Released && IsBeingDragged && touchLocation.Id == _touchId) { IsBeingDragged = false; Vector2 dragVector = (Body.Position - DragPosition) * ImpulseModifier; // apply an impulse to the Body, but a little bit off-center to give it a nice arcing motion Vector2 centerWithOffset = Body.WorldCenter + CenterOffset; Body.ApplyLinearImpulse(dragVector, centerWithOffset); return true; }

All what’s left now is to wire up the projectile to our Game and Draw the dragged projectile.

Add the following to Game.Update:

TouchCollection touches = TouchPanel.GetState(); _projectile.HandleInput(gameTime, touches);

Add this to Game.Draw, right after the bodies and floor have been drawn:

if (_projectile.IsBeingDragged) { PrefabUserData userData = ((PrefabUserData)_projectile.Body.UserData); spriteBatch.Draw( _sheet.Texture, _projectile.DragPosition * Constants.Scale, _sheet.SourceRectangle(userData.SpriteName), new Color(128, 128, 128, 128), _projectile.Body.Rotation, userData.Origin * Constants.Scale, 1f, SpriteEffects.None, 0f); }

As a little side note: you will want to move the new Color(128, 128, 128, 128), which makes the projectile being drawn at half-transparency, to the Constants class in order to avoid the memory allocation on every frame.

Run the application at this point and this should be the result:

I’m giving out free tweets today, so you might want to follow me: http://www.twitter.com/jodegreef.