I am trying to modularize the main SKScene
of my strategy game. I am not sure that my approach is correct so I would love to get a review of the following code.
The SKScene
adds the top level node of this class to its _world
SKNode
. It calls the update method in a fixed time step that can be increased or decreased by the player. When update is called, all the child nodes are properly rendered on the screen.
Previously, all of this code was in the GameScene
class. I think that putting this code in another class is cleaner, but again, I am not sure of this approach.
I have tried to refactor this class as much as possible in order to make the methods small and easy to understand. I have also tried to make the names as sensible as I can, and have tried to reduce code duplication as much as possible. I am not sure if I have overlooked anything, but I would love to hear about it if so.
The ultimate goal of all of this refactoring is to extend this code to have more complex animations based on the state of the game objects. For example, if a Dwarf is working, a certain animation will play. If a block item has just recently spawned on a floor, it will fly into the air and then land on the ground. Right now, the positions of everything are determined by the Game model, but I know this needs to change for the game to have sufficient visual polish. I am not totally sure how to accomplish this, but I feel like this refactoring is the first step.
DTGameRender.h
#import <Foundation/Foundation.h>
#import "DTGame.h"
@interface DTGameRender : NSObject
-(instancetype) initWithGame:(DTGame *)game screenSize:(CGSize)screenSize;
@property (readonly) SKNode *gameScene;
-(void) update;
@end
DTGameRender.m
#import <SpriteKit/SpriteKit.h>
#import "DTGameRender.h"
#import "DTTowerFloor.h"
@implementation DTGameRender {
DTGame *_game;
CGSize _initialScreenSize;
SKNode *_sceneTower;
SKNode *_sceneTowerBricks;
SKNode *_sceneTowerLadders;
SKNode *_sceneJobQueues;
SKNode *_sceneItems;
SKNode *_sceneDwarves;
SKNode *_sceneAnimals;
SKNode *_sceneEnemies;
SKNode *_sceneTowerOverlay;
NSArray *_dirtFromAtlas;
NSArray *_dirtFloorsFromAtlas;
NSArray *_rockFromAtlas;
NSArray *_rockFloorsFromAtlas;
}
#pragma mark - Initialization
-(instancetype) initWithGame:(DTGame *)game screenSize:(CGSize)screenSize {
self = [super init];
if (self) {
_game = game;
_initialScreenSize = screenSize;
_gameScene = [[SKNode alloc]init];
//Set up the texture atlases
int numFormats = 3;
_dirtFromAtlas = textureAtlas(@"dirtimages", @"dirt", numFormats);
_dirtFloorsFromAtlas = textureAtlas(@"dirtfloorimages", @"dirtfloor", numFormats);
_rockFromAtlas = textureAtlas(@"rockimages", @"rock", numFormats);
_rockFloorsFromAtlas = textureAtlas(@"rockfloorimages", @"rockfloor", numFormats);
[self setUpGameScene];
}
return self;
}
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;
}
-(void) setUpGameScene {
_sceneTower = [[SKNode alloc]init];
_sceneTowerBricks = [[SKNode alloc]init];
_sceneTowerLadders = [[SKNode alloc]init];
_sceneJobQueues = [[SKNode alloc]init];
_sceneItems = [[SKNode alloc]init];
_sceneDwarves = [[SKNode alloc]init];
_sceneAnimals = [[SKNode alloc]init];
_sceneEnemies = [[SKNode alloc]init];
_sceneTowerOverlay = [[SKNode alloc]init];
[_gameScene addChild:_sceneTower];
[_gameScene addChild:_sceneTowerBricks];
[_gameScene addChild:_sceneTowerLadders];
[_gameScene addChild:_sceneJobQueues];
[_gameScene addChild:_sceneItems];
[_gameScene addChild:_sceneDwarves];
[_gameScene addChild:_sceneAnimals];
[_gameScene addChild:_sceneEnemies];
[_gameScene addChild:_sceneTowerOverlay];
}
#pragma mark - Update
-(void) update {
if ([_game towerHasBeenDiscovered]) {
[self renderTower];
[self renderTowerFloorBricks];
[self renderTowerLadders];
[self renderJobQueues];
[self renderItems];
[self renderDwarves];
[self renderAnimals];
[self renderEnemies];
[self renderTowerOverlay];
}
}
-(void) renderTower {
[_sceneTower removeAllChildren];
for (DTTowerFloor *floor in [_game getTowerForRender].allValues) {
//basic floor background
SKSpriteNode *floorSprite = [self floorSpriteForFloor:floor];
floorSprite.position = CGPointMake(0, floor.floorNumber * _initialScreenSize.height/6);
floorSprite.name = @"towerFloor";
[_sceneTower addChild:floorSprite];
//floor bottom
if (floor.floorBuildState & FloorHasBottom) {
[floorSprite addChild:[self floorBottomFromSize:floorSprite.size]];
}
//floor walls
if (floor.floorBuildState & FloorHasWalls) {
SKSpriteNode *leftWallSprite = [self floorWallSpriteFromSize:floorSprite.size];
leftWallSprite.position = CGPointMake(-floorSprite.size.width/2.1, 0);
[floorSprite addChild:leftWallSprite];
SKSpriteNode *rightWallSprite = [self floorWallSpriteFromSize:floorSprite.size];
rightWallSprite.position = CGPointMake(floorSprite.size.width/2.1, 0);
[floorSprite addChild:rightWallSprite];
if (floor.floorBuildState & FloorHasSuperiorWalls) {
[leftWallSprite setColor:[SKColor grayColor]];
[rightWallSprite setColor:[SKColor grayColor]];
}
}
//floor room
if (floor.floorBuildState & FloorHasRoom) {
if (floor.floorBuildState & FloorHasRoomUpgrade) {
SKSpriteNode *roomUpgradeSprite = [self roomUpgradeSpriteForFloor:floor];
roomUpgradeSprite.name = @"towerRoomUpgrade";
roomUpgradeSprite.size = CGSizeMake(floorSprite.size.width*0.85, floorSprite.size.height*0.85);
[floorSprite addChild:roomUpgradeSprite];
} else {
SKSpriteNode *roomSprite = [[SKSpriteNode alloc]init];
roomSprite.name = @"towerRoom";
[floorSprite addChild:roomSprite];
roomSprite.size = CGSizeMake(floorSprite.size.width*0.80, floorSprite.size.height*0.80);
[roomSprite setColor:[SKColor purpleColor]];
}
}
//enemy cave
if (floor.enemyCave) {
SKSpriteNode *enemyCaveSprite = [[SKSpriteNode alloc]initWithColor:[SKColor yellowColor]
size:CGSizeMake(floorSprite.size.width/8, floorSprite.size.width/8)];
enemyCaveSprite.position = floor.enemyCave.locationForRender;
[floorSprite addChild:enemyCaveSprite];
}
}
}
-(void) renderTowerFloorBricks {
[_sceneTowerBricks removeAllChildren];
for (DTTowerFloor *floor in [_game getTowerForRender].allValues) {
for (int i = 0; i < (int)floor.groundBlocks.count; i++) {
DTGroundBlock *groundBlock = [floor.groundBlocks objectAtIndex:i];
SKSpriteNode *blockSprite = [self blockSpriteWithPosition:groundBlock.blockPosition floorNumber:floor.floorNumber];
[_sceneTowerBricks addChild:blockSprite];
}
}
}
-(void) renderTowerLadders {
[_sceneTowerLadders removeAllChildren];
for (DTTowerFloor *floor in [_game getTowerForRender].allValues) {
if (floor.floorBuildState & FloorHasLadder) {
SKSpriteNode *ladderSprite = [self ladderSpriteForFloor:floor.floorNumber];
[_sceneTowerLadders addChild:ladderSprite];
}
}
}
-(void) renderJobQueues {
[_sceneJobQueues removeAllChildren];
for (DTTowerFloor *floor in [_game getTowerForRender].allValues) {
SKNode *jobButtons = [[SKNode alloc]init];
NSMutableArray *jobList = [floor jobList];
for (int i = 0; i < jobList.count; i++) {
SKSpriteNode *jobButton = [self jobButtonForFloorNumber:floor.floorNumber buttonNumber:i];
if ([floor isJobQueuePaused]) {
[jobButton setColor:[SKColor redColor]];
}
[jobButtons addChild:jobButton];
}
//color the first button differently if a job is in progress
if ((int)jobList.count > 0) {
if ([floor areJobSlotsAvailable]) {
SKSpriteNode *firstButton = (SKSpriteNode *)[jobButtons childNodeWithName:@"jobButton0"];
[firstButton setColor:[SKColor colorWithRed:0.5 green:1 blue:0.5 alpha:0.5]];
}
}
[_sceneJobQueues addChild:jobButtons];
}
}
-(void) renderItems {
[_sceneItems removeAllChildren];
for (DTTowerFloor *floor in [_game getTowerForRender].allValues) {
for (DTItem *item in floor.itemsNeedingHauling) {
SKSpriteNode *itemSprite = [self itemSprite];
itemSprite.position = item.position;
[_sceneItems addChild:itemSprite];
}
}
}
-(void) renderTowerOverlay {
[_sceneTowerOverlay removeAllChildren];
if ([_game towerHasBeenDiscovered]) {
for (DTTowerFloor *floor in [_game getTowerForRender].allValues) {
SKSpriteNode *floorOverlaySprite = [self overlaySpriteForFloor:floor.floorNumber];
if (floor.floorState == FloorDestroyed) {
[floorOverlaySprite setColor:[SKColor colorWithRed:1 green:1 blue:1 alpha:0.5]];
} else {
[floorOverlaySprite setColor:[SKColor colorWithRed:0.00 green:0.00 blue:0.00 alpha:0]];
}
[_sceneTowerOverlay addChild:floorOverlaySprite];
}
}
}
-(void) renderDwarves {
[_sceneDwarves removeAllChildren];
for (DTDwarf *dwarf in [_game getDwarvesForRender]) {
SKSpriteNode *dwarfSprite = [self dwarfSpriteForDwarf:dwarf];
[_sceneDwarves addChild:dwarfSprite];
}
}
-(void) renderAnimals {
[_sceneAnimals removeAllChildren];
for (DTAnimal *animal in [_game getAnimalsForRender]) {
SKSpriteNode *animalSprite = [self animalSpriteForAnimal:animal];
[_sceneAnimals addChild:animalSprite];
}
}
-(void) renderEnemies {
[_sceneEnemies removeAllChildren];
for (DTEnemy *enemy in [_game getEnemiesForRender]) {
SKSpriteNode *enemySprite = [self enemySpriteForEnemy:enemy];
[_sceneEnemies addChild:enemySprite];
}
}
#pragma mark - Tower Sprites
-(SKSpriteNode *) floorSpriteForFloor:(DTTowerFloor *)floor {
GroundType groundType = floor.groundType;
int backgroundType = floor.backgroundType;
CGSize floorSize = CGSizeMake(_initialScreenSize.width, _initialScreenSize.height/7);
if (!floor.isBackgroundRevealed) {
if (groundType == NoGround) {
SKSpriteNode *floorSprite = [SKSpriteNode spriteNodeWithColor:[SKColor colorWithRed:0 green:0 blue:0 alpha:0] size:floorSize];
return floorSprite;
} else if (groundType == Dirt) {
SKTexture *texture = _dirtFromAtlas[backgroundType];
SKSpriteNode *floorSprite = [SKSpriteNode spriteNodeWithTexture:texture size:floorSize];
return floorSprite;
}else if (groundType == DirtRock) {
SKTexture *texture = _rockFromAtlas[backgroundType];
SKSpriteNode *floorSprite = [SKSpriteNode spriteNodeWithTexture:texture size:floorSize];
return floorSprite;
}
} else {
if (groundType == NoGround) {
SKSpriteNode *floorSprite = [SKSpriteNode spriteNodeWithColor:[SKColor colorWithRed:0 green:0 blue:0 alpha:0] size:floorSize];
return floorSprite;
} else if (groundType == Dirt) {
SKTexture *texture = _dirtFloorsFromAtlas[backgroundType];
SKSpriteNode *floorSprite = [SKSpriteNode spriteNodeWithTexture:texture size:floorSize];
return floorSprite;
} else if (groundType == DirtRock) {
SKTexture *texture = _rockFloorsFromAtlas[backgroundType];
SKSpriteNode *floorSprite = [SKSpriteNode spriteNodeWithTexture:texture size:floorSize];
return floorSprite;
}
}
//this will only happen if something goes wrong
SKSpriteNode *floorSprite = [SKSpriteNode spriteNodeWithColor:[SKColor blackColor] size:floorSize];
return floorSprite;
}
-(SKSpriteNode *) roomUpgradeSpriteForFloor:(DTTowerFloor *)floor {
SKSpriteNode *roomUpgradeSprite = [[SKSpriteNode alloc]init];
NSString *roomText = nil;
switch (floor.room.roomType) {
case RoomTypeBirther:
[roomUpgradeSprite setColor:[SKColor grayColor]];
roomText = @"Birther";
break;
case RoomTypePasture:
[roomUpgradeSprite setColor:[SKColor orangeColor]];
roomText = @"Pasture";
break;
case RoomTypeFarm:
[roomUpgradeSprite setColor:[SKColor greenColor]];
roomText = @"Farm";
break;
case RoomTypeFoodhall:
[roomUpgradeSprite setColor:[SKColor colorWithRed:0.0 green:0.0 blue:0.7 alpha:1.0]];
roomText = @"Foodhall";
break;
case RoomTypeDormitory:
[roomUpgradeSprite setColor:[SKColor colorWithRed:0.1 green:0.1 blue:0.5 alpha:1.0]];
roomText = @"Dormitory";
break;
case RoomTypeDangerroom:
[roomUpgradeSprite setColor:[SKColor colorWithRed:0.5 green:0.0 blue:0.0 alpha:1.0]];
roomText = @"DangerRoom";
break;
case RoomTypeForge:
[roomUpgradeSprite setColor:[SKColor colorWithRed:0.0 green:0.5 blue:0.5 alpha:1.0]];
roomText = @"Forge";
break;
case RoomTypeCrafthall:
[roomUpgradeSprite setColor:[SKColor colorWithRed:0.5 green:0.5 blue:0.0 alpha:1.0]];
roomText = @"Crafthall";
break;
case RoomTypeThroneroom:
[roomUpgradeSprite setColor:[SKColor colorWithRed:1.0 green:0.0 blue:1.0 alpha:1.0]];
roomText = @"Throneroom";
break;
case RoomTypeGateway:
[roomUpgradeSprite setColor:[SKColor blackColor]];
roomText = @"Gateway";
break;
case RoomTypeGatewayExit:
[roomUpgradeSprite setColor:[SKColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0]];
roomText = @"GatewayExit";
break;
case RoomTypeStockpile:
[roomUpgradeSprite setColor:[SKColor yellowColor]];
roomText = @"Stockpile";
break;
case RoomTypeSlaughterhouse:
[roomUpgradeSprite setColor:[SKColor redColor]];
roomText = @"Slaughterhouse";
break;
case RoomTypeOutpost:
[roomUpgradeSprite setColor:[SKColor colorWithRed:0.6 green:0.6 blue:0.0 alpha:1.0]];
roomText = @"Outpost";
break;
case RoomTypeAnimalBirther:
[roomUpgradeSprite setColor:[SKColor colorWithRed:0.8 green:0.6 blue:0.4 alpha:1.0]];
roomText = @"Animal Spawn";
break;
case RoomTypeFortification:
[roomUpgradeSprite setColor:[SKColor colorWithRed:0.5 green:0.4 blue:0.4 alpha:1.0]];
roomText = @"Fortification";
break;
case RoomTypeAgeExtender:
[roomUpgradeSprite setColor:[SKColor colorWithRed:0.4 green:8.0 blue:0.4 alpha:1.0]];
roomText = @"Life Extender";
break;
default:
break;
}
SKLabelNode *roomLabel = [[SKLabelNode alloc]initWithFontNamed:@"Arial"];
roomLabel.text = roomText;
roomLabel.fontSize = 20;
roomLabel.position = CGPointMake(-_initialScreenSize.width/2 + _initialScreenSize.width/4, 0);
[roomUpgradeSprite addChild:roomLabel];
return roomUpgradeSprite;
}
-(SKSpriteNode *) floorBottomFromSize:(CGSize)floorSpriteSize {
SKSpriteNode *bottomSprite = [[SKSpriteNode alloc]init];
bottomSprite.name = @"towerBottom";
bottomSprite.position = CGPointMake(0, -floorSpriteSize.height/1.71);
[bottomSprite setColor:[SKColor orangeColor]];
bottomSprite.size = CGSizeMake(floorSpriteSize.width, floorSpriteSize.height/5.75);
return bottomSprite;
}
-(SKSpriteNode *) floorWallSpriteFromSize:(CGSize)floorSpriteSize {
SKSpriteNode *wallSprite = [[SKSpriteNode alloc]init];
wallSprite.name = @"towerWall";
[wallSprite setColor:[SKColor orangeColor]];
wallSprite.size = CGSizeMake(floorSpriteSize.width/30, floorSpriteSize.height);
return wallSprite;
}
-(SKSpriteNode *) blockSpriteWithPosition:(CGPoint)blockPosition floorNumber:(int)floorNumber {
SKSpriteNode *blockSprite = [[SKSpriteNode alloc]init];
blockSprite.name = @"towerBlock";
[blockSprite setColor:[SKColor brownColor]];
[blockSprite setSize:CGSizeMake(_initialScreenSize.width/10, _initialScreenSize.height/7)];
blockSprite.position = CGPointMake(blockPosition.x, floorNumber * _initialScreenSize.height/6);
return blockSprite;
}
-(SKSpriteNode *) ladderSpriteForFloor:(int)floorNumber {
SKSpriteNode *ladderSprite = [[SKSpriteNode alloc]init];
ladderSprite.name = @"towerLadder";
[ladderSprite setColor:[SKColor colorWithRed:0.4 green:0.4 blue:0.2 alpha:0.4]];
[ladderSprite setSize:CGSizeMake(_initialScreenSize.width/10, _initialScreenSize.height/7)];
ladderSprite.position = CGPointMake(0, floorNumber * _initialScreenSize.height/6);
return ladderSprite;
}
-(SKSpriteNode *) jobButtonForFloorNumber:(int)floorNumber buttonNumber:(int)buttonNumber {
SKSpriteNode *jobButton = [[SKSpriteNode alloc]init];
[jobButton setSize:CGSizeMake(_initialScreenSize.width/10, _initialScreenSize.width/10)];
[jobButton setColor:[SKColor whiteColor]];
[jobButton setAlpha:0.5];
[jobButton setName:[NSString stringWithFormat:@"jobButton%i",buttonNumber]];
[jobButton setPosition: CGPointMake((-_initialScreenSize.width/2.5 + buttonNumber * _initialScreenSize.width/8), floorNumber * _initialScreenSize.height/6 - _initialScreenSize.height/30)];
return jobButton;
}
-(SKSpriteNode *) itemSprite {
SKSpriteNode *itemSprite = [[SKSpriteNode alloc]init];
[itemSprite setColor:[SKColor blueColor]];
[itemSprite setSize:CGSizeMake(_initialScreenSize.width/60, _initialScreenSize.width/60)];
return itemSprite;
}
-(SKSpriteNode *) overlaySpriteForFloor:(int)floorNumber {
SKSpriteNode *overlaySprite = [[SKSpriteNode alloc]init];
overlaySprite.name = @"towerOverlay";
overlaySprite.position = CGPointMake(0, floorNumber * _initialScreenSize.height/6);
overlaySprite.size = CGSizeMake(_initialScreenSize.width, _initialScreenSize.height/6);
return overlaySprite;
}
-(SKSpriteNode *) dwarfSpriteForDwarf:(DTDwarf *)dwarf {
SKSpriteNode *dwarfSprite = [[SKSpriteNode alloc]init];
[dwarfSprite setColor:[SKColor whiteColor]];
[dwarfSprite setSize:CGSizeMake(_initialScreenSize.width/60, _initialScreenSize.width/30)];
dwarfSprite.position = dwarf.dwarfMovement.currentPosition;
if (dwarf.age < 13) {
[dwarfSprite setColor:[SKColor yellowColor]];
} else if (dwarf.age > 180) {
[dwarfSprite setColor:[SKColor grayColor]];
}
if (dwarf.dwarfState == DwarfStateWorking) {
[dwarfSprite setColor:[SKColor orangeColor]];
} else if (dwarf.dwarfState == DwarfStateEating) {
[dwarfSprite setColor:[SKColor colorWithRed:0.2 green:1.0 blue:0.2 alpha:1]];
} else if (dwarf.dwarfState == DwarfStateResting) {
[dwarfSprite setColor:[SKColor colorWithRed:0.8 green:0.0 blue:0.8 alpha:1]];
}
if (dwarf.currentItem) {
SKSpriteNode *itemSprite = [[SKSpriteNode alloc]init];
[itemSprite setColor:[SKColor blueColor]];
[itemSprite setSize:CGSizeMake(_initialScreenSize.width/70, _initialScreenSize.width/70)];
[itemSprite setPosition:CGPointMake(_initialScreenSize.width/100, 0)];
[dwarfSprite addChild:itemSprite];
}
if (dwarf.currentAnimal) {
SKSpriteNode *animalSprite = [[SKSpriteNode alloc]init];
[animalSprite setColor:[SKColor brownColor]];
[animalSprite setSize:CGSizeMake(_initialScreenSize.width/60, _initialScreenSize.width/60)];
[animalSprite setPosition:CGPointMake(_initialScreenSize.width/100, 0)];
[dwarfSprite addChild:animalSprite];
}
return dwarfSprite;
}
-(SKSpriteNode *) animalSpriteForAnimal:(DTAnimal *)animal {
SKSpriteNode *animalSprite = [[SKSpriteNode alloc]init];
if (animal.animalType == 0) {
[animalSprite setColor:[SKColor colorWithRed:0.3 green:0.3 blue:0.3 alpha:1]];
} else if (animal.animalType == 1) {
[animalSprite setColor:[SKColor colorWithRed:0.6 green:0.3 blue:0.3 alpha:1]];
} else if (animal.animalType == 2) {
[animalSprite setColor:[SKColor colorWithRed:0.9 green:0.3 blue:0.3 alpha:1]];
}
[animalSprite setSize:CGSizeMake(_initialScreenSize.width/50, _initialScreenSize.width/50)];
animalSprite.position = animal.animalMovement.currentPosition;
return animalSprite;
}
-(SKSpriteNode *) enemySpriteForEnemy:(DTEnemy *)enemy {\
SKSpriteNode *enemySprite = [[SKSpriteNode alloc]init];
[enemySprite setColor:[SKColor redColor]];
[enemySprite setSize:CGSizeMake(_initialScreenSize.width/50, _initialScreenSize.width/50)];
enemySprite.position = enemy.movement.currentPosition;
return enemySprite;
}
@end