Introduction
So, did you get a chance to see some of the SpriteEdito2017's code? Because in this article, I'm going to try to explain some of the more important parts of the classSprite
along with some of the other classes involved in making the sprites go. If you recall, in the first article of the SpriteEditor2017 series, I mentioned an article and another SpriteEditor
that I wrote and published ten years ago here at CodeProject. The original article Sprite Editor for .NET got a lot of clicks and I'd like to believe was the source of many hours of creative sleepless nights, missed lectures and sick days from work. Much fun was had by all.
Some of the things I'm going to talk about today:
- the sprite maker - loads, creates & saves sprites
- the sprite class - compiles, rotates and positions sprite limbs to make animation frames
- how to include a sprite into your apps - to bring your dreams to life... (Nastia Liukin not included)
How Does a Sprite Work
Sprites, as I'm sure you know, are a collection of images dutiously positioned by passionate developers to create smooth animations which can be easily incorporated into any project. But how do they work. For a primer, you can re-read the article mentioned above and have a general idea about how the SpriteEditor2017 works (they're both essentially very similar) but we're going to get the rudimentary stuff down here anyway.
Every sprite has:
- a list of limbs that each has unique names
- these limbs have pointers stored in an AlphaTree which can store/retrieve them by name
- a list of animations that each also has unique identifying names
That's essentially all the major components.
The classSprite_Limb
is one of the sprite's essential components, so let's have a look at this. These images don't include all of the contents of MS intellisense when you're in the classSprite_Limb
but they show the essentials. This class by itself really only holds the information that the classSprite
uses to generate the animation frames. There isn't a lot of work being done in the limb after the images for the limb have been rotated. So this class is relatively simple.
You have properties:
bmpDraw
: the most recently used image of this limb DisplayPosition
: used by the SpriteEditor's UI DrawAngle
: the angle to which it was last drawn Hinge
: a pointer to the classSprite_Hinge
that binds this limb to its master-limb ptDrawCenter
: the position of the limb's center relative to the sprite's MasterLimb
in the most recently drawn animation frame SetDrawPosition
: a boolean variable used to determine whether this limb's position should be calculated in the frame which the classSprite
is currently drawing (can save many unnecessary trigonometric & long division calculations and speed up delivery of the animation)
Some of the fields are:
cAT_LimbImages
- AT stands for AlphaTree and the cATs abound in this project. They're ternary search engines that have 26+ branching nodes at each iteration of their searches, using different branches for each letter of the alphabet. They're pretty fast and can hold any kind of data that is converted to the generic VisualStudio object
class. Just remember what you've stored in which tree, keep all the objects in your tree of the same data type and the classAlphaTree
that I'll mention again later, is very fast, reliable and versatile. Here, as the name implies, this cAT holds the various images that this particular instance of the limb class holds. intImageList_Index
: the index of the list of images (same image rotated 128 times per full rotation) last used to draw this limb lstHinge_Slaves
: list of hinges that bind this limb as a master to other limbs that branch off of it. lstImages
- holds the same data as the cAT just mentioned and is often used for its .Count
method, e.g., when these images have to all be put to screen by the SpriteEditor2017.
public class classSprite_Limb_Images
{
public string Name = conDefaultLimbImageName;
public Bitmap[] bmpImages = null;
}
Let's have a look at the classSPrite_Hinge
.
Here, you can see some of the properties:
Limb_Master
- Limb_Slave
: are pointers to this hinge's Master & Slave limbs Angle
refers to the angle at which this hinge is rotated, positioning its slave limb at an angle relative to its master. SlaveContact_MagnitudeFactor
: the factor by which the magnitude of the radian coordinate that is used to position the slave limb relative to the hinge and its master limb is multiplied. These last two variables (Angle
and SlaveContact_MagnitudeFactor
) are set at every frame of every animation, allowing the developer greater freedom to position and pose the sprite in more versatile ways than the earlier version of the Sprite Editor.
The fields shown here are mostly repeats of the source of the Properties listed above except for:
Contact
: holds the radian coordinates Master
and Slave
that position the hinge relative to both the slave and master limbs, and as a consequence, the slave relative to its master. These radian coordinates are different from the Angle and SlaveContact_MagnitudeFactor
mentioned above. These are the values that get set using the SpriteEditor2017's blue colored HingeContact_Graphical
panel, whereas the Angle
and SlaveContact_MagnitudeFactor
are the values that can be changed using the orange colored FrameEditor
panel. The Contact
values are semi-permanent, in that, they can be changed in the SpriteEditor, but once the sprite is saved to file and ready to be used in an app, there is no convenient way to change these values mid-flight. Whereas, the Angle
and SlaveContact_MagnitudeFactor
variables are changed in every frame of every animation and are what make the sprite editor so great at draw 2D animations.
Of course, you might have already realized that most of the work happens in the classSprite
. Here's a complete list of that class's intellisense and we can go over some of the inner workings of this important class.. You don't need to know any of this to use the SpriteEditor, but much of this will come in handy when it comes time to include your sprite in other projects. So here we are ...
Properties:
DrawSizeFactor
: a double
variable used to resize the output image Name
: you can probably guess NumImagesPerQuadrant
: this is the number of times a limb's image will be rotated and drawn for every quarter rotation. The default value is 32
, but you can set it at any value. If you do, I'd suggest you do so before you start building your sprite. I wrote this method so that it will redimension and regenerate all the limb images when you set this value but I never tested it, which is the reason why the SpriteEditor does not give you the option to change the number of images used mid-stride. If you're going to change this value, change the constant default value and leave it that way. It probably works fine, but I never tested it and will not vouch for it. And 32 images per quarter rotation seems fine to me. UseRAMCache
: seems self-evident, but probably needs a bit of explaining. The RAMCache
is a boolean variable that determines whether or not the Sprite's frame caching system is used when generating animation frames. The draw_Animation_Frame(ref classSpriteData cSpriteData)
function asks itself whether the frame image being requested is the same as the image it has stored in memory for that frame. If it is, it doesn't bother doing all the work of positioning limbs and drawing the appropriately rotated limb image onto a new bitmap in the pre-determined draw sequence, instead it just spits back the same image it sent out the last time this animation frame was requested. It's much quicker to hand over a ready made image than to redraw everything, so when you can get away with it, using a cache is very convenient. The only problem is some frames are rotated, others are resized... here's the test it makes to decide:
if (UseRamCache
&& cFrame.cCache.DrawAngle == cLimb_Master.DrawAngle
&& cFrame.cCache.DrawSizeFactor == DrawSizeFactor
&& string.Compare(cFrame.cCache.strLimbDrawIndex, cFrame.strLimbDrawIndex_CacheTest) == 0)
{
cSpriteData.moveData();
cSpriteData.cDrawData_current = cFrame.cCache.cDrawData.Copy();
bmpRetVal = cFrame.cCache.bmp;
goto cacheExit;
}
Next, we have the fields:
cAT_Animations
- is an alphaTree
, exact same class as I mentioned earlier. They're easy to use, reliable and versatile. This one holds all of your sprite's animations, so if you don't have an enumerated type handy telling you which index to use to access a given animation, you can ask for it by name.
This first example is from the classSpriteMaker'
s Load()
function which loads the animation from a file and then stores a pointer to it in the cAT_Animations
alphatree
by converting the instance of classAnimation
into a generic variable of object type and then inserts it into the tree using the name of that animation.
object objAnimation = (object)cAnimation;
cRetVal.cAT_Animations.Insert(ref objAnimation, cAnimation.Name);
This next example uses the same alphatree
in order to tell the sprite class which animation the user has selected in the SpriteEditor's Animation
selection combobox
.
cSpriteData.cAnimationData.cAnimation
= (classAnimation)cSpriteData
.cAnimationData
.cSprite
.cAT_Animations
.Search(cmbAnimation.Text);
This is the first time I've mentioned the classSpriteData
. You're going to get acquainted with it because it's what you use to tell the classSprite
which frame you want and how you want it.
You can also traverse these alphaTree
s to get a complete list of the objects in them. Here, a traversal is made in order to rebuild the tree. Mind you, this could easily have been done using a list. I'm just a tree hugger on a binge selling my alphaTree
s all over town.
public void ChangeName(string NewName)
{
classAnimation cMyReference = this;
Name = NewName;
List<classalphanumtree.classtraversalreport_record>
lstTraversalReport = cSprite.cAT_Animations.TraverseTree_InOrder();
cSprite.cAT_Animations = new classAlphaNumTree();
for (int intAnimationCounter = 0;
intAnimationCounter <
lstTraversalReport.Count; intAnimationCounter++)
{
classAnimation cAnimation = (classAnimation)lstTraversalReport[intAnimationCounter].data;
cSprite.cAT_Animations.Insert(ref lstTraversalReport[intAnimationCounter].data,
cAnimation.Name);
}
}
lstAnimations
- holds all the same animations as the alphatree
. It's convenient to have a list around for several reasons. Ease with which they can be manipulated, indexing is faster than doing an alphaTree
search and they have a .Count
method which immediately tells you how many you have. cAT_Limbs
- you've probably noticed the way these variables are named that this is also an alphaTree
. These things are very convenient to have lying around. They don't cost much because in terms of memory, all you're doing is holding pointers to the data you already have, and pointers are not a great burden on your memory. Mind you, each level of the alpha tree has to carry null
26 pointers to branches of more nodes to the same tree so having longer names to identify your data will consume some memory but compared to the graphics and all these rotated images the sprite contains these pointers and node branches are insignificant. If you're having memory problems, you could probably look elsewhere to settle it, otherwise, use shorter names for your sprites and limbs that will reduce the overhead on the alphatree
s. I doubt any sprite you have would have so many limbs and animations that the alphatree
is your memory problem. Reduce the size of your sprite and keep going, or build several of the same sprite with different animations and load the ones you need for your user's current environment. lstLimbs
- This is a complete list of all your sprite's limbs. You can find them using an enumerated type for just that purpose. The limbs here are the same as those pointed to by the cAT_Limbs
above. cLimb_Master
- The Great Unmoved Mover, the Grand Poobah of limbs, the Chosen One. This is the limb I was talking about in the previous article. It is there when you create a new sprite. Everything is built around it and when you change the drawangle
, you're rotating the cLimb_Master
. It has no master limb and has no hinge. So tests have to be made throughout the classSprite
as well as the SpriteEditor
to differentiate between a regular limb and the MasterLimb
because half the work that's done on limbs involves referencing their hinges and master limbs which cLimb_Master
takes offense to. lstLimbSlavesGetRetVal
- is a list used internally by a recursing function which finds all of a given limb's slave limbs. semDrawAnimation_Frame
- because the sprite uses the classSpriteData
which is essentially a removable data cartridge you can slip in that tells the sprite which animation and frame to run. The 'data-cartridge' is needed because the sprite's location on the screen is changed by the sprite class whenever that sprite is drawn relative to a given limb and the transitions from one limb to another need to remember where that sprite was the last time it was drawn. That's all find when you're only drawing one instance of a given sprite but when you have twelve or twenty of the same sprite all on the screen wearing different costumes and doing the same animations (in the next article, I'll show you my gymnast-cloning machine which animates dozens of sprites using that are identical in lists of limbs and animations but each one wears a different 'uniform' and has to have her own classSpriteData
to remember where she was when she side-steps and cartwheels across the stage that is your desktop. So, to get to the point, a semaphore is needed to make sure that the data which the instance of sprite class is using the correct data and this correct data does not get corrupted with other dancing gymnasts in pink, or blue leotards. Animation
: Alphabetize
, Delete
, & New
- self explanatory - no surprises here Limb
: New
, Delete
- also fairly simple and clear Limb_Slaves_Get
- uses a recursing function that tracks down through all the slaves of a given limb and gathers them into a global list variable setDrawPoints_PrePass
- goes through the Animation Frame's DrawSequence
and tracks the slaves of each limb that has to be drawn. Each of these slaves' boolean variable tells the draw_Animation_Frame()
function which limbs need to be positioned and rotated during the setDrawPoints()
function. setDrawPoints
- branches out recursively from the MasterLimb
through all its slaves that have passed had their SetDrawPosition
boolean variables set during the PrePass
and positions these limbs relative to their master and the state of their own hinge angle
/SlaveMagnificationFactor
. The Sprite
class can position all the limbs, skip the PrePass
and ignore the SetDrawPosition
but doing this gives you the means to have as many limbs as you want and not go through the math of positioning the ones you don't see. What I had in mind at the time of writing this was with regards to any humanoid character, namely Nastia Liukin, to be specific. The Gymnast.sp3 sprite as it is at the time of writing has only a forward facing gymnast but in reality, I intend to take the time to make left and right profiles, as well as a back and put them all in the same sprite. You can do that with this SpriteEditor
, just slide the entire LeftProfile
list of limbs down below the -do-not-draw- marker in the DrawSequence
and go on about your day. With the setDrawPoints_PrePass
, you can have as many limbs as you want and not be bogged down doing all the long divisions and trigonometry involved in positioning and angling the limbs you don't need and that don't appear on the screen. Just make sure that each version of your sprite branches off from the MasterLimb
, then the PrePass
will find the first one, uncheck its boolean and skip and not waste time with the lot of them again. setFrameAnimationVariables
- takes the current Animation Frame and copies the variables that turn all the hinges of each limb to the values set by the designer of that sprite's animation to position the limbs as they are intended to appear setSpriteData_PT
- uses the classSpriteData
to calculate a sprite's current location after having been drawn in the current Animation Frame. draw_Animation_Frame
- This is the function that does it all. It prePasses and Passes and then Positions and draws. It even does Windows. You send it your bitmap request in the form of a dutiously filled instance of the classSpriteData
and this function right here will set you right up, take 30 microseconds and hand you the image you were looking for sized and angled just the way you like. It would press and fold, but the union reps have a problem with that.
The Animation
class doesn't actually do very much besides be a container for a list of Frames it manages with Add
, Sub
and Capture
functions.
cSprite
- is just a pointer back to the sprite it belongs to lstFrames
- its bread & butter, is all it's good for. An animation essentially really is nothing but a collection of frames. Name
- huh... what did I fall asleep...
The methods here don't really have very much either... the real meat is in the Frames
class.
Add
, Capture
& Sub
- nothing special going on here. DrawList_Add
- adds a newly created limb to ever frame of its list of frame
and that is about it.
So here is where everything that the sprite needs to know to draw the image you want has to be. It's not particularly complicated at all. Remember that each limb needs to be positioned on the screen just as it was positioned by the designer using the SpriteEditor2017 IDE. To do that, it has to know where everything belongs which it does by remembering the angles and SlaveMagnitudeFactor
of each limb. "But wait,," I can hear some enthusiastic spirits of the keener minds corner of the room saying there is neither the word Angle
nor SlaveMagnitudeFactor
in the lists provided in the image above, and you are right because those two very important variables are contained in the classAnimation_Frame_LimbData
which I'll happily go over with you in just a moment. We're almost there.
Limb_DrawRelativeTo
- is a pointer to the limb about which all other limbs are to be drawn. But what really happens inside the sprite when it generates a frame with a LimbDrawRelativeTo
that is not the MasterLimb
is the same thing as any other frame. The image is drawn exactly the same way, the only difference is the way the information is packaged afterward. The Limb
s themselves will tell you their positions relative to the MasterLimb
but the classSpriteData'
s cDrawData_Current
tells you where each limb is positioned relative to the top-left of the frame's bitmap. Since you know where you placed the bitmap on the screen, that may be easier for you to deal with and the classSprite
's limb's values will change with the next frame of the next call to draw_Animation_Frame()
which may or may not be the values that you're dealing with depending on how many of your animated characters use the same sprite. Horizontal
& Vertical Mirror
s - are boolean variables that determine whether the image you get back are flipped and the classSpriteData
's limb positions reflect their new appearances cCache
is a class that keeps track of the parameters and the bitmap of the last time this frame was drawn allowing you to speed things up by getting a copy of what's already in memory straight from cache rather than going through the trouble of drawing it all over again ptTL
& ptBR
- are the distances away from the MasterLimb
that when subtracted one from the other give you the size of the output bitmap lstData
- is a list of classAnimation_Frame_LimbData
type which tells the frame at what angle to draw each limb, how far to move it relative to its masterLimb
and the index of the LimbImage
it is to draw for the limbs. Respectively: Angle
, SlaveMultiplicationFactor
& Limb
as you can see in the image below. The only thing I've left out is that there's a copy of the name of the limb's LimbImage
so you can search for it by name. It also makes a copy of all the LimbImage
names in the list, then compares this list to the list of the frame requested to test to make sure the cache is fine. At the point of writing this, I realize there are many things I can change to improve the quality. Namely, the cause for this test since the frame's list of images don't change from one minute to the next and so you can safely assume the limb image requested for each limb will always be the same... -> useless test, I'll see what happens when I take it out. Also, the draw_Animation_Frame()
function multiplies everything by DrawSizeFactor
when it could just as easily go about its business of drawing the same old image and resizing it, flipping it from cache as required. I'll look into this and probably update the first article and classSprite
after a bit of testing... or since its stable, I could leave that up to you.
.
And next, we come to our big finale, the classSpriteMaker
whose intellisense is seen below:
eDebugOutput
- was essential for debugging the Save()
/ Load()
functions. fs
- is a global filestream
. Making a filestream
a global variable may not be recommended or advised but it helped me figure out some issues when I used it in my debugging functions to track the position of the stream at every step of the way. You may detect and infer from the slew of calls to the debugger to test my filestream and record everything twice for every read/save operation in the Load()
and Save()
function that I was very frustrated at the time of writing those functions. Adding things as I went along made it difficult to keep the file-stream stable and without these debugging tools, it would have taken me longer if I didn't eventually quit in aggravation. So now, the fs
variable is global
and I ain't changing it as a remember of those good 'ole days. - All of those
Debug
fields relate to the debugging involved in loading and saving the sprites to file. There's an rtx
textbox which formats the data and indents everything according to the loops in the code where embedded loops were indented a little further than the others.
Bugs
It's not what you think.
The bugs
App is a fun app that's a result of the fact that it's so easy to make a quick (two-three hours) animation from a new sprite. I had to use the SpriteEditor
in order to test it and find the bugs as I went along. It's pretty stable now all because of these bugs. Which for most coders, programmers, hackers and developers is a four-letter word. These critters were fun to make and gave me a great sense of satisfaction to watch them go.
So, the Bugs
app is the first example I'm going to provide you of how to use the classSprite
outside of the SpriteEditor
. It's a simple app that searches its working directory for sprites, then loads every one it can find and runs the first animation in their list of animations. Since these animations take account of the DrawRelativeTo
limbs that moves them around, they have three states: TurnLeft
, TurnRight
and Crawl
but all three of these are drawn with the same Crawl
animation. Even though the Ant
has a pincer animation, Bugs
ignores that. So they crawl in one direction for a while and then, they'll take a few steps turning slightly for a while and they walk on.
Simple!
They can crawl all over your desktop because each one of them uses code I downloaded from the CodeProject called Per Pixel Alpha Blend that paints forms so that everything of transparent color disappears and all you see is the painted image with your desktop appearing as if there was no square boxy form around it. And that is about it. so, let's have a look at the code that loads the sprites into the app.
It's got two forms, that is, the form that generates all the other forms formBugs.cs then it will generate as many instances of the second type of form that displays and takes on the shape & image of the bug it has loaded as a sprite. This form is declared with this line:
public partial class formCreepyCrawler : PerPixelForm.PerPixelAlphaForm
But first, what we have to do is declare an instance of the classSpriteMaker
which does all the loading/saving of sprites. Here, we see the instance of classSpriteMaker
is declared as a global
variable right near the top of the code of formBugs.cs:
classSpriteMaker cSpriteMaker = new classSpriteMaker();
After a bit of work searching the directory for the list of files with the sprite extension *.sp3, it then loads the sprites into a list of sprites it keeps handy.
for (int intSpriteCounter = 0; intSpriteCounter < strFileNames.Length; intSpriteCounter++)
{
classSprite cSprite = cSpriteMaker.Load(strFileNames[intSpriteCounter]);
cSprite.UseRamCache = true;
lstSprites.Add(cSprite);
}
At this point, it has searched the directory and stored all the files with *.sp3 extensions in the string
array strFileNames
the length of which is used to limit the for
-loop. The first line inside the for
-loop declares the new sprite to be loaded by the instance of classSpriteMaker
declared above and calls it cSprite
. Since the bugs have only one appearance each the UseRAMCache
is set to true
and we can let the classSprite
decide whether or not to take advantage of this depending on what angle your several instances of each bug is drawn. The UseRAMCache
will probably not be used very often when you have multiple examples of the same bug but drawn at different sizes and different angles as they wander around your screen but when you click the option to limit the amount of bugs to one of each, then the UseRAMCache
option will be more evident even though your computer shouldn't be burdened with the seven or whatever bugs it finds. As I've been writing this article and looking at things with a mind to explain them to others, it has given me a few ideas on how to improve it all by eliminating the DrawSizeFactor
and the LimbImage
tests that now fail the UseRAMCache
unnecessarily. But I may or may not do that right away and you're free to give it a shot if you like.
So, back to this bugs
thing. Here, we see the few lines that call the function AddBugs()
that adds a few instances of that particular sprite to the list of formCreepyCrawler
:
for (int intSpriteCounter = 0; intSpriteCounter < lstSprites.Count; intSpriteCounter++)
{
classSprite cSprite = lstSprites[intSpriteCounter];
intMax = cRND.getInt(3, 5);
AddBugs(intMax, ref cSprite);
}
and the AddBugs()
function looks like this:
void AddBugs(int intNum, ref classSprite cSprite )
{
for (int intCounter = 0; intCounter < intNum; intCounter++)
{
formCreepyCrawler frmBug = new formCreepyCrawler(ref cSprite);
lstBugs.Add(frmBug);
}
updateInfo();
}
So, that looks easy enough. It creates multiple instances of the formCreepyCrawler
, then adds these to a list called lstBugs
. After this, the formBugs.cs runs on a timer that calls each bug in its list and tells it to Animate()
.
So now, it's time we had a look at this formCreepyCrawler
. This is a screen capture of the bugs
in action crawling all over my screen. The formBugs.cs is visible at the top left of the screen with the menu context displayed. You'll notice that the bugs are crawling over top all the other forms except their parent form formBugs.cs which covers them. The Toggle TopMost
feature does just that, toggles whether or not they're drawn on top of all other forms. Add
& Sub
are pretty basic and the OneEach
option erases the current set of bugs you have and regenerates one of each bug for your viewing delight. Adorable, aren't they? I personally love the spider. The still shot doesn't do them justice. The Per Pixel Alpha thing is awesome!.
So, here's what happens when you declare an instance of the formCreepyCrawler
.
public partial class formCreepyCrawler : PerPixelForm.PerPixelAlphaForm
{
classMath3.classRandom cRND = new classMath3.classRandom();
public classSpriteData cData = new classSpriteData();
ContextMenu cmuContextMenu = new ContextMenu();
MenuItem mnuQuit = new MenuItem();
MenuItem mnuAddBugs = new MenuItem();
MenuItem mnuSubBugs = new MenuItem();
MenuItem mnuToggle_TopMost = new MenuItem();
Bitmap bmpImage = null;
public static bool bolTopMost = true;
enum enuMove { turnLeft, turnRIght, Crawl, _numMoves};
enuMove eMove = enuMove.Crawl;
The important stuff here is the line where the:
classSpriteData cData = new classSpriteData();
is declared. This is the information the sprite
class needs to animate and move this particular bug around. As we saw earlier, this holds the Frame
index, pointer to the animation, size, location and angle of your critter. After the initial declarations of the context menu, it declares the bmpImage
value which we'll see later and the static
variable bolTopMost
which the formBugs
uses to tell them all where they stand. But have a look at the enumerated type, pretty simple right. Left, right or crawl. just basic. This bugs
current move status is recorded in the variable eMove
and set to Crawl
.
MouseWheel += FormCreepyCrawler_MouseWheel;
int intY = cRnd.getInt(0, Screen.PrimaryScreen.WorkingArea.Height);
int intX_FromEdge = cRnd.getInt(0, Screen.PrimaryScreen.WorkingArea.Width);
Location = new Point(cRnd.getInt(intX_FromEdge,
Screen.PrimaryScreen.WorkingArea.Width - intX_FromEdge),
intY);
cData.cAnimationData.dblDrawAngle = cRnd.getDouble * classMath3.TwoPi;
cData.cAnimationData.intFrameIndex = cRnd.getInt(0,
cData.cAnimationData.cAnimation.lstFrames.Count - 1);
double dblRatioScreen = 0.08 + cRND.getDouble * 0.10;
cData.cAnimationData.dblDrawSizeFactor = 1.0;
Bitmap bmpSample = cData
.cAnimationData
.cSprite.draw_Animation_Frame(ref cData);
double dblBmpSize =(double)( bmpSample.Width > bmpSample.Height ? bmpSample.Width : bmpSample.Height);
cData.cAnimationData.dblDrawSizeFactor
= (dblRatioScreen / dblBmpSize) * (double)Screen.PrimaryScreen.WorkingArea.Height ;
FormBorderStyle = FormBorderStyle.None;
TopMost = true;
ShowInTaskbar = false;
Visible = true;
cData.pt = new Point(intX_FromEdge, intY);
The variables, intX
and intY
, are set randomly to position the bug somewhere on the screen and I didn't really give that much thought but what's important to note is the cData.cAnimationData.intFrameIndex
& cData.cAnimationData.dblDrawAngle
variables are set before the sprite
is put to the screen as well as the cData.cAnimationData.dblDrawSizeFactor
which gets set to some random value as a function of the size of the sprite's natural image when drawn at its natural height (setting 1.0) and the size of the screen. The dblDrawSizeFactor
is then set to that random value. The rest makes sure the form has no tell-tale border to give away that this critter isn't just another form. The most important step here is to note the way it's positioned on the screen using the cData.pt
variable.. Very important because that's the variable that the sprite
class will change to move the critter around when it pins its little creepy critter claws to the screen and shuffles about ambling as it goes.
Since we have so little to talk about, here's the Animate()
function in its entirety. It sets the TopMost
to the value held by the static
variable , then cycles through to the next frame index using the modulo %
operator to ensure it doesn't exceed the number of frames in the animation.
Then, it has a look at the enumerated type variable eMove
which it then uses to decide whether to rotate the bug a few degrees to the left or to the right. At this point, we're ready to ask the sprite
for the frame's bitmap
. The following line is not so scary. The classSpriteData
holds all the information you need to ask the classSprite
to give you the image you want so you can keep a pointer to your sprite somewhere else but there's one right here. What this line of code does is it uses the instance of this bug's sprite here referred to as cData.cAnimationData.cSprite
. Some may find this ugly and cumbersome and they can change the classSpriteData
all they want to reflect their own preferences. The sprite instance is what you need, however you go and get it, call the draw_Animation_Frame()
function with a reference to your classSpriteData
and all the parameters of the frame you want and store the bitmap it sends back in a variable. Have a look:
bmpImage = cData.cAnimationData.cSprite.draw_Animation_Frame(ref cData);
Next, the Animate()
function uses the PerPixelAlpha
class to draw the form. You'll have to read that article to understand how that works.
public void Animate()
{
if (TopMost != bolTopMost)
TopMost = bolTopMost;
cData.cAnimationData.intFrameIndex
= (cData.cAnimationData.intFrameIndex + 1)
% cData.cAnimationData.cAnimation.lstFrames.Count;
switch(eMove)
{
case enuMove.Crawl:
break;
case enuMove.turnLeft:
cData
.cAnimationData
.dblDrawAngle = cMath3.cleanAngle(cData.cAnimationData.dblDrawAngle - dblTurnAngle);
break;
case enuMove.turnRIght:
cData
.cAnimationData
.dblDrawAngle = cMath3.cleanAngle(cData.cAnimationData.dblDrawAngle + dblTurnAngle);
break;
}
bmpImage = cData.cAnimationData.cSprite.draw_Animation_Frame(ref cData);
SetBitmap(bmpImage, (byte)255);
classAnimation_Frame cFrame = cData.cDrawData_current.cFrame;
Point ptRelLimb = (Point)cData
.cDrawData_current
.cAT_LimbPositions
.Search(cFrame.Limb_DrawRelativeTo.Name);
Location = cMath3.SubTwoPoints(cData.pt, ptRelLimb);
if (Left + bmpImage.Width < 0)
cData.pt.X += (Screen.PrimaryScreen.WorkingArea.Width + bmpImage.Width);
if (Left > Screen.PrimaryScreen.WorkingArea.Width)
cData.pt.X -= (Screen.PrimaryScreen.WorkingArea.Width + bmpImage.Width);
if (Top + bmpImage.Height < 0)
cData.pt.Y += (Screen.PrimaryScreen.WorkingArea.Height + bmpImage.Height);
if (Top > Screen.PrimaryScreen.WorkingArea.Height)
cData.pt.Y -= (Screen.PrimaryScreen.WorkingArea.Height + bmpImage.Height);
cData.cAnimationData.intFrameIndex
= (cData.cAnimationData.intFrameIndex + 1)
% cData.cAnimationData.cAnimation.lstFrames.Count;
if (cData.cAnimationData.intFrameIndex == 0)
{
intSteps--;
if (intSteps <=0)
eMove = (enuMove)cRnd.getInt(0, (int)enuMove._numMoves);
}
}
The tough stuff happens next. We want to position the sprite
where it belongs and there is a little bit of leg-work (forgive the pun, a voice in my head thought it was funny and it kinds is) because your sprite
's DrawRelativeTo
limbs change around as they go, you have to draw the image at the right location relative to the limb around which your sprite is designed to be drawn around. To do this, you ask your sprite
to kindly give you the position of that limb relative to the TopLeft
of the bitmap
.
Point ptRelLimb = (Point)cData.cDrawData_current.cAT_LimbPositions
.Search(cFrame.Limb_DrawRelativeTo.Name);
It might look complicated, but it's not too bad. We access the current position of this particular bugs limbs that are stored in this particular bug's classSpriteData
. The information here reflects this bug and this bug only. Any other bug being drawn by this same sprite
and having the same appearance, animations, and frames, limbs and all will be in a different state and therefore have its limbs in different positions. So don't rely on the Sprite's Limb list to know what this critter is doing. Instead, we use the classSpriteData
to get what we need. Here, what we need is the limb position of the current frame's DrawRelativeTo
limb. We can get it by searching the LimbPositions
alpha tree using the name of the Frame
's DrawRelativeTo
limb. We force the object that search returns to the type we expect it to be, in this case the LimbPosition alphaTree
holds Point
variables so we force the data object to the (Point)
type before assigning the value to our recently declared ptRelLimb
point variable. Ticky?
We do a search of the cAT_LimbPosition
alpha tree that we accessed through this bug's instance of the classSpriteData
called cData
. The information, data, stored in all alphaTree
s has to first be converted to the generic data type called object
. So whenever we retrieve data from an alphaTree
, we have to convert that data from object to whatever type it was before it was put into the tree. This tree holds Point
variables so we convert what the tree returns into a Point
type and assign it to our variable ptRelLimb
.
Next, we do a bit of math. Remember that cData.pt
is the position on your screen or picture box. Since we want the animation to roll smoothly, we have to position the image a distance equal to the distance between the TopLeft
of the bitmap and the DrawRelativeTo
limb, the point we just got from the alphatree
just a minute ago. Since the position of the limb is measured in positive (X,Y
) values down and to the right away from the TopLeft
of the bitmap
we have to paint on the screen, making sure that the DrawRelativeTo
limb appears on screen where it belongs, we have to subtract these X
& Y
values from the cData.pt
values. The classMath
can do that with this simple call which assigns the result straight to the form's location on your screen.
Location = cMath3.SubTwoPoints(cData.pt, ptRelLimb);
The rest of the Animate()
function cycles through this sprite's one animation's frames and picks and counts down its steps until it reaches zero, then picks a new move (right
, left
or crawl
). After giving some attention to moving the sprite from side to side and top to bottom and over-under-sideways-down, to keep it crawling along, the functions relinquishes control to the calling function which calls the same Animate()
function of the next bug on its list.