[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.)
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.
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.
The SpriteManager class
This class manages a list of sprites and associated GameObjects.
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.
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.
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.
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.
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.
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.
"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.)
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.
This method returns a reference to the specified sprite so that the sprite can be directly manipulated if need be.
i - Index of the sprite in question.
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.
i - The index of the sprite to transform.
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.
i - The index of the sprite to update.
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.
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.
I've added this information to the community Wiki as well. You may find it here:
this sounds great, funnily i was just thinking about a solution for the problem getting lots of drawcalls for 2D stuff needed, too (Yours seems to go towards a similar direction as particle system, nice)
Do you have an example project where one can see it in action?
Hmmm, no, not at the moment. I've just implemented it into a project in production that I can't post here. But it isn't too difficult to throw something together (but after writing up all those docs, I'm all spent out on time at the moment). Just create a new GameObject, drag LinkedSpriteManager on to it, choose a material (use a particle shader), set allocBlockSize to whatever (anything but 0), then create a blank script that calls AddSprite() on the SpriteScript object above. Then create a cube with a rigidbody, attach the "blank" script I mentioned above to it, duplicate it a bunch of times, and then hit play. It should draw quads where each cube is and rotate them along with the cubes as they tumble.
Thanks for this - I will take a longer look at it soon, but it sounds fab.
This is sooo awesome! I was helplessly trying to find a 2D documentation / tutorial something and now you come and present a ready to use package!!!
A million thanks!
Well, I'm not sure if it's all that ready-to-use... well, it is, but the most natural setup to use it I've found is to have the camera looking along the positive Z axis, then orient your 2D game to play out in the XY plane. There's no reason you couldn't use it differently, of course. But that's how the quads are oriented by default.
I suppose that's another possible improvement - adding a simple setting that would dictate which world-space plane the quads were constructed on. That would help in cases where you may want to put the camera "overhead" (looking down the negative Y) and have the game play out on the XZ plane. I could see something like that for an Asteroids-type game. But of course, that's all personal preference, since there's no reason why an Asteroids game couldn't be written to use the XY plane instead.
I hope you find it useful! But to have a complete 2D solution, you'll probably also want to look into, for example, using joints, etc, to restrict object movement to a certain axis. Of course you'd only need that if you're using rigid body physics. And I'd use that sparingly on the iPhone.
BTW, there is a 2D gameplay Unity tutorial here:
I gave it a try now and while it overall seems to be quite nice all sprites are rotated wrong, i guess its due to my cam/ world space plane directions.Originally Posted by BradyI suppose that's another possible improvement - adding a simple setting that would dictate which world-space plane the quads were constructed on. That would help in cases where you may want to put the camera "overhead" (looking down the negative Y) and have the game play out on the XZ plane.
(I have it as you said on the xz plane).
Yep, that would do it. That needs to be high on the list of features to add. I may do that here soon when I get the chance.
Thanks, everyone for the kind comments.
Okay, I couldn't stand it anymore , so I went ahead and implemented a plane option. So now you should just have to choose which plane you want the sprites to be created in in the editor and it should work. I've updated the attached scripts as well as the wiki and documentation. Let me know if that works for you, tommosaur.
Thanks for this
It has all sprites rotated the right way it seems when i choose xz as plane. Now the problem with that is the texture is invisible unless i set the shader of the material to one of the particle ones. I guess they are somehow flipped and not shown then with the other shaders since they are not double sided. Its just a guess though
Awesome, Brady! We've got a game in the pipeline that could really use something like this, except with 3d meshes. I'll try to see if we can't tinker around with it and come up with something.
tommosaur, yeah, that's why in the docs I said it's highly recommended to use a particle shader. Otherwise, you may have to reverse the winding depending on where your camera is. That should be easy to do by modifying the Sprite class. I show which line is upper-left, lower right, etc, so it would just be a matter of moving those around to flip the winding.
Jarrod, to do it with 3D meshes, you basically need a cross between this and CombineChildren. But there's no reason it can't work. The ceiling on how many objects you can have before performance decreases noticeably would just be lower since each object would have more vertices, and transforming each vertex each frame is the real bottleneck. But I imagine, depending on how high-poly your meshes are, it would be faster than adding a drawcall.
Thanks Brady, I can't wait to dig into this!
if the sprites donīt move/rotate around is it then possible with any of the plane settings and a cam view position combination to get em to display without being set to a particle shader?
I tried several combinations now and always either the texture is not visible at all or is displayed mirrored.
Hmmm... I guess if you change the winding, you'll also have to change the UVs to match...
You know, I'm not sure why it would be doing that, since the quads are wound counter-clockwise from the perspective of looking down the positive Z in XY, negative Y in XZ, and positive X in YZ. Anyone have ideas?
EDIT: tommosaur, I just realized I didn't completely answer your question. Yeah, if they aren't showing up, you could reverse the position of your camera. i.e. if it's above the XZ plane, put it below it and look up instead. That's sort of inconvenient though since I'm sure you're used to thinking of your game's coordinate space a certain way. Is there a particular reason you can't use a particle shader? Perhaps the shader side of things would be the easiest workaround? But I'm still hoping someone can help shed light as to why CCW-wound tris would face the wrong way.
Nice! I'm glad I didn't invest too much time in my implementation of a sprite drawer! (It wasn't nearly as nice/robust as yours)
Dr. Jeff Craighead
Research Computer Scientist
James A. Haley VA Hospital, Tampa