Animating graphic objects in Windows Forms.

A cool animation is great to look at but how do you go about writing one? Games in particular can use animations of hundreds of objects at once. What defines an object? How does an object move around?

These questions are often seen in the various online forums but a comprehensive answer is difficult to give in a short post. This article will explain all the nitty-gritty details. In it, you will see how to create an application that bounces a few hundred animated shapes around screen in real-time.

Graphical objects.

GDI+ has no concept of a persistent graphical object such as a square or line that you can move from place to place or touch with the mouse cursor. The graphics are written to the display surface and then forgotten about completely. In order to have an object that can be repositioned or made larger and smaller we must create a retained mode graphics engine.

The basic thing we need to do is to define some sort of entity, a generic shape perhaps, that has the attributes of position, size and colour. This entity will know how to draw itself to a graphics object. There may be more than one type of shape, such as a rectangle, a pentagon and a star but they will all generally obey the same rules so object oriented architectures enable us to create a "shape" base class with all the basic attributes and then override the small details when we create our specialized objects.

The architecture for this particular retained mode system will contain a shape with the following basic capabilities:

  • Location. The X, Y position of the center of the shape object.

  • Size. The width and height of the shape.

  • BackColor. The colour of the background or interior.

  • ForeColor. The colour of the foreground or the outline of the object

  • LineThickness. The width of the line that surrounds the object.

  • Transparency. A measure of opacity of the object.

  • Rotation. The angle, in degrees to which to object is rotated.

  • Vector. The direction and speed at which this object moves

  • RotationDelta. The number of degrees that the angle of rotation will change at each animation step.

  • Limits. A rectangle that the object is not allowed to go outside of.

Here are the beginnings of the Shape class.

  public class Shape

  {

    Point _location;

    public Point Location

    {

      get{return _location;}

      set{_location=value;}

    }

 

    Size _size;

    public Size Size

    {

      get{return _size;}

      set{_size=value;}

    }

 

    Color _backColor;

    public Color BackColor

    {

      get{return _backColor;}

      set{_backColor=value;}

    }

 

    Color _foreColor;

    public Color ForeColor

    {

      get{return _foreColor;}

      set{_foreColor=value;}

    }

 

    int _lineThickness;

    public int LineThickness

    {

      get{return _lineThickness;}

      set{_lineThickness=value;}

    }

 

    float _transparency;

    public float Transparency

    {

      get{return _transparency;}

      set

      {

        _transparency=(value>=0 ? (value<=1 ? value : 1) : 0);

      }

    }

 

    float _rotation;

    public float Rotation

    {

      get{return _rotation;}

      set{_rotation=value;}

    }

 

    Size _vector;

    public Size Vector

    {

      get{return _vector;}

      set{_vector=value;}

    }

 

    float _rotationDelta;

    public float RotationDelta

    {

      get{return _rotationDelta;}

      set{_rotationDelta=value;}

    }

 

    Rectangle _limits;

    public Rectangle Limits

    {

      get{return _limits;}

      set{_limits=value;}

    }

  }

 

   Public Class Shape

    Private _location As Point

    

    Public Property Location() As Point

     Get

      Return _location

     End Get

     Set

      _location = value

     End Set

    End Property

    Private _size As Size

    

    Public Property Size() As Size

     Get

      Return _size

     End Get

     Set

      _size = value

     End Set

    End Property

    Private _backColor As Color

    

    Public Property BackColor() As Color

     Get

      Return _backColor

     End Get

     Set

      _backColor = value

     End Set

    End Property

    Private _foreColor As Color

    

    Public Property ForeColor() As Color

     Get

      Return _foreColor

     End Get

     Set

      _foreColor = value

     End Set

    End Property

    Private _lineThickness As Integer

    

    Public Property LineThickness() As Integer

     Get

      Return _lineThickness

     End Get

     Set

      _lineThickness = value

     End Set

    End Property

    Private _transparency As Single

    

    Public Property Transparency() As Single

     Get

      Return _transparency

     End Get

     Set

      _transparency = IIf(value >= 0, IIf(value <= 1, value, 1), 0)

     End Set

    End Property

    

    Private _rotation As Single

    

    Public Property Rotation() As Single

     Get

      Return _rotation

     End Get

     Set

      _rotation = value

     End Set

    End Property

    Private _vector As Size

    

    Public Property Vector() As Size

     Get

      Return _vector

     End Get

     Set

      _vector = value

     End Set

    End Property

    Private _rotationDelta As Single

    

    Public Property RotationDelta() As Single

     Get

      Return _rotationDelta

     End Get

     Set

      _rotationDelta = value

     End Set

    End Property

    Private _limits As Rectangle

    

    Public Property Limits() As Rectangle

     Get

      Return _limits

     End Get

     Set

      _limits = value

     End Set

    End Property

    

   End Class 'Shape

Transforms and order.

When creating such a retained-mode system, the individual objects will position themselves before they draw the content of the object. The easiest way to do this is with a graphic transform represented by a matrix. Many beginners see the transform as something that is applied to the whole page to scale the drawing or modify the coordinate system in some way but in retained mode systems the transform is generally used on a per-object basis in addition to whatever global transform is applied. Usually the current state of the Graphics system should be saved, the transform applied, the drawing done and the previous state of the graphics system restored before drawing of other objects takes place. This lets your code maintain an orderly and progressive series of state-changes rather than trying to apply a transform to create a particular state an then apply another to get to a second state. More complex systems that have hierarchies of objects will maintain a graphics stack with many levels of transforms. This demonstration only requires one.

The order that transforms are applied is of paramount importance. Applying two identical transforms in different orders will produce wildly different results. This animation system uses rotation to spin an object in space and displacement or translation to move it in an X or Y plane. Although it isn't implemented in this demo objects can also be scaled to make them larger or smaller or distort them along the axes. The order that transforms should be applied is generally Scale, Rotate, Translate. The code for the shape base-class provides a method for saving the state of the current Graphics object, applying a transform to the Graphics, drawing and then restoring the state of the Graphics object once more. The methods shown below show how the object sets up the transform, draws the object and then restores the state of the Graphics object.

    GraphicsState _state;

 

    /// <summary>

    /// Sets up the transform for each shape

    /// </summary>

    /// <remarks>

    /// As each shape is drawn the transform for that shape including rotation and

    /// location is made to a new Matrix object.

    /// This matrix is used to modify the graphics transform <i>For each shape</i>

    /// </remarks>

    /// <param name="g">The Graphics being drawn on</param>

    protected void SetupTransform(Graphics g)

    {

      _state=g.Save();    

      Matrix mx=new Matrix();

      mx.Rotate(_rotation,MatrixOrder.Append);

      mx.Translate(this.Location.X,this.Location.Y,MatrixOrder.Append);

      g.Transform=mx;

    }

 

    /// <summary>

    /// Simply restores the original state of the Graphics object

    /// </summary>

    /// <param name="g">The Graphics object being drawn upon</param>

    protected void RestoreTransform(Graphics g)

    {

      g.Restore(_state);

    }

 

    public void Draw(Graphics g)

    {

      SetupTransform(g);

      RenderObject(g);

      RestoreTransform(g);

    }

    Private _state As GraphicsState

    

    

    '/ <summary>

    '/ Sets up the transform for each shape

    '/ </summary>

    '/ <remarks>

    '/ As each shape is drawn the transform for that shape including rotation and location is made to a new Matrix object.

    '/ This matrix is used to modify the graphics transform <i>For each shape</i>

    '/ </remarks>

    '/ <param name="g">The Graphics being drawn on</param>

    Protected Sub SetupTransform(g As Graphics)

     _state = g.Save()

     Dim mx As New Matrix()

     mx.Rotate(_rotation, MatrixOrder.Append)

     mx.Translate(Me.Location.X, Me.Location.Y, MatrixOrder.Append)

     g.Transform = mx

    End Sub 'SetupTransform

    

    

    '/ <summary>

    '/ Simply restores the original state of the Graphics object

    '/ </summary>

    '/ <param name="g">The Graphics object being drawn upon</param>

    Protected Sub RestoreTransform(g As Graphics)

     g.Restore(_state)

    End Sub 'RestoreTransform

    

    

    Public Sub Draw(g As Graphics)

     SetupTransform(g)

     RenderObject(g)

     RestoreTransform(g)

    End Sub 'Draw

    

Animating the object.

Animation systems generally work from a timebase of some kind. In Windows Forms there are several sorts of timers that we can use to give the animated objects a "pulse". This system provides each object with a "Tick" method that uses the Vector to move a shape, the RotationDelta to rotate the shape, and the Limits to ensure that the shape remains inside the desired area while it bounces around the screen. The following listing shows how the Tick method performs the animations

    public virtual void Tick()

    {

      //ensure that the object is in the page.

      //this is in case the window was resized

      if(this.Location.X>this.Limits.Right)

        this.Location=new Point(this.Limits.Right-1,this.Location.Y);

      if(this.Location.Y>this.Limits.Bottom)

        this.Location=new Point(this.Location.X,this.Limits.Bottom-1);

 

      //Generate a new location adding in the vectors

      //check the limits and switch vector directions as needed

      int newx=this.Location.X+this.Vector.Width;

      if(newx>this.Limits.Right || newx<this.Limits.Left)

        this.Vector=new Size(-1*this.Vector.Width,this.Vector.Height);

      int newy=this.Location.Y+this.Vector.Height;

      if(newy>this.Limits.Bottom || newy<this.Limits.Top)

        this.Vector=new Size(this.Vector.Width,-1*this.Vector.Height);

 

      //This is the new position

      Location=new Point(

        this.Location.X+this.Vector.Width,

        this.Location.Y+this.Vector.Height);

 

      //Apply the rotation factor

      this.Rotation+=this.RotationDelta;

      //Limit just to be neat

      Rotation=(Rotation<360f ? (Rotation>=0 ? Rotation : Rotation+360f) : Rotation-360f);

    }

    Public Overridable Sub Tick()

     'ensure that the object is in the page.

     'this is in case the window was resized

     If Me.Location.X > Me.Limits.Right Then

      Me.Location = New Point(Me.Limits.Right - 1, Me.Location.Y)

     End If

     If Me.Location.Y > Me.Limits.Bottom Then

      Me.Location = New Point(Me.Location.X, Me.Limits.Bottom - 1)

     End If

     'Generate a new location adding in the vectors

     'check the limits and switch vector directions as needed

     Dim newx As Integer = Me.Location.X + Me.Vector.Width

     If newx > Me.Limits.Right OrElse newx < Me.Limits.Left Then

      Me.Vector = New Size(- 1 * Me.Vector.Width, Me.Vector.Height)

     End If

     Dim newy As Integer = Me.Location.Y + Me.Vector.Height

     If newy > Me.Limits.Bottom OrElse newy < Me.Limits.Top Then

      Me.Vector = New Size(Me.Vector.Width, - 1 * Me.Vector.Height)

     End If

     'This is the new position

     Location = New Point(Me.Location.X + Me.Vector.Width, Me.Location.Y + Me.Vector.Height)

     

     'Apply the rotation factor

     Me.Rotation += Me.RotationDelta

     'Limit just to be neat

     Rotation = IIf(Rotation < 360F, IIf(Rotation >= 0, Rotation, Rotation + 360F), Rotation - 360F)

    End Sub 'Tick

 

Drawing the object.

Finally, after all the setup work is done, the shape must draw itself onto the display. The Shape class provides a RenderObject virtual method to accomplish this. Derived classes can override this method and draw their own appearance when asked to do so. The Draw method provided by the base class performs the additional tasks of setting up and restoring the object.

    public virtual void RenderObject(Graphics g)

    {

    }

   Public Overridable Sub RenderObject(g As Graphics)

   End Sub 'RenderObject

Real shapes.

Now that the groundwork is laid we can construct real shapes. These override the Shape class and draw themselves in the RenderObject override. The following listing shows three shapes, a Square, a Star and a Pentagon that render themselves after the base class Draw routine has done all the setup work.

  public class Square : Shape

  {

    /// <summary>

    /// Draws a square. Note that the square is drawn about the origin.

    /// </summary>

    /// <param name="g">The graphics to draw on.</param>

    public override void RenderObject(Graphics g)

    {

      Pen p=new Pen(ForeColor,LineThickness);

      SolidBrush sb=new SolidBrush(

                 Color.FromArgb((int)(255*this.Transparency),

                 this.BackColor));

      g.FillRectangle(sb,

                 -this.Size.Width/2,

                 -this.Size.Height/2,

                 this.Size.Width,

                 this.Size.Height);

      g.DrawRectangle(p,

                 -this.Size.Width/2,

                 -this.Size.Height/2,

                 this.Size.Width,

                 this.Size.Height);

      sb.Dispose();

      p.Dispose();

    }

 

  }

 

  public class Star : Shape

  {

    /// <summary>

    /// Draws a star. Note that the star is drawn about the origin.

    /// </summary>

    /// <param name="g">The graphics to draw on.</param>

    public override void RenderObject(Graphics g)

    {

      Pen p=new Pen(ForeColor,LineThickness);

      SolidBrush sb=new SolidBrush(

                 Color.FromArgb((int)(255*this.Transparency),

                 this.BackColor));

      Point[] pts=new Point[11];

      bool pointy=true;

      float a=0;

      for(int c=0;c<10;c++)

      {

        float dist=pointy ? 1 : 0.6f;

        pts[c]=new Point(

            (int)(dist* (this.Size.Width/2)*Math.Cos(a)),

            (int)(dist *(this.Size.Height/2)*Math.Sin(a)));

        a+=(float)Math.PI*2/10;

        pointy=!pointy;

      }

      pts[10]=pts[0];

      g.FillPolygon(sb,pts);

      g.DrawPolygon(p,pts);

      sb.Dispose();

      p.Dispose();

    }

  }

 

  public class Pentagon : Shape

  {

    /// <summary>

    /// Draws a pentagon. Note that the pentagon is drawn about the origin.

    /// </summary>

    /// <param name="g">The graphics to draw on.</param>

    public override void RenderObject(Graphics g)

    {

      Pen p=new Pen(ForeColor,LineThickness);

      SolidBrush sb=new SolidBrush(

                 Color.FromArgb((int)(255*this.Transparency),

                 this.BackColor));

      Point[] pts=new Point[6];

      float a=0;

      for(int c=0;c<5;c++)

      {

        pts[c]=new Point(

            (int)((this.Size.Width/2)*Math.Cos(a)),

            (int)((this.Size.Height/2)*Math.Sin(a)));

        a+=(float)Math.PI*2/5;

      }

      pts[5]=pts[0];

      g.FillPolygon(sb,pts);

      g.DrawPolygon(p,pts);

      sb.Dispose();

      p.Dispose();

    }

 

  }

 

   Public Class Square

    Inherits Shape

    

    '/ <summary>

    '/ Draws a square. Note that the square is drawn about the origin.

    '/ </summary>

    '/ <param name="g">The graphics to draw on.</param>

    Public Overrides Sub RenderObject(g As Graphics)

     Dim p As New Pen(ForeColor, LineThickness)

     Dim sb As New SolidBrush( _

               Color.FromArgb(CInt(255 * Me.Transparency), _

               Me.BackColor))

      g.FillRectangle(sb, _

                      CSng(-Me.Size.Width / 2), _

                      CSng(-Me.Size.Height / 2), _

                      CSng(Me.Size.Width), _

                      CSng(Me.Size.Height))

      g.DrawRectangle(p, _

                      CSng(-Me.Size.Width / 2), _

                      CSng(-Me.Size.Height / 2), _

                      CSng(Me.Size.Width), _

                      CSng(Me.Size.Height))

     sb.Dispose()

     p.Dispose()

    End Sub 'RenderObject

   End Class 'Square

  

  

   Public Class Star