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).