I'm working on a basic strategy game for iOS and I have a question about the overall layout of the code in the program. I added some features to the game today and I had to add code to several different places to get the features to function properly. In general I think I am doing things correctly, but I fear I am falling into common programming pitfalls that I do not yet know about. In this question I will walk through the features I wanted to add to the program, and the way I approached them.
To start with I had basic enemies in the game that were randomly created on floors of the tower given certain conditions. When spawned they just stayed in place and did nothing. So I looked at the class and saw that once the enemies were created, there was no logic present to decide what they would do. So I added some logic to tell the enemies to move to attack the nearest wall if one was present on the floor that they spawned on.
Next I wanted to make the workers react to the presence of enemies on a floor. The floors have an enum that defines their state. After the tower creates an enemy, it immediately sends it to the correct floor. The floor accepts it with this code:
-(void) acceptEnemy:(DTEnemy *)enemy {
[self.enemyArray addObject:enemy];
if (self.floorState != FloorUnderAttack && self.floorState != FloorDestroyed) {
self.floorState = FloorUnderAttack;
}
}
Next I needed to cause the workers on the floor to react to the presence of the enemy. I did so with the following code:
-(void) updateFloor {
switch (self.floorState) {
case FloorUnderAttack:
[self cancelJobDueToAttack];
[self makeDwarvesFlee];
[self handleEnemies];
break;
This method also deals with the regular case of when the floor is not under attack, but that is not so important here. I also needed to add this code:
-(void) cancelJobDueToAttack {
[self abandonCurrentJob];
[_jobArray removeAllObjects];
[_availableJobSlots removeAllObjects]; //should be redundant
[_completedJobUnits removeAllObjects];
}
Basically this clears out all remnants of the job in the queue so that it cannot cause any problems in the future.
Now I needed to prevent the workers from accepting jobs on the floor if it was under attack, which I did so by preventing them from entering the floor in this case:
-(void) sendDwarvesToFloors {
NSMutableArray *dwarvesInTower = [[NSMutableArray alloc]init];
for (DTDwarf *dwarf in self.towerDwarves) {
if (dwarf.dwarfMovement.dwarfMovementState == DwarfAtDestinationFloor && dwarf.age < 13) {
dwarf.dwarfMovement.dwarfMovementState = DwarfIdleMovement;
}
if (dwarf.dwarfMovement.dwarfMovementState == DwarfAtDestinationFloor) {
NSNumber *floorNum = [NSNumber numberWithInt:dwarf.dwarfMovement.destinationFloor];
DTTowerFloor *tempFloor = [self.towerDict objectForKey:floorNum];
if (tempFloor.floorState == FloorUnderAttack) {
if (dwarf.jobPermissions & FightingJobsAllowed) {
[tempFloor acceptDwarf:dwarf];
}
} else if (tempFloor.floorState == FloorDestroyed) {
if (dwarf.jobPermissions & CleaningJobsAllowed) {
[tempFloor acceptDwarf:dwarf];
}
} else {
[tempFloor acceptDwarf:dwarf];
}
} else {
[dwarvesInTower addObject:dwarf];
}
}
self.towerDwarves = dwarvesInTower;
}
Now I wanted to make it so the enemy could destroy the walls, as well as other things. I did so with the following method:
-(void) handleEnemies {
for (DTEnemy *enemy in self.enemyArray) {
if (enemy.enemyMovement.enemyMovementState == EnemyJustSpawned) {
if (self.floorBuildState & FloorHasWalls) {
int positiveOrNegative = 0;
if (enemy.enemyMovement.currentPosition.x >= 0) {
positiveOrNegative = 1;
} else {
positiveOrNegative = -1;
}
enemy.enemyMovement.destinationPosition = CGPointMake((_worldSize.width / 2.1) * positiveOrNegative, enemy.enemyMovement.currentPosition.y);
enemy.enemyMovement.enemyMovementState = EnemyMovingToAttackPosition;
}
}
if (enemy.enemyMovement.enemyMovementState == EnemyInPositionForAttack) {
if (self.floorBuildState & FloorHasWalls) {
if ([self doDamageToWalls]) {
self.floorBuildState &= ~FloorHasWalls;
enemy.enemyMovement.destinationPosition = _floorCenterPosition;
enemy.enemyMovement.enemyMovementState = EnemyMovingToFloorExit;
}
}
}
[enemy updateEnemy];
}
}
Now the enemy would move to the location of the walls and begin to destroy them once they reached that position. Unfortunately I had to add ivars to handle the health of the walls on the floor for this to work.
Now I wanted to finish creating the jobs to deal with the aftermath of the presence of the enemies on the floor. I will call these Cleaning Jobs and Fighting Jobs. To do so, I needed to modify many parts of the code, and this is really the heart of the question because I had to change so many different things to make everything work properly.
I needed to add new jobs to the jobs enum and to the job permissions enum. Then, I went ahead and started creating methods to add the new jobs to the queue. I looked at the method that actually adds new jobs, and saw that it itself did not need to change:
-(void) addJobToQueueOfType:(JobType)jobType forFloor:(int)floorNumber {
NSNumber *floorNum = [NSNumber numberWithInt:floorNumber];
DTTowerFloor *floor = [self.towerDict objectForKey:floorNum];
DTJob *job = [[DTJob alloc]initWithType:jobType];
[floor addJob:job];
}
I looked above this method located in the tower class, to the game class that calls it and saw this:
-(BOOL) addJobToQueueOfType:(JobType)jobType forFloor:(int)floorNumber {
BOOL wasSuccessful = NO;
DTTower *tempTower = [_towerArray objectAtIndex:_currentTower];
if ([self hasEnoughResourcesForJob:jobType]) {
if ([tempTower checkIfFloor:floorNumber isValidForJob:jobType]) {
[tempTower addJobToQueueOfType:jobType forFloor:floorNumber];
wasSuccessful = YES;
}
}
return wasSuccessful;
}
So now I finding things that need to change. First, I need to add cases for hasEnoughResourcesForJob
for all of the new jobs, like so:
-(BOOL) hasEnoughResourcesForJob:(JobType)jobType {
BOOL hasEnoughResources = NO;
switch (jobType) {
case MiningJob:
hasEnoughResources = YES;
break;
case LadderJob:
if (self.currentCommonResources >= _ladderCost) {
hasEnoughResources = YES;
}
break;
case BottomBuildJob:
if (self.currentCommonResources >= _bottomCost) {
hasEnoughResources = YES;
}
break;
case WallBuildJob:
if (self.currentCommonResources >= _wallCost) {
hasEnoughResources = YES;
}
break;
case RoomBuildJob:
if (self.currentCommonResources >= _roomCost) {
hasEnoughResources = YES;
}
case SuperiorWallBuildJob:
if (self.currentCommonResources >= _superiorWallCost) {
hasEnoughResources = YES;
}
break;
case HaulJob:
hasEnoughResources = YES;
break;
case FightingJob:
hasEnoughResources = YES;
break;
case CleaningJob:
hasEnoughResources = YES;
break;
default:
break;
}
return hasEnoughResources;
}
Then, I needed to add entries to the checkIfFloor:isValidForJob:
method like so:
case SuperiorWallBuildJob:
{
NSInteger requiredBuildings = (FloorHasBottom | FloorHasWalls);
if ((tempFloor.floorBuildState & requiredBuildings) == requiredBuildings) {
isValidForJob = YES;
}
break;
}
case FightingJob:
{
if (tempFloor.floorState == FloorUnderAttack) {
isValidForJob = YES;
}
break;
}
case CleaningJob:
{
if (tempFloor.floorState == FloorDestroyed) {
isValidForJob = YES;
}
break;
}
default:
break;
}
Then I needed to add methods to create the products of these jobs once they were completed like so:
-(void) buildSuperiorWallsOnFloor:(DTTowerFloor *)floor {
[self addResourceForCollectionWithCommon:_superiorWallCost andRare:0];
floor.floorBuildState |= FloorHasSuperiorWalls;
self.hasTowerChanged = YES;
}
And I also needed to add values for the cost of the jobs, as well as adding cases to the method that subtracts the cost of the job from the total resources once it is completed.
Now I needed to modify the UI code to add buttons for each of the new job types. When pressed, these buttons check with the game to make sure the job is acceptable, and then pass the job to the tower if this is so. Then, the tower passes the job to the floor. If jobs are present on the floor and the worker has the proper permissions, they will go to the floor and carry out the jobs.
As you can (hopefully) see, just to add one new building or job to the game, I needed to modify code in several different classes. I needed to modify the enums for the job permissions as well as for the jobs themselves. I needed to add a new type of building to the enum for building types. I needed to add cases to check for the permissions for the new jobs for both the state of the floor and for the job permissions of the workers. And I needed to check the costs of the jobs and also decrement their costs once the buildings were complete. Also I needed to add special cases for the methods that add a certain number of jobs to the floor based on the type of job and the state of the floor. And, I am sure there are pieces of code that I had to add that I am forgetting to mention here.
Is this approach common or typical, or am I doing things wrong? Is it normal to have to modify many parts of the code to add this type of functionality? Is there a way to group these things together in a common class? It seems to me that their responsibilities are very different. It seems to me that pretty much I would have to keep documentation on hand listing all the places where I need to add code in order to add to certain features, or I would need to have that memorized. Is this the proper approach, or am I missing something?
if (tempFloor.floorState == FloorUnderAttack)
andif (tempFloor.floorState == FloorDestroyed)
-- you probably need to replace the==
with&
for your bitmask here. \$\endgroup\$