Sunday, November 6, 2011

Let's Be Friends. Core Data Relationships

With any good database tutorial, you will learn how to make employees, departments, and managers. Then how to reference the employees to a department and be overseen by which manager. Instead I'm going to talk about a data structure that I made to help me with my next project to help me handle game scenes, characters, and their animations.

The first time I heard of Core Data, I thought I wouldn't need to learn how to use it, I already know how to access SQL and can make use of the SQLite C library. And for a few of my early iOS projects thats what I used. I made lots of classes that handled my common queries and they grew fast as my need to cross reference tables got more complex. This all changed once I watched Dr. Brad Larson's Madison Area Tech course on Core Data. Now that I use Core Data, I find myself being able to do more with table relationships that I would have thought up had I handled the queries myself.

What I want to accomplish is to have multiple Scenes which will be home to Sprites and character Sprites with defined animations. In the past I usually approach this type of design by having a top tier object just contain a list of the child items, the child items would have their settings for position in the scene. I designed this first table relationship to contain multiple scenes, with multiple assets(sprites). For simple still sprites this will still be my basic foundation. I'm only containing the sprite file name reference and the position. The index field is to retain the z-layer of the sprite in the scene. More recently Core Data now offers to track many-to-one collections in a NSOrderedSet, but I've discovered are a few quarks to this still, so I still track the z-depth manually to control the sprite layers. When accessing the 'assets' set I get the ordered array again using:

NSArray* sorters = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"index" ascending:YES]];
NSArray* assets = [m_scene.assets sortedArrayUsingDescriptors:sorters];

Each asset would not be shared between different scenes. If I did reference a asset in two different scenes with different position they would overwrite the position field. My actor charters though are going to have  their own inner animation relationships. I'm starting with an actor that will have a name and a set of animations. They will likely include things like idle, run, jump, climb, fix, talk. Each animation then has a series of frame references to the file names used to comprise the animation, and the rate in which to play the frames. Then for sake of having a starting point I have another single animation relationship to decide which animation will be default when the actor is loaded. In most cases I set the animation field to equal the 'idle' named animation. But it is just a reference to the Animation object so the animation name is not important, and more importantly not defined as a string field. Again I use a index field in the Frame object to control the order of the frames. Because Animation.frames is stored as a NSSet they may come back out of desired order and need to be sorted like above.

A quick note on the power and feature of Core Data that I like the most, is inverse relationship mapping. Thats not to say it's a unique feature, it's a cross reference. But having the Core Data frame work set the relationship field for you in both records by just simply setting the property of one object, just makes storing the relationships all the more easier. Above I have the Animation object that has a relationship set of frames. In the Frame I have a relationship inverse to animation. So during the loop in my program to set these frames, all I need to do is allocate a new Frame, set the name, index, and simply set the animation to equal the Animation instance I created before entering the loop. Instead of requiring me to also access the Animation and add the frame to the NSSet, I'm getting that for free by the Core Data framework and the inverse connection I setup in the model.

Animation* ani = [animationRay objectAtIndex:selectedIndex];
for (NSString* file in fileNames) {
    NSEntityDescription* frmEntity = [NSEntityDescription entityForName:@"Frame" inManagedObjectContext:context];
    Frame* frameIn = (Frame*)[[NSManagedObject alloc] initWithEntity:frmEntity insertIntoManagedObjectContext:context];
    frameIn.name = file;
    frameIn.index = [NSNumber numberWithInt:[ani.frames count]];            
    frameIn.animation = ani;
}

Now with Actors setup with many animations, and many frames, I want to add the actors to many scenes. The reason is so I can have a character that is standing out by the boat by the docks, and/or in the kitchen making a sandvich. The things I want to avoid are the position of actor being overwritten by the different locations he will be at in those scenes, and from duplicating the relationship instances I just built for the one actor and his animation tree. I also need the actor to be added to the Scenes' assets set so the Actor can have a index(z-layer) in that scene, which will be a different index than another scene he may be in. If you watched Dr.Larson yet or know about Core Data you'll hear about the Abstract/Parent Entity setting. Using a new entity definition as a parent entity, SceneActor acts like a subclass of SceneAsset and then I can add a extra property to do a non-inversed relationship to a Actor. At this point I also added a non-inverse player relationship to the Scene so that I know which Actor object is defined as the player and to forward the user inputs to when the scene in created. Defining the parent entity and the model object classes that go along with the design this adds one last convenience when looping through the assets set to generate the scene. I can test the class type from the set and let it generate a actor sprite or base class sprite.

[assets enumerateObjectsUsingBlock:^(SceneAsset* obj, NSUInteger idx, BOOL *stop) {        

    NSString* sheetName = [[obj.sheet.file pathComponents] lastObject];
    [[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:sheetName];
    
    CCSprite* aSprite;
    if ([obj class] == [SceneActor class]) {
        SceneActor* sa = (SceneActor*)obj;
        TTActor* actor = [[TTActor alloc] init:sa];
        aSprite = actor;
    } else {
        aSprite = [CCSprite spriteWithSpriteFrameName:obj.file];
    }
}];

2 comments:

  1. This is an interesting read and a handy reference, I was planning to try something like this in the near future.

    Cheers,

    ReplyDelete
  2. hiii... thank u so much. this was very helpful to me. cleared my concepts

    ReplyDelete