by Victor Chelaru
The SpriteEditor (SE) was designed with rapid game development in mind. Who wants to assemble a level, compile to make sure it all works well, and then return to the code to change it repeatedly? Although the SE provides for creating complex objects and levels, sometimes you just want to get your hands dirty with the code. SpriteGrids in particular are a very powerful object in the FRB engine, but up to the writing of this article, the only coverage they’ve been given is in the SE. This article will give a much more in-depth look at SpriteGrids and answer questions that you as a programmer may have about creating and using them purely in code. Also, having a deeper understanding of SpriteGrids can help you feel more comfortable knowing what the SE is doing in the background when creating SpriteGrids in scenes. This article assumes you have a basic understanding of the FRB engine, specifically how Sprites are created and managed in the SpriteManager and how SpriteArrays work. Before I get into the code, I’d like to discuss some of the history behind SpriteGrids as it will help you come to the same conclusion I did about how they should work.
I started the shooter Silver Storm around October of
2004. At that time, I wasn’t sure
exactly how the game would work, but I was planning on the engine being changed
quite a bit by the advancements necessary to make this game. So I started making some basic demos with my
partner
We had a problem, and any time I’m confronted with such a problem, I try to find a solution that is not only elegant, but also is generic enough as to be implemented into the core engine rather than just solving the problem for a particular game. So I thought for a while about the problem. Clearly, scrolling “strips” of Sprites are common in many side-scrolling games. So first, I thought to create a class called SpriteStrip. This would reference a Sprite and make sure that a continual horizontal strip existed on screen at all times. Rather than having this reference the movement of the camera for Sprite addition and destruction, it’d be better if the SpriteStrip actually checked the visible bounds; the camera can slow down or zoom in or out. . . or even change direction.
I liked this idea quite a bit, and started working out some basic code. I thought about all of the games that this could be implemented in. It was useful for any side scrolling. I thought of some of my favorites like Streets of Rage 2, Actraiser, and yes, even Super Mario Bros. As an improvement, rather than having one continual strip across the entire level, these strips could have bounds - a start and end value for the strip. That way, levels could be made out of these strips and there could be multiple strips with gaps between them for pits or breaks in platforms.
The best thing about these strips is that they only needed to remember their start and end bounds and a copy of the Sprite to use. That means that levels would be much smaller than before. Rather than a level being hundreds or thousands of Sprites, they could be just a few strips. But the benefit didn’t stop here. Consider having a long strip of Sprites – 1,000 of them, but let’s say that only 20 of them are visible. If the strip of 1,000 were created as regular Sprites, the SpriteEditor would have to store all 1,000 in memory (using up RAM), and would have to conduct all of the regular management as well. That includes calling the methods to move the Sprites, calling their custom functions, checking for attachments, and culling them every frame. Although these are fast calls, they can add up with large number of Sprites. Strips in effect implemented something I call memory culling. Not only will a Sprite out of the screen not be drawn, but it won’t even exist in memory. Furthermore, this culling is very cheap. The strip doesn’t have to test every Sprite. It only checks the Sprites on the edges which is usually just two checks per frame – one on the left and one on the right. Subsequent checks are made only upon extension and contraction.
Then I realized that adding a second dimension was trivial. Rather than a strip, I could create a two dimensional grid which tests and can extend or contract on the x and y axis. I decided to call this a SpriteGrid. Now the usage of this class wasn’t limited to side scrolling games, but could now be used for any tiled game as well. The user could create enormous planes (with the number of Sprites in the thousands or even millions of Sprites), but not suffer any performance penalty. The tests were simple, memory usage was small, and rendering was just as fast as if the grid were only as big as what is visible on the screen.
What was a problem in Silver Storm transformed first into a simple solution which then transformed into a complex yet powerful solution. The code and math behind the SpriteGrids wasn’t very complicated, but refining it led to some weird bugs. Also, it was only natural to be able to create and edit SpriteGrids in the SpriteEditor. That meant that users could create enormous levels in the SpriteEditor and move around in real-time positioning other Sprite objects within the level and referencing the SpriteGrid. The SpriteGrid advanced through its usage in the SpriteEditor and I soon added a class called a TextureGrid which allows for SpriteGrids to be painted; rarely are tilemaps in games only one texture.
After nearly a year of usage and testing, the SpriteGrids have become powerful, stable, and convenient to use. What follows is a discussion of how to hard-code these SpriteGrids, common ways to edit and manage them in code, an explanation of what is going on behind the scenes, and some cautions about using SpriteGrids.
SpriteGrids require a few steps before they become functional FRB objects. I will cover each step explaining why it is necessary. But first, let’s present some code. Using the GameData.cs class from the FRB Template, add the bold lines:
SpriteGrid sg;
public
void Initialize(GameForm
form)
{
[initialize
engine managers and data]
Sprite bluePrint
= new Sprite(); // Don’t create
the Sprite through the SpriteManager
bluePrint.texture = sprMan.AddTexture("gfx/grass.BMP");
sg = new
SpriteGrid(sprMan, camera,
'y', bluePrint, random);
sg.xLeftBound = -10;
sg.xRightBound = 10;
sg.yTopBound = 10;
sg.yBottomBound = -10;
sg.PopulateGrid(0,0,0);
sg.RefreshPaint();
}
public
void Activity()
{
// this code will be
called once per frame.
sg.Manage();
}
I first declare a SpriteGrid outside of any methods so that it has class scope in the GameData class. What follows is something that looks common enough but actually rarely happens with the engine – I create a Sprite by directly calling its constructor. This deserves a short discussion. One might ask why I didn’t create the bluePrint Sprite through the SpriteManager like is usually done, or conversely, why Sprites are never created by their constructor but I chose to do so here. The answer lies in what we intend to do with a Sprite. When a Sprite is created, it is usually done so with the intent of being displayed on-screen and managed by the SpriteManager. Most Sprites are created for this reason; therefore, they are created through the SpriteManager. However, the bluePrint we are creating is not to be displayed on the screen or managed in any way. It merely serves as a design to be used for the SpriteGrid for when it adds new Sprites. Right now, the only important piece of information is the texture. We will use the gfx/grass.bmp file (capitalization isn’t important when adding textures). So I just created a new Sprite and set its texture.
Now that a bluePrint is created, we can create the SpriteGrid. There are two constructors for the SpriteGrid – the one we use above for creating a SpriteGrid from scratch and one referencing a SpriteGridSave object. The version referencing a SpriteGridSave is used internally by the SpriteManager when loading .scn files which contain SpriteGrids. The first two arguments are references to the SpriteManager and a Camera. The SpriteManager is used to create and destroy Sprites and the Camera is used to determine what is visible and what is out of the screen.
The third argument is a char. The SpriteGrid expects either ‘y’ or ‘z’ as its argument which determines whether the SpriteGrid will stretch on the x and y axes (parallel to the screen) or x and z axis (perpendicular to the screen). There is currently no support for grids which lie on the y and z axes. I decided to make a traditional xy SpriteGrid for this example.
The fourth argument is the reference to the bluePrint Sprite we just created. Finally, we pass a reference to a Random object. This Random reference is used if the SpriteGrid is using animated Sprites. It randomly selects a frame in the AnimationChain so that patterns don’t form when a screen scrolls.
The next four lines set up the bounds for the SpriteGrid. The behavior of SpriteGrids and bounds is worth mention, but I’ll save that for a little later.
Now that we have the SpriteGrid properties set, the SpriteGrid has all of the information it needs to manage itself. But at this point, it hasn’t created any visible Sprites. It only exists in memory and the SpriteManager hasn’t been told to create any Sprites. We now have to populate the SpriteGrid through the PopulateGrid method. The PopulateGrid takes an x,y,z coordinate which it will use to start building the SpriteGrid. Since the SpriteGrid only performs tests at its edges, a SpriteGrid must “crawl” to expand. Consider a situation where a SpriteGrid is created, but the camera begins at x = 100. If the SpriteGrid didn’t know where to begin, it might choose the origin. It would then create a Sprite at the origin and test to see where the camera was. Noticing that the camera is far to the right, it might add a Sprite to the right of this origin. It would continue to add Sprites to the right until it reached and passed the right visible edge of the camera. Finally, it would have to delete its path of Sprites that it created. Passing points eliminates this crawling and makes SpriteGrid initialization much faster. Our camera defaults to being centered at the origin so we pass 0,0,0. Since the grid is an xy grid, the z value doesn’t matter.
Finally, we have to refresh the textures on the SpriteGrid. We only have to do this when we call PopulateGrid. Afterwards, the SpriteGrid will automatically apply the appropriate FrbTexture when we call Manage().
The last piece of code I added is the Manage call in the Activity method. The Manage method is responsible for surveying the position of the camera relative to the SpriteGrid and creating and destroying Sprites as necessary to keep the number of Sprites as small as possible while keeping the screen full (or at least until the visible Sprites reach the bounds of the SpriteGrid).
Compiling the code as is will show a square group of Sprites. At this point, the SpriteGrid isn’t doing much. Every frame it surveys the bounds and sees that the edges are still on-screen and that it cannot expand any further without stepping over the set bounds.
Before investigating how Sprites are added and destroyed, I’d like to discuss how bounds affect the SpriteGrid. Specifically, it’s usually the case that bounds will not match up exactly with the edge of Sprites even in situations where we intentionally set the bounds to do so. Rounding error introduced by floating point calculations can cause bounds to be inexact.
Consider that we never set the x, y, and z positions of the bluePrint Sprite we created so they all default to 0. This means that our SpriteGrid seed is at the origin. Also, since we didn’t set the sclX and sclY, those also default to 1. This is important because it determines exactly how our SpriteGrid will tile the Sprites.
The constructor looks at the size of the bluePrint Sprite passed and assumes that you want the SpriteGrid to tile in such a way that the Sprites touch edge-to-edge with no gaps or overlapping. If our Sprite has a sclX and sclY of 1, then it is 2 units wide. The SpriteGrid seeds with a Sprite at 0,0 (all z’s are 0). Moving to the right, this center Sprite’s right edge is at x = 1. The next Sprite is placed at x = 2, and its right edge is at x = 3. The pattern continues with all Sprites appearing at x and y values which are multiples of two. The last Sprites on the right is positioned at x = 8 with its right bound at x = 9. The SpriteGrid doesn’t add another Sprite because doing so would result in a Sprite being positioned at x = 10 and a right edge of 11 which exceeds the xRightBound of 10 we set initially. This pattern extends in all four directions and gives us a SpriteGrid with visible bounds of -9 to 9 on both the x and y axes. Our grid has a total of 81 Sprites as we will see in the next section.
It’s difficult observe the removal of sprites when they are out of the screen for obvious reasons. One way to observe the removal of the Sprites is to display the number of Sprites that the SpriteManager has in memory. First, I’ll add in some code to move the camera:
public void Activity()
{
// this code will be
called once per frame.
camera.KeyboardControl(inpMan, 10);
sg.Manage(); // call after moving camera so
grid is updated before being drawn
}
If you’re not familiar, the KeyboardControl moves the camera at a velocity of 10 (the passed value) when the arrow keys of the + and – keys next to the backspace are pressed. Now, I’ll display the number of regular Sprites that the SpriteManager stores in memory:
public
void AfterFrameActivity()
{
textMan.BeginDrawing();
TextFormat tf = new TextFormat();
tf.x = camera.x;
tf.y = camera.y + 4;
tf.z = camera.z + 40;
textMan.Draw("Regular
Sprites: " + sprMan.numOfRegularSprites, tf);
}
Running the program confirms the claim that there are 81 Sprites in memory. More interestingly, moving the camera around so that the SpriteGrid moves out of the screen or zooming forward shows the number of Sprites is automatically reduced and increased. This is all done in the SpriteGrid’s Manage call.
SpriteGrid bluePrint
Properties
The easiest way to modify the appearance of the Sprites that the SpriteGrid creates is to modify the bluePrint Sprite. Keep in mind that changes to the bluePrint will only be reflected on Sprites created after the change has been made. To see this in action, add the following bold code:
sg.yTopBound = 10;
sg.yBottomBound = -10;
sg.bluePrint.tintRed = 100; //
changes will be reflected immediately
sg.bluePrint.colorOperation =
Microsoft.DirectX.Direct3D.TextureOperation.Add;
sg.PopulateGrid(0,0,0);
sg.RefreshPaint();
sg.bluePrint.rotZ = .78f; //
won't affect the Sprites until they are recreated
}
Notice that I made two sets of changes. The first adds a reddish tint to the Sprites. Since this change is made before the SpriteGrid is Populated, the Sprites start out red when the program runs. The rotation does not take affect yet since this last change was made after the Sprites were created; however, moving the camera around so that the Sprites are destroyed and recreated implements the final change of rotating the Sprite. The following properties are copied from the bluePrint to newly created Sprites:
Changes to Newly Created Sprites
Although there are a few ways to modify Sprites through the bluePrint Sprite, Sprites have many other properties which we may want to modify. Even more complicated situations can arise such as Sprites being created with properties which change depending on time, location, state, or other value. The SpriteGrid.Manage method returns a one-way SpriteArray containing all Sprites created from the call. Rather than storing this in a SpriteArray, I threw the call in a foreach loop:
public
void Activity()
{
// this code will be
called once per frame.
camera.KeyboardControl(inpMan, 10);
foreach(Sprite s in sg.Manage())
{
if(s.x
< 0)
s.tintBlue = 255;
else
s.tintGreen = 255;
}
}
Compile this code and move the camera so that some of the Sprites are destroyed and recreated. The Sprites on the left side of the grid will be colored blue and the right side will be colored green. Although the returned SpriteArray from the Manage call gives a lot of freedom, be careful modifying the position of the created Sprites either directly by directly changing positions, velocities, accelerations, or attaching the Sprite to other Sprites. The SpriteGrid extends and contracts itself by looking at the Sprites on the edges and changing the absolute position of any Sprites can cause unexpected results.
Although it can be dangerous, the SpriteGrid gives direct access to all of the Sprites it has created through the visibleSprites SpriteArrayArray. This is a dynamic 2 dimensional array of Sprites. The Sprite at 0,0 is the bottom left (or closest furthest left Sprite in a xz grid). Accessing variable [3][2] returns the fourth Sprite up and third Sprite from the left; positive values follow the coordinate system and the second index always represents the column (x value). Remember, since the SpriteGrid grows and shrinks, [3][2] at one time may represent both a different reference and a different location on the SpriteGrid.
Sprites – Here One Frame, Gone the Next
The SpriteGrid will create and destroy Sprites as necessary, and this means that Sprites that exit the screen will usually be lost to the garbage collector (or the SpriteEditor’s internal particle recycling). This is one reason why attachment and other kind of Sprite relationships or references which depend on a Sprite staying alive are generally not a good idea with SpriteGrids. One area where this is important is collisions and tile maps. Rather than tying collision methods to specific Sprites in a SpriteGrid, a better solution is to create separate collidable Sprites (which will probably be invisible) and run collisions against those.
Texturing SpriteGrids
One might think that a Sprite’s texture can be changed by directly accessing the Sprite through the visibleSprites SpriteArrayArray using its index, but this presents two problems. The first is that the particular Sprite may not be visible at a given time so it won’t even exist in visibleSprite. The second problem is that Sprites are not persistent, and any changes made directly to Sprites will not stick unless they are made every time the Sprite in a particular location is created (by accessing it through the one-way SpriteArray created when Manage is called) or if the bluePrint’s properties are changed. Neither is a good solution for changing just one Sprite’s texture. As a further inconvenience, in some situations, you may not know where a particular Sprite is located by its index. The indices count from the first visible Sprite, not the bounds or other absolute location.
As mentioned before, SpriteGrids store an internal reference to a TextureGrid object. These TextureGrids are also two-dimensional arrays of FrbTextures. TextureGrids attempt to reduce the amount of memory used by assuming a default FrbTexture which happens to be the texture of the bluePrint Sprite and storing only the rows and columns that differ from the default FrbTexture. As a side note, TextureGrids reduce the memory used even more by using “jagged edges” on both the left and right sides of the array. More information on this subject is covered in this article about SpriteGrids in the SpriteEditor.
Most importantly, TextureGrids store the textures for the entire SpriteGrid whether the Sprites are visible or not. Whenever a new Sprite is created through the SpriteGrid, the SpriteGrid checks to see what the texture should be at the particular index. Since the TextureGrid is separate from all visibleSprites, we can set textures on the SpriteGrid without having to worry whether a particular Sprite is visible or not. Textures can be “painted” through the SpriteGrid.PaintSprite method. Add the bold line to the initialize method:
. . .
sg.bluePrint.tintRed = 100;
sg.bluePrint.colorOperation
= Microsoft.DirectX.Direct3D.TextureOperation.Add;
sg.PopulateGrid(0,0,0);
sg.RefreshPaint();
sg.bluePrint.rotZ = .78f;
// won't affect the Sprites until they are recreated
sg.PaintSprite(0,0,0,
sprMan.AddTexture("redball.bmp"));
}
The PaintSprite method does not require a Sprite reference, but rather an absolute position. In this case, I’m painting the Sprite at the origin (again, the third 0 is for a z of 0 which doesn’t affect the call in this case). The last argument is the FrbTexture to paint the SpriteGrid. The call first remembers that the Sprite at this position should be a red ball rather than the default grass texture for the SpriteGrid. Since the center Sprite is visible, the method also changes the texture of that particular Sprite for us. Again, we didn’t have to know whether the particular Sprite was visible. In fact, the location can actually fall outside of the bounds of the SpriteGrid, although this is not recommended as the Sprites will never be visible and this just wastes memory by making the TextureGrid bigger than necessary.
SpriteGrids Without Functionality
Although SpriteGrids can improve the efficiency of your program both in terms of frame rate and memory usage, there are times when the management behavior is simply not needed. One common situation is when we know that a SpriteGrid will not move out of screen, or if it does, we don’t want the Sprites destroyed and recreated. An example of this may be a chess game where pieces reference the tiles they are on. Another common example is when making split screen games. Since a particular scene may be viewed from multiple cameras, we don’t want the SpriteGrid to perform any memory culling according to one camera s the other camera will see Sprites disappear and reappear when the first camera moves around.
Disabling the behavior of SpriteGrids is very simple – just don’t call Manage(). The visibleSprites will remain as is so long as the Manage method is not called. There is one problem related to filling the SpriteGrid. The PopulateGrid method will fill up the SpriteGrid but only to the visible bounds of the camera. If the camera is in a position such that not all Sprites are visible when the PopulateGrid method is called, not all Sprites will be filled. If later on the camera moves to a portion of the SpriteGrid that was not filled during the PopulateGrid method, the SpriteGrid will look to be incomplete. To fill a SpriteGrid, simply call the FillToBounds method, which will fill up the visibleSprites to the edges of the bounds and ignore the camera’s visible bounds. Of course, this should only be used in situations where the SpriteGrid is not being self-managed.
Victor
Chelaru
Flat
Red Ball Head Developer/Lead Programmer
Email: VicChelaru@gmail.com
This
article was posted on flatredball.com: 9/02/2005
Modified: 2/17/2006
ã 2004-2006 FlatRedBall. All rights reserved.