[NOTE: I’m no longer updating the documentation in this post, so please see the wiki entry for up-to-date documentation on how to use this.]
I think you will all agree that the method I present here for drawing 2D sprites will allow you to make certain 2D games with Unity iPhone that you otherwise would think twice about making due to the poor performance resulting from having too many draw calls. I’m providing this code for all to use on one condition: that you keep the notice at the top of each script intact and unmodified, and that if you make any improvements to the code, that you share them here so everyone (including the author! :)) can benefit from them. Oh, and I’d love if you could drop me a note if you use this in your project. I’d like to see what you’re able to do with it. I really would like to see the community take off with this and continue to add functionality. It has not yet been thoroughly tested for stability, but the code is pretty simple and I expect that it should be pretty stable. The next step I see for this approach is to allow for simple 3D meshes instead of just quads. This would come in handy for 3D games, whereas the current solution is most useful for 2D games.
Okay, on to the code! (See attached files for the sourcecode.)
Summary:
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.
Usage Overview:
-
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.
-
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.
-
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.
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
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.
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.
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.
Return value: the index of the sprite added. This is the ID that will be used in the future to access the sprite.
RemoveSprite()
“Removes” the sprite specified by i - the index of the sprite in the sprite array. (It actually just flags the sprite as available and reduces its dimensions to 0 so that it is invisible when rendered.)
Arguments:
i - The index of 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.
GetSprite()
This method returns a reference to the specified sprite so that the sprite can be directly manipulated if need be.
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.
Arguments:
i - The index of the sprite to transform.
UpdatePosition()
Transforms the vertices of the specified sprite and forces the vertices of the mesh to be re-copied in the next frame. 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. For now, it basically does the same thing as Transform(), but may have somewhat different functionality in the future. Performance note: Will cause the vertex buffer to be re-copied to the mesh object.
Arguments:
i - The index of the sprite to update.
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.
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.
In closing:
Please report all bugs you find or improvements you make to these classes. 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:
- Some way of having a more automated, but flexible, UV animation system that is encapsulated in the SpriteManager or Sprite classes to simplify the code needed in the client GameObjects to perform UV animation. The characteristics of an ideal system would be: A) support for an arbitrary number of animation sequences for a single sprite, B) support for different animation framerates for each sprite, C) a simple interface for defining, playing, pausing, and otherwise controlling animation playback on a per-sprite basis, D) it would not impose any undue restrictions or rigid conventions on the artist when creating the texture that contains the animation frames.
- 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!
Well, that’s it! I hope you benefit greatly from the use of these classes and that you can, in turn, help us all to improve upon them.
114685–4499–$spritedemopackage_186.unitypackage (236 KB)
114685–6117–$spritemanager_116.zip (12.4 KB)