4
\$\begingroup\$

I have spent quite a bit of time trying to optimize the performance of my 2D mining game. After a lot of research, experimentation, and testing, I finally found something very interesting that improved my performance pretty dramatically when rendering lots of tiles on the screen.

Initially I divided my textures up into texture atlases based on the way that I had seen many examples of texture organization for animations. I put all the textures for each tile in one directory with a name like dirtimages.atlas, and then I loaded those textures into the game by iterating over the images in the directory and accounting for the number of formats I provided images for (there are different formats for smaller versus larger resolution devices). I had a folder for each set of textures, so dirtimages.atlas, rockimages.atlas, etc. This provided very clear organization in the code, and I liked it very much.

Unfortunately when profiling the number of draws of the scene, I was seeing pretty much one draw for every single tile (hundreds of draws). I was hoping that the draws would not exceed the number of atlases, but for some reason, splitting the images up into different atlases caused the number of draws to increase by a huge amount.

Here is my workaround for this problem. I consider it a dirty hack, but I think that this approach is the best one for performance. I do remember seeing a mention somewhere on the internet about putting all of the images in one texture atlas, but I cannot find that link. I did find this link on stack overflow about efficient texture atlas setups and it recommends pretty much what I have implemented here.

First I load up ALL of the textures into memory like this:

int numFormats = 3;
_tilesFromAtlas = textureAtlas(@"tileimages", @"tile", numFormats);

NSArray* textureAtlas(NSString *name, NSString *prefix, int numFormats) {
    NSMutableArray *atlasImages = [[NSMutableArray alloc]init];
    SKTextureAtlas *atlas = [SKTextureAtlas atlasNamed:name];
    if (atlas) {
        int numImages = (int)atlas.textureNames.count;
        for (int i = 1; i <= numImages/numFormats; i++) {
            SKTexture *texture = [atlas textureNamed:[NSString stringWithFormat:@"%@%d", prefix, i]];
            [atlasImages addObject:texture];
        }
    }
    return atlasImages;
}

Now when I am assigning a texture to a sprite in the scene, I have to keep track of the number of images that I have for each different tile. Also, the way I have this set up, they have to have the same number of images:

-(SKTexture *) textureForType:(BlockType)type damageLevel:(int)damageLevel {
    int numImagesEach = 4;
    switch (type) {
        case BlockTypeDirt:
        {
            int amount = (type * numImagesEach) + damageLevel;
            return _tilesFromAtlas[amount];
        }
        case BlockTypeRock:
        {
            int amount = (type * numImagesEach) + damageLevel;
            return _tilesFromAtlas[amount];
        }
        default:
            break;
    }
    return nil;
}

I would love to get feedback on this approach. I think that it suffers from readability problems, but there is no denying the huge performance increase. After making this change the number of draws is usually somewhere between 1 and 2, and sometimes goes up to 10 or so if I zoom out a ways.

\$\endgroup\$
3
\$\begingroup\$

Just some quick minor complaints...

If braces are absolutely necessary in a switch, there's no reason why they shouldn't also be Egyptian-style like everywhere else in your code:

case BlockTypeDirt: {
    int amount = (type * numImagesEach) + damageLevel;
    return _tilesFromAtlas[amount];
}

But this seems unnecessary altogether. We can refactor the switch statement as such:

switch(type) {
    case BlockTypeDirt:
    case BlockTypeRock:
        return _tilesFromAtlas[(type * numImagesEach) + damageLevel];
    default:
        return nil;
}

Now, I'm not sure I actually particularly like cramming all that inside the brackets, and the extra variable doesn't hurt, so we could keep it in the switch, but still, let's combine these cases and fix our brackets:

switch(type) {
    case BlockTypeDirt:
    case BlockTypeRock: {
        int amount = (type * numImagesEach) + damageLevel; 
        return _tilesFromAtlas[amount];
    }
    default:
        return nil;
}

Our code has some borderline magic numbers:

int numFormats = 3;

int numImagesEach = 4;

I'll deal with the latter first. It's defined as the first line of a function, it's only used within the function, and it's used as a constant.

static int const kNumberOfImagesEach = 4;

The other variable is a little hard to say. It may be that it's simply out of context, and in reality, might be different values, as this number is passed into the function that actually uses it. I'll let you use your best judgment on that one, but you should think about it.


int numImages = (int)atlas.textureNames.count;

This is an example of why I default to NSInteger rather than int in all of my Objective-C code--that's what Apple has defaulted to, and I'd rather not have to cast all of my returns from Apple's methods, which you'll probably find that on balance, you're using Apple methods more than any other single developer's methods.

\$\endgroup\$
  • \$\begingroup\$ Personally, I find using braces for a switch-case a bad idea. \$\endgroup\$ – Simon Forsberg Oct 21 '14 at 11:56
  • 1
    \$\begingroup\$ Just for clarity, braces are required in Objective-C switch statements if any variables are defined inside of a case:. \$\endgroup\$ – bazola Oct 21 '14 at 11:57
  • 1
    \$\begingroup\$ Exactly. And using braces is better than widening the variable's scope. \$\endgroup\$ – nhgrif Oct 21 '14 at 13:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.