Showing posts with label coredata. Show all posts
Showing posts with label coredata. Show all posts

Thursday, January 17, 2013

A couple of notes about stores

A few posts ago I talked about how you can use Apple's core data in games by employing the techniques shown in the XCode Core-Data command line example.

That example works well because it doesn't assume any code templates - and if you're coding a game you're probably not using a core data template - and also it doesn't assume using utility classes like NSPersistentDocument.

However some of the file saving semantics of Core Data stores are pretty weird, so I thought I would document my current findings here.  The relationship between the NSPersistentStore object, and the on-disk file are also a bit obscure in the documentation.  So call this the "what they don't tell you in the Apple documentation" article.

Stores and Coordinators

All the three functions I discuss below are on NSPersistentStoreCoordinator.  I suggest never saving a reference to your store object.  The action of saving or moving a store will likely invalidate any instance of NSPersistentStore you have saved.  Instead do something like this:

- (NSPersistentStore *)store
{
    NSArray *stores = [[m_managedObjectContext persistentStoreCoordinator] persistentStores];
    
    NSAssert([stores count] <= 1, @"Each world can have (at most) one backing store!");
    return [stores lastObject];
}


In other words, use the coordinator as the place for all your store operations - not the instances of the stores themselves.  Weird - maybe but that is how Cocoa does it.

Who ya Gonna Call?

The documentation says: Adds a new persistent store of a specified type at a given location, and returns the new store.

What the documentation doesn't make specifically clear is that this function will create the on-disk file, in this case an SQLite data store if required - but it will also just transparently open the existing on-disk file if there is one there.  Nice.

Here the relation with the store object is clear: it creates the on-disk file (or opens an existing one) and returns a new store object.  You can use the returned store object for checking success, but as mentioned above I suggest not saving a reference to it.

This is your go-to function for opening a data store file, existent or not, given its location.  In the simple case you know your type and can simply specify it - eg NSSQLiteStoreType, and you don't have any configuration or options.

There's no need to use NSFileManager's exists functions or anything like that, to check for existence of the file first.  Of course you will need to make sure that any intermediate directories in the path for the URL do exist.

If you want to do basic error checking just test the returned value for nil - there's no need to get the error argument unless you want verbose error reporting.  To understand how this works simply doing the two modes (initial creation versus subsequent re-opening) checkout the main.cpp of the XCode Core-Data command line example.  Might not seem like a big deal, but as these are databases which normally require a bunch of setup, its nice to have a one-stop-shop for opening like this.

The Mad and the Bad

The documentation says: Sets the URL for a given persistent store.

No shit, Sherlock!  This is one of the least useful functions, and most poorly documented, on NSPersistentStoreCoordinator.  Basically the intent of it seems to be a very cheap way to open already existing data store files, where you absolutely know your existing store object has the right setup, and you know for sure you have an existing store file.  The fact it has a BOOL return looks promising for light-weight error detection (but it turns out not to be).

Here you must already have a store object.  Even though these calls are on NSPersistentStoreCoordinator, which can make new store objects, this call is not going to do that - you must have an existing store.  What if you don't save before calling this?  Changes to the store might get lost - in some cases.  See the documentation but its got to do with whether or not you have an atomic store - basically SQLite is non-atomic.

The arguments are the URL to open, and an existing store object - that is an instance of NSPersistentStore.  But what beats me is in what circumstances would you have a valid instance of a store object, but either not have it opened to a backing store URL, or want to just cut that store file loose?

Another problem is you must have an existing backing file - which the documentation says.  What it doesn't say is what happens if you don't?  Well, the function quite happily returns true if the file doesn't exist - huh??? - and then when you try to call save on your NSManagedObjectContext at a later point you get an exception.  Joy.

If anyone can find a great use for this function tell me - because as far as I can tell its mad, bad and dangerous to know.

Saving As

The documentation says: Moves a persistent store to a new location, changing the storage type if necessary.

This call is basically a addPersistentStoreWithType:configuration:URL:options:error: call using your old stores type, followed by a removePersistentStore:error: call on the old store (assuming you don't change the store type).

What I mean by that is if you have a store saved at URL A and  you want to make a copy of it at URL B, and do subsequent saves and operations on that new URL B, then this call will do what you need.

Here you are going to lose the store you pass in - but if you follow the idiom above of not saving a reference to your stores that is no problem.  Rather than BOOL this returns a new store which you can check against nil if you want to determine success.

Obviously its also the go to function if you need to migrate between store types for some reason - though I'm not really sure why you'd want to do that so much.  In most applications you decide on say SQLite and stay with it.

Conclusions

  • Use addPersistentStoreWithType:configuration:URL:options:error: to open all your data files, and also to create them the first time.  Don't keep a member variable of the store it returns.
  • Once a file is open, you can save the data to it by calling save: on your NSManagedObjectContext.  That will cause the attached stores to save to whatever URL's they're opened onto.
  • To do a save as use migratePersistentStore:toURL:options:withType:error: and pass in your existing store object, obtained from the - (NSPersistentStore *)store function listed above.
  • Closing files is not necessary - just let the objects go out of scope and ARC plus their destructors will clean everything up.  Make sure you save first obviously.
  • I suggest hiding all references to stores inside a class, and doing everything through NSPersistentStoreCoordinator.
Farewell and may all your data persist!

Monday, December 24, 2012

Traction

Finally starting to get some traction on the game creator having bullied CoreData, Key-Value coding, ArrayControllers and all the other mystical components of Cocoa into doing what I need.  Thanks to all who gave me encouragement!

Screenshot of game creator - 30% done


The game itself is still only partly set up to become data driven, but that I think will be a lot simpler than actually getting the data in there in the first place.  Because so much of it is about the rich world of objects, properties and descriptions the actual game content has to be data driven - hard coding it would have been a massive undertaking and very difficult to debug.

The buttons on the right are for creating exits from the room, and going ahead to the atoms screen to create the contents for that room.  These buttons, and the Atoms and Aspects screens are still on my todo list.  But I can use the room as a model and I'm sure it will be a lot quicker.

Things to add are a launcher button so I can launch the game from the creator, and have the game use [a copy of] the current view of the data as edited by the creator.

Monday, December 10, 2012

Customizing the core data store.

CoreData is a pretty awesome way to manage and persist your game objects, especially for a game with complex inter-related items, such as an adventure game.

Trouble is there is a helluva learning curve getting going with CoreData.  I just solved one little thing that was driving me crazy and wanted to share: how do you customize aspects of the CoreData store?

Image of apple core courtesy of http://www.wpclipart.com/


In my game I have two project targets: one is the game itself written in Cocos2D, and the other target is a game data editor written in Objective-C & Cocoa for the Mac.  They share a lot of classes, and the idea is that I can edit the games data, save it out as a SQLite file (which is one of CoreData's persistence formats) and then when someone goes to play the game for the first time they get a fresh instance of that data as the initial game state.

Trouble is, how do I tell the Mac Cocoa gui, to create my custom instance of an SQLite database, instead of popping up a dialog - which is the default thing you get using XCode's CoreData enabled app wizard?

And for that matter how do I get my database of game objects into my Cocos2D app?

If you're a CoreData beginner like me I suggest:
  • The answer begins with having to understand the bare essentials of CoreData.  For that I recommend Bob Uelands video tutorials.  He has a patient and careful manner that is a bit frustrating but his clarity is hard to beat.  If you've some familiarity with CoreData you can probably fast forward or even skip his stuff.
  • You could try Ray Wenderlich's tutorials - but I find they're a bit too specific and don't always give me the "why am I doing this" answers I want so I can build my own stuff.  But have a look and they may just have exactly what you need.
  • Go and run through Apple's CoreData command line tutorial.  It builds a CoreData app up from first principles all inside main.m.  If its all gobble-de-gook to you, then maybe skipping patient Bob's tutorials was a bit hasty...
What is great about the Apple CoreData tutorial is that the final result - a CoreData app that does not start with any of AppKit or UIKit, or any XCode scaffolding - is exactly what you need for dropping CoreData into a Cocos2D app.  If you have the time and want a good understanding work through the tutorial building up the code in the order given, and resist the temptation to just past the whole source.

Seriously - if you're used to skipping the boring documentation in the Apple developer site you're missing a buried gem with this tutorial.  They give the steps to building a whole data persistence stack.

I found working through these gave me the answers I needed.  So for example to solve my problem with customising the data store, I took the code from Apple's CoreData tutorial managedObjectContext() function and overrode the managedObjectContext function in the NSPersistentDocument sub-class I was using in my Mac OSX game editor GUI.

Edit:  OK, this looked like it was working but wasn't - my apologies.  Basically you can use this code, but you need to create your own window controller and UI class, not use the NSPersistentDocument - which is built to handle arbitrary "documents", not one well-known path for a database.  Sigh.

That let me specify the details of my store:

- (NSManagedObjectContext*)managedObjectContext
{
    static NSManagedObjectContext *moc = nil;
    if (moc != nil) {
        return moc;
    }
    
    NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc]
                                                 initWithManagedObjectModel:[self managedObjectModel]];
    
    NSString *STORE_TYPE = NSSQLiteStoreType;
    NSString *STORE_FILENAME = @"GameData.sqlite";
    
    NSError *error;
    NSString *path = [[PreferenceValues sharedPreferenceValues] saveGamePath];
    NSURL *url = [NSURL fileURLWithPath:path isDirectory:YES];
    url = [url URLByAppendingPathComponent:STORE_FILENAME];
    
    NSLog(@"Setting up the store with type %@ - at %@", STORE_TYPE, [url path]);
    
    NSPersistentStore *newStore = [coordinator addPersistentStoreWithType:STORE_TYPE
                                                            configuration:nil
                                                                      URL:url
                                                                  options:nil
                                                                    error:&error];
    
    if (newStore == nil)
    {
        NSLog(@"Store Configuration Failure\n%@",
              ([error localizedDescription] != nil) ?
              [error localizedDescription] : @"Unknown Error");
    }
    
    moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [moc setPersistentStoreCoordinator:coordinator];
    
    return moc;
}

Wait, what is that PreferenceValues thing doing in there? That is my own NSUserDefaults front-end. More on that in another post.

How do I get my data into Cocos2D?  I'll show you how I did it, but that's for another blog post.  Subscribe - give me feedback and I will post more!  Thanks for reading!