The Principles of Movement in Windows Phone 7

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.


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:

b1.gif

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.