SpriteManager

Terms of Use
This code is provided for all to use on one condition: that the notice at the top of each script is kept intact and unmodified, and that if you make any improvements to the code, that you share them with the Unity community so everyone can benefit from them (please post to this thread). This revision has not yet been thoroughly tested for stability, but the code is pretty simple and it should be pretty stable.

Overview
Drawing lots of simple, independently-moving sprites for a 2D game can be performance prohibitive in Unity iPhone because the engine was designed with 3D in mind. For each object that has its own transform, another draw call is normally required. The significant overhead of a draw call quickly adds up and will cause framerate problems with only a modest number of objects on-screen. To address this, my SpriteManager class builds a single mesh containing the sprite "quads" to be displayed, and then "manually" transforms the vertices of these quads at runtime to create the appearance of multiple, independently moving objects - all in a single draw call! This dramatically increases the number of independently moving objects allowed on-screen at a time while maintaining a decent framerate.

While these classes were designed as a solution to performance limitations on the iPhone, they should work perfectly well in reducing draw calls using regular Unity as well.

Usage
1. Create an empty GameObject (or you may use any other GameObject so long as it is located at the origin (0,0,0) with no rotations or scaling) and attach the SpriteManager or LinkedSpriteManager script to it. (NOTE: It is vital that the object containing the SpriteManager script be at the origin and have no rotations or scaling or else the sprites will be drawn out of alignment with the positions of the GameObjects they are intended to represent! This gets forced in the Awake method of SpriteManager so that you don't have to worry about it in the editor.  But do not relocate the object containing SpriteManager at run-time unless you have a very good reason for doing so!)  Fill in the allocBlockSize and material values in the Unity editor. The SpriteManager is now ready to use.

2. To use it, create GameObjects which you want to represent using sprites at run-time. Add a script to each of these objects that contains a reference to the instance of the SpriteManager script you created in step 1.

3. In Start of each such GameObject, place code calling the appropriate initialization routines of the SpriteManager object to add the sprite you want to represent this GameObject to the SpriteManager. Depending on the animation techniques used, you may also need to add code to Update to manually inform the SpriteManager of changes you have made to the sprite at run-time. (In a later revision, all the necessary update calls could be made automatically to the SpriteManager through the Sprite class's own property accessors.)

The Sprite Class
The Sprite class contains all the relevant information to describe a single quad sprite (two coplanar triangles that form a quadrilateral). Each sprite has a width and height to indicate world-space dimensions. It also has the location of the lower-left UV offset (which can be changed at runtime to create UV animations) as well as the width and height of the UV (m_UVDimensions).

Each sprite contains four vertices which define the shape of its "quad" in local space. These vertices will be transformed by the SpriteManager class at runtime to orient the quad in world-space.

Finally, each sprite is associated with a GameObject referred to as the "client". This client object is the object to be represented by the quad. The quad will be transformed according to the client's transform. So when the client moves, the quad will follow, exactly as if the quad were simply part of the client GameObject.

drawLayer
This is an integer value that indicates roughly where in the drawing order the sprite should be drawn relative to other sprites in the same SpriteManager. Lower values are drawn first, higher values are drawn later. For sprites with equal drawLayer values, the order is undefined, unless MoveBehind or MoveInFrontOf are used to manually place one in front of the other in the drawing order.

SetDrawLayer
Sets the drawing layer for the sprite as described above for drawLayer, but also automatically calls the associated SpriteManager's SortDrawingOrder. This should only be used if you are only sorting a single sprite in a single game cycle. If you are assigning drawLayer values to multiple sprites in a single frame/update cycle, you should assign the values directly to their drawLayer members, then make a single call to the SpriteManager's SortDrawingOrder. The reason is that SortDrawingOrder can be very expensive.

Arguments

 * v - The value indicating the relative drawing order.

SetColor
Causes all four vertices of the sprite to be rendered using the specified color and transparency (alpha). (Automatically instructs SpriteManager to update the color entries for the sprite.) In addition to changing the color tint of the sprite, this is also useful for fading a sprite in or out.

Arguments

 * c - The color and transparency to use for the sprite.

SetSizeXY, SetSizeXZ, SetSizeYZ
Sets the dimensions of the sprite in the indicated plane (XY, XZ, or YZ).

SetAnimCompleteDelegate
Sets the delegate routine to be called when an animation is finished playing.

Arguments

 * del - The delegate to be called upon animation completion.

AddAnimation
Adds a UV animation to the Sprite's list of animations.

Arguments

 * anim - A variable of type UVAnimation, describing the UV animation to add.

PlayAnim
Begins playing the specified UV animation.

PlayAnimInReverse
Plays the specified animation in reverse - starting at the end.

PauseAnim
Pauses the currently-playing animation mid-stream, if any. Has no effect if no animation is playing.

UnpauseAnim
Unpauses the previously playing animation.

The UVAnimation class
This class describes a series of UV coordinates as well as a few parameters that describe a basic UV animation. That is, animation that uses multiple UV coordinates, displayed in sequence, to create the appearance of animation, very similar to stop-motion animation. This would typically be accomplished by creating a series of sprites placed at regular intervals in a row or column on a single texture. Then the coordinates of each individual sprite image are given, in sequence, to define the animation sequence. While you may simply manually define an array of Vector2 UV coordinates to setup your animation, some methods are provided to automate the process as much as possible.

Public members

 * name - A string containing the name of the animation. Should be unique from other animations associated with a given sprite object.


 * loopCycles - The number of times to loop the animation before stopping. 0 results in a "one-shot" animation. 1 would cause the animation to loop over once, etc. -1 causes the animation to loop infinitely.


 * loopReverse - If true, the play direction is reversed once the end of the animation sequence is reached. If set to false, a loop iteration is counted each time the end of the animation sequence is reached.  If set to true, a loop iteration isn't counted until the animation returns to the beginning.


 * framerate - The rate at which to advance from frame to frame, in frames per second. i.e. a value of 15 would cause 15 frames of animation to be advanced every second.

BuildUVAnim
This method automatically builds a UV animation sequence from a few pieces of information. The layout of an animation sequence grid is assumed to be left-to-right, top-to-bottom.

Arguments

 * start - The UV coordinates of the lower-left corner of the first sprite in the sequence


 * cellSize - The size, in UV space, of each sprite animation "cell" (to use BuildUVAnim, all sprite images in the sequence must have the same dimensions).


 * cols - The number of columns in the animation sequence grid.


 * rows - The number of rows in the animation sequence grid.


 * totalCells - The total number of cells in the animation sequence grid.


 * fps - The number of frames of animation that should advance per second (the framerate of the animation).

Return value

 * An array of Vector2 UV coordinates representing the animation sequence.

The SpriteManager class
This class manages a list of sprites and associated GameObjects.

Memory management

 * Currently, as sprites are added, the list (and associated vertex, uv, and triangle buffers) increase in size. As sprites are removed from the manager, the lists remain the same size, but the "open slots" are flagged and are re-used when new sprites are added again, removing the performance penalty of re-allocating all the buffers and copying their contents over again.  This approach was taken not only for the aformentioned performance reasons, but also because it would add significant complexity to reduce the size of the buffers since client GameObjects hold the indices of their associated sprites, and if the buffers were sized down, those indices could then point to invalid offsets.  The only way to resolve this would be to add either additional complexity to the design, or less performant ways of keeping track of sprites, or both.

allocBlockSize

 * Since allocating large new buffers and copying their contents can be a big performance hit at runtime, SpriteManager allows the developer to choose how many sprites should be pre-allocated at a time. If, for example, you expect your game to never use more than 100 sprites, you should probably set this value to 100, resulting in a one-time allocation of sprites so the player does not experience a "hiccup" mid-game as the buffer is re-allocated and new contents are copied over during gameplay.  If you pre-allocate 100 sprites and have filled up the sprite buffer, then find yourself having to create one more sprite (for a total of 101), if you have set allocBlockSize to 100, then another 100 sprites will be allocated even though you have added only 1.  So use caution in the value you assign to allocBlockSize.  Try to balance memory waste with frequency of having to re-allocate new buffers at runtime.  In the above case, using an allocBlockSize of 25, if you created 101 sprites, you would only have an "overage" of 24 sprites, but the buffers would have to be re-allocated and re-copied 5 times.

material

 * The material with which to render the sprites. Simply assign the materal you wish to use for your sprites here.  It is strongly advised that for sprites, you use one of the particle shaders so that backface culling is not an issue.  All the sprites for this SpriteManager will use this material.  So for a typical application, you would want to combine as many of your sprites as possible into a single texture atlas and assign that material to the SpriteManager.

plane

 * The plane in which the sprites are to be created. The options are XY, XZ, or YZ.  For example, an Asteroids type game might typically use sprites created in the XZ plane, while a Tetris-like game would probably use the XY plane.

winding

 * Which way to wind the generated polygons. The possible values are "CCW" (counter-clockwise) and "CW" (clockwise).  This affects the direction the polygons are considered to be "facing".  If you can't see the sprites at runtime with your particular setup, try changing the winding order.

PixelSpaceToUVSpace

 * This utility method will convert values from pixel space to UV space according to the material currently assigned to the SpriteManager object. For example, to use 256x256 pixels of a 512x512 texture, you would normally use the UV value 0.5,0.5.  However, PixelSpaceToUVSpace will allow you to specify 256,256 and will perform the conversion.  There is a version that accepts Vector2s and one that accepts ints.  Returns the converted UV-space value in a Vector2.
 * NOTE: Do not use PixelSpaceToUVSpace to get UV coordinates. PixelSpaceToUVSpace is intended to convert widths and heights from pixel space to UV space.  For coordinates, please use PixelCoordToUVCoord!

PixelCoordToUVCoord

 * This utility method will convert coordinates from pixel space to UV space according to the material currently assigned to the SpriteManager object. For example, to specify a point at the center of a 512x512 texture you would normally use the UV value 0.5,0.5.  However, PixelCoordToUVCorrd will allow you to specify 256,256 and will perform the conversion.  There is a version that accepts Vector2s and one that accepts ints.  Returns the converted UV-space coordinate in a Vector2.
 * NOTE: Do not use PixelCoordToUVCoord to convert widths or heights (dimensions). PixelCoordToUVCoord is only intended for actual coordinates and it inverts the Y-component since UV coordinates are given in the opposite Y-direction from how pixel coordinates are typically represented.  To convert heights and widths, please use PixelSpaceToUVSpace!

AddSprite

 * This method will add a sprite to the SpriteManager's list and will associate it with the specified GameObject. The sprite list as well as the vertex, UV, and triangle buffers will all be reallocated and copied if no available "slots" can be found.  The buffers will be increased according to allocBlockSize.  Performance note: Will cause the vertex, UV, and triangle buffers to be re-copied to the mesh object. NOTE: a versions also exist that allow you to specify the UV coordinates using pixel-space values that is not documented here.

Arguments

 * client - The GameObject that is to be associated with this sprite. The sprite will be transformed using this object's transform.


 * width and height - The width and height of the sprite in world space units. (This assumes that you have not applied scaling to the object containing the SpriteManager script - which you probably should not do unless you really know why you're doing it.)


 * lowerLeftUV - The UV coordinate of the lower-left corner of the quad.


 * UVDimensions - The width and height of how much of the texture to use. This is a scalar value.  Ex: if lowerLeftUV is 0.5,0.5 and UVDimensions is 0.5,0.5, the quad will display the associated texture from the center extending out to the extreme top and right edges.


 * offset - the sprite will be offseted by the specified Vector3 relative to the client transform. For example, applying an offset of (0.5, 0, -0.5) might help aligning a mouse cursor sprite so that the arrow tip corresponds to the mouse position. If ignored, Vector3.zero will be used.


 * billboarded - Whether or not the sprite should be rendered so that it always faces toward the camera (actually, it only faces in the direction from which the camera is looking, but it's close enough in almost all cases). Currently, this incurs a noticeable performance hit if many such billboards are used, so only use billboarding when necessary.

Return value

 * A reference to the sprite added.

SetBillboarded
Causes the specified sprite to be rendered so that it is oriented facing the direction from which the camera is looking. This type of orientation is somewhat more performance demanding, so be sure to only use it when necessary.

Arguments

 * sprite - A reference to the sprite to be billboarded.

RemoveSprite
"Removes" the sprite specified. (It actually just flags the sprite as available and reduces its dimensions to 0 so that it is invisible when rendered.)

Arguments

 * sprite - A reference to the sprite to remove. This should be the value returned by AddSprite.

Performance note: Will cause the vertex buffer to be re-copied to the mesh object.

HideSprite
Hides the sprite specified. This is accomplished by setting the sprite's associated vertices to 0. In addition, the sprite is removed from active lists so that it does not needlessly incur the overhead of being transformed. This method can be invoked indirectly by simply writing something such as:

NOTE: The sprite will not remain hidden if Transform is called on it, as Transform will cause its vertices to be re-copied to the mesh's vertex buffer, meaning they will no longer be set to zero (which is how they were "hidden" in the first place). So if you wish to keep a sprite hidden, be sure not to manually call Transform on it.

Arguments

 * sprite - A reference to the sprite to hide. This should be the value returned by AddSprite.

Performance note: Will cause the vertex buffer to be re-copied to the mesh object.

ShowSprite
Unhides the sprite specified which was previously hidden. This method can be invoked indirectly by simply writing something such as:

Arguments

 * sprite - A reference to the sprite to unhide. This should be the value returned by AddSprite.

Performance note: Will cause the vertex buffer to be re-copied to the mesh object.

MoveToFront
Moves the specified sprite to the front visually by moving it to the end of the drawing order, meaning it will be drawn on top of all other sprites in the SpriteManager.

MoveToBack
Moves the specified sprite to the back visually by moving it to the beginning of the drawing order, meaning it will be drawn first and all other sprites in the SpriteManager will be drawn intop of it.

MoveInFrontOf
Moves the first sprite in front of the second sprite by placing it later in the drawing order. If the sprite is already in front, nothing is changed.

Arguments

 * toMove - The sprite to move
 * reference - The sprite we wish "toMove" to appear in front of.

MoveBehind
Moves the first sprite behind the second sprite by placing it earlier in the drawing order. If the sprite is already behind, nothing is done.

Arguments

 * toMove - The sprite to move
 * reference - The sprite we wish "toMove" to appear behind.

SortDrawingOrder
Sorts all sprites' in the drawing order according to their respective drawLayer values with lower values being drawn first, and higher values being drawn later. NOTE: This routine is expensive and should only be called after setting the drawLayer of all the sprites you intend to sort in a given update cycle. It is automatically called when a sprite's SetDrawLayer method is called. Also note that if MoveToFront, MoveToBack, MoveInFrontOf, or MoveBehind have been used on a sprite but it has not had the appropriate drawLayer value set, calling SortDrawingOrder may undo these operations. So when using drawLayer values to order your sprites, be sure that their respective drawLayer values do not conflict with the ordering you specify through these other routines.

GetSprite
This method returns a reference to the specified sprite so that the sprite can be directly manipulated if need be. Use of this routine is now discouraged since the preferred way of referring to sprites is by reference. This is largely because in the future, the internal buffers may be resized at any time, at which point any indices obtained earlier may be invalid.

Arguments

 * i - Index of the sprite in question.

Transform
This method transforms the vertices associated with the specified sprite by the transform of its client GameObject. In plain English, if a GameObject wants to manually synch a sprite up with its current orientation, it should call this method. This method will transform that sprite, and that sprite alone, leaving all the other sprites un-updated. Performance note: Will cause the vertex buffer to be re-copied to the mesh object in the next LateUpdate.

Arguments

 * sprite - A reference to the sprite to transform.

TransformBillboarded
This method operates identically to Transform except that it orients the quad so that it faces the direction from which the camera is looking (billboarding). Performance note: Will cause the vertex buffer to be re-copied to the mesh object in the next LateUpdate.

Arguments

 * sprite - A reference to the sprite to transform.

UpdatePositions
Informs the SpriteManager that vertices have changed and they need to be updated (copied to the mesh object) at the earliest opportunity (usually the end of the current frame when LateUpdate is called). This is used if a GameObject has made changes to a sprite (such as changing its dimensions) and its vertices should be re-copied to the mesh to reflect these changes. Performance note: Will cause the vertex buffer to be re-copied to the mesh object in the next LateUpdate. NOTE: Under normal circumstances, caling SetSizeXX or Transform on a Sprite object will automatically call this routine. You should only have need to call it directly if you have manually modified the vertices directly.

Arguments

 * None

UpdateUV
Updates the UVs of the sprite in the local UV buffer (which mirrors that of the mesh object), and forces the UVs of the entire mesh to be re-copied to the mesh object. Use this when you manually change the UV offset or dimensions of the sprite between frames and want to inform the SpriteManager of the change so that it may update its UV buffer. Performance note: Will cause the UV buffer to be re-copied to the mesh object in the next LateUpdate. NOTE: Under normal circumstances, modifying lowerLeftUV or uvDimensions of a Sprite object will automatically call this routine. You should only have need to call it directly if you have manually modified one of the UV coordinates directly.

Arguments

 * sprite - The index of the sprite to update.

UpdateColors
Informs the SpriteManager to copy over the color values of the specified sprite. Performance note: Will cause the UV buffer to be re-copied to the mesh object in the next LateUpdate. NOTE: Under normal circumstances, modifying color of a Sprite object using SetColor will automatically call this routine. You should only have need to call it directly if you have manually modified a sprite's color directly.

Arguments

 * sprite - The index of the sprite to update.

UpdateBounds
Forces SpriteManager to recalculate the bounding volume of the mesh in the next LateUpdate. This is important to do periodically to ensure that Unity's visibility culling can cull the mesh when not visible, and also so that the mesh is not culled erroneously when it is visible. If your objects all remain pretty much in the same area, it is less important to call this frequently. However, if your objects move around quite a bit and the bounds are not recalculated, it is very easy for them to extend beyond the bounds of the previously updated bounding volume, causing Unity to cull the entire mesh based on the earlier boundaries, resulting in parts of your mesh which should be visible to disappear. ''Performance note: Will cause Unity to recalculate the bounds of the mesh, which can be very performance intensive with a large number of sprites. Consider using ScheduleBoundsUpdate to keep from recalculating bounds each frame.''

ScheduleBoundsUpdate
Instructs SpriteManager to call UpdateBounds at a regular interval. This can be useful if you have a large number of sprites that move around quite a bit so that recalculating the mesh's bounds each frame would be performance prohibitive. This way the bounds are only recalculated periodically.

Arguments

 * seconds - The interval in seconds.

CancelBoundsUpdate
Cancels any previously scheduled bounds update created by ScheduleBoundsUpdate.

The LinkedSpriteManager class
This class inherits from SpriteManager and adds the functionality of automatically transforming the vertices of all sprites each frame, removing the need to call "Transform" whenever the position of a GameObject is changed. The trade-off is that if you have lots of sprites that do not move most of the time, you will be transforming the vertices of these sprites needlessly each frame. If you have lots of sprites, this could impact performance noticably. If, however, the typical case is that your sprites will be in almost constant motion, it will be faster to use LinkedSpriteManager since all transformations are handled under a single function call (TransformSprites) rather than having each GameObject call Transform separately, thereby reducing call overhead.

Misc
Please report all bugs you find or improvements you make to these classes by posting to this thread. I put a bit of work into creating these and am sharing with everyone in the hope that more brains working on this will result in a more robust, efficient, and stable solution than I have time to commit to making happen here by myself. I truly believe that this approach will unlock game types and other possibilities that have, until now, been out of the question for Unity iPhone because of the overhead of the required draw calls.

Possible features to be implemented by the community in the future

 * Devise a method for reducing the size of the sprite and other buffers without adding significant complexity or performance overhead and without compromising stability.
 * Create a 3D version of SpriteManager for use with fully 3D objects.
 * Anything else you can think of!

SpriteManager.cs
You can also change the Reset function on the SpriteManager to make sure the first frame is played in the first loop:

v0.64

 * Corrected a bug in the "SortDrawingOrder" function: CW winding polygons were being incorrectly drawn.
 * Added rudimentary support for calculating normals. The mesh produced by the SpriteManager should be able to be correctly affected by Unity's lighting system (with the use of appropriate shaders, like Diffuse, for example).
 * Exposed the Sprite "offset" property so it can be used in the "SpriteManager.AddSprite" function.

v0.633

 * Fixed bug where when a sprite grid was used, BuildUVAnim was incrementing the y-coordinate in the wrong direction	(changed + to -).

v0.632

 * Added animation-handling facilities to allow the Sprite class to not derive from Monobehavior anymore, leading to better performance.
 * Added protection in some of the draw order management routines so that if calling IndexOf returns -1 because no matching sprite was found in the list, we don't attempt to access the array out of bounds.

v0.631

 * Added features allowing the sorting of the drawing order of sprites.

v0.63

 * Moved the Sprite class to its own file since it needs monoBehavior to use Invoke for animation.
 * Implemented UVAnimation class
 * Fixed problems where various properties of a removed sprite weren't getting reset, such as color, flags, etc.
 * Reordered some sprite code to make sure the proper settings were set before adding/removing sprites from active lists.

v0.622

 * Added ability to hide sprites without removing them.

v0.621

 * Fixed bug in TransformBillboarded where the camera transform was not being obtained.

v0.62

 * Added ability to specify a color (and alpha) for each sprite.
 * Added code to produce billboarded sprites
 * Added code to ensure mesh bounds are recalculated when the user desires

v0.61

 * Fixed bug where removing a sprite would not automatically result in the updating of the sprite's vertices to reflect that it had been removed (zeroed out).
 * Sprite objects now have direct access to their associated vertices and UVs in the SpriteManager's buffers (not the same as the actual buffers held by the mesh object). This should reduce overhead when transforming sprite vertices and updating UVs.
 * Interfacing with the SpriteManager is now reference-based rather than index based. In other words, info going in and out of SpriteManager that pertains to a particular sprite now uses references to the sprite in question rather than its index.  For example, AddSprite now returns a reference to the new Sprite, and RemoveSprite accepts a reference to the sprite to remove rather than its index.
 * Optimized transforming by storing cached reference to each sprite's client GameObject's transform locally.
 * Added utility function(s) PixelSpaceToUVSpace and PixelCoordToUVCoord that convert pixel space and coordinates to UV space and coordinates for the currently assigned material's texture.
 * There is now less of a need to manually inform SpriteManager when UVs are changed or when the sprite's dimensions change. Sprite objects, upon setting the size or changing UVs, will take care of informing the SpriteManager.
 * Added an offset property to the Sprite class. This can be used to offset all of the vertices by the same amount from its client gameObject.
 * Added the ability to specify texture coordinates using pixel-space values when calling AddSprite.

v0.64

 * Slight changes to support polygon normals.

v0.632

 * Added animation-handling facilities to allow the Sprite class to not derive from Monobehavior anymore, leading to better performance.

v0.631

 * Removed vestigial vars from TransformSprites
 * Moved temporary vars to class scope to avoid re-allocation each frame.

v0.52

 * Fixed a bug where if no sprite was added, the UV coordinates did not get copied to the mesh object.
 * Added code to produce billboarded sprites.
 * Added code to ensure mesh bounds are recalculated when the user desires

v0.64

 * Slight changes to support polygon normals.

v0.633

 * Fixed problem where animation wasn't getting reset when a sprite is supposed to be cleared of its states for re-use.
 * Fixed problem where calling PlayAnim while an animation is already playing results in the animation getting added to the playingAnimation list in the SpriteManager more than once, causing it to appear to play more rapidly, since its StepAnim gets called more than once per frame.

v0.632

 * Moved Sprite references in SpriteDrawLayerComparer so that they are statically allocated once for the entire class.
 * Sprite is no longer derived from Monobehavior to improve performance. Instead of needing Invoke, etc, a list of	actively animating sprites is maintained in the SpriteManager and the animation gets updated (if need be) each frame from there.  This also means that there is no more need for dummy GameObjets to host each Sprite object.

v0.631

 * Removed line in "hidden" property assigning the value to m_hidden___DoNotAccessExternally since this value is already set in SpriteManager::Hide/ShowSprite, and otherwise, these latter routines would be confused and exit early since they would mistakenly think this had already been set (well, the flag would have been set, but none of the operations performed)
 * Added drawLayer member which indicates roughly where in the drawing order the sprite should be drawn relative to other sprites. Higher drawLayer values result in a later drawing order.
 * Added class to compare sprite drawing layers (used for sorting)

v0.63

 * Moved sprite code from SpriteManager.cs to Sprite.cs so that Sprite can inherit from monoBehavior to allow use of Invoke for animation.
 * Changed SetSize* routines to call Transform instead of UpdatePositions so that when not using LinkedSpriteManager, you don't have to manually call Transform after calling SetSize*.