What is a sprite?
If you look up "sprite" in a dictionary or on the internet, you'll get references to fairies, Advanced Dungeons and Dragons, and maybe even a bit of pixie dust. That may be what you were thinking when you clicked on the link that brought you here, but this article is about making 2D graphics a little bit more flexible. A sprite, in this article, is a character you want to include in your shoot'em up video game, and this Sprite Editor program, along with its classSprite, allows you to take an image off the internet or wherever, cut it up into separate limb components, and then reattach these with flexible hinges that let you rotate each limb separately. You then have yourself a graphics puppet you can program and animate.
How it works
A sprite is composed of limbs. Each limb has its base image, which is then copied at 16 different angles, 22.5° apart from each other, and are attached one to the other via hinges. There is one master-limb which has one or more slave limbs, and each slave limb may or may not have one or more slave limbs of its own.
This might sound confusing to start with, but the concept isn't that difficult. And, instead of trying to explain the mechanism of the whole project in twenty words or less, or considering expanding the idea into a lengthy dissertation, let's see if we can step you through the process of making your own sprite. Then, I'll tell you about how the sprockets are wired in more detail later.
Using the editor
Meet our new friend Grumpy Smurf. He's not very chatty, and right now, all he knows how to do is walk around and grumble. No worries, because I know you'll be able to make him do all sorts of things once you get the hang of this program. But first, I will walk you through the process of actually turning the image below into the sprite above.
You can see in this image that you have all you need to make a sprite. However, you might have noticed that his left hand is concealed, and so if you've already downloaded the Sprites.zip file this page links to, and have taken a look at the images in the Smurf directory, you'll see that Grumpy's left hand has been edited. A bit of blue, a bit of black, no real stretch for anyone with a mouse, and a bit of patience.
Since you've already got all the images you need to get started, and you've got your C# IDE up and running with the SpriteEditor.cs source code, you're ready to create your first sprite. The first thing you do is select File on the menu and then click 'New Sprite', and when you do, the 'Create New Sprite' form will appear with the default values for the sprite's name and the sprite's Master-Limb's name, as well as an image with the words 'Click on image to load' written on it. In the first focused text box, just type the word 'Smurf' or 'Grumpy Smurf' or whatever pet-name you have for your new sprite, and then tab to the next text box and type the name of your sprite's 'Master Limb', in this case, 'torso' is an easy and descriptive name to use. Then, click on the image and find your way to the smurf directory in the sprites.zip file which you downloaded and extracted. From there, you select the 'torso.bmp' file, and when you're done, your 'Create New Sprite' form should look like this:
You can choose to make your sprite rotate or not. You may as well keep it checked; you'll get the same options later when you add limbs, and for most of them, you'll want to allow rotation. As for the 'always vertical' concept, suppose you had a gun hand attached to an arm. If they both rotate and your sprite is shooting at someone behind him, then the rotated image will appear upside down. In the case of the gun hand (and maybe even the arm), you can decide that the image should always be vertical. This means it will rotate, but in such a way that a mirror image will appear on the other side, keeping the gun image vertical where it would have rotated to an upside down position.
... but that's more detail than I wanted to give you just yet. Let's move on.
At this point, this is what we have.
So next, we want to add some limbs: head, arms, and legs. But first, we need to go to the 'Selected Limb' combo box and select the name of the master limb we just created our sprite with: 'torso'. With the torso limb as the Selected-Limb, we can now go ahead and add a new limb to our Smurf sprite. To do this, we go to the 'Limbs' menu and click on the 'Create new' option, and a default 'Create New Limb' form will appear with the Master Limb's image and name on it, along with that same 'Click on image to load' picture, and two default pre-set check boxes and blank textboxes. But don't be scared, Grumpy's always a little difficult, so there's no need to get worried. First, type the name of your sprite's new limb. We'll add the head first, so we call this new limb 'head'. Tabbing to the next text-box, type the name of the new limb's hinge. It is best if you use intuitive and logical names for your limbs and hinges. In this case, the easiest thing to remember for later would be to say that the 'head' (limb) is attached to the torso (limb) by the 'neck' (hinge), so we call the hinge that ties the head to the torso 'neck'; we keep the check boxes as they are, and click the OK button.
You can see in this image below that with the head centered exactly over the torso, we can no longer see the torso at all! But, there are a few things wrong with the way the head is fixed right now, so we'll have to set the hinge parameters and right Grumpy's head, or he'll get a kink he won't forget. Now's a good time to look at some of the options on the screen. You probably noticed those buttons on the left of the image, and though the values in the text boxes are initialized to zero for now, you might have a clue what they're about. As I mentioned before, each limb has one or more slave limbs, and there's only one master-limb. In Grumpy's case, his master-limb is his torso, and right now, he only has one limb, his head. Yes, Grumpy thinks with his stomach! But, who doesn't, right?
In order for the hinge to hold the head right, we have to position the hinge somewhere relative to the master limb torso's center. This location is fixed to the center, and rotates with the image, and this is true for all hinges: the hinge's master location is relative to the hinge's master limb's center and rotation.
Though the sprite has only one Master Limb, each slave limb has its own master which may or may not be the sprite's master limb. A more fully articulated sprite than Grumpy here may have a hand limb attached to a forearm limb by a wrist hinge, and the forearm limb attached to the arm limb by an elbow hinge, and in turn, the arm limb would logically be attached to the torso by the shoulder hinge. Suppose in this hypothetical example that the torso is the sprite's master limb. Then, though the arm's master limb is the same as the sprite's master limb, the torso, the arm is also the forearm's master limb. The arm is then both one of the torso's slave limbs and the forearm's master limb, even though the arm is not the sprite's master limb.
Each hinge has two radial coordinates and an angle. Since the hinges connect a slave limb to its master, the radians that describe where the connections between these two limbs are made are called master-rad and slave-rad. The hinge's master-rad is the hinge's angle from and distance to the center of the hinge's master limb. And, the hinge's slave-rad is the hinge's angle from and distance to the center of the hinge's slave limb.
This probably sounds more complicated than it actually is. I'll explain further on in more detail how the images are positioned on the screen, but for now, let's just get Grumpy's head right. First, we'll set the neck's master-rad. We can do this either graphically or the hard-core way, and write the values directly into the textboxes. To set the Selected-Hinge's master-rad graphically, press the button circled in the image below.
Looking at the name of the group boxes on the left of the screen, you'll know that we're setting the radial-coordinates that position the 'neck' hinge which connects the 'torso' to the head. Since the 'torso' is the master limb, not only of this sprite, but also of this hinge, the torso appears above and the 'head' below on this hinge's parameter configuration. The neck hinge's torso radial coordinate is the position of the hinge relative to the center of the torso's image, and the hinge's head radial coordinate is located relative to the center of the head's image. So, when we click on the 'Mouse Set' button for the neck hinge's torso master-rad coordinate, a form with an image of the torso, like the one in the image below, will appear on your screen.
Moving the mouse around will show you the vector which the radial coordinate will describe once it has been set, and to set the position of the neck hinge relative to its master limb's center, just click the image where the neck hinge goes on the torso. You can see in the image below that the mouse was at the top edge of the line that is drawn from the image's center when the screen capture was taken, and that's probably where you'll want to put Grumpy's neck too. When you click on the form where you want the neck to go, the "Master-rad coordinate graphic set form" will disappear.
Well, now. That's ~better~, and I don't want to discourage you, but that's just not quite right. Grumpy's neck is going to get awful sore if you leave him like that. You know you could get union trouble if you don't help him out, so you'd better get a move on and get rid of that kink. You'll notice now that the master-rad coordinate values, the neck hinge's position relative to the torso, are no longer set to zero. Instead of graphically positioning Grumpy's neck like we just did, what you can do is change the values in the textbox and get your hands dirty a little playing around with it.
OK, for now, let's set the head's rad coordinate relative to the neck. The head is the slave-limb in this case, and its hinge slave coordinates are in the lower groupbox labeled 'head'. Setting these values is a concept almost identical to what we just did: click on the button circled below.
So, what we want to do here is select a spot on the head image where the neck goes. Just click on the image at the right spot, and this form will disappear.
OK, you're getting the hang of this. You can see that Grumpy's head is now in the right place, but he's still got a problem, and this is where things get interesting.
If you click on the button circled in the image below, you can move the mouse over the image in the main form, and you will see Grumpy nod his head (you can select this button by pressing Alt-A; this will come in handy when you're creating animations later). Because the gods of trigonometry put zero at 3 O'clock, it is easiest and most intuitive to control the angle which a limb makes with the screen if you've started with images that point to the right in the first place. In this case, Grumpy's head limb's main image had him facing upwards, and the neck (location of master rad for the head's hinge) is directed to the right.
If you move the mouse to a location near the bottom of the screen, you'll see that his head will be just about where Grumpy likes it. Well, no. Actually, Grumpy doesn't really ~like~ anything, but at least that way, he can avoid stubbing his toes when he walks because he really hates stubbing his toes.
OK. Now, you just click on the screen to leave good enough alone, and let's move on to the rest of this sprite. I think you get the idea, so let's see if you can guess how we add another limb. To give Grumpy a left-arm, first go to the limb-combo box and select torso. You might have noticed a small circle in Grumpy's right-eye when the mouse is near. If you move the mouse close to the center of Grumpy's head image, then the circle will appear again, and by clicking the screen there, you'll have selected his head. You can't do that with the master-limb, so what you'll have to do is just pick it out of the combo box instead. Once you have the 'torso' selected, you click on the 'Create New Limbs' menu option and add the limb 'arm(far)' with the hinge name 'shoulder(far)' (you can name it whatever you like, but far and near might be easier to use than left and right when you're looking at a mirror image of it later on).
Now, you can go through the process of setting the rad-coordinates for the shoulder hinge.
At this point, we have a different problem. The left arm needs to be on the far side of the torso, but the program is drawing the torso, then adding the head and drawing the left arm last, which means the left arm is drawn on top of everything else.
How ever shall we fix this?!?
No worries. Just click on the Limbs - Edit Draw Sequence menu option, and a new list box will appear.
This list box shows you the order in which the limbs will be drawn before the final image is put on the screen every time the mouse moves over your sprite. You can edit the order easily enough by clicking on the limb you want to move up or down in the list and then moving it around by pressing the up/down keys on your keyboard, or alternately, you can use the mouse-wheel. So, go ahead and click on the 'arm(far)' limb's entry in this listbox, and then mouse-wheel or key-press your way to the top with it, and you'll see the image on the screen will be corrected accordingly.
Alright. You have the images. You have the editor. Get to it! No one could put Humpty-Dumpty back together again, but maybe you can assemble ol' Grumpy here.
Animation
What's the point of having a sprite if you can't animate it? Not much. You might as well just take a picture. I've got a few old projects that have a lot of depth of play and some cool features, but the animation I was using back then (two months ago!) was no better than stick figures. So, I set out to make this Sprite Editor. And with it, you too can now animate your own sprites and put a bit of life into your projects. To do this, you first need to create a new 'Configuration'. In hindsight, I think 'Animation' would have been a better name for this, but I called them Configurations when I was setting up a way to store hinge angle values for all the limbs into sequences of arrays which could then be recalled to set the hinge angles of the limbs and place their images into pre-determined 'configurations'.
But really, they're animations.
Let's get to it. No time to waste. Just go ahead and click that 'Create New' menu item on the 'Configurations' drop-down menu, and get a move on!
When you've created a new Configuration, another group-box will appear below the Hinge parameters groupbox that's been on the screen since start-up.
The image above shows the groupbox labeled 'Configuration Parameters'. And, there's nothing frightening here. First, you'll see that the new configuration's name is 'New Configuration', and you'll also notice that the same default name appears in the right-most combo box at the top of the screen labeled 'Selected Configuration'. So, the first thing you'll want to do is change the name of this configuration to 'walk'. Then, press Enter to record the change. You can change the configuration's name at any time, just be sure not to give any of your configurations the same name as another one, or you'll get into trouble.
Below the name of the configuration, you've got another Draw Sequence listbox. The reason for this is that you may want to not-draw a certain limb for a given configuration. Let's say, you want ol'Grumpy to blink. You could have him stand there and blink by having an extra image of his head with his eyes closed. Then, you attach this new head to the old head, and only draw it when you want him to blink, by moving the blink-head into the draw sequence. If you look at Jessica Rabbit's sprite and some of her 'stand' sequences, one of them has her far-hand reach down and scratch her near-foot. The only way she could do that is by having a separate animation-configuration which places the far hand closer to the near foot in order for her to scratch her ankle.
No need to bother you with that just yet, you'll have plenty of time to experiment later. For now, let's say we want to make a simple walk-sequence for our friend Grumpy here. The walking animation is probably the most common, and there are a few videos on YouTube to show you how to draw them. This program makes it easy to experiment with animation at will, so what you do once you've created your new configuration and name it walk is, you set up the limbs into a position that Grumpy will be in at the start of the cyclic sequence of images that we're (you're!) going to generate. To do this, you can move the mouse over the limb you want to move, click the screen with the mouse when you see the circle at the center of its image appear (sometimes images are on top of each other and you may have to resort to selecting the limb you want from the combo-box at the top left of the screen), and then you press the 'mouse set : angle' button in the hinge-parameters box (or press Alt-A), and you move the selected limb around until you've got it where you want. Then, click the screen again, and it'll be set there.
But an animation sequence takes more than one image, so you'll have to add more as you go. And that's what the buttons 'add' and 'del' below Configurations Parameter's Draw List Sequence are for. So, you just click the 'add' button, and a new 'step' will be added to your sequence. You can then cycle through each step using the 'animation' toggle button, or 'step' through them one at a time using the 'step' button. The name of this button is the same as the name of the configuration's array of the hinge angle values which describe the position of your sprite. So, the number of 'steps' in your configurations reflects the number of images in your animation.
You just need to place the limbs in whatever -configuration- you want for each step, and tinker with your animation until you get what you like.
Once you have your sprite assembled, creating a new animation requires more artistic skill than technical knowledge, and that's just something you learn with practice, though there are a few good links on the internet that can help you with this. Play around with it and see what you come up with.
The NumberUpDown
object next to the 'del' key lets you choose which step in the selected configuration you're editing, and while the 'step' button acts like the 'up' arrow on the NumberUpDown
object, the 'step' button also cycles around to zero while the 'up' arrow does not.
There are a couple of interesting things you need to know here. You have the option of rotating the entire sprite using the horizontal-scroll bar at the bottom of the screen. If you want your sprite to do flips in your animation sequences, then you'll have to start your sprite off with a blank image as your master-limb. By blank image here, I mean an image so small it hides behind another image, 2x2 pixels is perfect. In the case of Grumpy, you use this 2x2 bitmap as Grumpty's master-limb, and then have a 'master-hinge' with default parameters connecting it to his torso, and then you'll be able to rotate him. You'd need an extra one to create a hydraulics effect if you want the animation to move up/down without actually changing the sprite's location in your code.
Also, you can flip the image around to get a mirror image of your original configuration. This makes things a lot easier if you're the lazy type. I know if I have Grumpy walking in the right direction, I'm not going to bother teaching him how to walk another way, because we don't need to see the far-side of him, so I just say 'Grumpy walk-right', and then I fool my user into believing he's walking left by using a 'horizontal' mirror which flips the image around. Handy, so long as you don't have to explain why he's the same on both sides. Just use the Flip-H and Flip-V check boxes on the Sprite-Editor to see what this looks like. Mind you, this only works for configurations.
Something very important here is the 'use cache image' check box. There is no real reason for you to use cached images while you're editing your sprite.
Let me explain 'cached images' first. When your sprite loads, it has its base images from the file, and then generates the 16 rotated images for each limb without creating any output images of them combined together. Then, when a configuration is called to the screen, the sprite tests its cache to see if it has gathered the appropriate limb images already for this particular configuration/step/mirror, before creating it again. If it has one in cache and it's not forced by the calling function to create a new one, it won't create a new one. It'll just send a copy of the one it has stored in memory. This is great for end-use when you've got your sprite dancing around the screen with a hundred other things going on, and you're cutting edges for speed, and doing what you can to get things working faster. But, while you're editing, you want to see the changes you've made, so, though that check-box is there ... there is no real reason for you to use it during the editing process.
Using the code
Once you have your sprite built and saved, putting him on the screen is really very easy. Here's the class I wrote to demonstrate this project's output. classSmurf
has a loc Point
, a travel 'delta-X' value, a configStep
integer variable, a mirror, and a sprite. When the 'animate(ref Bitmap bmp)
' function is called, the location is shifted by intDX
until it reaches the edge of the screen, at which point, intDX
is negated and the mirror value is set to reflect this change (no pun intended), and finally, the sprite is put to the screen with a call to classSprite
's function PutConfigurationToScreen
.
public class classSmurf
{
public Point loc;
public int intDx = 25;
public Sprite.classSprite mySprite;
Sprite.enuMirror mirror = Sprite.enuMirror.none;
int intConfigStep = 0;
public enum enuSmurf_Configurations
{
walk,
_numCon
}
public classSmurf()
{
Sprite.classSpriteMaker cLibSpriteMaker =
new Sprite.classSpriteMaker();
string strDir = System.IO.Directory.GetCurrentDirectory();
strDir = strDir.ToUpper();
strDir = strDir.Replace("BIN\\DEBUG", "RESOURCES\\");
strDir = strDir.Replace("BIN\\RELEASE", "RESOURCES\\");
mySprite= cLibSpriteMaker.loadSprite(strDir + "Smurf (small).spr");
}
public void animate(ref Bitmap bmp)
{
loc.X += intDx;
loc.Y = bmp.Height - 100;
if (loc.X > bmp.Width || loc.X < 0)
intDx *= -1;
mirror = intDx > 0 ? Sprite.enuMirror.none :
Sprite.enuMirror.horizontal;
mySprite.putConfigurationOnScreen(
ref bmp,
(int)enuSmurf_Configurations.walk,
intConfigStep,
loc,
0,
1.0,
mirror,
false);
intConfigStep = (intConfigStep + 1) %
mySprite.Configurations[
(int)enuSmurf_Configurations.walk].steps.Length;
}
}
The Sprite Editor's Configuration menu has a 'Reorder Configurations' item which allows you to reorder the configurations and create a sample enum list you can use in your project. Cut'n-paste this from the Sprite-Editor to your code, and you're all set to use your sprite. When you have a sprite that has only one configuration, you can't get confused because it'll always be using configuration index '0', but when you have a sprite with a lot of different configurations, then keeping them in a manageable order and creating an accurate enum list can pose problems, so you'll find this feature quite handy.
Consider the example below taken from the Jessica Rabbit sprite:
public enum enuJessica_Rabbit_Configurations
{
Walk_new,
Shoot_Straight,
Shoot_High,
Shoot_Low,
Shoot_Up,
Shoot_Down,
standStill,
Stand_flick_far_hair,
Stand_hold_gun_up,
Stand_flick_near_hair_with_gun,
Stand_fix_far_shoe,
Stand_fix_near_shoe,
Stand_scratch_far_calf_with_gun,
stand_scratch_head,
stand_swipe_near_arm_with_far_hand,
go_to_sleep,
wake_up,
_numCon
}
classSprite
also includes a PutSpriteOnScreen()
function which acts in a way similar to PutConfigurationOnScreen()
does, but the complications involved in altering the hinge angles on the fly are more difficult than at first imagined, so you might want to stick to pre-defined configurations.
Collision detection may be something you want to consider. Unfortunately, this project does not go very far with that; however, what you can do is use the 'getLimbPos()
' function. This function is over-loaded several ways so you have a lot of options to call it. And, what it does is give you the location of the limb you asked for relative to the master-limb. When you call the function, you have to either tell it what angle and mirror, or let it assume angle=0 and mirror=none, and the Point
value you get is located relative to the position of the master limb. Note that when you call PutConfigurationToScreen()
(or PutSpriteToScreen()
), you send this function the point on the screen you want to put it, and that location is the same as the center of the master-limb's location. So, if you put the sprite at (453, 148) and the result of getLimbPos()
is (47, 52), then the limb you're looking for is at (500, 200).
Here's an example out of my Night-Stalker project, the sprite's name in this class is Sprite
, and the last configuration put to the screen is pointed to by its public variable of type classSpriteConfiguration
called 'conCurrent
' (current configuration, -not- a constant type prefix, you'll find 'hin', 'lim', and 'con' are used regularly as prefixes in the Sprite
namespace):
Point ptGunHand = Sprite.conCurrent.getLimbPos("gunhand(near)",
mirror,
intConfigStep,
0);
Point ptBulletLoc = new Point(ptLoc.X + ptGunHand.X,
ptLoc.Y + ptGunHand.Y);
Easy enough, right?
The fourth parameter in this particular call to getLimbPos()
is an integer value representing a direction between 0-15, where 0 is east. These match the images stored in each limb's base image array, and are used as that array's indices when retrieving the cache image stored by: configuration, mirror, step, and direction (of sprite rotation 0-15). Though you can specify any angle you like, these angles are run through the code and then calibrated to fit in with the limit of 16 images rotated from the original base images. Changing the project to generate 32, 100, or whatever number of images you like for each limb is not altogether out of reach for anyone who is determined to do so, and you'd get better output with smoother animation at the cost of increased load time and increased memory requirements, which most computers today can handle.
So at this point, animating your sprite is just a matter of calling PutConfigurationToScreen()
and keeping track of the location yourself, along with which configuration and configuration step to use.
Place, action, and step.
But how does it work?
You can see that Grumpy is a relatively simple sprite with five limbs, all attached to the same master limb, but if you load some of the other sprites in the sprites.zip file, you'll see that there really is no limit to the complexity these sprites can have. And, since the images which are generated at run time are stored in the sprite's cache, animation isn't slowed down by the process of assembling the images into any new output bitmaps once they're already cached.
But how does it work?
Rotate original image
The first thing the program does when it loads a sprite is take each limb's original base image and rotate it three times and then store these four images in an array where image[0]
is the original, image[1]
is the same as image[0] but rotated 22.5°, image[2]
is the same as image[0]
but rotated 45°, and image[3]
is the same as image[0]
but rotated 22.5° more than image[2]
to 67.5° from the original.
You can see the three different bitmaps 1, 2, and 3, in the image below:
This is done in classSpriteLimb
's function generateBaseFourImagesFromOriginal()
. The rest of the 16 images are flipped and rotated using .NET's native Bitmap
function 'RotateFlip()
' which is called for each of the remaining 12 directions in the same class' getLimbImageFromArray_ByDir()
function.
In the next image (further down in the text), you'll see Grumpy's right arm and torso are boxed. These boxes are there to help show where each limb's bitmap was pasted onto the final image before it was put to the screen. What facilitates the calculations involved in placing each limb is the use of the center of the bitmap as the point of reference for all calculations. The limb may rotate, and the image size may fluctuate with each possible angle which the limb may take, but the center of the image remains the fulcrum about which the hinges are located and slave limbs are subsequently placed. To do this, the appropriate image sizes need to be calculated when generating the rotated images.
Rotating images
The actual 22.5 degree rotations are all done in the function:
Bitmap rotate(Bitmap bmpInput,
double dblAngleRotation,
ref classMath cLibMath)
(classMath
referenced here is included in the Sprite
namespace.)
This rotate()
function works by first calculating the maximum possible size that may be needed for the output image, and it creates a temporary bitmap into which the output image is guaranteed to fit. Then, it scans each pixel on the original base-image, and rotates them one at a time using the cos/sin trig functions, and pastes the original pixel at a rotated location onto the new bitmap. To keep track of the required size, it retains two Point
variables called 'ptTL
' and a 'ptBR
', which keep track of the farthest Top-Left corner and the farthest Bottom-Right corner from the original, which defines the smallest rectangle which contains the output image. And, when the scan is complete, the required size is known.
But ...
there are often holes in the rotated image.
These holes are due to the quantized nature of pixel locations and the whole-numbers that are derived by rounding the floating point values which the trigonometric equations involved in the rotation scheme spew back at you. After trying a few different algorithms to fix these holes, I found that one way to do this is to scan the resultant image, again looking for holes, and then count the colors around the holes and see if we can get a consensus as to what color this hole should take. Since there are 8 pixels immediately around any hole, if a minimum of 2 are the same, that's just good enough. A higher value might be safer, but I've found that this seems to be fine. As I write this article and look at the code to recall what value I had settled on, I find it difficult to believe that 2! was enough, because without this test, there were often errors that created a domino effect which filled in entire regions of the rotated image where the original was left bare. But, if the code says 2, then the code must be right, because it's been working well and there's no reason to change it.
However, if all the pixels around the hole are of a different color, as often may happen with JPEGs or GIFs, the original pixel is examined by doing the reverse trig and looking back at the base image before setting this hole to the color found there. This method is more certain, but much slower.
Placing the limbs
The function:
public udtBmpAndPtTLInfo getImageOnBitmap(double DisplaySize,
enuMirror mirror,
classSpriteLimb[] limDrawSequence)
returns an element of the structure type defined here:
public struct udtBmpAndPtTLInfo
{
public Bitmap bmp;
public Point ptTL;
public udtConfigStepLimbPosition[] limbPositions;
}
The returned bitmap is the smallest bitmap that can hold the image of this configuration at the DisplaySize
required, while the Point
tells the calling function where the center of the image is so that it can be placed on the screen at the correct location. This is needed because the sprite's limbs can be extended in any direction, making the output image larger on one side or the other, and you still need to know where the master limb's center is. The GetLimbPosition()
function mentioned above makes use of the limbPositions
seen here by storing them in the configuration step's cache along with the image, as this is the actual data structure which is held in the cache.
In order to place each limb at its correct location, the function first initializes the return value's ptTL
to the rotated master limb image's center. Then, once it has placed the master limb by doing this (recall that ptTL
describes the master's limb position with respect to the returned bitmap), it calls a recursion function that receives a 'master-limb' (any limb it treats as master for that specific function call), places this 'master-limb's' slave limbs, and keeps track of the minimum output bitmap required in the process, then recurses through each of them until the extremities of each limb are reached and each call has returned.
An example may clarify this process for those not familiar with recursion: first, a reference to Grumpy's torso is sent to the function. The torso call then cycles through each of its slave limbs, and for each one, given the torso's position and angle, places them (the slave limbs) at the location which their hinge-angle puts them, and then recurses into each of these limbs. Had Grumpy been carrying a stick, then the recursion involving the stick-hand would place the stick and call itself again, sending the stick as the 'master limb', which has no slave and simply falls out without doing anything. In the case of a more complex sprite, the torso recurses for each of its slaves, starting with the 'arm', then the arm recurses through each of its slaves, starting with the 'forearm', and the forearm does the same with the 'hand', and the hand repeats the process for the 'gun', and if you want to belabor the issue, the gun then recurses for the 'silencer'. And, when the silencer is reached, and it has no slave, then the program flow falls back the way it came: gun, hand, forearm, arm, and torso, where the torso now cycles through to the other arm, and so forth, until all of the torso's slave limbs and all of their slave limbs have been placed.
At this point, the images are still not on a bitmap, and that's because we can't place them on a bitmap in the correct DrawSequence
until we know where they all go. If we want to draw the hand before the torso, then we'll need to know where the hand goes, but we don't know that until we know where the forearm goes and where the arm belongs. So, now that we know, we just go ahead and scan through each limb in the ordered Draw Sequence, and use a call to graphics.DrawImage()
for each limb, placing the correct angled image of each limb at the pre-calculated location.
The image below may help describe how the slave limbs are placed on the screen. First, each limb's master hinge is positioned relative to the master limb's center and angle. Remember that we're dealing with radial coordinates, and the center of the original image is always at the center of the rotated image so that if the hinge's master-radial coordinate is pi and if the limb attached to the master's end of the hinge is also rotated by pi radians, then the hinge is located a certain distance away from the center of the master limb's image at an angle of 2pi radians (the distance being measured in pixels, and kept in the hinge's master rad-coordinate, then multiplied by the drawSizeFactor
the function receives as an input parameter). Once we have placed the hinge on the master limb, then we can calculate the slave limb's location by adding the master limb's angle with the hinge angle to get the slave's angle, and then place the slave's center so many pixels away from the hinge position at an angle equal to the new angle plus the hinge's slave rad-coordinate's angle. This is the same as adding all three angles: the master limb's angle, the hinge's slave rad angle, and the hinge's ~swivel~ angle (or Configuration angle). The English here sounds twisted, and I find it difficult to explain more clearly, but if you look at the code and draw yourself a picture, you might get a better idea.
Can you say 'enumeration'?
But, what's the point of all this if you can't figure out how to call it all in your end-product? Part of the problem in making a project like this is that the configurations have to be accessed easily and without confusion. You can always use the getConfigurationByName()
function whenever you want to do something, and if that suits you, then go ahead, but you can also use the Sprite Editor's 'reorder configurations' menu option, which looks like this:
If you your sprites have similar configurations, then it may be easier for you to reorder their different configurations so that they all match, but if you hadn't thought of that first, then you'd be in a bit of a mess trying to do this later without this handy feature. Grumpy's only got one configuration, as you can see on the right above the Ok button, but if he had more, you'd be able to use the same mouse-wheel/key-press interface described above (in reference to the Draw Sequences), with this listbox to reorder the lists of configurations in whatever order you like, and then use only one enum type for all your sprites.
The 'copy enum to clipboard' button here does the same thing as highlighting the contents of the left textbox and then pressing Ctrl-C, so that you have a copy on your clipboard ready to be pasted into your project.
Demo and Graphics Editor
The demo included with this article demonstrates how to make use of the sprites, and though the Graphics Editor's flood fill (to border) feature is particularly handy when cutting up images and turning them into sprites, it is not the best graphics editor around, and I recommend using it for no other reason than this flood fill and the straighten feature. You may want to use MS-Paint for most of your work, and then load the image in this GraphicsEditor only to do your straightening and flood-fill(ing?), save and then continue in MS-paint. The way this flood-fill feature works is like this: you select a color much like you would in MS-Paint, and then by scanning over the options panel and moving the mouse to the paint can flood-fill icon, a small picture box appears beside it with a red dot on it. The red dot is the color which the flood-fill will set the region it paints to, and you set that by clicking it after having chosen the color you want from the palette. Then, the label that appears next to this picture box allows you to toggle between the 'to border' flood-fill mode or the 'same color' flood-fill mode. The two can be quite handy. The 'to color' flood fill is the same as in MS-Paint, and nothing more but the 'to border' flood-fill fills an entire region with a uniform color until it is stopped by a pre-determined border color. You can set the mode by clicking on the label next to the pic-box with the dot when the label reads "switch to 'to border' mode" and then clicking on the pic-box-with-the-dot's border and setting it to whatever color you've previously selected.
Then, you click on the screen where you want to flood the image with the color of the dot, and that color will spread until it reaches the border color. The reason MS-Paint does not have this is probably because it's so easy to let a single pixel-sized hole in your flood-border which allows the flood to pass and fill the whole screen. You really have to plug those holes, but with 'un-do' anything, you do 'do' without wanting to be corrected.