This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
Much of the core of an XNA program is dedicated to moving sprites
around the screen. Sometimes these sprites move under user control; at
other times they move on their own volition as if animated by some
internal vital force. Instead of moving real sprites, you can instead
move some text. The concepts and strategies involved in moving text around the
screen are the same as those in moving sprites.
A particular text string seems to move around the screen when it's given a
different position in the DrawString method during subsequent calls of the
Draw method in
Game.
The Naive Approach
For this first attempt at text movement, I want to try something
simple. I'm just going to move the text up and down vertically so the
movement is entirely in one dimension. All we have to worry about is
increasing and decreasing the Y coordinate of textPosition.
If you want to play along, you can create a Visual Studio project named
NaiveTextMovement and add the 14-point Segoe UI Mono font to the Content
directory. The fields in the Game1 class are defined
like so:
public
class Game1 :
Microsoft.Xna.Framework.Game
{
const float
SPEED = 240f; // pixels per second
const
string TEXT = "Hello, Windows Phone 7!";
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont segoe14;
Viewport viewport;
Vector2 textSize;
Vector2 textPosition;
bool isGoingUp =
false;
....
}
Nothing should be too startling here. I've defined both the SPEED and TEXT as
constants. The SPEED is set at 240 pixels per second. The Boolean isGoingUp
indicates whether the text is currently moving down the screen or
up the screen.
The LoadContent method is very familiar and the viewport is saved as a
field:
protected
override void
LoadContent()
{
spriteBatch = new
SpriteBatch(GraphicsDevice);
viewport = this.GraphicsDevice.Viewport;
segoe14 = this.Content.Load<SpriteFont>("Segoe14");
textSize = segoe14.MeasureString(TEXT);
textPosition = new
Vector2(viewport.X + (viewport.Width -
textSize.X) / 2, 0);
}
Notice that this
textPosition centers the text horizontally but
positions it at the top of the screen. As is usual with most XNA programs, all
the real calculational work occurs during the Update
method:
protected
override void
Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back
== ButtonState.Pressed)
this.Exit();
if (!isGoingUp)
{
textPosition.Y += SPEED * (float)gameTime.ElapsedGameTime.TotalSeconds;
if (textPosition.Y + textSize.Y
> viewport.Height)
{
float excess =
textPosition.Y + textSize.Y - viewport.Height;
textPosition.Y -= 2 * excess;
isGoingUp = true;
}
}
else
{
textPosition.Y -= SPEED * (float)gameTime.ElapsedGameTime.TotalSeconds;
if (textPosition.Y < 0)
{
float excess = -
textPosition.Y;
textPosition.Y += 2 * excess;
isGoingUp = false;
}
}
base.Update(gameTime);
}
The logic for moving up is (as I like to say) the same but completely
opposite. The actual Draw
override is simple:
protected
override void Draw(GameTime
gameTime)
{
GraphicsDevice.Clear(Color.Navy);
spriteBatch.Begin();
spriteBatch.DrawString(segoe14, TEXT, textPosition,
Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
What's missing from the NaiveTextMovement program is any concept of
direction that would allow escaping from horizontal or vertical
movement. What we need are vectors.
A Brief Review of Vectors
A vector is a mathematical entity that encapsulates both a direction and a
magnitude. Very often a vector is symbolized by a line with an arrow.
A vector has magnitude and dimension but no location., but like the point a
vector is represented by the number pair (x, y) except that it's
usually written in boldface like
(x, y)
to indicate a vector rather than a point. A
normalized vector represents just a direction without magnitude, but it can be
multiplied by a number to give it that length. If
vector has a
certain length and direction, then -vector
has the same length but the opposite direction.
I'll make use of this operation in the next program coming up.
Moving Sprites with Vectors
That little refresher course should provide enough knowledge to revamp the
text-moving program to use vectors. This Visual Studio project is called
VectorTextMovement. Here are the fields:
public
class Game1 :
Microsoft.Xna.Framework.Game
{
const float
SPEED = 240f; // pixels per second
const
string TEXT = "Hello, Windows Phone 7!";
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont segoe14;
Vector2 midPoint;
Vector2 pathVector;
Vector2 pathDirection;
Vector2 textPosition;
....
}
The text will be moved between two points (called position1 and
position2 in the LoadContent method), and the midPoint field will store the point midway between those two points. The
pathVector field is the vector from position1 to position2,
and pathDirection is
pathVector
normalized.
The LoadContent
method calculates and initializes all these fields:
protected
override void
LoadContent()
{
spriteBatch = new
SpriteBatch(GraphicsDevice);
Viewport viewport =
this.GraphicsDevice.Viewport;
segoe14 = this.Content.Load<SpriteFont>("Segoe14");
Vector2 textSize =
segoe14.MeasureString(TEXT);
Vector2 position1 =
new Vector2(viewport.Width
- textSize.X, 0);
Vector2 position2 =
new Vector2(0,
viewport.Height - textSize.Y);
midPoint = Vector2.Lerp(position1,
position2, 0.5f);
pathVector = position2 - position1;
pathDirection = Vector2.Normalize(pathVector);
textPosition = position1;
}
Note that pathVector is the entire vector from position1 to position2
while pathDirection is the same vector normalized. The method concludes by
initializing textPosition
to position1. The use of these fields should become
apparent in the Update
method:
protected
override void
Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back
== ButtonState.Pressed)
this.Exit();
float pixelChange = SPEED * (float)gameTime.ElapsedGameTime.TotalSeconds;
textPosition += pixelChange * pathDirection;
if ((textPosition -
midPoint).LengthSquared() > (0.5f * pathVector).LengthSquared())
{
float excess = (textPosition -
midPoint).Length() - (0.5f * pathVector).Length();
pathDirection = -pathDirection;
textPosition += 2 * excess * pathDirection;
}
base.Update(gameTime);
}
After a few seconds of textPosition increases, textPosition will go beyond position2.
That can be detected when the length of the vector from midPoint to
textPosition is greater than the length of
half the pathVector.
The direction must be reversed: pathDirection is set to the negative of itself, and textPosition
is adjusted for the bounce.
Notice there's no longer a need to determine if the text is moving up
or down. The calculation involving
textPosition and
midPoint works for both cases. Also notice
that the if statement performs a comparison based on LengthSquared but the calculation of excess requires the actual Length method. Because the if clause is calculated for every Update call, it's good to try to keep the code efficient. The
length of half the
pathVector never changes, so I could have been
even more efficient by storing Length or
LengthSquared
(or both) as fields.
The Draw method is the same as before:
protected
override void
Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Navy);
spriteBatch.Begin();
spriteBatch.DrawString(segoe14, TEXT, textPosition,
Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
Working with Parametric Equations
It is well known that when the math or physics professor says "Now
let's introduce a new variable to simplify this mess," no one really
believes that the discussion is heading towards a simpler place. But
it's very often true, and it's the whole rationale behind parametric
equations. Into a seemingly difficult system of formulas a new variable
is introduced that is often simply called t,
as if to suggest
time. The value of t usually ranges from 0 to 1 (although that's just a
convention) and other variables are calculated based on t.
Amazingly enough, simplicity often results.
Let's think about the problem of moving text around the screen in terms of a
"lap." One lap consists of the text moving from the upper-right corner (position1)
to the lower-left corner (position2)
and back up to position1.
where
pathVector (as in the previous program) equals
position2 minus position1. The only really tricky part is the calculation of
pLap based on tLap.
The ParametricTextMovement project contains the
following fields:
public
class Game1 :
Microsoft.Xna.Framework.Game
{
const float
SPEED = 240f;
// pixels per second
const string TEXT =
"Hello, Windows Phone 7!";
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont segoe14;
Vector2 position1;
Vector2 pathVector;
Vector2 textPosition;
float lapSpeed;
// laps per second
float tLap;
}
The only new variables here are
lapSpeed and
tLap. As is now
customary, most of the variables are set during the
LoadContent method:
protected
override void
LoadContent()
{
spriteBatch = new
SpriteBatch(GraphicsDevice);
Viewport viewport =
this.GraphicsDevice.Viewport;
segoe14 = this.Content.Load<SpriteFont>("Segoe14");
Vector2 textSize =
segoe14.MeasureString(TEXT);
position1 = new Vector2(viewport.Width -
textSize.X, 0);
Vector2 position2 =
new Vector2(0,
viewport.Height - textSize.Y);
pathVector = position2 - position1;
lapSpeed = SPEED / (2 * pathVector.Length());
}
In the calculation of
lapSpeed, the numerator is in units of pixels-per-second.
The denominator is the length of the entire lap, which is two times the length
of pathVector;
therefore the denominator is in units of pixels-per-lap. Dividing
pixels-per-second by pixels-per-lap give you a speed in units of
laps-per-second.
One of the big advantages of this parametric technique is the sheer elegance
of the Update method:
protected
override void
Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back
== ButtonState.Pressed)
this.Exit();
tLap += lapSpeed * (float)gameTime.ElapsedGameTime.TotalSeconds;
tLap %= 1;
float pLap = tLap < 0.5f ? 2 * tLap
: 2 - 2 * tLap;
textPosition = position1 + pLap * pathVector;
base.Update(gameTime);
}
The tLap field is incremented by the lapSpeed times
the elapsed time in seconds. The second calculation removes any integer part, so
if tLap
is incremented to 1.1 (for example), it gets bumped back down to 0.1.
I will agree the calculation of
pLap from
tLap-which
is a transfer function of sorts-looks like an indecipherable mess at first. But
if you break it down, it's not too bad: If tLap is less
than 0.5, then pLap is twice tLap,
so for tLap from 0 to 0.5,
pLap goes from 0 to 1. If tLap is greater
than or equal to 0.5, tLap
is doubled and subtracted from 2, so for tLap from 0.5
to 1, pLap
goes from 1 back down to 0.
The Draw method remains the same:
protected override
void Draw(GameTime
gameTime)
{
GraphicsDevice.Clear(Color.Navy);
spriteBatch.Begin();
spriteBatch.DrawString(segoe14, TEXT, textPosition,
Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
There are some equivalent ways of performing these calculations.
Instead of saving
pathVector as a field you could save position2.
Then during the
Update method you would calculate textPosition using the Vector2.Lerp method:
textPosition = Vector2.Lerp(position1, position2, pLap);
Scaling the Text
Rotation and scaling are always relative to a point. This is most obvious
with rotation, as anyone who's ever explored the technology of propeller beanies
will confirm. But scaling is also relative to a point. As an object grows or
shrinks in size, one point remains anchored; that's the point indicated by the
origin argument to DrawString. (The
point could actually be outside the area of the scaled object.)
The ScaleTextToViewport project displays a text string
in its center and expands it out to fill the viewport. As with the other
programs, it includes a font. Here are the fields:
namespace ScaleTextToViewport
{
public class
Game1 : Microsoft.Xna.Framework.Game
{
const float
SPEED = 0.5f;
// laps per second
const string TEXT =
"Hello, Windows Phone 7!";
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont segoe14;
Vector2 textPosition;
Vector2 origin;
Vector2 maxScale;
Vector2 scale;
float tLap;
public Game1()
{
graphics =
new GraphicsDeviceManager(this);
Content.RootDirectory =
"Content";
// Frame rate is 30 fps by default for
Windows Phone.
TargetElapsedTime =
TimeSpan.FromTicks(333333);
}
protected override
void Initialize()
{
base.Initialize();
}
protected override
void LoadContent()
{
spriteBatch =
new SpriteBatch(GraphicsDevice);
Viewport viewport =
this.GraphicsDevice.Viewport;
segoe14 =
this.Content.Load<SpriteFont>("Segoe14");
Vector2 textSize =
segoe14.MeasureString(TEXT);
textPosition =
new Vector2(viewport.Width / 2, viewport.Height /
2);
origin =
new Vector2(textSize.X / 2, textSize.Y / 2);
maxScale =
new Vector2(viewport.Width / textSize.X,
viewport.Height / textSize.Y);
}
protected override
void UnloadContent()
{
}
protected override
void Update(GameTime
gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back
== ButtonState.Pressed)
this.Exit();
tLap = (SPEED * (float)gameTime.TotalGameTime.TotalSeconds)
% 1;
float pLap = (1 - (float)Math.Cos(tLap
* MathHelper.TwoPi)) / 2;
scale =
Vector2.Lerp(Vector2.One,
maxScale, pLap);
base.Update(gameTime);
}
protected override
void Draw(GameTime
gameTime)
{
GraphicsDevice.Clear(Color.Navy);
spriteBatch.Begin();
spriteBatch.DrawString(segoe14, TEXT, textPosition,
Color.White,
0, origin, scale,
SpriteEffects.None, 0);
spriteBatch.End();
base.Draw(gameTime);
}
}
}
As you run this program, you'll notice that the vertical
scaling doesn't make the top and bottom of the text come anywhere close to the
edges of the screen. The reason is that
MeasureString
returns a vertical dimension based on the maximum text
height for the font, which includes space for descenders, possible diacritical
marks, and a little breathing room as well.
It should also be obvious that you're dealing with a
bitmap font here:
The display engine tries to smooth out the jaggies but it's debatable
whether the fuzziness is an improvement. If you need to scale text and
maintain smooth vector outlines, that's a job for Silverlight. Or, you
can start with a large font size and always scale down.