This chapter is taken from book "Programming Windows Phone 7" by Charles Petzold published by Microsoft press. http://www.charlespetzold.com/phone/index.html
Game Components
To give your fingers a target to touch and drag, the programs display translucent disks at the Texture2D corners. It would be nice to code these draggable translucent disks so they're usable by multiple programs. In a traditional graphics programming environment, we might think of something like this as a control but in XNA it's called a game component.
Components help modularize your XNA programs. Components can derive from the GameComponent class but often they derive from DrawableGameComponent so they can display something on the screen in addition to (and on top of) what goes out in the Draw method of your Game class.
To add a new component class to your project, right-click the project name, select Add and then New Item, and then pick Game Component from the list. You'll need to change the base class to DrawableGameComponent and override the Draw method if you want the component to participate in drawing.
A game generally instantiates the components that it needs either in the game's constructor or during the Initialize method. The components officially become part of the game when they are added to the Components collection defined by the Game class.
As with Game, a DrawableGameComponent derivative generally overrides the Initialize, LoadContent, Update, and Draw methods. When the Initialize override of the Game derivative calls the Initialize method in the base class, the Initialize methods in all the components are called. Likewise, when the LoadComponent, Update, and Draw overrides in the Game derivative call the method in the base class, the LoadComponent, Update, and Draw methods in all the components are called.
As you know, the Update override normally handles touch input. In my experience that attempting to access touch input in a game component is somewhat problematic. It seems as if the game itself and the components end up competing for input.
To fix this, I decided that my Game derivative would be solely responsible for calling TouchPanel.GetState, but the game would then give the components the opportunity to
process this touch input. To accommodate this concept, I created this interface for GameComponent and DrawableGameComponent derivatives:
using Microsoft.Xna.Framework.Input.Touch;
namespace Petzold.Phone.Xna
{
public interface IProcessTouch
{
bool ProcessTouch(TouchLocation touch);
}
}
When a game component implements this interface, the game calls the game component's ProcessTouch method for every TouchLocation object. If the game component needs to use that TouchLocation, it returns true from ProcessTouch, and the game then probably ignores that TouchLocation.
The first component I'll show you is called Dragger, and it is part of the Petzold.Phone.Xna library. Dragger derives from DrawableGameComponent and implements the IProcessTouch interface:
public class
Dragger :
DrawableGameComponent, IProcessTouch
{
SpriteBatch spriteBatch;
int? touchId;
public event
EventHandler PositionChanged;
public Dragger(Game
game)
: base(game)
{
}
public
Texture2D Texture { set;
get; }
public
Vector2 Origin { set;
get; }
public
Vector2 Position { set;
get; }
....
}
A program making use of Dragger could define a custom Texture2D for the
component and set it through this public Texture property, at which time it
would probably also set the Origin property. However, Dragger defines a default
Texture property for itself during its LoadContent method:
protected
override void LoadContent()
{
spriteBatch = new
SpriteBatch(this.GraphicsDevice);
// Create default texture
int radius = 48;
Texture2D texture =
new Texture2D(this.GraphicsDevice,
2 * radius, 2 * radius);
uint[] pixels =
new uint[texture.Width
* texture.Height];
for (int
y = 0; y < texture.Height; y++)
for (int
x = 0; x < texture.Width; x++)
{
Color clr =
Color.Transparent;
if ((x - radius) * (x -
radius) +
(y - radius) * (y - radius) <
radius * radius)
{
clr = new
Color(0, 128, 128, 128);
}
pixels[y * texture.Width + x] = clr.PackedValue;
}
texture.SetData<uint>(pixels);
Texture = texture;
Origin = new
Vector2(radius, radius);
base.LoadContent();
}
The Dragger class implements the IProcessTouch interface so it has a
ProcessTouch method that is called from the Game derivative for each
TouchLocation object. The ProcessTouch method is interested in finger presses
that occur over the component itself. If that is the case, it retains the ID and
basically owns that finger until it lifts from the screen. For every movement of
that finger, Dragger fires a PositionChanged event.
public bool
ProcessTouch(TouchLocation touch)
{
if (Texture ==
null)
return
false;
bool touchHandled =
false;
switch (touch.State)
{
case
TouchLocationState.Pressed:
if ((touch.Position.X >
Position.X - Origin.X) &&
(touch.Position.X < Position.X - Origin.X +
Texture.Width) &&
(touch.Position.Y > Position.Y - Origin.Y) &&
(touch.Position.Y < Position.Y - Origin.Y +
Texture.Height))
{
touchId = touch.Id;
touchHandled = true;
}
break;
case
TouchLocationState.Moved:
if (touchId.HasValue &&
touchId.Value == touch.Id)
{
TouchLocation previousTouch;
touch.TryGetPreviousLocation(out
previousTouch);
Position += touch.Position - previousTouch.Position;
// Fire the event!
if (PositionChanged != null)
PositionChanged(this,
EventArgs.Empty);
touchHandled = true;
}
break;
case
TouchLocationState.Released:
if (touchId.HasValue &&
touchId.Value == touch.Id)
{
touchId = null;
touchHandled = true;
}
break;
}
return touchHandled;
}
The Draw override just draws the Texture2D at the new position:
public override
void Draw(GameTime
gameTime)
{
if (Texture !=
null)
{
spriteBatch.Begin();
spriteBatch.Draw(Texture, Position,
null, Color.White,
0, Origin, 1,
SpriteEffects.None, 0);
spriteBatch.End();
}
base.Draw(gameTime);
}
Affine and Non-Affine Transforms
Sometimes it's convenient to derive a transform that maps a particular set
of points to a particular destination. For example, here's a program that
incorporates three instances of the Dragger component I just described, and lets
you drag three corners of the Texture2D
to arbitrary locations on the screen:

This program uses an affine transform, which means that rectangles are always mapped to parallelograms. The fourth corner isn't draggable because it's always determined by the other three:

You can't choose just any three points. Everything goes kaflooey if you attempt to make an interior angle greater than 180 degree.
A static class named MatrixHelper in the Petzold.Phone.Xna library has a method named ComputeAffineTransform that creates a Matrix object based on these formulas:
static
Matrix ComputeAffineTransform(Vector2
ptUL, Vector2 ptUR,
Vector2 ptLL)
{
return new
Matrix()
{
M11 = (ptUR.X - ptUL.X),
M12 = (ptUR.Y - ptUL.Y),
M21 = (ptLL.X - ptUL.X),
M22 = (ptLL.Y - ptUL.Y),
M33 = 1,
M41 = ptUL.X,
M42 = ptUL.Y,
M44 = 1
};
}
This method isn't public because it's not very useful by itself. It's not very useful because the formulas are based on transforming an image that is one-pixel wide and one-pixel tall. Notice, however, that the code sets M33 and M44 to 1. This doesn't happen automatically and it is essential for the matrix to work right.
To compute a Matrix for an affine transform that applies to an object of a particular size, this public method is much more useful:
public
static Matrix
ComputeMatrix(Vector2 size,
Vector2 ptUL,
Vector2 ptUR, Vector2 ptLL)
{
// Scale transform
Matrix S =
Matrix.CreateScale(1 / size.X, 1 / size.Y,
1);
// Affine transform
Matrix A =
ComputeAffineTransform(ptUL, ptUR, ptLL);
// Product of two transforms
return S * A;
}
The first transform scales the object down to a 1*1 size before applying the computed affine transform. The AffineTransform project is responsible for the two screen shots shown above. It creates three instances of the Dragger component in its Initialize override, sets a handler for the PositionChanged event, and adds the component to the Components collection:
public
class Game1 :
Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D texture;
Matrix matrix =
Matrix.Identity;
Dragger draggerUL, draggerUR,
draggerLL;
...
protected
override void Initialize()
{
draggerUL = new Dragger(this);
draggerUL.PositionChanged += OnDraggerPositionChanged;
this.Components.Add(draggerUL);
draggerUR = new Dragger(this);
draggerUR.PositionChanged += OnDraggerPositionChanged;
this.Components.Add(draggerUR);
draggerLL = new Dragger(this);
draggerLL.PositionChanged += OnDraggerPositionChanged;
this.Components.Add(draggerLL);
base.Initialize();
}
...
}
Don't forget to add the components to the Components collection of the Game class!
The LoadContent override is responsible for loading the image that will be transformed and initializing the Position properties of the three Dragger components at the three corners of the image:
protected
override void
LoadContent()
{
// Create a new SpriteBatch, which can be
used to draw textures.
spriteBatch = new
SpriteBatch(GraphicsDevice);
Viewport viewport =
this.GraphicsDevice.Viewport;
texture = this.Content.Load<Texture2D>("PetzoldTattoo");
draggerUL.Position = new
Vector2((viewport.Width - texture.Width) / 2,
(viewport.Height - texture.Height)
/ 2);
draggerUR.Position = draggerUL.Position +
new Vector2(texture.Width, 0);
draggerLL.Position = draggerUL.Position +
new Vector2(0, texture.Height);
OnDraggerPositionChanged(null,
EventArgs.Empty);
}
Dragger only fires its PositionChanged event when the component is actually dragged by the user, so the LoadContent method concludes by simulating a PositionChanged event, which calculates an initial Matrix based on the size of the Texture2D and the initial positions of the Dragger components:
void
OnDraggerPositionChanged(object sender,
EventArgs args)
{
matrix = MatrixHelper.ComputeMatrix(new
Vector2(texture.Width, texture.Height),
draggerUL.Position,
draggerUR.Position,
draggerLL.Position);
}
The program doesn't need to handle any touch input of its own, but Dragger
implements the IProcessTouch interface, so the program funnels touch input to
the Dragger components. These Dragger components respond by possibly moving
themselves and setting new Position properties, which will cause PositionChanged
events to be fired.
protected
override void
Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back
== ButtonState.Pressed)
this.Exit();
TouchCollection touches =
TouchPanel.GetState();
foreach (TouchLocation
touch in touches)
{
bool touchHandled =
false;
foreach (GameComponent
component in this.Components)
{
if (component
is IProcessTouch
&&
(component as
IProcessTouch).ProcessTouch(touch))
{
touchHandled = true;
break;
}
}
if (touchHandled ==
true)
continue;
}
base.Update(gameTime);
}
It is possible for the program to dispense with setting handlers for the PositionChanged event of the Dragger components and instead poll the Position properties during each Update call and recalculate a Matrix from those values. However, recalculating a Matrix only when one of the Position properties actually changes is much more efficient.
The Draw override uses that Matrix to display the texture:
protected
override void
Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(SpriteSortMode.Immediate,
null, null,
null, null,
null, matrix);
spriteBatch.Draw(texture, Vector2.Zero,
Color.White);
spriteBatch.End()
base.Draw(gameTime);
}
As you experiment with AffineTransform, you'll want to avoid making the interior angles at any corner greater than 180 degree. (In other words, keep it convex.) Affine transforms can express familiar operations like translation, scaling, rotation, and skew, but they never transform a square into anything more exotic than a parallelogram.
Non-affine transforms are much more common in 3D than 2D. In 3D, non-affine transforms are necessary to implement perspective effects. A long straight desert highway in a 3D world must seem to get narrower as it recedes into the distance, just like in the real world. Although we know that the sides of the road remains parallel, visually they seem to converge at infinity. This tapering effect is characteristic of non-affine transforms.
Although non-affine transforms are essential for 3D graphics programming, I wasn't even sure if SpriteBatch supported two-dimensional non-affine transforms until I tried them, and I was pleased to discover that XNA says "No problem!" What this means is that you can use non-affine transforms in 2D programming to simulate perspective effects.
A non-affine transform in 2D can transform a square into a simple convex quadrilateral-a four-sided figure where the sides meet only at the corners, and interior angles at any corner are less than 180 degree. Here's one example:

This one makes me look really smart:

This program is called NonAffineTransform and it's just like AffineTransform except it has a fourth Dragger component and it calls a somewhat more sophisticated method in the MatrixHelper class in Petzold.Phone.Xna. You can move the little disks around with a fair amount of freedom; as long as you're not trying to form a concave quadrilateral, you'll get an image stretched to fit.
The math of NonAffineTransform has been incorporated
into a second static
MatrixHelper.ComputeMatrix method in the Petzold.Phone.Xna library:
public
static Matrix
ComputeMatrix(Vector2 size,
Vector2 ptUL,
Vector2 ptUR,
Vector2 ptLL, Vector2 ptLR)
{
// Scale transform
Matrix S =
Matrix.CreateScale(1 / size.X, 1 / size.Y,
1);
// Affine transform
Matrix A =
ComputeAffineTransform(ptUL, ptUR, ptLL);
// Non-Affine transform
Matrix B =
new Matrix();
float den = A.M11 * A.M22 - A.M12 *
A.M21;
float a = (A.M22 * ptLR.X - A.M21 *
ptLR.Y +
A.M21 * A.M42 - A.M22 * A.M41) / den;
float b = (A.M11 * ptLR.Y - A.M12 *
ptLR.X +
A.M12 * A.M41 - A.M11 * A.M42) / den;
B.M11 = a / (a + b - 1);
B.M22 = b / (a + b - 1);
B.M33 = 1;
B.M14 = B.M11 - 1;
B.M24 = B.M22 - 1;
B.M44 = 1;
// Product of three transforms
return S * B * A;
}
I won't show you the NonAffineTransform program here because it's pretty much the same as the AffineTransform program but with a fourth Dragger component whose Position property is passed to the second ComputeMatrix method.
The big difference with the new program is that non-affine transforms are much more fun!

Join the conversation! Your thoughts help the community grow.