7
\$\begingroup\$

I have implemented some basic multithreading in my 2D mining game that I asked about in a previous question. The first part is working out great (creating the initial object instances in the background while displaying a loading screen rather than just having a black screen while they load). However, implementing multithreading for code that runs during the actual game is not giving me the performance gains that I was hoping for. That being said, the code does run and I do gain a small amount of performance.

This might just be a limitation of the software because for games in iOS, all of the rendering must take place on the main thread. I have tried to cheat and create sprites on another thread and then add them to the scene on the main thread, but doing it this way reduces performance rather than increases it. Based on my testing and profiling of the time used by the methods, almost all of the time is used by rendering the actual scene.

Mostly I want a review of my actual implementation of multithreading (using Grand Central Dispatch here), but any other performance enhancing tips would be appreciated.

Here is the first part (the loading screen):

-(void) didMoveToView:(SKView *)view {
    self->_draw_queue = dispatch_queue_create("scene queue", nil);

    _initialScreenSize = self.size;
    _screenBufferX = _initialScreenSize.width*kBufferFactor;
    _screenBufferY = _initialScreenSize.height*kBufferFactor;

    self.backgroundColor = [SKColor colorWithRed:0.10 green:0.10 blue:0.2 alpha:1.0];

    _world = [[SKNode alloc]init];
    [self addChild:_world];

    [_world addChild:[self loadingSprite]];

    _gameSpeed = 0.15;

    dispatch_async(self -> _draw_queue, ^{
        _game = [[DWGame alloc]init];
        dispatch_async(dispatch_get_main_queue(), ^{
            SKNode *spriteToRemove = [_world childNodeWithName:@"loadingSprite"];
            [spriteToRemove removeAllChildren];
            [spriteToRemove removeFromParent];

            _gameRender = [[DWGameRender alloc]initWithGame:_game];
            [_world addChild:_gameRender.gameSprites];

            self.backgroundColor = [SKColor colorWithRed:0 green:0 blue:0 alpha:1.0];

            //Set up the gesture recognizers
            _panRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePanFrom:)];
            [[self view] addGestureRecognizer:_panRecognizer];
            _pinchRecognizer = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(handlePinch:)];
            [[self view] addGestureRecognizer:_pinchRecognizer];
        });
    });
}

Here is the multithreading code that runs during each update: of the SKScene:

#pragma mark - Update
-(void) updateWithWorldBounds:(CGRect)worldBounds {
    dispatch_async(self->_draw_queue, ^{
        [_game.gameWorld loadChunksForBounds:worldBounds];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self renderNewChunks];
            [self removeChunks];
        });
    });
}
-(void) renderNewChunks {
    for (DWChunk *chunk in _game.gameWorld.loadedChunks) {
        if (![_visibleChunks childNodeWithName:chunk.chunkName]) {

            SKNode *chunkNode = [[SKNode alloc]init];
            chunkNode.name = chunk.chunkName;
            [_visibleChunks addChild:chunkNode];

            for (DWBlock *block in chunk.blocks.allValues) {
                [chunkNode addChild:[self blockSpriteForBlock:block]];
            }
        }
    }
}
-(void) removeChunks {
    for (DWChunk *chunk in _game.gameWorld.chunksToRemove) {
        SKNode *chunkNode = [_visibleChunks childNodeWithName:chunk.chunkName];
        if (chunkNode) {
            //[chunkNode removeAllChildren];
            [chunkNode removeFromParent];
        }
    }
    dispatch_async(self->_draw_queue, ^{
        [_game.gameWorld.chunksToRemove removeAllObjects];
    });
}
-(void) updateBlocks {
    for (DWBlock *block in _game.gameWorld.blocksToUpdate) {
        SKNode *chunkNode = [_visibleChunks childNodeWithName:block.chunkName];
        if (chunkNode) {
            SKNode *blockNode = [chunkNode childNodeWithName:block.blockName];
            if (blockNode) {
                [blockNode removeFromParent];
                [chunkNode addChild:[self blockSpriteForBlock:block]];
            }
        }
    }
    dispatch_async(self->_draw_queue, ^{
        [_game.gameWorld.blocksToUpdate removeAllObjects];
    });
}

#pragma mark - Block rendering
-(SKSpriteNode *) blockSpriteForBlock:(DWBlock *)block {
    SKSpriteNode *blockSprite = nil;
    if (block.type == BlockTypeNumTypes) {
        blockSprite = [SKSpriteNode spriteNodeWithColor:[SKColor blackColor] size:CGSizeMake(kBlockSpriteWidth, kBlockSpriteWidth)];
    } else {
        blockSprite = [SKSpriteNode spriteNodeWithTexture:[self textureForType:block.type damageLevel:block.damageLevel]
                                                 size:CGSizeMake(kBlockSpriteWidth, kBlockSpriteWidth)];
    }

    blockSprite.anchorPoint = CGPointZero;
    blockSprite.name = block.blockName;
    blockSprite.position = block.calculatedPosition;
    return blockSprite;
}
-(SKTexture *) textureForType:(BlockType)type damageLevel:(int)damageLevel {
    switch (type) {
        case BlockTypeDirt:
            return _dirtFromAtlas[damageLevel];
            break;
        case BlockTypeRock:
            return _rockFromAtlas[damageLevel];
        default:
            break;
    }
    return nil;
}

The queue is created and initialized like this:

dispatch_queue_t _draw_queue;
self->_draw_queue = dispatch_queue_create("game update queue", nil);

For completeness I will note that I have tried various other approaches for increased performance, but none of them seemed to make any difference. I switched all properties to nonatomic, which supposedly increases the speed of a program. I tried to create an elaborate copy system where I created an instance of all possible sprites at the very start of the game, and only copied them later during the call to blockSpriteForBlock. I tried to do the work found in renderNewChunks and removeChunks completely in a separate thread. These things did not end up being worth the effort.

Finally though I will say that the reason I am posting this question is to make sure that I have properly implemented the Grand Central Dispatch approach to multithreading. I just wanted to be thorough about the things that I have tried already to improve the program.

I am also open to any other criticism of the code provided here (readability, clarity, organization, etc).

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

Personally, I don't really care for Grand Central Dispatch. I prefer NSOperationQueue. Although, if I'm honest, I'm not sure what the performance difference (if any) there exists between the two. To me, NSOperationQueue just looks cleaner.

But with that said, I do see some problems.

For example, it is considered best practice to use the reverse domain name look up scheme to naming your threads. And also... as with every where else, we should be using named constants rather than literal strings.

NSString * const kQUEUE_GameUpdate = @"com.yourdomain.gameUpdate";

// ....

self->_draw_queue = dispatch_queue_create(kQUEUE_GameUpdate, nil);

dispatch_async(self -> _draw_queue, ^{
    _game = [[DWGame alloc]init];
    dispatch_async(dispatch_get_main_queue(), ^{

We're doing very little on the background queue here. Unless the init method is really intensive, I'm curious as to how much is actually gained here.

Can we put some more on the background here?

-(void) didMoveToView:(SKView *)view {
    self->_draw_queue = dispatch_queue_create("scene queue", nil);

    _initialScreenSize = self.size;
    _screenBufferX = _initialScreenSize.width*kBufferFactor;
    _screenBufferY = _initialScreenSize.height*kBufferFactor;

    self.backgroundColor = [SKColor colorWithRed:0.10 green:0.10 blue:0.2 alpha:1.0];

    _world = [[SKNode alloc]init];
    [self addChild:_world];

    [_world addChild:[self loadingSprite]];

    _gameSpeed = 0.15;

    dispatch_async(self -> _draw_queue, ^{
        _game = [[DWGame alloc]init];
        dispatch_async(dispatch_get_main_queue(), ^{
            SKNode *spriteToRemove = [_world childNodeWithName:@"loadingSprite"];
            [spriteToRemove removeAllChildren];
            [spriteToRemove removeFromParent];

            _gameRender = [[DWGameRender alloc]initWithGame:_game];
            [_world addChild:_gameRender.gameSprites];

            self.backgroundColor = [SKColor colorWithRed:0 green:0 blue:0 alpha:1.0];

            //Set up the gesture recognizers
            _panRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePanFrom:)];
            [[self view] addGestureRecognizer:_panRecognizer];
            _pinchRecognizer = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(handlePinch:)];
            [[self view] addGestureRecognizer:_pinchRecognizer];
        });
    });
}

Could we rewrite this into something more like the following?

- (void)didMovetoView:(SKView *)view {
    self->_draw_queue = dispatch_queue_create(kQueueScene,nil);

    dispatch_async(self->_draw_queue, ^{
        _initialScreenSize = self.size;
        _screenBufferX = _initialScreenSize.width*kBufferFactor;
        _screenBufferY = _initialScreenSize.height*kBufferFactor;

        _world = [[SKNode alloc]init];
        [self addChild:_world];

        [_world addChild:[self loadingSprite]];

        _gameSpeed = 0.15;

        _game = [[DWGame alloc]init];

        SKNode *spriteToRemove = [_world childNodeWithName:@"loadingSprite"];

        _gameRender = [[DWGameRender alloc]initWithGame:_game];
        [_world addChild:_gameRender.gameSprites];

        _panRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePanFrom:)];
        _pinchRecognizer = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(handlePinch:)];

        dispatch_async(dispatch_get_main_queue(), ^{
            // all the code that *must* be done on the main queue
        });
    });
}

Essentially, the gist is, if you're going to multithread, just remember that anything that touches the UI must be on the main thread. After that, you should move everything else into the background if you're going to bother multithreading at all.

And as a disclaimer, I'm not familiar with Sprite Kit at all, so some of the stuff I left in the background may actually be UI code that must be in the foreground.

\$\endgroup\$

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.