cocos2d Best Practices
Improving performance
-
Use this guide as reference: performance tests
Xcode Thumb
Turn off thumb compilation in XCode for ARMv6, but turn it on for ARMv7.
-
Thumb compiling creates smaller compiled sizes at the threat to floating point math
-
Thumb code is MUCH slower than the non-thumb code
-
Instructions to change this setting on a project can be found here.
CCDirector
Director:
-
Use
DisplayLink
.DisplayLink
is the best Director, but is only available in SDK 3.1 or higher. IfDisplayLink
is not present, useMainLoop
orThreadMainLoop
.
// must be called before any other call to the director if( ! [CCDirector setDirectorType:kCCDirectorTypeDisplayLink] ) [CCDirector setDirectorType:kCCDirectorTypeMainLoop];
-
Or use NSTimer
CCDirector
with a very low (quick) interval like 1/240
// If you are using "NSTimer" Director you could set a very low interval [[CCDirector sharedDirector] setAnimationInterval:1/240.0];
Texture Atlas
When possible try to use texture atlas:
-
Make
CCSprite
from a correspondingCCSpriteSheet
spritesheet object. -
Use
CCLabelBMFont
orCCLabelAtlas
instead ofCCLabelTTF
-
Use
CCTMXTileMap
orCCTileMapAtlas
to render tiles
The Atlas versions of these objects provide faster rendering at the expense of code and art complexity. The Atlas versions utilize an AtlasManager that keeps one large image with multiple frames, and the individual Atlas object refer to a frame of that larger image. This saves on texture counts and accelerates OpenGL ES calls.
Atlas = a book of illustration or diagrams. In this context it means you have one big image loaded into an opengl texture which actually holds a series of smaller images. As opposed to all the smaller images each loaded in to their own texture.
Textures
When possible, try to use 4-bit or 16-bit textures
-
16-bit textures for PNG/GIF/BMP/TIFF images
-
4-bit or 2-bit textures: Try to use PVRTC textures.
-
Use 32-bit textures as the last resort
Add this line before loading Textures:
[CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGBA4444]; // add this line at the very beginning
For background images you can drop the alpha channel by using RGB565 format. This format has the advantage that the visual quality is better than using RGBA4444.
Simply reducing the color by setting the pixel format has the disadvantage of quality loss and the size of the resources still stays big.
Dithering TODO: explain dithering.
PVR textures TODO: explain the benefits of PVR TODO: explain .pvr.gz / .pvr.ccz
The PVR format lets you create RGBA8888, RGBA4444, RGBA5551 and RGB565 textures
Using PVR-Sprites:
CCSprite *sprite = [CCSprite spriteWithFile: @"sprite.pvr"];
Particles
* There are 2 type of particles: Quad and Point particle system. * Point particle system seems to be a little bit faster on 1st and 2nd gen devices, but it is much slower on 3rd gen devices / iPad.
So, either you can check the device at runtime, or, the “lazy” approach would be to use Quad Particle.
Reducing Memory
-
Use 16-bit or 4-bit textures (see Improving performance)
-
Use the
CCTextureCache
-
CCTextureCache
caches all images -
Even when the image is not used anymore, it will remain in memory
-
To remove it from memory do:
-
// textures with retain count 1 will be removed // you can add this line in your scene#dealloc method [[CCTextureCache sharedTextureCache] removeUnusedTextures]; // since v0.8 // removes a certain texture from the cache CCTexture2D *texture = [sprite texture]; [[CCTextureCache sharedTextureCache] removeTexture: texture]]; // available in v0.7 too // removes all textures... only use when you receive a memory warning signal [[CCTextureCache sharedTextureCache] removeAllTextures]; // available in v0.7 too
Timers
-
Try NOT to use Cocoa’s
NSTimer
. Instead use cocos2d’s own scheduler. -
If you use cocos2d scheduler, you will have:
-
automatic pause/resume.
-
when the CCLayer (CCScene, CCSprite, CCNode) enters the stage the timer will be automatically activated, and when it leaves the stage it will be automatically deactivated.
-
Your target/selector will be called with a delta time
-
/**********************************************************/ // OK OK OK OK OK /**********************************************************/ -(id) init { if( (self=[super init] ) ) { // schedule a callback [self scheduleUpdate]; // available since v0.99.3 [self schedule: @selector(tick2:) interval:0.5]; } return self; } -(void) update: (ccTime) dt { // bla bla bla } -(void) tick2: (ccTime) dt { // bla bla bla } /**********************************************************/ // BAD BAD BAD BAD /**********************************************************/ // Why BAD ? // You can't pause the game automatically. -(void) onEnter { [super onEnter]; timer1 = [NSTimer scheduledTimerWithTimeInterval:1/FPS target:self selector:@selector(tick1) userInfo:nil repeats:YES]; timer2 = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(tick2) userInfo:nil repeats:YES]; } -(void) onExit { [timer1 invalidate]; [timer2 invalidate]; [super onExit]; } -(void) tick { // bla bla bla } -(void) tick2 { // bla bla bla }
Draw vs. update
-
try not to update any state variable inside the draw selector.
-
try not to draw anything inside a scheduled selector
-
Instead update the state variables inside a scheduled selector.
-
Instead draw everything inside the draw selector
-
If you update state variables inside the draw selector, the pause/resume won’t work as expected.
-
If you draw something inside a scheduled selector, it can’t be transformed
-
draw is called every frame
-
scheduled selectors can be called with any frame rate, but no more frequently than the application’s FPS rate.
/**********************************************************/ // OK OK OK OK OK /**********************************************************/ -(void) draw { [item draw]; // OK: DRAW INSIDE DRAW } -(void) update:(ccTime) dt { item.position = dt * finalPosition; // OK, UPDATE STATE IN SCHEDULED SELECTOR } /**********************************************************/ // BAD BAD BAD BAD 1 /**********************************************************/ -(void) draw { dt = [self calculateDelta]; // DONT UPDATE STATE IN DRAW. item.position = dt * finalPosition; // Pause won't work [item draw]; } /**********************************************************/ // BAD BAD BAD BAD 2 /**********************************************************/ -(void) update:(ccTime) dt { item.position = dt * finalPosition; [item draw]; // <--- DON'T DRAW IN SCHEDULED SELECTOR // because transformations won't alter your image }
Director flow control
-
If possible try to use
replaceScene
instead ofpushScene
-
pushScene
is very handy, but it will put the pushed scene into memory, and memory is a precious resource in the iPhone.
// TRY TO AVOID A BIG STACK OF PUSHED SCENES -(void) mainMenu() { // etc [[CCDirector sharedDirector] pushScene: gameScene]; } // stack: // . game <-- running scene // . mainMenu -(void) game { [[CCDirector sharedDirector] pushScene: gameOverScene]; } // stack: // . gameOver <-- running scene // . game // . mainMenu -(void) showGameOver { [[CCDirector sharedDirector] pushScene: hiScoreScene]; } // stack: // . scores <-- running scene (4 pushed scenes... expensive) // . gameOver // . game // . mainMenu
Creating Nodes (CCSprite, CCLabel, etc..)
-
When possible try to create
CCNode
objects (CCSprite
,CCLabel
,CCLayer
, etc) or any other kind of object in theinit
selector and not in thedraw
and any other scheduled selector -
The creation of nodes might be expensive, so try to have them pre-created
-
On the other hand, be careful with the memory. Don’t have unnecessary objects in memory.
/**********************************************************/ // OK, MOST OF THE TIME /**********************************************************/ -(id) init { // etc... sprite1 = [CCSprite create]; // <-- USUALLY IT IS BETTER TO CREATE OBJECTS IN INIT // etc... } -(void) tick: (ccTime) dt { // etc... if( someThing ) { [sprite1 show]; // <--- BUT IF YOU DON'T USE THEM FREQUENTLY, MEMORY IS WASTED } } /**********************************************************/ // BAD, MOST OF THE TIME /**********************************************************/ -(void) tick: (ccTime) dt { // etc... if( someThing ) { sprite = [CCSprite create]; // <--- EXPENSIVE [sprite1 show]; //... [sprite1 release]; // <-- AT LEAST MEMORY IS RELEASED } }
Hierarchy of Layers
-
Don’t create a big hierarchy of layers. Try to keep them low when possible.
Actions
-
It is expensive to create certain actions, since it might require a lot of
malloc()
. For example: ACCSequence
of aCCSpawn
with aCCRotateBy
with a anotherCCSequence
, etc… is very expensive. -
So try to reuse actions.
-
Once the action is used, save it for later if you know you will execute that type of action again. Then, instead of allocing a new action, you can just initialize it.
2010.09.26 - This is confusing because most ObjectiveC classes do NOT expect their init method to be called again on an already initialized object. And this will NOT work for some CCAction subclasses, CCIntervalAction as an example. CCSequence will leak memory if you call InitOne:Two: on it a second time. Etc. To make CCSequence reusable I think you'd have to write a setActionOne:andTwo: and code the class to release the previous actions and then retain the new ones. As a result, I do not believe this “you can just initialize it” advice is good or complete enough if there is some other kind of re-initialization that will work.
Buttons
this is not a best practice but a Tip
-
Use a Menu with a
MenuItemImage
and place your menu using menu.position = ccp(x,y). See theMenuTest
example for more details.
How does the pause/resume works ?
this is not a best practice
-
when the director receives the pause message it won’t call any scheduled target/selector.
-
but the draw selector will be called at a rate of 4 FPS (to reduce battery consumption)
-
when the director receives the resume message, the scheduled target/selectors will be called again every frame.