Partager via


Silverlight Animation Part II: Sprites

In the previous post in this series, you learned how to create a simple animation with Silverlight. The next step is to learn how to create animated objects called sprites. This post will also explain how to ensure that a Sprite’s movement is restricted to a bounded area, as shown below in Application 1. Logic like this can be used in simple games, or in programs that want to use animation to capture or focus the user’s attention.

Application 1: Two sprites moving in a bounded area

The XAML

As shown below in Listing 1, the XAML for this application is very simple. A light green Grid hosts a TextBlock and a dark green Canvas named myCanvas. The Canvas contains a red and blue Rectangle named myRect and purple and blue gradient Image named myImage. The TextBlock, shown at the top of the application, display a string showing the width and height of the Canvas, and the current X and Y location of the Rectangle.

Listing 1: The XAML which serves as the starting point for the simple animation shown in above in Application 1.

 <UserControl x:Class="SilverlightAnimatedTimer01.Page"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="LightGreen">            
        
        <TextBlock x:Name="myTextBlock" TextAlignment="Center" />
        
        <Canvas Background="Green" Name="myCanvas" Height="250" Width="350" Loaded="StartTimer">             
            
            <Rectangle Stroke="Red" StrokeThickness="5" 
                       Width="25" Height="25" 
                       Fill="Blue" 
                       Name="myRect" />                            
            
            <Image Source="Images/MrBlue.png" 
                   Name="myImage" />
            
        </Canvas>
    </Grid>
</UserControl>

The Canvas object has a Loaded event associated with it, which is used to start a Timer. Code of this type was described in the previous article in this series, so I will not discuss the implementation of the Timers again in this post. I will however, reference the relevant code once near the end of this article.

Note also that from a technical perspective it doesn’t matter which control control contains the Loaded event. In this case I’ve chosen the Canvas, but I could just as easily have moved it to the TextBlock. The decision to associate the event with the Canvas was an aesthetic, rather than a technical, decision.

C# Code

The code we create for this program is broken up into two files. The first file defines  the Sprites that move across the surface of the Canvas, and the second part defines the program’s very simple logic and structure. Again, for aesthetic, rather than technical reasons, I decided to put the Sprite code in its own library. This helps to establish a design principle that I believe will prove useful as the program’s code grows more complex. However, from a technical perspective, the code could just as easily have been placed in the main program.

Figure 1 gives an overview of the program’s structure. The main program logic is stored in the Silverlight project, primarily in the file called GameCore.cs. The logic for the Sprites is stored in the SilverlightGameLibrary, primarily in the file called SpritesLib.cs. Again, I do this not out of necessity, but simply because I feel it povides a neat division of labor that helps make the program easy to maintain. Notice also that since libraries form discrete reusable blocks of code, they could easily be transferred to a second project which required similar logic.

Figure00

Figure 1: The Solution Explorer provides an overview of the Solution’s structure. Note that there are three projects in this solution, one of the Silverlight interface, one for ASP.NET web code, and one of the library where shared code can be stored.

The Sprites

In applications of this type, it is simplest to create a small Sprite class that encapsulates the behavior of the objects that move across the screen. There are many different ways to compose such a class, but most people follow a pattern similar to the one shown in Figure 2.

Figure01

Figure 2: A Simple Sprite class with two descendants, one that encapsulates the Rectangle type, and one that encapsulates the Image type.

The structure of this class hierarchy is easy to discern. A simple Sprite class has two descendants, one tailored for use with Rectangle controls, and one for use with Image controls. The common code which applies to both types of controls is stored in the base class, which is called Sprite.

With of some its lengthier methods foreshortened, the Sprite class looks like this:

 public class Sprite
{
    internal enum MoveType { None, Up, Down, DownLeft, DownRight, UpLef

    internal Rect containerBounds = new Rect();
    internal FrameworkElement SpriteShape { get; set; }

    public Double Y { get; set; }
    public Double X { get; set; }
    private Double incValue = 1.0;
    internal MoveType ShapeMoveType { get; set; }

    public Sprite()
    {
        ShapeMoveType = MoveType.None;
    }

    public Sprite(FrameworkElement initSpriteShape, Double initX, Doubl
        : this()
    {
        this.SpriteShape = initSpriteShape;
        this.X = initX;
        this.Y = initY;
    }

    internal void CheckBounds()
    {
       // Code omitted here        
    }

    virtual public void Move()
    {
        CheckBounds();
        // Code omitted here 
        SpriteShape.SetValue(Canvas.LeftProperty, X);
        SpriteShape.SetValue(Canvas.TopProperty, Y);
    }    
     public void SetBounds(int initWidth, int initHeight)
    {
        containerBounds.X = 0;
        containerBounds.Y = 0;
        containerBounds.Height = initHeight;
        containerBounds.Width = initWidth;
    }
}

The heart of the Sprite class are the SpriteShape, X and Y fields:

 internal FrameworkElement SpriteShape { get; set; }

public Double Y { get; set; }
public Double X { get; set; }

These two points define the upper left corner of a sprite. The Y field is the top bound, and the X field defines the left bound of the sprite. The SpriteShape field derives from FrameworkElement, which is a base class for both the Image and the Rectangle type. This base type contains enough logic to allow us to use the type to animate the Sprite as it moves across the canvas.

A method called SetBounds is used to define the range over which we want the Sprite to travel. In this program, the Rect type called containerBounds is initialized to values defining the Left, Top, Width and Height of the Canvas on which the sprites will live. The code states, in effect, that the sprites are not allowed to wander outside the bounds of the Canvas. We will revisit this code in the next section of the program, where you will see exactly how the bounds are initialized.

An enumeration captures the six basic movements of which our sprites are capable:

 internal enum MoveType { None, Up, Down, DownLeft, DownRight, UpLeft, UpRight };

In a more complex program, we could replace these simple movements with complex calculations, or even with code based on the physics of a particular object that you might want to encapsulate in your program. For instance, advanced mathematicians could describe the physics of a pool table, or of the waves on an ocean. In this case, however, the simple type defined here meets our needs.

A lengthy but logically very simple method called CheckBounds is used to compare the current X and Y location of the Sprite with the known bounds of its container. If the Sprite threatens to move outside these bounds, then is course is changed.

NOTE: Code I will show you later initializes the direction the Sprite is moving as the program begins. For instance, the Image control is initialized to begin moving down and to the right (MoveType.DownRight), while the Rectangle control is initialized to move down and to the left (MoveType.DownLeft).

An abbreviated version of the CheckBounds method is shown in the excerpt from the Sprite class shown above. Here is the complete definition of the method:

 internal void CheckBounds()
{
    if ((Y + SpriteShape.ActualHeight) > containerBounds.Height)
    {
        if (ShapeMoveType == MoveType.DownRight)
        {
            ShapeMoveType = MoveType.UpRight;
        }
        else
        {
            ShapeMoveType = MoveType.UpLeft;
        }
    }
    else if (Y < 0)
    {
        if (ShapeMoveType == MoveType.UpRight)
        {
            ShapeMoveType = MoveType.DownRight;
        }
        else
        {
            ShapeMoveType = MoveType.DownLeft;
        }
    }
    else if (X + SpriteShape.ActualWidth > containerBounds.Width)
    {
        if (ShapeMoveType == MoveType.UpRight)
        {
            ShapeMoveType = MoveType.UpLeft;
        }
        else
        {
            ShapeMoveType = MoveType.DownLeft;
        }
    }
    else if (X < 0)
    {
        if (ShapeMoveType == MoveType.DownLeft)
        {
            ShapeMoveType = MoveType.DownRight;
        }
        else
        {
            ShapeMoveType = MoveType.UpRight;
        }
    }
}

The first line of code in this method checks if the sprite is about to wander off the lower edge of the Canvas. If it is, then its trajectory is changed from a downward to an upward direction. In particular, if the shape is moving down and to the right, then it is told to start moving up and to the right, and so on.

The first else block in the code checks if the Sprite is about to move off the top of the canvas. If it is, then it is told to begin moving in a downward direction, and so on.

The Move method defines the engine the actually drives a Sprite across the canvas. Again, an abbreviated version of this method was shown in the original version of the Sprite class shown above. Here is the complete method:

 virtual public void Move()
{
    CheckBounds();

    switch (ShapeMoveType)
    {
        case MoveType.None:
            break;

        case MoveType.DownRight:
            X += incValue;
            Y += incValue;
            break;

        case MoveType.DownLeft:
            X -= incValue;
            Y += incValue;
            break;

        case MoveType.UpRight:
            X += incValue;
            Y -= incValue;
            break;

        case MoveType.UpLeft:
            X -= incValue;
            Y -= incValue;
            break;

        default:
            throw new Exception("Bad move type");
    }

    SpriteShape.SetValue(Canvas.LeftProperty, X);
    SpriteShape.SetValue(Canvas.TopProperty, Y);
}

The code begins by calling the CheckBounds method, to ensure that the Sprite is not set on a course that will allow it to wander off the straight and narrow path of virtue. The program then enters a switch statement, which defines how to move the sprite in five of the seven possible directions in which it can move. This program never moves the sprite straight up or straight down, though it would not be hard to see how to define the code to implement that logic. A value called incValue is declared to define how far the sprite moves. In this version of the program, that value is set to one, which means that the sprite moves one pixel at a time. If you wanted the sprite to move faster, you could set this value to higher number, or you could change the rate at which the program’s timer is called.

The logic for moving the sprite down and to the left and right, or up and to the left and right, is so simple that I will not enumerate it here. it is simpler for you to simply look at the code in the Move method, and to derive your understanding of its logic from what you see there.

If you look up at Figure 1, you can see that I create two simple descendants of the Sprite class: one for the Rectangle and one for the Image. I do place a small amount of logic in these classes, but really, they are place holders in case we find some reason later on for customizing the behavior of the Rectangle or of the Image. For instance, if you wanted to change the color of the border of the Rectangle, that logic would apply only to Rectangles, and not to Images. Hence it would belong in the MrRect class, and not in the base class.

I’ll show you only one of these classes, since they are nearly identical:

 public class MrBlue : Sprite
{
    public Image myImage { get; set; }

    public MrBlue()
        : base()
    {

    }

    public MrBlue(Image initImage,
        Double initX, Double initY)
        : base(initImage, initX, initY)
    {
        myImage = initImage;
        ShapeMoveType = MoveType.DownRight;
    }        
}

You can see that this class initializes the direction the Image moves in, setting it to MoveType.DownRight.  I also save a copy of the original shape. Neither of these bits of logic really justifies the existence of this class, but I create it anyway because I feel – for the reasons outlined above -- that it is likely to be useful in later iterations of this type of program.

Program Logic

Now that the Sprite logic is defined, the program logic is very easy to write. I define a simple class called GameCore, and create an instance of MrBlue and MrRect inside it. Also included in the class is a method called:

  • MainLoop for driving the movement of the sprites
  • ShowProgramData for updating the user with an ongoing description of the program’s state
  • InitBounds for defining the range inside which the Sprites can move.
 public class GameCore
{
    private MrBlue mrBlue = null;
    private MrRect mrRect = null;
    private Page page = null;        

    public GameCore(Page page)
    {
        this.page = page;
        mrBlue = new MrBlue(page.myImage, 0.0, 0.0);
        mrRect = new MrRect(page.myRect, 325.0, 0.0);
        InitBounds();
    }

    public void MainLoop()
    {
        mrRect.Move();
        mrBlue.Move();
        ShowProgramData();
    }

    private void ShowProgramData()
    {
        string xValue = Strings.PadLeft(mrRect.X.ToString(), '0', 3);
        string yValue = Strings.PadLeft(mrRect.Y.ToString(), '0', 3);
        if (Double.IsNaN(page.myCanvas.ActualHeight) || Double.IsNaN(page.myCanvas.ActualWidth))
        {
            page.myTextBlock.Text = "Not a number";
        }
        else
        {
            page.myTextBlock.Text = "Height " + page.myCanvas.ActualHeight.ToString()
                + " " + "Width = " + page.myCanvas.ActualWidth.ToString()
                + " " + "X = " + xValue
                + " " + "Y = " + yValue;                
        }
    }

    private void InitBounds()
    {
        mrRect.SetBounds(Convert.ToInt32(page.myCanvas.ActualWidth), Convert.ToInt32(page.myCanvas.ActualHeight));
        mrBlue.SetBounds(Convert.ToInt32(page.myCanvas.ActualWidth), Convert.ToInt32(page.myCanvas.ActualHeight));
    }
}

The constructor is used simply to initialize the starting places for the Image and Rectangle controls, and to initializes the bounds over which they can range.

The timer for the program is implemented in the file called by default Page.xaml.cs. As described in previous posts, this file is auto-generated by the Visual Studio IDE. It is used to drive the program logic by repeatedly calling the method called MainLoop. Here is the implementation of the Timer:

 public partial class Page : UserControl
{

    GameCore gameCore = null;

    public Page()
    {
        InitializeComponent();
        gameCore = new GameCore(this);                   
    }

    private void StartTimer(object sender, RoutedEventArgs e)
    {
        System.Windows.Threading.DispatcherTimer myDispatcherTimer = 
            new System.Windows.Threading.DispatcherTimer();
        myDispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 10); 
        myDispatcherTimer.Tick += new EventHandler(Each_Tick);
        myDispatcherTimer.Start();
    }        
    
    public void Each_Tick(object o, EventArgs sender)
    {
        gameCore.MainLoop();
    }
}

The MainLoop method calls the Move methods of the two shape controls:

 public void MainLoop()
{
    mrRect.Move();
    mrBlue.Move();
    ShowProgramData();
}

It also calls a method named ShowProgramData, which allows you to display any other information you want to share with the user. In this case, I use it to display in a TextBlock the current location of the Rectangle control.

Summary

In this post, you have seen how to create a simple Silverlight animation which moves simple sprites inside a bounded rectangle. The post describes how to create simple Sprite classes that know how to move in pre-defined directions across a drawing surface, and which know how to limit their movement to a bounded rectangle.

There is quite a bit of code in this post, but most of it is extremely simple. In general, programs of this type are very easy to create, but they provide you with a framework from which you can build a wide variety of animations that can be used to decorate a standard program or to serve as the core engine for a simple game.

Download the code from the LINQ Farm on Code Gallery.

kick it on DotNetKicks.com

Comments

  • Anonymous
    April 22, 2009
    You've been kicked (a good thing) - Trackback from DotNetKicks.com

  • Anonymous
    April 22, 2009
    Thank you for submitting this cool story - Trackback from DotNetShoutout

  • Anonymous
    April 22, 2009
    The comment has been removed

  • Anonymous
    April 23, 2009
    Charlie Calvert's Community Blog : Silverlight Animation Part II: Sprites

  • Anonymous
    April 27, 2009
    This is exactly the kind of Silverlight stuff that I (at least) could care less about - Show me data-connected apps! Animation may be cool n all, but what the real world cares about is securely presenting and maintaining data. Period.

  • Anonymous
    April 30, 2009
    I noticed that the code is cut off to the right mergen.  I am not sure if this is due to my browser or how the article is posted on the page.

  • Anonymous
    May 04, 2009
    jcraigue. I, for one, welcome our new graphic overlords. If you or anyone else wishes to ignore the lesson in the blog, feel free to discount it, but to deride it is bothersome. We wish commentary based on the content, not your own personal needs. Please utize any search engine you wish for data applicable to your predicament, but leave topics that do not pertain to your self-fulfillment clean of... well, you. Period.

  • Anonymous
    May 06, 2009
    Great article.  I really like how you separated out the timer-driven animation into C# code, leaving the XAML for the basic layout of the form. This makes it intuitive and readable. So many WPF/Silverlight examples try to cram everything into the XAML, including the animation and transforms.

  • Anonymous
    May 07, 2009
    Great article Charlie. Thanks for the silverlight articles (Part I and II).

  • Anonymous
    January 06, 2010
    Wow.. great article .. thanks for posting buddy