The Ultimate Cocos2D Project is: Kobold2D!
Put simply: Kobold2D is designed to make Cocos2D developers more productive.
Original Post
Time for a weekly update. This time about startup code and configuration. One of the things that I frequently encountered following the development of Cocos2D and working with it, is how any change to the startup code - the main function, the App delegate and the root ViewController - caused issues and headscratching among developers.
I decided it doesn’t need to be this way.
The main() function
A code snippets speaks more than words:
[cc lang=”ObjC”]
int main(int argc, char *argv[])
{
return KKMain(argc, argv, NULL);
}
[/cc]
That’s right, all of the startup code is now part of the project’s source code. You can still do whatever you need to do before and after the call to KKMain (probably nothing, except maybe anti-piracy code). And the third parameter (NULL) to KKMain is reserved for future use, to pass in any configuration parameters if the need arises.
Let’s see what KKMain does:
[cc lang=”ObjC” height=”650″]
int KKMain(int argc, char* argv[], SMainParameters* userParameters)
{
SMainParameters parameters;
initMainParameters(¶meters, userParameters);
#ifdef __IPHONE_OS_VERSION_MAX_ALLOWED
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
#else
// Mac OS X specific startup code
[MacGLView load_];
#endif
// This makes the CCDirector class known to wax:
[CCDirector class];
// wax setup is sufficient for all intents and purposes
wax_setup();
[KKLua doString:kLuaInitScript];
[KKLua doString:kLuaInitScriptPlatformSpecific];
[KKLua doString:kLuaInitScriptForWax];
// This loads the config.lua file
[KKConfig loadConfigLua];
// run the app with the provided general-purpose AppDelegate which handles a lot of tedious stuff for you
#ifdef __IPHONE_OS_VERSION_MAX_ALLOWED
int retVal = UIApplicationMain(argc, argv, nil, parameters.appDelegateClassName);
[pool release];
#else
int retVal = NSApplicationMain(argc, (const char**)argv);
#endif
return retVal;
}
[/cc]
The usual, really, except that it also initializes Wax and thus Lua for your App as well as providing the necessary startup code for both supported platforms: iOS and Mac OS X. The KKLua class is an Objective-C wrapper around the most imortant Lua functions, most notably it has the doString and doFile methods which allow you to run any Lua code or file containing Lua code.
KKConfig is a class that loads a Lua table and stores it in a NSDictionary for fast access to Lua parameters at runtime. I’ll discuss it in detail another time. The main purpose of KKConfig is to loadConfigLua, which loads the config.lua script returning a table containing startup parameters and making those parameters available to Objective-C.
Config.lua in detail
Let’s have a quick look at an excerpt of the config.lua file. It contains all of the startup parameters a developer using Cocos2D would ever want to tweak in a conveniently editable Lua script:
[cc lang=”Lua” height=”400″]
local config =
{
KKStartupConfig =
{
— load first scene from a class with this name, or from a Lua script with this name with .lua appended
FirstSceneClassName = “GameScene”,
— set the director type, and the fallback in case the first isn’t available
DirectorType = DirectorType.DisplayLink,
DirectorTypeFallback = DirectorType.NSTimer,
MaxFrameRate = 60,
DisplayFPS = YES,
DisplayFPSInAdHocBuilds = NO,
— Render settings
DefaultTexturePixelFormat = TexturePixelFormat.RGB565,
GLViewColorFormat = GLViewColorFormat.RGB565,
GLViewDepthFormat = GLViewDepthFormat.DepthNone,
GLViewPreserveBackBuffer = NO,
GLViewMultiSampling = NO,
GLViewNumberOfSamples = 0,
Enable2DProjection = NO,
EnableRetinaDisplaySupport = YES,
— … and many more settings!
},
}
return config
[/cc]
Since you don’t want to guess what those settings mean, I’ve documented them for you:
Config.lua Parameter Documentation (PDF)
This should also illustrate the kind of documentation I’m striving for. Documentation will be available online. It’s created in a Confluence Wiki with the help of ScreenSteps for more visual, step-by-step documentation.
App Delegate & Root ViewController
You may be wondering how you can modify and tweak the App Delegate and Root ViewController if they’re both part of the distribution, rather than copied into each project? That’s actually very simple: both are regular Objective-C classes, so they can be subclassed and methods overridden as needed.
Both KKAppDelegate and KKRootViewController provide a default implementation which you can tweak with the config.lua parameters. If that shouldn’t be enough, for example if you have to plug in some 3rd party code into the App Delegate, each project will have a subclass of KKAppDelegate and KKRootViewController in which you can override any of the UIApplicationDelegate and UIViewController protocol methods. Usually you would first call the super implementation, unless you want to entirely replace the default behavior.
The KKAppDelegate method calls one specific method called initializationComplete at the end of the delegate method applicationDidFinishLaunching. This allows you to run any custom code right before the first scene is shown. You can use that to call the CCDirector runWithScene method manually, in case you have more than one scene which might be run as first scene depending on certain conditions.
If you set the FirstSceneClassName config.lua setting, the project will first check if there’s a classname.lua file. If so, it will run this Lua script, assuming it contains the implementation of the first scene (more on that some other time). Otherwise it checks if there’s an existing Objective-C class derived from CCScene with that name, and if so allocates and initializes this scene and calls runWithScene for you.
In essence
From your point of view, the execution of the App now starts with the first scene, before that there’s no code that you’ll have to concern yourself with. Any startup configuration tweaks that you need to do can be done comfortably via the config.lua file, and the only setting you’ll need to change is the name of the first scene’s class name or Lua script. In addition you’ll get access to some features out of the box, for example adding iAd banners is now a simple on/off switch.
Moreover, any time there’s a change in Cocos2D’s startup code, or the startup code in any other library (most notably Wax), I can just make those changes for you and release a new version. This isn’t something you need to concern yourself with anymore, and makes upgrading existing projects to new versions of Cocos2D and other libraries even easier.
While helping others solve their cocos2d project issues over the past year it became obvious that many projects have at least one major problem in one of these areas:
- memory management
- resource management
- code structure
Examples
Memory management issues normally range from allocating too much memory, either by loading too many textures up front which are only going to be needed later, or by memory leaks such as scenes not deallocating when switching scenes. Resource management problems range from not adding the right resources to the right target, often resulting in increased App size because resources are added to the bundle but never used by the App. It could also mean loading identical resource files except that they have different filenames (copies?), using up additional memory. Or not tightly packing sprites into Texture Atlases but instead using one Texture Atlas per game object - while this is understandable from a standpoint of logical seperation it does waste opportunities for optimization.
Finally, code structure or lack thereof regularly leads to “everything in one class” code design which is most likely an evolutionary process rather than intentional. It’s not uncommon to see classes with thousands of lines of code, sometimes even going past 10,000 lines of code in one class. Other things are using too many CCLayers without them adding a clear benefit, for example just to group all nodes at a specific z order together or to group them by functionality, eg one layer for enemies, one for players, one for background, one for UI, one for score, one for particle effects, and so on - without any of these layers being used for what they’re really good at: modifying multiple nodes at once, like moving, scaling, rotating or z-reordering them. And of course there’s the copy & paste hell, large blocks of code reproduced in various places only to modify some parameters instead of creating a method which takes the modifiable parameters as arguments. Even professionals I worked with got so used to doing that it became hard just to overcome the resistance of letting go of old habits. But they learned.
Summary
Nothing of this code design and structuring strikes me as odd or surprising. I’ve written code like this myself. I also believe if it’s good enough and works, then why the hell not? It’s a matter of experience and it’s only with experience that you clearly see how to improve things. This boils down to the regular learning curve where only training and tutoring and just simply making mistakes and learning from them helps in the long run. That’s how we learn things.
On the other hand, the things like Memory and Resource Management can also be learned but they have a different nature. They can be statistically assessed, they could be calculated and verified automatically. This makes me wonder if there isn’t some kind of automation and information tools that would help developers achieve better results in terms of memory usage and resource management? In the meantime it’s all about raising awareness …
Raising Memory Awareness
Most importantly I think we need to raise more awareness to these issues to cocos2d developers. One step towards that would be for cocos2d to display a “available memory counter” alongside the FPS counter. I used to patch CCDirector to simply display memory instead of FPS since that was always more important to me. Fellow cocos2d developer Joseph sent me his version to display both - I simply didn’t think of the obvious. So if you’d like to see FPS and available memory next to each other I think you can handle the changes to CCDirector outlined here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
// CCDirector.h, add below @interface: +(double) getAvailableBytes; +(double) getAvailableKiloBytes; +(double) getAvailableMegaBytes; // CCDirector.m, add as appropriate: #include <sys/sysctl.h> #import <mach/mach.h> #import <mach/mach_host.h> [...] +(double) getAvailableBytes { vm_statistics_data_t vmStats; mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT; kern_return_t kernReturn = host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmStats, &infoCount); if (kernReturn != KERN_SUCCESS) { return NSNotFound; } return (vm_page_size * vmStats.free_count); } +(double) getAvailableKiloBytes { return [CCDirector getAvailableBytes] / 1024.0; } +(double) getAvailableMegaBytes { return [CCDirector getAvailableKiloBytes] / 1024.0; } [...] -(void) showFPS { frames++; accumDt += dt; if ( accumDt > CC_DIRECTOR_FPS_INTERVAL) { frameRate = frames/accumDt; frames = 0; accumDt = 0; // the only change in showFPS is this line: NSString *str = [[NSString alloc] initWithFormat:@"%.1f %.1f", frameRate, [CCDirector getAvailableMegaBytes]]; [FPSLabel setString:str]; [str release]; } } |
Raising awareness to leaking Scenes
In addition I highly, strongly and with utmost reinforcement (without pulling out a gun) recommend to cocos2d developers to frequently check your scene’s dealloc methods. Preferably add a breakpoint there, or at the very least add the logging line: CCLOG(@”dealloc: %@”, self). If you want a more visible but less intrusive method you could do something like flashing the screen or playing a sound whenever the last scene is deallocated, so that you get so used to it that when you’re not seeing or hearing it anymore it immediately raises your attention.
If at any time during the development of your project the dealloc method of a scene isn’t called when you change scenes, you’re leaking memory. Leaking the whole scene is a memory leak of the worst kind. You want to catch that early while you can still retrace your steps that might have caused the problem. Once you get to using hundreds of assets and thousands of lines of code and then realize the scene isn’t deallocated, you’ll be in for a fun ride trying to figure out where that’s coming from. In that case, removing nodes by uncommenting them until you can close in on the culprit is probably the best strategy, next to using Instruments (which I haven’t found too helpful in those cases).
I ran into such a problem once because I was passing the CCScene object to subclasses so that they have access to the scene’s methods. The subclass retained the scene and was itself derived from CCNode and added to the CCScene as child. The problem with that: during cleanup of the scene it correctly removed all child nodes but some of the child nodes still retained the scene. Because of that their dealloc method was never called, and in turn the scene was never deallocated.