Click here to Skip to main content
15,867,771 members
Articles / Programming Languages / C#

Using Sprites Inside Windows Forms

Rate me:
Please Sign up or sign in to vote.
4.94/5 (40 votes)
3 Oct 2017CPOL22 min read 90.5K   5.7K   56   43
This is an article about making 2D sprites in Windows Forms.
This is a class for making simple sprites within WinForms. This article explains how to use the class, as well as how the class works if you want to make something like it.

Introduction

Windows Forms have been around for quite some time, but it is still not very easy to make graphical games in WinForms. There are many complex libraries, and other systems for making games in, and using those extensive systems is how they recommend most people do their games. This is a simple sprite engine for use within Windows Forms, with the intent of keeping things simple. While you can make a complete game with this library, it is not a very polished system in and of itself. It is mainly to get people started in graphical programming. Probably the first comment I will get from this is, "why not use a real gaming library?" The point of this library is for simple games, for an entry into graphical programming. After someone uses this, then they will probably have the desire to work with DirectX, OpenGL, Unity, UnrealEngine, etc.

This SpriteController class allows you to take a PictureBox and turn it into a gaming field. You give it a background to draw on, and then plunk your sprites on it. All the work of animating and moving the sprites is taken care of. It also contains a simple system for determining if keys have been pressed (since that is one of the big things that stymies fledgling programmers). You can use this as an example for how to make your own sprite-controller, or use this one.

C# Winforms are not built to do complex graphics easily, but it does not take much to spark the interest of a fledgling programmer. And using a class like this is a fairly simple way to get started. First, I will explain how to use the code for those who are interested in simply using this library. And then, I will explain a bit about how the library works.

Background

I recently had the opportunity to start to teach a nephew some programming. His mindset was the same as my mindset eons ago, when I started thinking about programming. “How can I make a game?” I like C# and Visual Studio. It is very easy for someone who does not know much about programming, yet wants to make something that works. It has excellent built-in documentation, and can be as complex, or as simple, as you need it to be. But it does not do graphics easily, unless you are very simplistic (one image in a box), or go to OpenGL, DirectX, Unity, or one of those fairly complex systems. Granted, there is a lot of documentation and tutorials to make those “simple.” But, using this class is even simpler. This is my attempt for putting graphical game creation into the hands of fledgling C# programmers.

2D programming is a whole lot easier to make images for. With the right languages, it may not be a lot more complex to program for 3D, but so much of programming is in building the graphics. It is still a bit of a chore to build an animation for a 2D world, but it is doable without a lot of rendering knowledge.

Using the Code

Building the Library

After you download the source code for the library, you need to open the project and "build" it. Down at the bottom of Visual Studio, it should tell you that it has been built. The library cannot be executed on its own; you need to make a project (or download and run the demo) to see it in action. The resulting DLL file should be in the projects SpriteLibrary/SpriteLibrary/Bin/Release directory.

Adding a Reference

In your project (make a new project if you do not have one already), right-click your project name in the “Solution Explorer” and “Add” and “Reference.” Go down to “Browse” and find the SpriteLibrary DLL. If you have built it (see above), it should be in your projects/SpriteLibrary/SpriteLibrary/Bin/Release directory. If you develop a game and use the built-in ClickOnce installer, this DLL will be automatically installed, along with your package, now that you have added it as a reference. So you should only need to do this once.

Installing the Documentation

In the Doc\Help directory of the project is a .msha file, which has the SpriteLibrary documentation on it.  If you install this file (instructions below), and if you are set to “Launch help in Help Viewer” (not the online help), then, when you press F1 on anything relating to the SpriteLibrary, then context-sensitive help for those items will pop up.  If you prefer the online documentation, you can manually go to http://tyounglightsys.com/SpriteLibrary/doc to find the same documentation (you can browse this documentation; Visual Studio does not have it set up to merge another website into their online visual studio repository.)

To install the msha file and configure Visual Studio for accessing the online help, open Visual Studio and go to “Help” -> “View Help.”  The Microsoft Help Viewer should come up, usually with the “Manage content” tab selected.  If it is not selected, open the “manage content” tab.

Then, select “Disk” and browse to your SpriteLibrary project, finding the Documentation.msha file under the Doc\Help directory.  Once you have found that, click the “Add” button next to the new documentation file.

Image 1Once that has been added, you still need to press “Update” in the bottom right corner of the window.  At the time of this writing, SpriteLibrary has not been digitally signed, and a notification of that will pop up during the install.

Using the sprite Library

You need to add a “using SpriteLibrary;” at the top of your main form, and in any file where you reference the pieces of the Sprite Library (the Sprites, the SpriteController, etc.)

Initializing the sprite Library

In the form that you want to use for a game, you will want to create a SpriteController. You will need to define it first as a variable. Something like:

C#
SpriteController MySpriteController;

You may notice that we do not yet instantiate it. We need to have a PictureBox associated with the SpriteController when we make it. So, after the form is loaded and the InitializeComponents(); has been run, then we can instantiate the SpriteController. In this example, we have a PictureBox named MainDrawingArea. You can have multiple sprite controllers per game, but you only want to have one per PictureBox. So you will want to store the variable, and pass it to the appropriate places that your program needs.

C#
MainDrawingArea.BackgroundImageLayout = ImageLayout.Stretch;

MySpriteController = new SpriteController(MainDrawingArea);

We set the background layout to stretch. This is important for being able to resize the window and for a number of other things.

Adding your First sprite

First, you must have a sprite to add. Sprites are basically a series of frames on one image. Each frame is one picture, and is the same size (for example, 100x100). And there may be many frames in the image. The resulting image might be 400 x 100, or 200 x 200. The sprite will display each frame, one at a time. So you might have an image of your main character standing there. The next frame will be with his leg slightly extended. The next has his leg out. When we see it animate, the leg moves out as if your adventurer is walking. This sprite controller assumes the image will already have the transparency set so you can see the background behind the sprite, which basically means you are using png files with transparency.

There are a few ways to create your sprite and load an animation. All the examples have us reading in the sprite from a sprite-sheet in the project resources. These resource files are included along with the package in the click-once deployment. So it is a good place to put them if you are hoping to give your project to someone else. Loading a sprite looks something like this:

C#
Sprite JellyMonster = new Sprite(new Point(0, 100), MySpriteController, 
			Properties.Resources.monsters, 100, 100, 200, 4);

JellyMonster.SetName("jelly");

In this example, we are making a sprite named JellyMonster by pulling the animation out of the second row of the image. (0,0 is the first row, 0,100 is the second row.) We pass it the sprite controller, and then the image file. We specify that the image we are pulling out is of the size 100 x 100. We use an animation speed of 200ms per frame, and we pull 4 frames out of the image. When we print the sprite, we can grow, or shrink the sprite on the PictureBox. It does not need to remain at 100x100. That is just the size of the individual frame in the sprite sheet image.

The last step we do in the above code is to name the sprite. We name our master sprites, and then clone them when we want to have a bunch of them. We usually will not have the named sprites display on the screen without cloning (though you can). The main reason we do this is so that we can destroy sprites at our leisure, and make as many of them again whenever we want. When you destroy a clone, it is easy to make more from the master. But destroying the master means you need to start from scratch. It is very efficient to clone sprites, but it takes a lot more effort to generate new ones from scratch.

Telling Your sprite To Do Something

Now that we have a sprite, we can put it somewhere. Once it is placed on the background, it will automatically start displaying the first animation (animation 0). You can have multiple animations for each sprite (walk left, walk right, fall left, fall right, die, etc.) You can tell a sprite to change animations, or to animate once (leaving the sprite displaying the final frame of the animation). Here is some code for how the Sprite is configured in the ShootingDemo.

C#
JellyMonster.AutomaticallyMoves = true;
JellyMonster.CannotMoveOutsideBox = true;
JellyMonster.SpriteHitsPictureBox += SpriteBounces;
JellyMonster.SetSpriteDirectionDegrees(180);
JellyMonster.PutBaseImageLocation(new Point(startx, starty));
JellyMonster.MovementSpeed = 30;

We tell the Sprite that it will automatically move. We tell it that it cannot go outside the bounds of the picturebox. And then we tell it where it starts, and which direction it moves. The += line is an event. SpriteBounces is a function that gets executed when the Sprite hits the picturebox. (See "Sprite Events" below).

Sprite Events

You can add events to sprites. For example, you can add some code that is run when one sprite hits another sprite. In the ShootingDemo, we add that event to the monster sprites. Many times a second, the sprites check to see if they have hit another sprite, and if they do, they execute the code in that event. There are events for when sprites hit the edge of the picturebox, or if they have exited the picturebox. There is even an event that fires off before a sprite moves to a new location. You can use that one to adjust, or cancel the movement location.

You need to create an event, and add that event to the sprite. Events are cloned with the sprites, so you can add events to the parent Sprite, and those events will work for all the cloned sprites.

Payload

Each sprite has a payload that is of an empty class,“SpritePayload.” This means you can store virtually anything there. This is in case you want to add extra attributes to your sprites. If you want to track the health of different sprites, some attributes for the sprite AI, or other things, you can create a class and store that data in the Sprite.payload. You will want to make a class that overrides the SpritePayload that contains the values you want to store:

C#
public class MonsterPayload : SpriteLibrary.SpritePayload
    {
        public int Health = 1;
    }

You then make a new payload, set the data, and store it in the Sprite.payload.

A walk-through of the Shooting Demo

The ShootingDemo, which is a downloadable example, is a very simple “Space Invaders" style game. You are a spaceship at the bottom of the screen, and sprites are bouncing across the top. You shoot them. When the monsters are hit by the shot, they explode.

When the game instantiates, the “InitializeComponents” function fires off, and makes the picturebox. After that, we set the background image of the picturebox, and set the backgroundlayout = stretch. This is important for the sprites to function well. We do it in this order because the PictureBox needs to exist and be configured before we create our SpriteController, which uses that PictureBox.

After the PictureBox is defined and configured, the SpriteController is create and given the PictureBox it is supposed to draw the sprites on.

The last main item in the setup process is that an event is added to the SpriteControllerTick.” The Tick happens multiple times a second, and we add a function that reacts to keypresses.

Now that the sprite controller has been created, we want to load the various sprites. The demo shows a number of different types of sprites. The sprite demos have multiple sprites on one image, have one image that contains one sprite, and has an image that contains multiple animations for the one sprite. Some of the sprites have events added to them (more on that below).

After the sprites are loaded, we add a number of them to the game, and the game begins.

The first, simple demo

When the game begins, this particular game works off a Timer. The sprites are set to move left or right, and they move until they hit an edge. At that time, an event fires off to tell the sprite to move the opposite direction.

Meanwhile, the “CheckForKeypress” function fires off many times a second to see what the user is up to. If the user is using a left or right key, we tell the sprite to move to the left or right. If the space is pressed, we generate a “shot” and send it moving in a straight line up.

If a monster gets hit by the shot, the monster gets destroyed. An “explosion” sprite is generated, which is the same size as the monster that got hit. The explosion is told to animate once, and when the animation is complete, for it to run one last “event.” The explosion done event destroys the explosion, which removes it from the game. Whenever an explosion finishes, we count the monster sprites. If they are all gone, the game is over.

This is a very simplistic “game.” It is meant as a demo, not an actual game you would play over and over. But it shows off most of the functionality of the SpriteController.

How It Is Done (How the Code Works)

Time

The SpriteController works in a time-based system. It has a Timer (This is the name of the C# class) that fires off many times a second. Because of how C# handles time, we cannot guarantee precisely how often that timer goes off. So everything keeps track of the last time it did anything. For example, a Sprite will keep track of the last time it moved. It knows how many pixels a millisecond it traverses, and so it can compute how far it should move when the tick goes and it finds out that 15.3 milliseconds have transpired since the last time it moved. It does the same thing with animations; it computes the time since the last time it changed frames. If it needed to change a frame 10 milliseconds ago, but 15 milliseconds have passed, it will change the frame, but pretend that the last time it changed the animation was 5 milliseconds ago. That way, the next animation is still set to happen at the right time, even though there was a tiny stutter in the timing. By tracking time in this way, everything appears to move smoothly even though some events take longer than others to happen.

Note: The SpriteController does not yet have something to adjust the time if things begin taking too long on a regular basis. Most games will recognize that things are updating slowly and will adjust the effective time passage accordingly. This is not too difficult to do, but I have not yet done it.

Drawing

What makes the SpriteController able to function is how it handles drawing sprites. The SpriteController has a copy of the original background image without any sprites on it. This is used to grab pieces to erase our sprites with. To do that, it simply grabs the original background that comes from the entire rectangle that the sprite is in, and it draws that on the background (that usually has the sprites on it). We also have another image, which is the one that is displayed on the PictureBox. This is the one that has the sprites drawn all over it. When we re-draw the sprites, we draw them on the image of the PictureBox (the same one we "erased"). There are some issues that we can have with this, particularly if two sprites are overlapping. In that case, we tell each sprite that might have had part of it erased to re-draw itself. This is rather inefficient, but it works very well for a simple sprite controller.

The SpriteController is relatively simple in design. Each Sprite has one or more “frames.” Each frame is an image, and has a “duration.” Every tick, each sprite checks itself to see if it needs to be refreshed. If it needs to be redrawn, it erases itself (copying the portion of the background image behind itself and drawing that little back on the master image). It then moves or changes its animation, and then draws itself again in the new location or with the new image. For both of these, it invalidates the picturebox, but only the portion of the picturebox that covers that particular sprite. The system figures out when to actually redraw the image, but it usually works pretty smoothly.

So the simplistic description is that we have one larger image, which is displayed inside the PictureBox. That larger image is re-drawn by erasing or drawing the sprite images onto the image. And the PictureBox is “invalidated” with only the portions that have changed. So the program only needs to redraw a small portion of the screen each time. By redrawing just the little portion, it can be done fairly quickly, which makes it look smooth.

Movement

The smoothest method for telling a sprite how to move is by giving it a speed and direction. The direction can be specified as an angle in degrees, an angle in radians, or a vector. Most people will use degrees, as that seems to be how us simple game writing people think. Vectors work quite nicely if you are doing three-dimensional games. But this Sprite Controller does not handle 3D easily.

Rotating

You can tell a sprite to rotate, and the sprite will change the angle in which it is drawn. So you can have one car sprite, and have it rotate around to look like it is driving in a bunch of different directions. There is a small issue with the way we do rotation; we rotate the image, and then draw the rotated image back inside the original shape rectangle. This causes a little bit of shrinkage when we are at non-right angles. But it looks pretty good.

Mirroring

You can take an animation and tell it to mirror vertically or horizontally. This allows you to use one walking animation (walk left) and flip it so it looks like it is walking right. For example:

C#
Sprite oneSprite = OurSpriteController.DuplicateSprite("walker");

oneSprite.MirrorHorizontally = true;

Remember to turn off mirroring when you want it to flip back.

MoveTo

To make sprite controls a little saner, there is a Sprite.MoveTo function. You can give it a single point, or a list of points. The Sprite will move from where it currently is to the specified point, and then continue on to each of the following points. You can cancel the MoveTo with CancelMoveTo();

When the sprite reaches each of the waypoints (the points along the way, except the last point are all waypoints) the event, SpriteArrivedAtWaypoint fires off. And, when the final point is reached, the event SpriteArrivedAtEndPoint gets fired off.

Making Games using SpriteLibrary

The Thread

We have already mentioned that the SpriteLibrary functions based off a timer "Tick" that happens many times a second. We also need to mention that all this happens within one "Thread". We have each event following another event, but they usually do not interrupt each-other. It looks somewhat like this:

Image 3

Where the black lines are the main pieces of code that get fired off every so often. The blue and green are things that Windows puts in when it needs to do so. For example, the blue is re-drawing the screen, and the green is "Garbage Collection" (cleaning up un-used memory).

The important thing about this is to realize that there are a number of things you can do that will keep the thread from moving on. You can use a "Thread.Sleep(100)" command, but that will halt everything in the "thread." The result is that sprites do not move or animate while the thread is sleeping. They are all part of the same thread.

The SpriteLibrary assumes that you will have triggered events, and that the events will return control to the SpriteController when they are done.

The Main Routine

You want to have one central function, usually tied in with the SpriteController "Tick." The first thing you want this function to do is to determine what the status is of your code. Are you waiting for something? Should you be checking to see if something has completed? And, finally, check to see if you should be getting input and changing your status based on the current input.

The ShootingDemo only has one check. It makes sure we have not won already. If we have not won, then check to see if we should move anywhere or do anything.

So, let me give you a more complex example. Let's say we have a function called myDoTick that is added to the SpriteController DoTick event. We have three states, moving (the sprite is using a MoveTo function to travel to a specific point on the screen), waiting for keypress, and "the game has ended."

C#
enum GameState { moving, waiting, end_game }

void myDoTick()
{
    select (myGameState)
    {
        case moving:
            if(!mySprite.MovingToPoint)
            {
                myGameState = waiting;
            }
            break;
        case waiting:
            check_for_keypress();
            break;
        case end_game: 
            TimeSpan duration = DateTime.UtcNow - TimeWhenWeStartedEnd;
            if(duration.TotalSeconds >= 5)
            { //We have delayed 5 seconds after the end of game was reached. Now close
                Close();
            }
    }
}

When we decide that we have won the game, we want to set a DateTime variable:

C#
void EndGame()
{
  Sprite newSprite = mySpriteController.DuplicateSprite("GameOver");
  newSprite.PutPictureBoxLocation(endGamePoint);
  myGameState = GameState.end_game;
  TimeWhenWeStartedEnd = DateTime.UtcNow;
}

The next time it goes to the myDoTick function, it takes us to the "case end_game", which checks to see if five seconds have passed. If so, then we close the window.

The CheckForKeypress function would check to see if any keys had been pressed, and, if so, tell the player's sprite what it should be doing.

Sprite Events

There are many events that sprites go through. They collide with the edges of the picture-box, they run into other sprites, and they move to new locations. Life can be made a lot simpler if you can use those events to trigger different things in your game. For example, in the ShootingDemo, each of the monsters check to see what they got hit by. Often they are hit by other monsters, but, they can also get hit by the "shot." When they do, they explode. This is the joy of object oriented programming. You tell the object that, when it is hit by a shot, it explodes. And, then it just seems to happen when the game goes on. If you can famialiarize yourself with the events that a sprite can have, you will soon become an expert in programming with the SpriteLibrary.

Weaknesses (When You Run Into Them)

This SpriteLibrary works nicely for simple games, but it will bog down if you make something that is too complex. If there are too many sprites running around, or if you need a lot more features that is programmed into it, you will end up needing a more complex gaming system. But this should work nicely for your first few games, at least.

But the two main weaknesses are that the SpriteLibrary has limited scope, mainly to try to keep it simple, and it has limitations due to how it deals with time.

Limited Scope

It does not have scrolling backgrounds, it has limited collision detection, and it really only works well with a limited number of sprites. As you program, you will run into these limitations. You will want to find some way to do something, and not see it there.

Time Issues

I talked a bit about the limitations previously. Here, I want to discuss how to notice that you are running into time based issues.

If you have too many sprites, or if you are doing too many things in one function, you will begin to notice strange behavior. You will notice that sprites are moving in a fairly jerky manner. They will disappear and re-appear in a different location, etc. And you will also see issues with collision detection. Sprites will pass through other sprites without it being triggered. So, you will shoot something and the shot will pass through it harmlessly.

In such cases, think about what you are doing and how to make it run faster or run more concisely. You want to give the SpriteController enough time for it to be able to handle everything it needs to.

Resources

History

  • 01-06-2022: Version 1.0.7
    • Changed the documentation URL
  • 11-23-2016: Version 1.0.6
    • Greatly increased documentation
    • Added support for transparent mouse-over functions
    • Added the SpriteDatabase and linked SpriteControllers
    • Added the first version of my documentation PDF
    • Adding a link to the development source code
  • 11-23-2016: Version 1.0.5
  • 7-19-2016
    • Added a second demo (submarine game) and a link to another page showing how to make an RPG game with the SpriteLibrary
  • 6-30-2016: Version 1.0.4
    • Lots of bug fixes (listed in the release notes)
    • Added some more features for managing animations, some sound file fixes, and some other stuff
  • 4-10-2016
    • A number of bug fixes (and I added a release notes file), and additional help for designing a program using the SpriteLibrary
  • 3-25-2016
    • Updates using CodeProject recommendations
  • 3-17-2016
    • First version published

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionSpriteLibrary Pin
Member 1589456915-Jan-23 21:10
Member 1589456915-Jan-23 21:10 
AnswerRe: SpriteLibrary Pin
BouncyTarget16-Jan-23 4:10
BouncyTarget16-Jan-23 4:10 
QuestionHello and thank you for this lib! Pin
Servaas Doornberg4-Feb-22 13:28
Servaas Doornberg4-Feb-22 13:28 
AnswerRe: Hello and thank you for this lib! Pin
BouncyTarget4-Feb-22 14:10
BouncyTarget4-Feb-22 14:10 
GeneralRe: Hello and thank you for this lib! Pin
Servaas Doornberg4-Feb-22 14:19
Servaas Doornberg4-Feb-22 14:19 
GeneralRe: Hello and thank you for this lib! Pin
BouncyTarget5-Feb-22 4:56
BouncyTarget5-Feb-22 4:56 
GeneralRe: Hello and thank you for this lib! Pin
Servaas Doornberg6-Feb-22 23:33
Servaas Doornberg6-Feb-22 23:33 
GeneralRe: Hello and thank you for this lib! Pin
BouncyTarget7-Feb-22 2:09
BouncyTarget7-Feb-22 2:09 
GeneralRe: Hello and thank you for this lib! Pin
Servaas Doornberg13-Feb-22 14:48
Servaas Doornberg13-Feb-22 14:48 
GeneralRe: Hello and thank you for this lib! Pin
BouncyTarget14-Feb-22 4:11
BouncyTarget14-Feb-22 4:11 
GeneralRe: Hello and thank you for this lib! Pin
Servaas Doornberg14-Feb-22 8:51
Servaas Doornberg14-Feb-22 8:51 
GeneralRe: Hello and thank you for this lib! Pin
BouncyTarget14-Feb-22 9:07
BouncyTarget14-Feb-22 9:07 
GeneralRe: Hello and thank you for this lib! Pin
Servaas Doornberg14-Feb-22 11:29
Servaas Doornberg14-Feb-22 11:29 
GeneralRe: Hello and thank you for this lib! Pin
Nelek14-Feb-22 11:30
protectorNelek14-Feb-22 11:30 
QuestionDocumentation\Other download ... not online Pin
RedDk6-Jan-22 6:21
RedDk6-Jan-22 6:21 
AnswerRe: Documentation\Other download ... not online Pin
BouncyTarget6-Jan-22 6:35
BouncyTarget6-Jan-22 6:35 
AnswerRe: Documentation\Other download ... not online Pin
BouncyTarget6-Jan-22 12:31
BouncyTarget6-Jan-22 12:31 
QuestionCheckSpriteHitsSprite Pin
Giorgio Lamarca29-May-19 7:53
Giorgio Lamarca29-May-19 7:53 
SuggestionDo you mind if I make a WPF version of your project Pin
Daszbin5-Oct-17 3:57
professionalDaszbin5-Oct-17 3:57 
GeneralRe: Do you mind if I make a WPF version of your project Pin
BouncyTarget6-Oct-17 10:24
BouncyTarget6-Oct-17 10:24 
GeneralMy vote of 5 Pin
Jim_Snyder4-Oct-17 8:55
professionalJim_Snyder4-Oct-17 8:55 
PraiseGood Work Pin
Ocean Airdrop9-Aug-16 6:33
Ocean Airdrop9-Aug-16 6:33 
Praise5 stars! Pin
Twiggy Ramirezz20-Jul-16 17:17
Twiggy Ramirezz20-Jul-16 17:17 
SuggestionTime precision Pin
Patrick CGDM1-Jul-16 2:06
Patrick CGDM1-Jul-16 2:06 
QuestionNice Pin
Bassam Abdul-Baki30-Jun-16 11:00
professionalBassam Abdul-Baki30-Jun-16 11:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.