Mobile Sprites



Now, I know we have a SpriteAnimation class that we can move around, but wouldn't it be nice if our sprites did some of that work for us? That's where our next class comes in. The MobileSprite class will be responsible for the following:

  • Providing an interface to the SpriteAnimation objects below
  • Move sprites at a defined speed towards a targeted point
  • Allow a "path" of points to be assigned to a sprite
  • Provide collision information

Sounds complicated (especially that whole "path of points" thing) but in reality it won't be that bad. Lets add a new class to the project and call it MobileSprite. As normal, we need the using statements:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;


We're going to declare a bunch of variables, but I've left all the comments in. I'll discuss a few of them below the code block:

        // The SpriteAnimation object that holds the graphical and animation data for this object
        SpriteAnimation asSprite;
 
        // A queue of pathing vectors to allow the sprite to move along a path
        Queue<Vector2> queuePath = new Queue<Vector2>();
 
        // The location the sprite is currently moving towards
        Vector2 v2Target;
 
        // The speed at which the sprite will close with it's target
        float fSpeed = 1f;
 
        // These two integers represent a clipping range for determining bounding-box style
        // collisions.  They return the bounding box of the sprite trimmed by a horizonal and
        // verticle offset to get a collision cushion
        int iCollisionBufferX=0;
        int iCollisionBufferY=0;
 
        // Determine the status of the sprite.  An inactive sprite will not be updated but will be drawn.
        bool bActive = true;
 
        // Determines if the sprite should track towards a v2Target.  If set to false, the sprite
        // will not move on it's own towards v2Target, and will not process pathing information
        bool bMovingTowardsTarget = true;
 
        // Determines if the sprite will follow the path in it's Path queue.  If true, when the sprite
        // has reached v2Target the next path node will be pulled from the queue and set as
        // the new v2Target.
        bool bPathing = true;
 
        // If true, any pathing node popped from the Queue will be placed back onto the end of the queue
        bool bLoopPath = true;
 
        // If true, the sprite can collide with other objects.  Note that this is only provided as a flag
        // for testing with outside code.
        bool bCollidable = true;
 
        // If true, the sprite will be drawn to the screen
        bool bVisible = true;
 
        // If true, the sprite will be deactivated when the Pathing Queue is empty.
        bool bDeactivateAtEndOfPath = false;
 
        // If true, bVisible will be set to false when the Pathing Queue is empty.
        bool bHideAtEndOfPath = false;
 
        // If set, when the Pathing Queue is empty, the named animation will be set as the
        // current animation on the sprite.
        string sEndPathAnimation = null;


Ok, so many of these are self explanatory (bVisible, etc), but several deserve some explanation:

v2Target : Our MobileSprite class supports automatic movement of the sprite towards a targeted point. The v2Target variable holds the current point we are moving towards (if automatic moving is enabled).

iCollisionBufferX, and iCollisionBufferY : One of the properties we will be adding is a CollisionBox property, which is simply the sprite's bounding box with the edges shrunken in by these two values. This has the effect of giving the collision detection a bit of a cushion to make it look more accurate.

bPathing and bLoopPath : These control how the Queue (I'll talk about it below) are used. If bPathing is false, then the queue isn't used at all. If bLoopPath is false, the path will be executed once and stop. If it is true, the path will be repeated.

bDeactivateAtEndOfPath and bHideAtEndOfPath : These booleans control what happens when the end of the path is reached (and bLoopPath is false).

sEndPathAnimation : If bLooopPath if false and then end of the path has been reached, the sprite will play this animation (if it is defined).

queuePath : Ok, I saved it for last for a good reason. There is a lot to talk about here. First, what is a queue? There are a number of Generic Collection structures in the .NET Framework, and Queue is one of them. Essentially all of these objects (List, Stack, Queue, and even the Dictionary we are using for animations) allow you to store groups of the same type of object in different ways. The key differences are how information gets in and out of the collection:

List : Objects are added with the .Add method and removed with .Remove. However, you cannot iterate over the list once you remove an item. You have to start back at the beginning again. Generally, you use a "for each" loop to go through the items in a list.

Dictionary : As discussed above, a dictionary has "key, value" pairs in it. You look up values by key. You could iterate over the Dictionary by getting the keys and then looping through them, but generally you wouldn't do that (or you would be using a List instead).

Stack : A Stack is just like a stack of papers on your desk. It is a Last-In-First-Out (LIFO) structure. If I put a piece of paper on top of the pile, it is the first one I'm going to get when I get something off the pile. Items are Pushed onto the stack and Popped off of the stack. Whatever was pushed last will be what you get when you pop.

Queue : A Queue is like a line at the bank. When you enter the queue (via the Enqueue method) you stand at the back of the line. Anyone who Enqueues after you is behind you, waiting their turn. At the counter, people are being "Dequeued" from the queue in the same order they entered the queue.

So, as you can see if the order that things were added to the collection is important (and it certainly is if we are setting down a path of points for our sprite to follow), the Queue is a good choice for a structure since we can pull the points off in order. We don't really care what happens to them after we are done with them (ok that isn't quite true as we will see later) and when the queue is empty we are done moving.

So now we need to add our properties. Again, there are a bunch of them:

        public SpriteAnimation Sprite
        {
            get { return asSprite; }
        }
 
        public Vector2 Position
        {
            get { return asSprite.Position; }
            set { asSprite.Position = value; }
        }
 
        public Vector2 Target
        {
            get { return v2Target; }
            set { v2Target = value; }
        }
 
        public int HorizontalCollisionBuffer
        {
            get { return iCollisionBufferX; }
            set { iCollisionBufferX = value; }
        }
 
        public int VerticalCollisionBuffer
        {
            get { return iCollisionBufferY; }
            set { iCollisionBufferY = value; }
        }
 
        public bool IsPathing
        {
            get { return bPathing; }
            set { bPathing = value; }
        }
 
        public bool DeactivateAfterPathing
        {
            get { return bDeactivateAtEndOfPath; }
            set { bDeactivateAtEndOfPath = value; }
        }
 
        public bool LoopPath
        {
            get { return bLoopPath; }
            set { bLoopPath = value; }
        }
 
        public string EndPathAnimation
        {
            get { return sEndPathAnimation; }
            set { sEndPathAnimation = value; }
        }
 
        public bool HideAtEndOfPath
        {
            get { return bHideAtEndOfPath; }
            set { bHideAtEndOfPath = value; }
        }
 
        public bool IsVisible
        {
            get { return bVisible; }
            set { bVisible = value; }
        }
 
        public float Speed
        {
            get { return fSpeed; }
            set { fSpeed = value; }
        }
 
        public bool IsActive
        {
            get { return bActive; }
            set { bActive = value; }
        }
 
        public bool IsMoving
        {
            get { return bMovingTowardsTarget; }
            set { bMovingTowardsTarget = value; }
        }
 
        public bool IsCollidable
        {
            get { return bCollidable; }
            set { bCollidable = value; }
        }
 
        public Rectangle BoundingBox
        {
            get { return asSprite.BoundingBox; }
        }
 
        public Rectangle CollisionBox
        {
            get
            {
                return new Rectangle(
                    asSprite.BoundingBox.X + iCollisionBufferX,
                    asSprite.BoundingBox.Y + iCollisionBufferY,
                    asSprite.Width - (2 * iCollisionBufferX),
                    asSprite.Height - (2 * iCollisionBufferY));
            }
        }


A couple of these (BoundingBox and Position) are simply pass-throughs to the underlying SpriteAnimation object. We could have just used the Sprite.Position notation, but having the properties at this level doesn't really hurt anything and is convenient.

There is only one property in the list that isn't a simple get/set value pair, and that is the CollisionBox property I mentioned earlier, which returns a bounding box with the edges pulled by the offset values.

The other important things to note here are the toggles:

IsMoving : If true, the sprite will move towards it's Target.

IsPathing : If true, the sprite will follow pathing points from queuePath.

LoopPath : If true, each pathing node will be requeued at the back of the queue after it is dequeued.

One last thing to note is that IsCollidable is not used for anything in the MobileSprite code itself. It is there for you to set when writing your own collision detection routines.

As with the SpriteAnimation class, the constructor for MobileSprite is very simple:

        public MobileSprite(Texture2D texture)
        {
            asSprite = new SpriteAnimation(texture);
        }


This is because, of course, we will be setting animations, paths, etc up for the sprite after it has been created.

Finally, a few methods. We'll do this in a couple of parts:

        public void AddPathNode(Vector2 node)
        {
            queuePath.Enqueue(node);
        }
 
        public void AddPathNode(int X, int Y)
        {
            queuePath.Enqueue(new Vector2(X, Y));
        }
 
        public void ClearPathNodes()
        {
            queuePath.Clear();
        }


These three methods are for managing the queuePath list. Two ways to add a note (via a Vector2 or with an X,Y) and a method to clear all nodes from the queue. Next, our Update and Draw:

        public void Update(GameTime gameTime)
        {
            if (bActive && bMovingTowardsTarget)
            {
                if (!(v2Target == null))
                {
                    // Get a vector pointing from the current location of the sprite
                    // to the destination.
                    Vector2 Delta = new Vector2(v2Target.X - asSprite.X, v2Target.Y - asSprite.Y);
 
                    if (Delta.Length() > Speed)
                    {
                        Delta.Normalize();
                        Delta *= Speed;
                        Position += Delta;
                    }
                    else
                    {
                        if (v2Target == asSprite.Position)
                        {
                            if (bPathing)
                            {
                                if (queuePath.Count > 0)
                                {
                                    v2Target = queuePath.Dequeue();
                                    if (bLoopPath)
                                    {
                                        queuePath.Enqueue(v2Target);
                                    }
                                }
                                else
                                {
                                    if (!(sEndPathAnimation == null))
                                    {
                                        if (!(Sprite.CurrentAnimation == sEndPathAnimation))
                                        {
                                            Sprite.CurrentAnimation = sEndPathAnimation;
                                        }
                                    }
 
                                    if (bDeactivateAtEndOfPath)
                                    {
                                        IsActive = false;
                                    }
 
                                    if (bHideAtEndOfPath)
                                    {
                                        IsVisible = false;
                                    }
                                }
                            }
                        }
                        else
                        {
                            asSprite.Position = v2Target;
                        }
                    }
                }
            }
            if (bActive)
                asSprite.Update(gameTime);
        }
 
        public void Draw(SpriteBatch spriteBatch)
        {
            if (bVisible)
            {
                asSprite.Draw(spriteBatch, 0, 0);
            }
        }


Again, Draw is super-simple (even moreso this time... we pass the 0, 0 for offsets as we aren't applying an offset to all of our sprites.

Update is, as usual, where everything happens. The first thing we do is check to see if we are active and moving. (If we are active but not moving we will still update the SpriteAnimation object but we won't run any of the moving/pathing code).

If we should be moving, we have to figure out where we are going to move to. In order to do so, we get a vector by subtracting our current position (where we are) from our Target position (where we want to be).

Next, we check the length of the Delta vector to see if we can move the full "speed" amount and still be on our way to the point. If so, we normalize the Delta vector, which sets it's length to 1 while keeping the directional information and multiply it by our speed factor. We add this to our current position.

If we couldn't move the full speed, one of two things is true. Either we are already sitting at the point, or we are close to it and should go to the point as our next move. We check to see if we are there first. If we are, we need to pull our next point out of queuePath and make it our newe v2Target. Notice that we check to see if bLoopPath is true. If it is, right after we dequeue the new target we enqueue it back onto the end of the queue. This has the effect of making our path a large loop which will never end.

If, however, there are no more points in queuePath (queuepath.Count is 0) then we check to see what we should do now that the end of the path has been reached. We may play an animation, deactivate the sprite, or hide the sprite at this point.

If we were "close to" our target point, but not quite there yet, we simply set our location to the target point and wait for the next loop to happen.

Finally, the last thing the Update method does is call Update for the underlying SpriteAnimation object, which will update our FrameAnimations.

(Continued in Part 5)



































 

 
 
Site Contents Copyright 2006 Full Revolution, Inc. All rights reserved.
This site is in no way affiliated with Microsoft or any other company.
All logos and trademarks are copyright their respective companies.
RSS FEED