Sprites

From SDL.NET

Using Sprites in SDL.NET

A tutorial by David Hudson (jendave at yahoo dot com)

Table of contents

Introduction

At the simplest level, sprite is a 2D object that can be shown on and moved around the screen. Each sprite has a Surface and a Rectangle to define what the sprite looks like and where it is located.

This tutorial will provide some insight into creating sprites, displaying them and having them respond to input events such as the mouse or keyboard.

Sprite Class

The sprite class consists of a Surface and a Rectangle. The Surface object is displayed when the Sprite is Blitted to the screen. The Surface can consist of a graphic image, a rendered text string or a primitive for example.

The rectangle is used to show what part of the Surface should be displayed. Typically, the rectangle should encompass the entire surface, but there may be reasons that the rectangle differs from the size of the surface. For example if the Surface actually consists of multiple images to be used for animation, the rectangle can be used to mark which part of the image is to be currently displayed.

The Sprite class has virtual Update() methods which are to be overridden by inheriting classes. These methods, used in conjunction with SpriteCollections will update the Sprite’s position and/or Surface when input events such as mouse button presses, mouse motion, keyboard presses occur.

The Render() method is provided to perform an action on a Surface before it is Blit to the Screen. In the Sprite class, the Render() method simply returns the Surface. In an inherited class, however, this method is often overridden to perform a task on the surface before it is Blit. For example, when a text string may need to be converted to a Surface before it is Blit to the screen. The Render() method can be overridden to do that.

The are some other properties of a Sprite:

AllowDrag (bool)

Can the sprite be dragged (usually with the mouse)?

Visible (bool)

Is the Sprite supposed to visible on the screen?

BeingDragged (bool)

Is the Sprite currently being dragged?

The Sprite class itself does not do much; its Update() methods are empty. The Sprite class is really meant to be extended with its Update() methods overridden by new logic. The BounceSprite class has overridden Update methods that determine what happens when a mouse button, mouse motion or tick event happens.

Some classes extended from Sprite are included in SDL.NET. The TextSprite displays text rendered as a sprite. The AnimatedSprite provides a basis for displaying multiple surfaces over time to create the animation.

SpriteCollection class

The SpriteCollection class is a generic collection for sprites that helps aggregate sprites and execute events on them.

Besides having methods for adding, removing and accessing the Sprites in the collection, SpriteCollection has methods for enabling events on the Sprites it contains. For example, the SpriteCollection.EnableMouseButtonEvent() allows all the sprites in the collection to respond to mouse button clicks. Once the event is fired, the SpriteCollection passes the event and EventArgs to each sprite.

private void Update(object sender, MouseButtonEventArgs e)
{
     for (int i = 0; i < this.Count; i++)
     {
          this[i].Update(e);
     }
} 

The sprite can then decide how to respond. Usually, the sprite will have logic to check if the mouse click happened on which sprite. For example:

public override void Update(MouseButtonEventArgs args)
{ 
     if (this.IntersectsWith(new Point(args.X, args.Y))) 
     { 
          // If we are being held down, pick up the marble 
          if (args.ButtonPressed) 
          { 
                if (args.Button == MouseButton.PrimaryButton) 
                { 
                      this.BeingDragged = true; 
                } 
                else 
                { 
                      this.Kill(); 
                } 
          } 
          else 
          { 
                this.BeingDragged = false; 
          } 
     } 
}

If you click on the spirte with the primary mouse button, you can drag the sprite. If you click on it with the secondary button, it will remove the sprite from the screen and the SpriteCollection.

SpriteCollections can be used to categorize sprites into logical groups. A program may have SpriteCollection for the player’s spaceship and another collection for the enemies. Another SpriteCollection could contain only those sprites that are close to the player’s ship to do faster collision detection.

BounceSprites Demo

Let’s take a look at the Sprites in action. This demo is included with the SDL.NET source. BounceSprite.cs is the sprite class. BounceSprites.cs is the demo app itself.

BounceSprite.cs

using System;
using System.Drawing;

using SdlDotNet.Core;
using SdlDotNet.Input;
using SdlDotNet.Graphics;
using SdlDotNet.Graphics.Sprites;

namespace SdlDotNetExamples.SmallDemos
{
    /// <summary>
    /// 
    /// </summary>
    public class BounceSprite : AnimatedSprite
    {
        #region Fields
        Random rand = new Random();
        //Move sprites 5 pixels per tick
        private int dx = 5;
        private int dy = 5;
        private int dz;

        //Sprites will be bounded by the screen edges minus 
        //their size so they will not go off the screen
        private Rectangle bounds = new Rectangle();
        #endregion Fields

        #region Constructor
        /// <summary>
        /// 
        /// </summary>
        /// <param name="surfaces"></param>
        /// <param name="coordinates"></param>
        public BounceSprite(SurfaceCollection surfaces, Point coordinates)
            : base(surfaces, coordinates)
        {

            if (surfaces == null)
            {
                throw new ArgumentNullException("surfaces");
            }
            //Sprites will be bounded by the screen edges minus 
            //their size so they will not go off the screen
            this.bounds =
                new Rectangle(0, 0, Video.Screen.Rectangle.Width -
                (int)surfaces.Size.Width, Video.Screen.Rectangle.Height -
                (int)surfaces.Size.Height);
            //The sprite can be dragged
            this.Animate = true;
            this.AllowDrag = true;
        }
        #endregion Constructor

        #region Event Update Methods
        /// <summary>
        /// Every tick will update the animation frame
        /// </summary>
        /// <param name="args"></param>
        public override void Update(TickEventArgs args)
        {
            if (args == null)
            {
                throw new ArgumentNullException("args");
            }
            //Call the base method
            base.Update(args);

            //Change the sprite coordinates if the sprite is not being dragged
            if (!this.BeingDragged)
            {
                this.X += dx;
                this.Y += dy;
                
                //this.Z += dz;

                // Bounce off the left
                if (this.X < bounds.Left)
                {
                    this.X = bounds.Left;
                    this.Z = rand.Next(0, 41);
                }

                // Bounce off the top
                if (this.Y < bounds.Top)
                {
                    this.Y = bounds.Top;
                    this.Z = rand.Next(0, 41);
                }

                // Bounce off the bottom
                if (this.Y > bounds.Bottom)
                {
                    this.Y = bounds.Bottom;
                    this.Z = rand.Next(0, 41);
                }
                // Bounce off the right
                if (this.X > bounds.Right)
                {
                    this.X = bounds.Right;
                    this.Z = rand.Next(0, 41);
                }
                // Bounce off the left
                if (this.Z < 0)
                {
                    this.Z = 0;
                }
                // Bounce off the bottom
                if (this.Z > 40)
                {
                    this.Z = 40;
                }
                if (this.Z == 0)
                {
                    dz = (Math.Abs(this.dz));
                }

                if (this.Z == 40)
                {
                    dz = -1 * (Math.Abs(this.dz));
                }

                // Reverse the directions when the sprite hits an edge
                if (this.X == bounds.Left)
                {
                    dx = (Math.Abs(this.dx));
                }

                if (this.X == bounds.Right)
                {
                    dx = -1 * (Math.Abs(this.dx));
                }

                if (this.Y == bounds.Top)
                {
                    dy = (Math.Abs(this.dy));
                }

                if (this.Y == bounds.Bottom)
                {
                    dy = -1 * (Math.Abs(this.dy));
                }
                //Console.WriteLine("Z: " + this.Z);
            }
        }
        /// <summary>
        /// If the mouse click hits a sprite, 
        /// then the sprite will be marked as 'being dragged'
        /// </summary>
        /// <param name="args"></param>
        public override void Update(MouseButtonEventArgs args)
        {
            if (args == null)
            {
                throw new ArgumentNullException("args");
            }
            if (this.IntersectsWith(new Point(args.X, args.Y)))
            {
                // If we are being held down, pick up the marble
                if (args.ButtonPressed)
                {
                    if (args.Button == MouseButton.PrimaryButton)
                    {
                        this.BeingDragged = true;
                        this.Animate = false;
                    }
                    else
                    {
                        this.Kill();
                    }
                }
                else
                {
                    this.BeingDragged = false;
                    this.Animate = true;
                }
            }
        }

        /// <summary>
        /// If the sprite is picked up, this moved the sprite to follow
        /// the mouse.
        /// </summary>
        public override void Update(MouseMotionEventArgs args)
        {
            if (args == null)
            {
                throw new ArgumentNullException("args");
            }
            if (!AllowDrag)
            {
                return;
            }

            // Move the window as appropriate
            if (this.BeingDragged)
            {
                this.X += args.RelativeX;
                this.Y += args.RelativeY;
            }
        }
        #endregion Event Update Methods

        #region IDisposable
        private bool disposed;

        /// <summary>
        /// Destroys the surface object and frees its memory
        /// </summary>
        /// <param name="disposing">If ture, dispose unmanaged resources</param>
        protected override void Dispose(bool disposing)
        {
            try
            {
                if (!this.disposed)
                {
                    if (disposing)
                    {
                    }
                    this.disposed = true;
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }
        #endregion IDisposable
    }
}

BounceSprites.cs

using System;
using System.IO;
using System.Drawing;
using System.Collections.ObjectModel;
using System.Collections.Generic;

using SdlDotNet.Core;
using SdlDotNet.Graphics;
using SdlDotNet.Graphics.Sprites;
using SdlDotNet.Input;

namespace SdlDotNetExamples.SmallDemos
{
    /// <summary>
    /// Demo of Bouncing Balls using Sprites. 
    /// The Bouncesprites will respond to Tick Events by spinning. 
    /// You can click on each sprite and move them around the 
    /// screen as well (MouseButton and MouseMotion events).
    /// </summary>
    public class BounceSprites : IDisposable
    {
        #region Fields
        private Surface screen; //video screen
        private SpriteCollection master = new SpriteCollection(); //holds all sprites
        private int width = 640; //screen width
        private int height = 480; //screen height
        private int maxBalls = 10; //number of balls to display
        private Random rand = new Random(); //randomizer
        string dataDirectory = "Data";
        string filePath = Path.Combine("..", "..");
        private Surface background;
        #endregion Fields

        #region EventHandler Methods
        //Handles keyboard events. 
        // The 'Escape' and 'Q'keys will cause the app to exit
        private void KeyboardDown(object sender, KeyboardEventArgs e)
        {
            if (e.Key == Key.Escape || e.Key == Key.Q)
            {
                Events.QuitApplication();
            }
        }

        Collection<Rectangle> rects = new Collection<Rectangle>();

        //A ticker is running to update the sprites constantly.
        //This method will fill the screen with black to clear it of the sprites.
        //Then it will Blit all of the sprites to the screen.
        //Then it will refresh the screen and display it.
        private void Tick(object sender, TickEventArgs args)
        {
            rects = screen.Blit(master);
            screen.Update(rects);
            screen.Erase(master, background);
        }

        private void Quit(object sender, QuitEventArgs e)
        {
            Events.QuitApplication();
        }
        #endregion EventHandler Methods

        #region Methods
        //Main program loop
        private void Go()
        {
            //Set up screen
            if (File.Exists(Path.Combine(dataDirectory, "background.png")))
            {
                filePath = "";
            }
            background = new Surface(Path.Combine(filePath, Path.Combine(dataDirectory, "background.png")));
            Video.WindowIcon();
            Video.WindowCaption = "SDL.NET - Bounce Sprites";
            screen = Video.SetVideoMode(width, height);
            screen.Blit(background);
            screen.Update();

            //This loads the various images (provided by Moonfire) 
            // into a SurfaceCollection for animation
            SurfaceCollection marbleSurfaces = new SurfaceCollection();
            marbleSurfaces.Add(new Surface(Path.Combine(filePath, Path.Combine(dataDirectory, "marble1.png"))), new Size(50, 50));

            for (int i = 0; i < this.maxBalls; i++)
            {
                //Create a new Sprite at a random location on the screen
                master.Add(new BounceSprite(marbleSurfaces,
                    new Point(rand.Next(screen.Rectangle.Left, screen.Rectangle.Right),
                    rand.Next(screen.Rectangle.Top, screen.Rectangle.Bottom))));
            }

            //The collection will respond to mouse button clicks, mouse movement and the ticker.
            master.EnableMouseButtonEvent();
            master.EnableMouseMotionEvent();
            master.EnableTickEvent();

            //These bind the events to the above methods.
            Events.KeyboardDown +=
                new EventHandler<KeyboardEventArgs>(this.KeyboardDown);
            Events.Tick += new EventHandler<TickEventArgs>(this.Tick);
            Events.Quit += new EventHandler<QuitEventArgs>(this.Quit);

            //Start the event ticker
            Events.Run();
        }

        /// <summary>
        /// Entry point for App.
        /// </summary>
        [STAThread]
        public static void Main()
        {
            BounceSprites bounce = new BounceSprites();
            bounce.Go();
        }

        /// <summary>
        /// Lesson Title
        /// </summary>
        public static string Title
        {
            get
            {
                return "BounceSprites: Bouncing balls";
            }
        }
        #endregion Methods

        #region IDisposable Members

        private bool disposed;

        /// <summary>
        /// Destroy object
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Destroy object
        /// </summary>
        public void Close()
        {
            Dispose();
        }

        /// <summary>
        /// Destroy object
        /// </summary>
        ~BounceSprites()
        {
            Dispose(false);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                if (disposing)
                {
                    if (this.background != null)
                    {
                        this.background.Dispose();
                        this.background = null;
                    }
                }
                this.disposed = true;
            }
        }

        #endregion
    }
}

The demo app will create a 800x600 screen that features 10 rotating gray sprites. They will respond to Tick events by spinning. If a user clicks on one with a mouse, the marble will stop moving and be dragged along with the mouse (MouseButton and MouseMotion events).

Sprite surfaces were created by D.R.E. Moonfire (d.moonfire AT mfgames DOT com)

See Also