3
\$\begingroup\$

The world in my strategy game is comprised of a number of towers. At the start of the game, only one tower is generated, and when the player discovers new towers they are created and added to the array of towers that the Game controls. Towers after the first tower start with different settings. For example, they do not start with workers and animals, and different floors are revealed and configured.

Previously, I had all of this code in the initialization of the Tower class. That class has too many responsibilities and has grown to over 1200 lines, so I have been looking for ways to refactor it to remove as much code as possible so that it is easier to understand. To achieve this goal, I have created a factory class to create a tower. The Game calls the class method of the factory class, and gets a fully configured tower in return. This has reduced the Tower class by almost 200 lines.

This is the first time that I have created a factory class, and I think that my implementation could definitely use some feedback. I decided to go with a class method so that I did not need an actual instance of this factory class to create the towers. The result of this is that I needed to use C functions in the factory class. Other options would have been to create a class like normal, and create an instance of the factory class at the start of the Game. The other option would be to create a singleton to get similar syntax to that of a class method while also having the ability to have instance methods.

I should note that the reason that worldSize is passed around everywhere is to make sure that the game is compatible with different screen resolutions.

I am open to criticism about all aspects of the code, but the most important thing is whether this is a good implementation of a factory class.

The header only contains the class method, so I will omit that.

DTTowerFactory.m

#import "DTAnimal.h"
#import "DTDwarf.h"
#import "DTTower.h"
#import "DTTowerFactory.h"
#import "DTTowerFloor.h"

@implementation DTTowerFactory

+(DTTower *) towerWithWorldSize:(CGSize)worldSize isFirstTower:(BOOL)isFirstTower {
    DTTower *tower = [[DTTower alloc]initWithWorldSize:worldSize];

    if (isFirstTower) {
        createTowerFloors(tower, worldSize);
        setBackgroundTypes(tower);
        setupInitialTowerFloors(tower);
        createTowerDwarves(tower, worldSize);
        createTowerAnimals(tower, worldSize);
    } else {
        createTowerFloors(tower, worldSize);
        setBackgroundTypes(tower);
        setupTowerFloors(tower);
    }

    return tower;
}

#pragma mark - Tower Defaults
void createTowerFloors(DTTower* tower, CGSize worldSize) {

    int topFloor = 10; // later this will be influenced by a gravity setting
    //a random number of floors underground, not less than 90
    int randomBottomFloor = arc4random_uniform(31) + 90;
    int bottomFloor = -randomBottomFloor;

    //create the floors and set them all to dirt to start
    for (int i = bottomFloor; i <= topFloor; i++) {
        NSNumber *floorNumber = [NSNumber numberWithInteger:i];
        DTTowerFloor *floor = [[DTTowerFloor alloc]initWithType:Dirt andFloor:i andWorldSize:worldSize];
        [tower.towerDict setObject:floor forKey:floorNumber];
    }

    //the top floors do not have ground or blocks for mining
    for (int i = topFloor; i >= 0; i--) {
        NSNumber *floorNumber = [NSNumber numberWithInt:i];
        DTTowerFloor *floor = [tower.towerDict objectForKey:floorNumber];
        [floor.groundBlocks removeAllObjects];
        floor.groundType = NoGround;
    }

    //randomly determine where the different ground types start
    int randomStartingDirtRockFloor = (arc4random_uniform(15) + 15);
    int randomStartingRockFloor = (arc4random_uniform(30) + randomStartingDirtRockFloor);
    int randomStartingRockLavaFloor = (arc4random_uniform(30) + randomStartingRockFloor);
    //have to make them all negative inside these loops
    for (int i = -randomStartingDirtRockFloor; i > -randomStartingRockFloor; i--) {
        NSNumber *floorNumber = [NSNumber numberWithInt:i];
        DTTowerFloor *floor = [tower.towerDict objectForKey:floorNumber];
        floor.groundType = DirtRock;
    }
    for (int i = -randomStartingRockFloor; i > -randomStartingRockLavaFloor; i--) {
        NSNumber *floorNumber = [NSNumber numberWithInt:i];
        DTTowerFloor *floor = [tower.towerDict objectForKey:floorNumber];
        floor.groundType = Rock;
    }
    for (int i = -randomStartingRockLavaFloor; i > bottomFloor; i--) {
        NSNumber *floorNumber = [NSNumber numberWithInt:i];
        DTTowerFloor *floor = [tower.towerDict objectForKey:floorNumber];
        floor.groundType = RockLava;
    }
}
void setBackgroundTypes(DTTower* tower) {
    for (id key in tower.towerDict) {
        DTTowerFloor *floor = [tower.towerDict objectForKey:key];

        //could use this later to have a weighted average
        //int floorNumber = floor.floorNumber;
        //int backgroundType;

        //but for now it will be a simple range
        int randomNumber = arc4random_uniform(80);
        //int randomNumber = skRand(0, 80);

        if (randomNumber < 10) {
            floor.backgroundType = 0;
        } else if (randomNumber >= 10 && randomNumber < 20) {
            floor.backgroundType = 1;
        } else if (randomNumber >= 20 && randomNumber < 30) {
            floor.backgroundType = 2;
        } else if (randomNumber >= 30 && randomNumber < 40) {
            floor.backgroundType = 3;
        } else if (randomNumber >= 40 && randomNumber < 50) {
            floor.backgroundType = 4;
        } else if (randomNumber >= 50 && randomNumber < 60) {
            floor.backgroundType = 5;
        } else if (randomNumber >= 60 && randomNumber < 70) {
            floor.backgroundType = 6;
        } else if (randomNumber >= 70 && randomNumber < 80) {
            floor.backgroundType = 7;
        }
    }
}

#pragma mark - First Tower
void setupInitialTowerFloors(DTTower* tower) {
    for (int i = 0; i > -4; i--) {
        NSNumber *floorNumber = [NSNumber numberWithInt:i];
        DTTowerFloor *floor = [tower.towerDict objectForKey:floorNumber];
        floor.isRevealed = YES;
        //adds ladders here for convenience
        floor.floorBuildState |= FloorHasLadder;
    }
    //have to do three floors manually for the top and bottom and floor 0
    NSNumber *floorNumber0 = [NSNumber numberWithInt:0];
    DTTowerFloor *tempFloor0 = [tower.towerDict objectForKey:floorNumber0];
    tempFloor0.floorBuildState |= FloorHasWalls | FloorHasBottom | FloorHasRoom;
    NSNumber *floorNumber1 = [NSNumber numberWithInt:1];
    DTTowerFloor *tempFloor1 = [tower.towerDict objectForKey:floorNumber1];
    tempFloor1.isRevealed = YES;
    tempFloor1.floorBuildState |= FloorHasBottom;
    NSNumber *floorNumber2 = [NSNumber numberWithInt:-4];
    DTTowerFloor *tempFloor2 = [tower.towerDict objectForKey:floorNumber2];
    tempFloor2.isRevealed = YES;
}
void createTowerDwarves(DTTower* tower, CGSize worldSize) {
    int numStartingDwarves = 5;
    for (int i = 0; i < numStartingDwarves; i++){
        DTDwarf *newDwarf = startingDwarf(worldSize);
        [tower.towerDwarves addObject:newDwarf];
        [tower.dwarfListForRender addObject:newDwarf];
    }
}
DTDwarf* startingDwarf(CGSize worldSize) {
    DTDwarf *dwarf = [[DTDwarf alloc]initWithWorldSize:worldSize];
    dwarf.dwarfMovement.currentFloor = -1;
    dwarf.dwarfMovement.destinationFloor = dwarf.dwarfMovement.currentFloor;
    dwarf.dwarfMovement.currentPosition = CGPointMake(0, dwarf.dwarfMovement.currentFloor * worldSize.height/6 - worldSize.height/6/3);
    return dwarf;
}
void createTowerAnimals(DTTower* tower, CGSize worldSize) {
    DTTowerFloor *floor = [tower.towerDict objectForKey:[NSNumber numberWithInt:0]];
    int numStartingAnimals = 3;
    for (int i = 0; i < numStartingAnimals; i++) {
        [floor acceptAnimal:startingAnimal(worldSize)];
    }
}
DTAnimal* startingAnimal(CGSize worldSize) {
    int randomNumber = arc4random_uniform(3);
    DTAnimal *animal = [[DTAnimal alloc]initWithAnimalType:randomNumber andWorldSize:worldSize];
    return animal;
}

#pragma mark - Additional Towers
void setupTowerFloors(DTTower* tower) {
    int startingFloorOffset = -51;
    int randomNumber = arc4random_uniform(25);
    int startingFloorInt = startingFloorOffset + randomNumber;
    int oneFloorAboveInt = startingFloorInt + 1;
    int oneFloorBelowInt = startingFloorInt - 1;

    [tower assignStartingFloorForNewTower:startingFloorInt];

    NSNumber *startFloorNum = [NSNumber numberWithInt:startingFloorInt];
    DTTowerFloor *startFloor = [tower.towerDict objectForKey:startFloorNum];
    [startFloor.groundBlocks removeAllObjects];
    startFloor.isRevealed = YES;

    startFloor.floorBuildState |= FloorHasLadder | FloorHasWalls | FloorHasBottom | FloorHasRoom | FloorHasRoomUpgrade;

    [startFloor buildRoomUpgrade:RoomTypeGatewayExit];

    NSNumber *oneFloorAboveNum = [NSNumber numberWithInt:oneFloorAboveInt];
    DTTowerFloor *oneFloorAbove = [tower.towerDict objectForKey:oneFloorAboveNum];
    oneFloorAbove.isRevealed = YES;

    NSNumber *oneFloorBelowNum = [NSNumber numberWithInt:oneFloorBelowInt];
    DTTowerFloor *oneFloorBelow = [tower.towerDict objectForKey:oneFloorBelowNum];
    oneFloorBelow.isRevealed = YES;
}

@end

Here is the initialization of the Tower, which I will include since this method calls it to begin creating the Tower. There are both instance variables and properties included here. I could have initialized the properties in the factory class, but I left them in here for simplicity. I am not sure if this is the right choice.

DTTower.m

-(id) initWithWorldSize:(CGSize)worldSize {
    self = [super init];
    if (self){
        _worldSize = worldSize;

        _towerDict = [[NSMutableDictionary alloc]init];

        _balanceSettings = [[DTGameBalance alloc]init];

        _completedJobs = [[NSMutableArray alloc]init];
        _resourcesForCollection = [[NSMutableArray alloc]init];
        _dwarvesForGamePickup = [[NSMutableArray alloc]init];

        _startingFloorForNewTower = 0;

        _towerEnemies = [[NSMutableArray alloc]init];
        _towerDwarves = [[NSMutableArray alloc]init];
        _towerAnimals = [[NSMutableArray alloc]init];

        _deadDwarfStatsForPickup = [[NSMutableArray alloc]init];

        _dwarfListForRender = [[NSMutableArray alloc]init];
    }
    return self;
}
\$\endgroup\$
2
\$\begingroup\$

I think the factory class is completely unnecessary. I don't see how you're saving 200 lines out of a single file either.

The way I see it, the code in DTTowerFactory.m comes from several different files (and probably needs to go back to those files).


towerWithWorldSize:isFirstTower belongs in DTTower.m


createTowerFloors() ought to return an NSDictionary object and belongs in DTTowerFloor.m. You should be doing:

someTower.towerDict = createTowerFloor(worldSize);

setBackgroundTypes() probably should just be part of the createTowerFloors() function. Is there any reason why you can't set a floor's background type as you create the floor?


setupInitialTowerFloors() and setupTowerFloors() again could be part of DTTowerFloor.m, just like createTowerFloors().


startingDwarf() and createTowerDwarves can be moved into DTDwarf.m.


startingAnimal() and createTowerAnimals() can be moved into DTAnimal.m.

\$\endgroup\$
  • \$\begingroup\$ That bad huh? I will try to integrate your suggestions. Thanks for the feedback! \$\endgroup\$ – bazola Sep 13 '14 at 1:19
  • \$\begingroup\$ The problem, as I see it, is that you've somewhat arbitrarily decreased file size at the expense of organization. \$\endgroup\$ – nhgrif Sep 13 '14 at 13:04
  • \$\begingroup\$ I see your point. The reason I did it that way, and the reason that all of that code was in the Tower class, was because things are set up a certain way when the tower is first created, but any other time they are set up "normally" with different settings. I suppose I could have a DwarfFactory and AnimalFactory that handle those differences? \$\endgroup\$ – bazola Sep 13 '14 at 13:17
  • \$\begingroup\$ Also, the tower has 90+ Floor objects in its dictionary, so I don't see how TowerFloor could return that dictionary when they are contained inside it? The same with Dwarf and Animal, how could the Dwarf class return a dictionary of Dwarves? \$\endgroup\$ – bazola Sep 13 '14 at 13:25
  • \$\begingroup\$ chat.stackexchange.com/transcript/message/17655592#17655592 \$\endgroup\$ – nhgrif Sep 13 '14 at 13:41

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.