Cocos2D, AI, and a simple Asteroids clone

Things have been a bit hectic at work lately and I couldn’t really devote as much time to programming as I would like. That said, I have managed to spend a bit of time taking a detour and learning a some game development.

Without a framework, game development on iOS would involve low-level interaction with the OpenGL api. That’s not an easy task, although it is possible. First of all there’s working with C libraries where functions often have cryptic names—Objective-C might be verbose, but at least you can get an understanding of the code just by looking at it, but standard C is almost impossible to decipher if you’re unfamiliar with it. Then there’s managing memory, which even professional developers have trouble doing consistently without causing leaks.

Thankfully, the Cocos2D framework abstracts away all of that low-level stuff and most of the memory management and provides an Objective-C object model to make games. While game development is still more involved than general app dev, it’s still much easier with the middle-man in place.

After working through the first few chapters of Learning Cocos2D (etc.) I was able to throw together this quick and dirty Asteroids clone for iPad. Ignore the graphics and interface at the moment, I did say it was quick. The ship doesn’t move as I would like at the moment, since it’s just mapped 1-1 with the joystick control on the left (that’s why it’s pointing away from the bullets in the screenshot: I had to take my thumb off the joystick to take it) but everything else works.

I do actually have an idea of a way to expand this into a full game that’s no longer Asteroids but a sort of top-down competitive shooter. The tricky part will be figuring out if having two people holding an iPad between them to compete against each other is a feasible control method. I’ve also been delving a bit into some real-time AI methodology so you’d also be able to play against the computer.

Anyway, I’ve been finding the AI part really interesting. Just learning about the different techniques that some of my favourite games use to give the impression of intelligence is fascinating. Putting them into practice will be a lot harder, but it’s something to work towards.

Saving and loading data: NSCoding and NSKeyedArchiver

There are several ways to store data for Cocoa applications. You could implement your own file loading and saving methods and store the data in any number of formats you want, but it’s time-consuming. You could use Cocoa’s built-in database functionality, called Core Data, but that’s not ideal for everything and it’s very hands-off in its approach.

If you decide to store your data outside of a database, it would be great if Cocoa provided a means to easily manage files and get your data into them, without having to do any of the low-level stuff.

That’s where the NSKeyedArchiver comes in.

Archiving and unarchiving data

When Cocoa refers to archiving data, it doesn’t mean making a compressed zip file, it means putting all the relevant data together and then saving it to a single file. These files have a certain structure that Cocoa can then unarchive. The archiving takes place via the NSKeyedArchiver object, and is read back via the (you guess it!) NSKeyedUnarchiver.

The way NSKeyedArchiver works is by taking a “root” object. This object will know what information needs to be stored, and “tells” the NSKeyedArchiver what it is and how to do it. The information is stored using string keys so that it can be retrieved using the same keys.

Now, the root object could be an array (like it is with my Todo app) or it could be an object that contains an array and links to other objects. The thing all of these have in common is that must know how to give the NSKeyedArchiver the correct information to do its job.

They do this by conforming to the NSCoding protocol.

Saving data to a file

The first thing that needs to happen when attempting to save our objects is to implement the method in the NSCoding protocol that our coder (NSKeyedArchiver) expects to find, encodeWithCoder:

Here it is for our base object, TaskStore.

1
- (void)encodeWithCoder:(NSCoder *)coder
2
{
3
    [coder encodeObject:taskStore forKey:@"taskStore"];
4
}

This is pretty simple, but our TaskStore object only has one property, taskStore, which is an array of our root groups (sections).

As you can see, all we need to do encode our array is call encodeObject:forKey: on our coder object. Arrays conform to the NSCoding protocol, so they know how to encode themselves; we don’t need to iterate through arrays, but they do assume that the objects contained in them conform to NSCoding as well.

Here is the encodeWithCoder: method for our TaskGroup object:

1
- (void)encodeWithCoder:(NSCoder *)coder
2
{
3
    [coder encodeObject:name forKey:@"name"];
4
    [coder encodeObject:subgroups forKey:@"subgroups"];
5
    [coder encodeObject:tasks forKey:@"tasks"];
6
    [coder encodeBool:expanded forKey:@"expanded"];
7
}

The difference here is that TaskGroup is more complex, with a number of different properties: an NSString, two NSMutableArrays, and a BOOL. The string and arrays can both call encodeObject:forKey: because they are Cocoa objects that conform to NSCoding.

The boolean, on the other hand, is a primitive C type and needs to be handled separately, which is why it has it’s own method (encodeBool:forKey:) on the coder. You would need to call any of the relevant methods for the other C types as well, like int or float.

I will leave the method of the tasks themselves up to your imagination.

Now that our objects know how to encode themselves, we need to get our NSKeyedArchiver to do so. We’ll implement the relevant methods on our TaskStore because it’s the root object. Once it initiates the load, it can read in the rest of the object tree.

First we’ll have a method that gives us a standard file location. This should probably be externalised to a settings file, but for such a simple project we’ll hardcode the file location (in the user Library).

01
- (NSString *) pathForDataFile
02
{
03
    NSFileManager *fileManager = [NSFileManager defaultManager];
04
 
05
    NSString *folder = LIBRARY_PATH;
06
    folder = [folder stringByExpandingTildeInPath];
07
 
08
    if ([fileManager fileExistsAtPath:folder] == NO)
09
    {
10
        [fileManager createDirectoryAtPath:folder withIntermediateDirectories:NO attributes:nil error:nil];
11
    }
12
 
13
    NSString *fileName = @"WhatNow.taskStore";
14
    return [folder stringByAppendingPathComponent: fileName];    
15
}

All this does is check to see if the our program folder (LIBRARY_PATH, a constant declared at the beginning of the file) exists, creates it if it doesn’t, and then returns a string to the file including the full folder path.

We then need a method to use NSKeyedArchiver to save it to disk:

01
- (void)saveDataToDisk
02
{
03
    NSString * path = [self pathForDataFile];
04
 
05
    NSMutableDictionary *rootObject;
06
    rootObject = [NSMutableDictionary dictionary];
07
 
08
    [rootObject setValue:[self taskStore] forKey:@"taskStore"];
09
    [NSKeyedArchiver archiveRootObject:rootObject toFile: path];
10
}

The method we call on NSKeyedArchiver is a class method (there’s no reason to create an object from it). We first have to put our array into an NSMutableDictionary, but we could also put in other objects that are separate from our TaskStore if we wanted. If this were a document-based application, we might have document-specific settings, for example.

Anyway, this method is called from our Application Delegate whenever the app is closed. Because this is not a document-based app, auto-saving will need to be implemented separately.

That’s it for saving data. Our saveDataToDisk: method tells the shared NSKeyedArchiver to start saving data on our root array. The array iterates through the groups and tells them to encode. The groups tell their subgroups to encode, and all of the leaf groups tell the tasks to encode.

Now that saving works, we need to go the other way and rebuild the object tree from the file.

Reading data from a file

When we make our objects conform to NSCoding, they need to know how to initialise themselves not only when being created from scratch but also when being initialised from the file via NSKeyedUnarchiver. To do this, they need a new initialisation method called initWithCoder:

Here is the method for our base object, TaskStore:

1
- (id)initWithCoder:(NSCoder *)coder
2
{
3
    self = [super init];
4
    if (self)
5
    {
6
        taskStore = [coder decodeObjectForKey:@"taskStore"];
7
    }
8
    return self;
9
}

This one is quite simple and is basically the reverse of what we did to encode it. decodeObjectForKey: will return, in this instance, an array and we simply assign it to our instance variable. If we decoded the wrong key, it would raise an exception.

Our TaskGroup object is a little more complicated, but not by a whole lot:

01
- (id)initWithCoder:(NSCoder *)coder
02
{
03
    self = [super init];
04
    if (self)
05
    {
06
        [self setName:[coder decodeObjectForKey:@"name"]];
07
        [self setSubgroups:[coder decodeObjectForKey:@"subgroups"]];
08
        [self setTasks:[coder decodeObjectForKey:@"tasks"]];
09
        [self setExpanded:[coder decodeBoolForKey:@"expanded"]];
10
    }
11
    return self;
12
}

Same as for the root object. Again, the boolean has its own method, decodeBoolForKey: because it is a primitive type. In this case we use the accessor method to assign the values because I’ve only set them up as properties, not with instance variable as well.

As with encoding, I’ll leave decoding the task to your imagination.

We also need to tell NSKeyedUnarchiver to get the data for us:

1
- (void)loadDataFromDisk
2
{
3
    NSString     * path        = [self pathForDataFile];
4
    NSDictionary * rootObject;
5
 
6
    rootObject = [NSKeyedUnarchiver unarchiveObjectWithFile:path];    
7
    [self setTaskStore:[rootObject valueForKey:@"taskStore"]];
8
}

We call this method when the application starts.

And that’s it for saving and loading to a file. The NSKeyedUnarchiver will tell the root object to decode from the file. The root object will unarchive all of it’s objects (in the array), which will then unarchive its object, ending up with exactly the same object tree that was saved.

Concepts: Delegation and protocols

One concept that pops up frequently in Cocoa programming is that of delegation. In fact, we’ve already seen that concept in my previous post about NSOutlineView. We set the delegate of our NSOutlineView to be the TodoController. We also set the data source to be TodoController—a data source is just another delegate.

A delegate is described as a person who acts on behalf of another, and this is pretty much exactly what it sounds like in Cocoa. When we set a delegate (or data source) of an object which accepts one, we’re telling the object to pass a certain amount of control over to the object we specify. The delegate is acting on behalf of (performing some of the function of) the original object.

That’s all well and good, but how does the delegate know what to do? That’s where protocols come in.

In Objective-C, protocols describe the methods that delegates can or must implement in order to perform the activities they’ve been delegated to do. They don’t have to implement those methods, but incomplete implementations may mean that things don’t work correctly. This is particularly important for things like data sources, where they’re asked to provide actual data to be displayed to the screen: if you don’t implement them right, the data doesn’t get delivered.

Once we’ve implemented those methods, and set our delegates correctly (either in Interface Builder or programmatically), our original object can then call those methods on the delegate to handle those functions.

In the case of our NSOutlineView, one of those methods was outlineView:viewForTableColumn:item:. This method took the task of creating a view which could be returned to the NSOutlineView to be displayed on the screen. If we didn’t implement this method, as we found out, nothing would appear on the screen; it’s a required method for NSOutlineViewDelegate protocol.

Although there are four required delegate methods to programmatically get an NSOutlineView to display data, there are many other methods that can be implemented to customise the behaviour of the outline view. One of these methods is, for example, outlineViewItemDidExpand:. If we implemented this method in our delegate, we can customise the behaviour or perform certain actions that happen after a group is expanded. We will actually need to do this in order to store the expanded state with our groups. This method is not required—leaving it out will have no effect on the function of the outline view—but implementing it gives us greater control over the function of our application.

Anyway, that’s a basic rundown of how delegation and protocols work in Objective-C/Cocoa. It’s a very powerful way to customise function without subclassing objects.

Sources lists and NSOutlineView

First up on my list of tasks to perform is to set up the sidebar to display the groups and sections. When choosing from the elements in Interface Builder (IB), there are two options: Outline Views, or Source Lists. An Outline View is a stock standard expandable/collapsible hierarchical listing. Source lists, on the other hand, or more like the sidebar in Mac Mail, with icons and headers for sections.

Naturally, the source list is what I’ve chosen. Technically, however, the source list is really an NSOutlineView, but is view-based rather than cell-based. This means that the cells are NSView objects, and can contain any elements you want. It also makes dealing with them a touch more difficult. From now on I will refer to the NSOutlineView and Source List to mean the same thing.

To get the data into the Source List, the custom controller class (TodoController) needs to be set as its delegate and data source. When the source list initialises or redraws, it will call methods on the data source to request certain information. These methods are: outlineView:isItemExpandable:, outlineView:numberOfChildrenOfItem:, outlineView:child:ofItem:, and outlineView:objectValueForTableColumn:byItem:.

01
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(TodoGroup *)item
02
{
03
    return ([[item childGroups] count] > 0);
04
}
05
 
06
- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(TodoGroup *)item
07
{
08
    if (item == nil)
09
    {
10
        return [rootGroups count];
11
    }
12
    else
13
    {
14
        return [[item childGroups] count];
15
    }
16
}
17
 
18
- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
19
{
20
    if (item == nil)
21
    {
22
        return [rootGroups objectAtIndex:index];
23
    }
24
    else
25
    {
26
        return [[item childGroups] objectAtIndex:index];
27
    }
28
}
29
 
30
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
31
{
32
    return [item groupName];
33
}

The first two are used for information only, to help the outline view build its structure, with the first informing the view about whether it can be expanded (i.e. has child groups), and the second reporting how many children it has.

The third method is used to get a specific child group of an a section or parent group. The “child” variable is actually an integer that will directly correspond to the an index of the array where a child exists.

The last method was where everything was coming unstuck. It should have worked, but the code simply wasn’t being called. There was nothing in the data source protocol documentation that would point me to why it didn’t work, so it had me stumped. I tried copying code directly from the example code provided by Apple, but it still didn’t work (even though the example project ran fine).

Anyway, it turns out (after a frustrating hour or so of Google-fu and trial and error), that the necessary method to implement is not, in fact, a part of the data source, it is a part of the delegate. The reason for this is that NSOutlineView is based on NSTableView, which uses a delegate method to return the view for the cell.

So, for view-based outline views/source lists, we need to implement outlineView:viewForTableColumn:item:

01
- (NSView *)outlineView:(NSOutlineView *)ov viewForTableColumn:(NSTableColumn *)tableColumn item:(TodoGroup *)item {
02
 
03
    NSTableCellView *result;
04
    if ([item isHeaderGroup]) {
05
        result = [ov makeViewWithIdentifier:@"HeaderCell" owner:self];
06
    }
07
    else {
08
        result = [ov makeViewWithIdentifier:@"DataCell" owner:self];
09
    }
10
 
11
    [[result textField] setStringValue:[item groupName]];
12
    return result;
13
}

And that’s it. When I plug in some sample data, it builds the source list correctly, finally! Now, this method will need to be modified down the track when I add a token to indicate the number of incomplete tasks, but for now it’s functionally usable.

Next stop is buttons to add and remove groups and sections from the list.

What’s Next? A first project.

I was originally thinking about jumping straight into developing my major idea. I even wrote up a list of features and have a screen layout sitting inside my head. Unfortunately, it’s probably a little too big for me to tackle this early on.

There’s also a little problem: I tend to be terribly unorganised.

Writing up the feature list got me thinking about how I would manage the list itself, and everything else I set out to do. What I needed was some sort of task-management (other wise known as Getting Things Done) software. I know, I know, there’s plenty of them out there, but I either have to pay for them, or they don’t work the way I like work (usually both). Having other people solve my problems also doesn’t help me improve my programming skills.

So we have an idea for a first project: task-management.

This project will be fairly simple. Tasks will belong to groups, and groups to sections. The sections will be things like “Projects” and “Home Tasks”. Groups will be things like major projects or planning for our next holiday. These can be split into any number of levels: individual projects could have task groups for interface design, development, testing, etc.

Tasks will have optional completion dates, but the aim of the program isn’t to act as a scheduler: we have iCal for that. I might add the functionality to send a task to iCal for a reminder. They will also be able to be marked as “in progress” so you can see what has been started, but not yet finished.

Local storage will take place in a file in the Application Support folder for the app. This means there is only one central task repository that’s not easily accessible by the user. Because of this I will look into exporting tasks into various formats later on so it can be backed up, although since Macs have Time Machine, losing data shouldn’t be a big issue.

Now, this is getting further down the line, but eventually I would like to be able to sync with iCloud, which will then sync with an iPhone/iPad version of the app. Once I have the basics of the Mac app in place, it should be fairly easy to port to iOS and iCloud storage is the preferred method of synching for that. That also covers backing up the data, which is good, but introduces complexity when it comes to ensuring data integrity.

For looks, I’ll try and follow the general look of the Mail.app for Macs. The left-hand side will be the hierarchical group view, and the right will be list of the tasks for that group. I want to keep it fairly simple, so tasks will not have “detail views” at all: they’ll just be descriptions, optional dates, and marked for their various stages of progress.

So, I’ve already started programming this app and within five minutes bumped up against my first issue: getting the NSOutlineView (the hierarchical “group” sidebar) to display data. Next post will explain (amongst other things) what exactly the issue was, and how to use the OutlineView.

On web and software development

It’s been a while since I’ve blogged. There are a few reasons for that, but one of the mains ones is that web development hasn’t really been exciting me much lately. Mainly I think this is because it’s often frustrating, both the development itself, but also dealing with clients who don’t know what they want. There’s nothing worse than having a client say “well, you’re the designer” but who then wants to change everything when you do use your best judgement.

The process of web development is also tedious and annoying. Cross-browser and cross-platform compatibility issues take a lot of time away from doing things that are more fun, like designing or coding. Getting a website to look exactly like you designed it in Photoshop is one thing; getting it to look like that in every browser is another.

It’s not fun anymore, which is a shame, but it’s also not totally unexpected.

Anyway, that leads me to here. Anybody who knows me would know I’m a big Apple user, with an iPhone, iPad, and Mac Pro, so it’s only natural that I would want to start creating things that work for them and try to make some money out of it. The natural choice was iPhone/iOS and Mac development.

Over the last few months I’ve been touching up on my existing programming knowledge and then extending that learning Objective-C/Cocoa for the Mac and CocoaTouch for iOS. It’s been a fun process, and it really gets my brain working like it hasn’t for a long time and I can tell my problem-solving and logic skills getting a work out.

I’m now at the point where I can start to put that knowledge to use. That means writing apps. Hopefully even selling them (though I am aware of the success rate of apps on the various app stores).

This also gives me the opportunity to start blogging again as I explore the frameworks that make up the Cocoa/CocoaTouch environments. As an “intermediate” level programmer it can serve as both a record of my progress, but hopefully it will help others who come across the same problems I do.

So, stay tuned while I start on what will hopefully be a nice career change.

In my next post I will describe my first smaller project, and the first problem I’ve butted my ahead against.

Appendix

Chronology