[C# Helper]
Index Books FAQ Contact About Rod
[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

[C# 24-Hour Trainer]

[C# 5.0 Programmer's Reference]

[MCSD Certification Toolkit (Exam 70-483): Programming in C#]

Title: Use a sprite class to animate bouncing cats in C#

[Use a sprite class to animate bouncing cats in C#]

A sprite is an object that you use to control a single object in an animation or game. Normally a sprite class must do two things: draw its object and move its object.

This example defines an ImageSprite class that draws spinning, bouncing cat heads. The following sections describe the ImageSprite class and the main program.

ImageSprite

The following code shows the class's beginning.

class ImageSprite { public float Angle, DAngle; public PointF Center, Velocity; public Bitmap Picture; private float Radius; public ImageSprite(float angle, float dangle, PointF center, PointF velocity, Bitmap picture) { Angle = angle; DAngle = dangle; Center = center; Velocity = velocity; Picture = picture; Radius = Math.Min(picture.Width, picture.Height) / 2f; }

The class begins with fields that define the position and orientation of the sprite's image. The Angle value holds the angle at which the image is currently rotated. DAngle indicates the amount by which the Angle is modified whenever the sprite moves its image.

The Center value stores the image's location. The Velocity value's X and Y properties indicate how much the Center is updated in the X and Y directions.

Note that the DAngle and Velocity values are measured in change per second.

The Picture field holds the picture that the sprite should draw. All of this example's sprites hold the same cat image, but you could give different sprites different images if you like. Finally, the Radius field indicates the radius that the image should have.

The class's constructor simply initializes the sprite's fields.

The following code shows how the class updates the sprite's position and orientation.

public void Move(Rectangle bounds, float elapsed) { Center.X += Velocity.X * elapsed; float right = Center.X + Radius; if (right > bounds.Right) { right = bounds.Right - (right - bounds.Right); Center.X = right - Radius; Velocity.X = -Velocity.X; } float left = Center.X - Radius; if (left < 0) { left = -left; Center.X = left + Radius; Velocity.X = -Velocity.X; } Center.Y += Velocity.Y * elapsed; float bottom = Center.Y + Radius; if (bottom > bounds.Bottom) { bottom = bounds.Bottom - (bottom - bounds.Bottom); Center.Y = bottom - Radius; Velocity.Y = -Velocity.Y; } float top = Center.Y - Radius; if (top < 0) { top = -top; Center.Y = top + Radius; Velocity.Y = -Velocity.Y; } Angle += DAngle * elapsed; }

To update the Center field's X value, the method adds Velocity.X times the amount of time that has elapsed since the last time the Move method was called. Multiplying the change per second by elapsed seconds allows the program to keep roughly the same amount of movement no matter how quickly or slowly the program calls the Move method. That's helpful because the program's timer doesn't necessarily always fire with the same frequency. It may fire more or less quickly depending on the other programs running on the system and the number of sprites running in this program.

Next, the method checks the sprite's X coordinate to see if it is moving off of the left or right edges of the bounds passed into the method (which represent the edges of the program's form). If the X coordinate is moving out of bounds, the code updates the X coordinate and reverses the Velocity.X value so the sprite starts moving in the other direction.

The method then performs similar steps to update the sprite's Y position.

The method finishes by adding DAngle times the elapsed time to the Angle value to rotate the sprite.

The final part of the sprite class is the following Draw method.

public void Draw(Graphics gr) { GraphicsState state = gr.Save(); gr.ResetTransform(); gr.RotateTransform(Angle); gr.TranslateTransform(Center.X, Center.Y, MatrixOrder.Append); gr.DrawImage(Picture, new PointF(-Radius, -Radius)); gr.Restore(state); }

This method draws the sprite. It first saves the Graphics object's state so it won't interfere with any other graphics method that may run later. In this case, saving and restoring the state prevents the sprite from interfering with the other sprites.

The method then calls the Graphics object's ResetTransform method to remove any current transformations. It adds a rotation by the sprite's Angle and translates the sprite to place it at its Center. The method draws the sprite's image and the restores the Graphics object's state.

Main Program

The following code shows how the program initializes its sprites.

private List<ImageSprite> Sprites = new List<ImageSprite>(); private void Form1_Load(object sender, EventArgs e) { Random rand = new Random(); for (int i = 0; i < 10; i++) { float angle = rand.Next(-360, 360); float dangle = rand.Next(-180, 180); float scale = rand.Next(2, 5) / 20f; Bitmap bm = ResizeImage(Properties.Resources.cat, scale); float rx = bm.Width / 2f; float ry = bm.Height / 2f; PointF center = new PointF( rand.Next((int)rx, picCanvas.ClientSize.Width - (int)rx), rand.Next((int)ry, picCanvas.ClientSize.Height - (int)ry)); float vx = rand.Next(10, 50); if (rand.Next(0, 2) == 1) vx = -vx; float vy = rand.Next(10, 50); if (rand.Next(0, 2) == 1) vy = -vy; PointF velocity = new PointF(vx, vy); Sprites.Add(new ImageSprite(angle, dangle, center, velocity, bm)); } LastTime = DateTime.Now; tmrFrame.Enabled = true; }

This code starts by defining a list of ImageSprite objects. The form's Load event handler uses a loop to create 10 sprites. Most of the loop uses random numbers to initialize various sprite properties.

Possibly the most interesting piece of code here is the call to ReizeImage. That method simply takes an image as a parameter, resizes it, and returns the resized result. I'll describe that method shortly.

After it generates the sprite's parameters, the Load event handler uses them to create the sprite and adds the new sprite to the Sprites list.

The following code shows the ResizeImage method.

private Bitmap ResizeImage(Bitmap bm, float scale) { int width = (int)(bm.Width * scale); int height = (int)(bm.Height * scale); Bitmap result_bm = new Bitmap(width, height); using (Graphics gr = Graphics.FromImage(result_bm)) { PointF[] dest_points = { new PointF(0, 0), new PointF(width, 0), new PointF(0, height), }; RectangleF src_rect = new RectangleF( 0, 0, Properties.Resources.cat.Width, Properties.Resources.cat.Height); gr.DrawImage(Properties.Resources.cat, dest_points, src_rect, GraphicsUnit.Pixel); } return result_bm; }

This method calculates the scaled image's width and height, and makes a bitmap of that size. It creates an associated Graphics object, makes a dest_points array indicating where the image should be drawn on the new bitmap, and makes a src_rect indicating which part of the image to draw. The method calls the Graphics object's DrawImage method to draw the original method onto the new bitmap and returns the result.

When the main form's timer fires, the following code executes.

private DateTime LastTime; private void tmrFrame_Tick(object sender, EventArgs e) { DateTime now = DateTime.Now; float elapsed = (float)(now - LastTime).TotalSeconds; foreach (ImageSprite sprite in Sprites) { sprite.Move(picCanvas.Bounds, elapsed); } LastTime = now; picCanvas.Refresh(); }

The LastTime value records the last time that the timer fired. This form-level variable is initialized to the time when the program starts.

The timer's event handler gets the current time and subtracts LastTime from it to see how much time has passed since the last Tick event. The code then loops through the sprite's calling their Move methods and passing the method the number of seconds that have elapsed.

After it finishes moving the sprites, the code refreshes the picCanvas control to make the following Paint event handler execute.

private void picCanvas_Paint(object sender, PaintEventArgs e) { e.Graphics.InterpolationMode = InterpolationMode.High; foreach (ImageSprite sprite in Sprites) { sprite.Draw(e.Graphics); } }

This event handler simply loops through the sprites calling their Draw methods.

Conclusion

Yes, I know you're unlikely to need to draw a bunch of bouncing, spinning cats. However, you can use the same techniques to draw all sorts of animated objects. You can also use different sprite classes to manage different kinds of objects. For example, you could build an asteroids game by using separate classes to manage the ship, bullets, and asteroids. (Although note that the asteroids in that game wrap around the screen rather than bouncing off of its edges.)

Download the example to experiment with it and to see additional details.

© 2009-2023 Rocky Mountain Computer Consulting, Inc. All rights reserved.