Hey Phil, thanks for your feedback. I agree, there are always new ideas coming along during development. A GDD can never be totally complete and accurate. However, when you discuss new ideas with other people working on the project (especially the graphics artists and level designers), it's important to explain them how these changes would affect the code and/or how much time it would take to integrate them. Sometimes you have to reject new ideas in order to meet the deadline. You can also keep them in mind for a future update. If you're close to releasing your game, you probably don't want to add new features, see this interesting article on the Valve Developer Community's wiki.
Back to today's topic : the Singleton design pattern. I briefly talked about it in this post. In fact, I don't have much to add ;) I don't do unit tests in my game programs so I don't really care about singletons possibly breaking them. Global state can be tricky and that's why you need to know what you're doing. By the way, an interesting discussion on the Singleton controversy can be found here : google-singleton-detector/wiki/WhySingletonsAreControversial (read the comments as well).
As Phil said about singletons : "I've found them very useful, as long as you plan out what you want them to store in the first place". Agreed. And if you followed the steps I described in my previous post on this thread, you should be able to determine what needs to be made global (in general, it's what needs to be accessed from different places in the code). I'm not going to paste the code to create a Singleton class since it's been done more than once on this forum (and the old google one) and you can always look at the following cocos classes which are all singletons : Director, TextureMgr, TouchDispatcher, ActionManager, SimpleAudioEngine, Scheduler. Instead, I'm going to talk about how I used the singleton pattern to both hold my global variables during the game and manage save and load stuff.
But before that, please read this thread : Component based entity systems. The topic has been quickly hijacked by ob1 and slipster216. It turned into a very interesting discussion around MVC and NSCoding. Worth reading !
.
.
.
Alright ! Let me explain how I wrote the save/load functions in the following game : Dr Bubbles (this links to the cocos2d games announcement forum).
My singleton code is in the GameState class. In this singleton instance, I put all the global variables needed for the game. For example : level, shotsLeft, score, currentColor, bonusX2 and bubbles (of course, there are a lot more but let's keep it simple). If you read the Component based entity systems thread, these variables represent my model in the MVC pattern (though it's not really MVC, as ob1 pointed out in that thread). This model is all I need to resume a game. So I want the singleton instance to be serializable to easily save and load games. To achieve that, you have it conform to the NSCoding protocol. This what my GameState.h file looks like :
@interface GameState : NSObject <NSCoding> {
int level, shotsLeft, score, currentColor;
BOOL bonusX2;
NSMutableArray *bubbles;
}
Of course, every variable is defined as a property (eg. @property BOOL bonusX2;). I then define a bunch of methods. The relevant one regarding the singleton pattern is :
+(GameState *)gameState;
Since I want to be able to recreate the singleton from a saved file, I also have this method :
+(void)loadGameStateFromFile:(NSString *)file;
In my GameState.m file, I have all the @synthesize statements for my variable. I have the regular + (GameState *)gameState { ... } and +(id)alloc { ... } functions for the singleton pattern. The singleton instance is defined as usual : static GameState *_gameState = nil;
And the loadGameStateFromFile :
+(void)loadGameStateFromFile:(NSString *)file {
@synchronized([GameState class]) {
[_gameState release];
_gameState = [[NSKeyedUnarchiver unarchiveObjectWithFile:file] retain];
}
}
I then have the - (void) dealloc { ... } and - (id) init { ... } methods. In init, I initialize my variables with default values. In dealloc I release what needs to be released (in my case, I release the bubbles array). What's more interesting is the 2 methods of the NSCoding protocol :
- (void) encodeWithCoder:(NSCoder *)coder {
[coder encodeInt:level forKey:@"level"];
[coder encodeInt:shotsLeft forKey:@"shotsLeft"];
[coder encodeInt:score forKey:@"score"];
[coder encodeInt:currentColor forKey:@"curColor"];
[coder encodeBool:bonusX2 forKey:@"bonusX2"];
[coder encodeObject:bubbles forKey:@"bubblesArray"];
}
- (id) initWithCoder:(NSCoder *)coder {
self = [super init];
level = [coder decodeIntForKey:@"level"];
shotsLeft = [coder decodeIntForKey:@"shotsLeft"];
score = [coder decodeIntForKey:@"score"];
currentColor = [coder decodeIntForKey:@"curColor"];
bonusX2 = [coder decodeBoolForKey:@"bonusX2"];
bubbles = [[coder decodeObjectForKey:@"bubblesArray"] retain];
return self;
}
Important :
My bubbles array contains objects of type Bubble. This Bubble class needs to conform to the NSCoding protocol, otherwise the above function won't be able to serialize the array. For example, in Bubble.h :
@interface Bubble : NSObject <NSCoding> {
int index, color;
BOOL captured;
}
@property int index, color;
@property BOOL captured;
@end
And in Bubble.m :
@implementation Bubble
@synthesize index, color, captured;
- (void) encodeWithCoder:(NSCoder *)coder {
[coder encodeInt:index forKey:@"index"];
[coder encodeInt:color forKey:@"color"];
[coder encodeBool:captured forKey:@"captured"];
}
- (id) initWithCoder:(NSCoder *)coder {
self = [super init];
index = [coder decodeIntForKey:@"index"];
color = [coder decodeIntForKey:@"color"];
captured = [coder decodeBoolForKey:@"captured"];
return self;
}
To save the GameState, when the application is about to terminate, you just do :
[NSKeyedArchiver archiveRootObject:[GameState gameState] toFile:file];
And when the app is launched again, you check if the saved file exists, and if it does, you do :
[GameState loadGameStateFromFile:file];
And that's it. It works very well for a game like Dr Bubbles because it's easy to dissociate the bubble sprite from the bubble data. If I had subclassed the Sprite class to make a BubbleSprite containing the index, color and captured variables as well as the TextureNode code to draw itself, I wouldn't have been able to save the game like that because the cocos classes don't conform to the NSCoding protocol.