How to improve the performance is a big and tough issue and there are countless books aiming at this problem. Here, I just want to share some of my experience of using cocos2d on how to optimize mass sprites rendering. By mentioning mass sprites, I mean rendering thousands sprite at the same time. And my attitude toward performance optimization is twofold. First, good coding habit is very important and can save you a lot of time. Second, only optimizing your program when it’s necessary.
OK, let’s get down to the business.
Step 1 : Using CCSpriteBatchNode and atlas of textures
The benefit of using batchnode: Rendering a sprite needs three steps: preparation, rendering, cleaning up. A group of sprites using the same texture need only one time preparation and cleaning up, which reduces the load of rendering.
The drawback of using batchnode: Sprites in the same batchnode share the same z-order. Though you can specify their z-orders, their z-orders are unclear when comparing to other sprites out of the batchnode.
The benefit of using texture atlas: Loading texture into memory is a consuming work, so atlas reduce the times of loading texture into memory, thus improve the performance.
The drawback of using texture atlas: Loading too many textures into memory consumes a lot memory, so it will increase the memory requirement and increase the loading time. What’s more, it’s more likely to receive memory warning. So plan carefully when and which sprites should be loaded.
There are two principles of using texture atlas in cocos2d.
- Putting as many textures as you can into one texture atlas. The edge of an atlas is usually some times of 2. (e.g. 256, 512). So no matter how much margin you left in the atlas, it will still occupy the memory of its origin size. So you’d better making full use of every atlas texture.
- If you have too many textures that will take too much memory, categorize them according to their relevance. If the probability of two textures’ appearing at the same time is very high, put them into same atlas.
A code example of using batch node and texture atlas.
Remember that batch node’s child can only be CCSprite and the sprite must be included in the file that created the batch node. In the above example, sprites must contains sprite1.
Step 2: Using object pooling technique
The normal way to render a sprite is to generate it and add it into a batch node. When you don’t want this sprite, remove it from the batch node. Simple and direct, right? However, when you are doing mass sprite rendering, it’s painful. When a batch node has a large number of children, the method addChild and removeChild become very costly. It takes a long time to add a sprite to the batch node or find a sprite and remove it. You don’t want your player to wait when they are playing, right? OK, then do it before they start playing. It’s better to move the overhead to the loading phase. So what we should do is to generate enough sprites and add them into the batch node beforehand. Also remember set them as invisible and put them into a hidden position. Whenever you want a new sprite, just grab a hidden one and set it a right position and make it visible. It’s also simple and with no pain. What you should do is to preload the batch node and modify the interface of getting a sprite to use.
Step 3: Using multiple batch nodes, even for the same sprites.
If you think that object pooling technique is awesome enough to ensure a good performance, I regret to say NO. Every batch node actually has limitation on the size of children array. The limitation is around 50,000, I don’t remember exactly. However, this is not the real reason why we use multiple batch nodes even for the same sprites. The real reason is that we can’t afford the mass rendering actually. We have to do the recycling. Can you imagine that you render more than 100 thousand sprites at the same time, you don’t really need to do this right? If you have to, sorry, you’d better come up with a way to integrate some sprites into background texture or something like that. Here I want to discuss some recycling tricks we may use when mass rendering. Categorize your relevant sprites into same batch node so that you can recycle them by group.
Please refer to the above picture for our model. Every ellipse stands for a batch node and the outside rectangle represents the object pool for all the sprites. For each batch node, we have a signal indicating whether this batch node needs to be recycled or not. When should you set the signal to true or false depends on your game logic, however, my suggestion is that keep relevant sprites in same batch node, so you have higher chance to recycle them together. This is very tricky and you have to come up with a way of grouping your sprites according to your game nature. The reason we don’t do sprite recycle one by one is that we can’t afford the overhead of polling every sprite. So we group them and try to only poll the groups. (Here you have to find a way to make a batch node stands for all the sprites in it. It also depends on your game logic, yet it’s common that you can do it on your regular update method with low overhead. Just try to think about it.) At regular intervals, we poll for every group, remove some batch nodes from layers, and recycle all the sprites in those batch nodes. Here the action “remove” is simply removing batch nodes from their fathers’ children list. After done this magic grouping, you will see a much improvement on your frame rate. Of course, the degree of improvement depends on your grouping strategy.
Step 4: Eliminating the performance spike
Here comes the final tip if you follow my advice to group your sprites and recycle them by groups. Though the frame rate is good, you may encounter some performance spikes every time you poll for your batch nodes. When you are doing polling, the frame rate will become very low for a moment and back to normal quickly. This is a little bit annoying and may affect player’s game experience. Hence, we need to streamline the performance spike by dividing the polling action into several groups as well. For example, see the graphs below.
We split poll process into 4 groups, and we run all the polling in turn. For example, this time of polling, we run poll1 only, and next time of update mere poll2 is run. This simple method streamline the spikes and still you can maintain a high frame work even if under heavy rendering work.
The above steps are my recent experience when doing mass sprite rendering in my game. My game requires thousands sprites to be rendered at the same time, and update their positions all the time. The only good thing is that I can design a good grouping strategy to group them and thus recycle them. So I can maintain a relative good frame rate under heavy load. And the above steps are my personal experience and I know there are many optimization methods that also work like a charm. Please feel free to share with me. Also, if you have any question about mass sprite rendering performance, please feel free to ask me. I ‘d like to help you if I can or discuss with you.
Below is an snapshot of my game.