Monday, July 13, 2009

Memory Management for Cocoa composite objects

While working on Pathology, I had a lot of trouble understanding memory management of composite data structures such as arrays and dictionaries.  In particular, I had need to implement a deep copy of heavily nested data structures (e.g. a dictionary item that is an array of arrays of custom objects, etc.)  in order to take a "snapshot" of my game state at known stable points in the game loop.

Here is some example code that I used to test my understanding of how to copy collection objects (NSArray, NSDictionary) as either a shallow copy or a deep copy. Also, how to implement proper copying in your own class for deep and shallow copying:



int DEEP_COPY = 1;

// This class has no distinction between mutable and immutable so I do not need to implement
@interface MyClass : NSObject
{
  NSMutableDictionary *myDict;
  int myID;
}

@property (nonatomic, retain) NSMutableDictionary *myDict;
  @propertyintmyID;

-(id) initWithID: (int) _ID;
- (id)copyWithZone:(NSZone *)zone;

@end



@implementation MyClass
@synthesize myDict, myID;

-(id) initWithID: (int) ID {
  if (self = [super init]) {
    self.myDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:@"one",@"first",@"two",@"second",nil];
    myID = _ID;
  }
   returnself;
}

- (id)copyWithZone:(NSZone *)zone {
  MyClass *copy;

  if (DEEP_COPY) {
// Deep copy
    NSLog(@"Performing deep copy in copyWithZone (MyClass)");
    copy = [[MyClass allocWithZone:zone] initWithID:myID];  // This sets the value field
// Continue the copy chain on dictionary elements
    copy.myDict = [[NSMutableDictionary allocWithZone:zone] initWithDictionary:myDict copyItems:YES];
  } else {
// Shallow copy
    NSLog(@"Performing shallow copy in copyWithZone (MyClass)");
    copy = NSCopyObject(self, 0, zone);
  }

  return [copy autorelease];

}


-(void) dealloc {
  [myDict release];
  [super dealloc];
}

@end






#import

@interface MemoryTestAppDelegate : NSObject {
    UIWindow *window;
    NSArray *firstArray;
    NSMutableArray *secondArray;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) NSArray *firstArray;
@property (nonatomic, retain) NSMutableArray *secondArray;

@end



@implementation MemoryTestAppDelegate
@synthesize window, firstArray, secondArray;

- (void)applicationDidFinishLaunching:(UIApplication *)application {   

// * Convenience method used, so should use self-dot accessor to retain.
// * Each element is created, then retained by the array, then should be autoreleased so their retain count is balanced.


  self.firstArray = [NSArray arrayWithObjects:[[[MyClass alloc] initWithID:1] autorelease], [[[MyClass alloc] initWithID: 2] autorelease], nil];
  [[[firstArray objectAtIndex:0] myDict] setValue:@"Initialized" forKey:@"first"];

  NSMutableArray *outerArray = [[NSMutableArray alloc] init];
  [outerArray addObject:firstArray];  //firstArray retain count incremented




// Test 1 - using copy
// * New memory to hold a second array is allocated, but each object in the array points to the same elements
//   as firstArray.  Each element's retain count in the first array is incremented


// * Should use mutableCopy if copying into a mutable array

// * If copy is used here, the following line would cause an exception

// * Since a copy function is called, do not use self dot, which would cause an additional retain

  secondArray = [firstArray mutableCopy];
  [secondArray addObject :[[[MyClassalloc] initWithID: 2] autorelease]];


// * New memory to hold a second array is allocated, but no new elements created.  firstArray retain count incremented.
// * Elements of firstArray are not affected.

  NSArray *secondOuterArray = [outerArray copy];


// * After changing the value in the original array, I expect to see the same change in the copied arrays
//   because only shallow copies have been made so far.

  NSLog(@"Changing first array element to 3");
  [[firstArray objectAtIndex:0] setMyID:3];
  NSLog(@"Second array element ID is %0d (Expected value: 3)", [[secondArray objectAtIndex:0] myID]);
  NSLog(@"Second outer array element ID is %0d (Expected value: 3)", [[[secondOuterArray objectAtIndex:0] objectAtIndex:0 ] myID]);



// ***** test 2 - using initWithArray:copyItems
  [secondArray release];  // Sends a release to each element, balancing the retain from the copy
  [secondOuterArray release];  // Sends a release to firstArray, firstArray's elements are unaffected.


// * This creates memory for secondArray and sends a copy message to each element in the first array.
// * The copy message then calls copyWithZone on each element, so the elements must have that method defined.
// * This will create a new MyClass object for each element in secondArray, but how pointers in that object are
//   treated will depend on the copyWithZone implementation.
// * Value fields will be independent copies.

  secondArray = [[NSMutableArray alloc] initWithArray: firstArray copyItems: YES];
  [secondArray addObject :[[[MyClassalloc] initWithID: 2] autorelease]];

// * The only array element in this case is a NSMutableArray, so the copy message is sent to NSMutableArray.
// * It creates a new NSArray element, but the child elements are not copied.  No new MyClass objects are created.
// * NSMutableArrays and NSArrays always perform a shallow copy.

  secondOuterArray = [[NSArray alloc] initWithArray: outerArray copyItems: YES];


  NSLog(@"Changing first array element to 4");
  [[firstArray objectAtIndex:0] setMyID: 4];



// ***** Test the value object
// * In the case of secondArray, new MyClass objects were created by the copy, and the myID field is a value field.
// * So, I expect to see no effect from the change to the original array.

  NSLog(@"Second array element ID after initWithArray:copyItems is %0d (Expected value: 3)", [[secondArray objectAtIndex:0]  myID]);


// * Here, no new MyClass objects were created in secondOuterArray, so I expect to see the change reflected here

  NSLog(@"Second outer array element ID after initWithArray:copyItems is %0d (Expected value:4)", [[[secondOuterArray objectAtIndex:0] objectAtIndex:0 ] myID]);




// ***** Test a pointer object
// * The results here depend on how copyWithZone is implemented in MyClass.

  [[[firstArray objectAtIndex:0] myDict] setValue:@"Changed" forKey:@"first"];
  if (DEEP_COPY) {
    NSLog(@"Second array element dict value after initWithArray:copyItems is %@ (Expected value: \"Initialized\")", [[[secondArray objectAtIndex:0]  myDict] objectForKey:@"first"]);
  } else {
    NSLog(@"Second array element dict value after initWithArray:copyItems is %@ (Expected value: \"Changed\")", [[[secondArray objectAtIndex:0]  myDict] objectForKey:@"first"]);
  }

// * Here, as in the value object case, no new MyClass elements are in the copy, so the result will always
//   match the firstArray

  NSLog(@"Second outer array element ID after initWithArray:copyItems is %@ (Expected value: \"Changed\")", [[[[secondOuterArray objectAtIndex:0] objectAtIndex:0 ] myDict] objectForKey: @"first"]);




  [secondOuterArray release];
  [outerArray release];

  [window makeKeyAndVisible];
}


- (void)dealloc {
  [firstArray release];
  [secondArray release];
  [window release];
  [super dealloc];
}


@end

Thursday, May 14, 2009

The first decision

After deciding on the game I wanted to develop, the first major decision was: do I use OpenGL or not?

I have absolutely no experience in OpenGL, but was prepared to slog through learning it. I really did not want to use it if I did not have to, though.

My requirements for the game were:
- Drag a square from a "tray" area to the game board and double-tap to place it
- Rotate the square in 90 degree increments
- Animate tokens along a pre-calculated path made of lines, arcs, and curves

So, I looked through the view and animation APIs, and I bought the 3 available books on iPhone programming. It looked like I would not have to learn OpenGL. The trickiest part would be doing the animation along a path, since rotating a view is fairly straightforward. From the docs, it looked like the CGAnimation class had what I needed, and it turned out that it works great for me.

So, one major design decision down, and a sigh of relief from me, because I would not have to learn OpenGL for this game.

Sunday, May 10, 2009

Putting my Daughter to Work

Last night, my family went to see the new Star Trek movie. While waiting in line, my 9 year-old daughter asked me if she could use my iPhone to play "Daddy's game." Not having sold any copies of the game in about a week, I told her "Sure," but she would have to hold the phone out prominently so that someone might catch a glimpse of the game and be curious about it. Yes, I'm that desperate.

Sure enough, the people in line behind us were quietly watching my daughter play the game, and looked interested, so I told them about it. They bought and downloaded a copy on the spot! Good thing my daughter doesn't know about the concept of commission yet :) She did get a big hug from Dad!

This underscores the need for exposure when developing a game. I know that people will like the game, but how do I go about competing for attention from the > 10000 other games (over 2000 in the same category)? That's my next big hurdle. I've made a fun game with good quality. I have solid enhancements planned and already well under development. I just need a way for people to know about it.

Friday, May 8, 2009

Yet another iPhone game is born

For anyone interested in writing a game for the iPhone, I thought that I'd try to journal some of my experiences developing Pathology.

My sons and I regularly play board games with friends, and someone brought in a game called Tsuro, which my sons really enjoyed playing. I took a look at the game, saw how simple the rules are, combined with deceptive strategy and depth, and I thought "This would be cool to play on the iPhone." I had also been looking into the iPhone APIs and I saw a good fit. I could leverage animation along a path as well as touch/drag, and the core of the game was born in my head.