Sgt. Conker We are "absolutely fine"

7Mar/100

Article: Draw me a line (Zune HD)

by Sgt. Conker

This article will show you how to create a dotted line based on touch input from the Zune HD.
This can be very useful for indicating to the user a path a game entity is going to take.
If you've ever played Flight Control for the iPhone you'll know exactly what I mean.

Firstly, if we were doing this in a real game, we would most likely have some sort of engine.
I've built this sample with a very small Micro Engine. This Micro Engine simply contains a list of World Entities and with each update/draw call it loops through each entity requesting it to update and draw.
The Micro Engine has a main MicroEngine class and an Entity class which holds position/size/texture etc.

I won't explain this in detail here as it's out of scope of the article.

The first thing we should do, is make this work with a mouse on Windows. This will help us develop the feature much faster and we will add Zune HD touch input later.

Because we're using SpriteBatch, we'll also need a texture to use as the dotted line. Our texture will be one of the dashes and we will draw it multiple times to form a dotted line. This texture looks like this

In our main game loop we first call update to the MicroEngine class so that it can update the mouse input state.

this.engine.Update(gameTime);

Next we need to detect that there has been a new touch from the mouse. Our engine contains a WasNewTouch method which checks the last and current state of the mouse to provide the result.

if (engine.WasNewTouch())
{

If there was a new touch we want to start drawing a new dotted line. I've created a custom Entity so that we can utilise the MicroEngine.
Inside this custom entity which I named CustomLineEntity, we store a list of Vector2 items to reference as each dot in the line.

public class CustomLineEntity : Entity
{
    public List<Vector2> Points { get; set; }

    public CustomLineEntity()
    {
        this.Points = new List<Vector2>();
    }

We then want to override the simple draw method of a world entity and loop through our points and draw each one.

public override void Draw(SpriteBatch spriteBatch)
{
    for (int index = 0; index < this.Points.Count; index++)
    {
        this.DrawIndex(spriteBatch, index);
    }
}

To actually draw the dot in the line, we need to work out the rotation of the dot so that it's a smooth curvy line as we draw. It's also done in the draw section so that the line can be adaptive, it's not always known what rotation the first or last items would be until the line is complete.

private void DrawIndex(SpriteBatch spriteBatch, int index)
{
    var item = this.Points[index];
    float rotation = 0;
    Vector2 heading = Vector2.Zero;

    if (index < this.Points.Count - 1)
    {
        // Get the direction of this index from the next one in the list.
        heading = this.Points[index + 1]- item;
    }
    else if (this.Points.Count > 1)
    {
        // It's the last one so get the direction of this index from the previous one in the list.
        heading = this.Points[index - 1]- item;
    }

    float angle = (float)Math.Atan2(heading.Y, heading.X);
    rotation = angle;

And then simply draw the sprite at the correct position and angle.

Vector2 origin = new Vector2(this.Texture.Width / 2, this.Texture.Height / 2);
spriteBatch.Draw(
                this.Texture,
                item,
                null,
                Color.White,
                rotation,
                origin,
                1,
                SpriteEffects.None,
                0);

Going back to the main game loop, we detected a new touch with

if (engine.WasNewTouch())
{

Now we can create the new CustomLineEntity and give it a starting position.
We also set a pickedUp flag so that we know we're in the drawing a new line state.

   // Start the line dragging by adding a CustomLineEntity to the engines WorldEntities
   this.pickedUp = true;

   this.currentLine = new CustomLineEntity();
   this.currentLine.Texture = dot;
   this.currentLine.Position = engine.MousePosition;
   this.currentLine.Points.Add(new Vector2(engine.MousePosition.X, engine.MousePosition.Y));
   this.engine.WorldEntities.Add(this.currentLine);
}

Adding it to the WorldEntities ensures it will receive the Draw & Update method calls.

Still in our main game update.
We need to detect when the user lifts off so that we can end the drawing of the line.
To make this a little more fun, we will add a bot into the game world which will follow the newly created line. We will describe the bot a little later, for now, we'll just add it to the world.

if (engine.WasTouchReleased())
{
    //Create a new bot and stick it on the line we just created
    var bot = this.CreateBot(this.currentLine);
    this.engine.WorldEntities.Add(bot);

    this.pickedUp = false;
    this.currentLine = null;
}

The final thing in our main game update is to continue creating the line if we're moving around with our mouse.

if (this.pickedUp)
{
    this.ContinueCreatingLine();
}

To do this we must calculate the distance between the last dot in the line and the new position of our mouse.
If the distance is greater than the size of a dot, we can add a new one to the line.
We also give it a little gap so that it looks prettier.

private void ContinueCreatingLine()
{
    // Get the position of the last item in the points
    Vector2 lastPos = this.currentLine.Points[this.currentLine.Points.Count - 1];

    // If the distance is greater than the width of a dot
    if (Vector2.Distance(engine.MousePosition, lastPos) > dot.Width)
    {
        // Get the direction from the last point
        Vector2 dir = engine.MousePosition - lastPos;
        dir.Normalize();

        // Push the new point out at by the width of a dot plus a gap
        int gap = 5;
        var newPosition = lastPos + (dir * (dot.Width + gap));

        // Add it on to the current lines list.
        var newPoint = new Vector2(newPosition.X, newPosition.Y);

        this.currentLine.Points.Add(newPoint);
    }
}

And that's it, we can drag our mouse cursor around the screen and draw a dashed line.
As stated a little earlier we want to make this more interesting by adding something which will follow this line. Otherwise it's fairly pointless :)

Going back to the line which created a new world entity when the line was completed.

var bot = this.CreateBot(this.currentLine);
this.engine.WorldEntities.Add(bot);

I've created a custom BotEntity which has custom update logic.

The creation of the BotEntity sets the current line as it's path.

private BotEntity CreateBot(CustomLineEntity line)
{
    BotEntity bot = new BotEntity();
    bot.Texture = this.botTexture;
    bot.Size = new Point(bot.Texture.Width, bot.Texture.Height);
    bot.LineToFollow = line;
    bot.Position = line.Position;
    bot.Layer = 1;
    return bot;
}

In the main game class we also have a Clear method which clears out the world entities if you press escape.

Here's the complete class.

    public class Game1 : Microsoft.Xna.Framework.Game
    {
        private GraphicsDeviceManager graphics;
        private SpriteBatch spriteBatch;
        private Texture2D dot;
        private Texture2D botTexture;
        private MicroEngine engine;

        private bool pickedUp;
        private CustomLineEntity currentLine;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            this.IsMouseVisible = true;
            this.graphics.PreferredBackBufferWidth = 480;
            this.graphics.PreferredBackBufferHeight = 272;
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            dot = Content.Load<Texture2D>("dot");
            this.botTexture = Content.Load<Texture2D>("bot");
            this.engine = new MicroEngine();
        }

        protected override void Update(GameTime gameTime)
        {
            if (Keyboard.GetState().IsKeyDown(Keys.Escape))
            {
                this.Clear();
            }

            this.engine.Update(gameTime);

            if (engine.WasNewTouch())
            {
                // Start the line dragging by adding a CustomLineEntity to the engines WorldEntities
                this.pickedUp = true;

                this.currentLine = new CustomLineEntity();
                this.currentLine.Texture = dot;
                this.currentLine.Position = engine.MousePosition;
                this.currentLine.Points.Add(new Vector2(engine.MousePosition.X, engine.MousePosition.Y));
                this.engine.WorldEntities.Add(this.currentLine);
            }

            if (engine.WasTouchReleased())
            {
                //Create a new bot and stick it on the line we just created
                var bot = this.CreateBot(this.currentLine);
                this.engine.WorldEntities.Add(bot);

                this.pickedUp = false;
                this.currentLine = null;
            }

            if (this.pickedUp)
            {
                this.ContinueCreatingLine();
            }

            base.Update(gameTime);
        }

        private void ContinueCreatingLine()
        {
            // Get the position of the last item in the points
            Vector2 lastPos = this.currentLine.Points[this.currentLine.Points.Count - 1];

            // If the distance is greater than the width of a dot
            if (Vector2.Distance(engine.MousePosition, lastPos) > dot.Width)
            {
                // Get the direction from the last point
                Vector2 dir = engine.MousePosition - lastPos;
                dir.Normalize();

                // Push the new point out at by the width of a dot plus a gap
                int gap = 5;
                var newPosition = lastPos + (dir * (dot.Width + gap));

                // Add it on to the current lines list.
                var newPoint = new Vector2(newPosition.X, newPosition.Y);

                this.currentLine.Points.Add(newPoint);
            }
        }

        private BotEntity CreateBot(CustomLineEntity line)
        {
            BotEntity bot = new BotEntity();
            bot.Texture = this.botTexture;
            bot.Size = new Point(bot.Texture.Width, bot.Texture.Height);
            bot.LineToFollow = line;
            bot.Position = line.Position;
            bot.Layer = 1;
            return bot;
        }

        private void Clear()
        {
            this.engine.WorldEntities.Clear();
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            this.engine.Draw(this.spriteBatch);

            base.Draw(gameTime);
        }
    }

The bot is a little more complex and is slightly out of scope of the article. But i'll briefly go over what this entity does.

On creation it was supplied a CustomLineEntity class to use as it's path.
We want the bot to turn as it goes around the curvy line as it moves along it.
For this we use the TurnToFace and WrapAngle methods from the Chase And Evade sample on creators

http://creators.xna.com/en-US/sample/chaseevade

In the main Update of the BotEntity we first check if we have a valid target. If we don't we must be just starting or we've got to the end.
Use the first point in the list as our reference point.

public override void Update(float elapsed)
{
    if (!this.destinationReached && this.LineToFollow != null)
    {
        if (!this.target.HasValue)
        {
            if (this.LineToFollow.Points.Count > 0)
            {
                this.target = this.LineToFollow.Points[0].Position;
                this.targetIndex = 0;
            }
        }

If the bot does have a valid target, we should move towards it and turn smoothly.
If the distance is short enough to be considered reached, we move onto the next target.
If there's no next target available, we're at our destination.

this.MoveToTarget(elapsed);
if (Vector2.Distance(this.Position, this.target.Value) < distThreshold)
{
    if (targetIndex + 1 < this.LineToFollow.Points.Count)
    {
        this.targetIndex++;
        this.target = this.LineToFollow.Points[this.targetIndex].Position;
    }
    else
    {
        this.destinationReached = true;
    }
}

I'm not going to print out the TurnToFace method here, but this is how we use it to turn and move towards our target.
We get the direction between our current position and our target.
We then use the simple calculation of direction * speed * time to move it smoothly towards the target.
And finally we set the rotation using the TurnToFace method.

private void MoveToTarget(float elapsed)
{
    var direction = this.target.Value - this.Position;
    if (direction == Vector2.Zero)
    {
        return;
    }

    direction.Normalize();

    const float speed = 50;
    const float turnSpeed = 0.025f;
    this.Position += direction * speed * elapsed;
    this.Rotation = TurnToFace(this.Position, this.target.Value, this.Rotation, turnSpeed);
}

If we now run our sample we can drag a curvy dotted line onto the screen with our mouse and a world object will follow it nicely :) cool uh?

Now we need to make this work on the Zune.
First we must create a new project type for Zune. this is easy using the built in visual studio create copy feature.

Luckily for us, the Micro Engine already supports Zune and has the code to treat a touch just like a mouse touch.

It has a few #if(ZUNE) batches of code inside WasTouchReleased and WasNewTouch

They look like this

public bool WasNewTouch()
{
    if (this.LastMouseState.LeftButton == ButtonState.Released &&
        this.CurrentMouseState.LeftButton == ButtonState.Pressed)
    {
        return true;
    }
#if(ZUNE)
    if (this.currentTouchState != null)
    {
        if (currentTouchState.State == TouchLocationState.Pressed)
        {
            if (lastTouchState != null)
            {
                if (lastTouchState.State == TouchLocationState.Released)
                {
                    return true;
                }
            }
            else
            {
                return true;
            }
        }
    }
#endif

    return false;
}

public bool WasTouchReleased()
{
    if (this.LastMouseState.LeftButton == ButtonState.Pressed &&
        this.CurrentMouseState.LeftButton == ButtonState.Released)
    {
        return true;
    }
#if(ZUNE)
    if (this.currentTouchState != null)
    {
        if (currentTouchState.State == TouchLocationState.Released)
        {
            return true;
        }
    }
#endif

    return false;
}

This now runs on the Zune HD :D

I'm sure there are lots of improvements that can be made here. But it's good enough for my prototype :)
The main one I would like to improve on in future would be the smoothness of the curve much like the Flight Control example at the beginning.

Here's the full source code
Download Source

About Sgt. Conker

The Sergeant!
Comments (0) Trackbacks (0)

No comments yet.


Leave a comment


No trackbacks yet.