I've posted a revised version of this here.
I posted about this basic problem a while ago, and I got the recommendation to move the code for managing the Jobs of my game to another class. I have been working on it for a little while now, and I have code that I believe is ready for review. The result is three classes, one for the Queue itself, one for the Job objects that count themselves down, and one for the Units which the Job class creates in order to count itself down. These Units are exposed to Workers when they are in the appropriate state in order to limit the number of workers at one time. The workers do not actually enter the queue, but only enter the Floor that contains the Queue. I'm hoping to hear about any major pitfalls or problems I may be falling into, and also whether or not the design makes sense to other programmers looking at the code.
DTJobQueueStates.h
typedef NS_ENUM(NSInteger, JobQueueStates){
JobQueueIdle = 0,
JobQueueWorking,
JobQueuePaused
};
DTJobQueue.h
#import <Foundation/Foundation.h>
#import "DTJob.h"
#import "DTJobQueueStates.h"
@interface DTJobQueue : NSObject <NSCoding>
-(void) updateJobQueue;
@property JobQueueStates jobQueueState;
@property NSMutableArray *completedJobsForPickup;
-(void) addJob: (DTJob *)job;
-(JobType) activeJobType;
-(void) cancelAllJobs;
-(void) abandonCurrentJob; //used when dwarves die
-(BOOL) areJobSlotsAvailable;
-(int) remainingJobSlots;
-(void) reduceRemainingJobSlots;
-(void) increaseRemainingJobSlots;
-(NSMutableArray *)availableJobSlotsForWork;
-(void) finishOneJobUnit;
-(int) numberOfJobsInArray;
@property NSMutableArray *blocksOnFloor;
@property NSMutableArray *itemsOnFloor;
@property NSMutableArray *enemiesOnFloor;
@end
DTJobQueue.m
#import "DTJobQueue.h"
#import "DTGroundBlock.h"
#import "DTItem.h"
#import "DTEnemy.h"
@implementation DTJobQueue {
DTJob *_activeJob;
NSMutableArray *_jobArray;
}
-(id) init {
self = [super init];
if (self) {
_jobArray = [[NSMutableArray alloc]init];
_jobQueueState = JobQueueIdle;
_completedJobsForPickup = [[NSMutableArray alloc]init];
_blocksOnFloor = [[NSMutableArray alloc]init];
_itemsOnFloor = [[NSMutableArray alloc]init];
_enemiesOnFloor = [[NSMutableArray alloc]init];
}
return self;
}
#pragma mark - Update Loop
-(void) updateJobQueue {
switch (self.jobQueueState) {
case JobQueueIdle:
if ([self chooseAnActiveJob]) {
[self startActiveJob];
self.jobQueueState = JobQueueWorking;
}
break;
case JobQueueWorking:
[_activeJob updateJob];
if (_activeJob.status == JobCompleted) {
[self resolveFinishedJob];
}
break;
default:
break;
}
}
-(BOOL) chooseAnActiveJob {
BOOL choseAJob = NO;
if (_jobArray.count > 0 && _activeJob == nil) {
_activeJob = [_jobArray firstObject];
[_jobArray removeObjectAtIndex:0];
choseAJob = YES;
}
return choseAJob;
}
-(void) startActiveJob {
switch (_activeJob.jobType) {
case MiningJob:
_activeJob.blocksOnFloor = self.blocksOnFloor;
break;
case HaulJob:
_activeJob.itemsOnFloor = self.itemsOnFloor;
break;
case FightingJob:
_activeJob.enemiesOnFloor = self.enemiesOnFloor;
break;
default:
break;
}
[_activeJob startJob];
}
-(void) resolveFinishedJob {
[self.completedJobsForPickup addObject:_activeJob];
[self clearTemporaryObjects];
_activeJob = nil;
self.jobQueueState = JobQueueIdle;
}
-(void) clearTemporaryObjects {
switch (_activeJob.jobType) {
case MiningJob:
[_blocksOnFloor removeAllObjects];
break;
case HaulJob:
[_itemsOnFloor removeAllObjects];
break;
case FightingJob:
[_enemiesOnFloor removeAllObjects];
break;
default:
break;
}
}
#pragma mark - Job Handling
-(void) addJob: (DTJob *)job {
if (self.jobQueueState != JobQueuePaused) {
if (![self alreadyHaveJobOfThisType:job.jobType]) {
[_jobArray addObject:job];
}
}
}
-(BOOL) alreadyHaveJobOfThisType:(JobType)jobType {
BOOL alreadyHaveJobOfThisType = NO;
if (_activeJob.jobType == jobType) {
alreadyHaveJobOfThisType = YES;
}
for (DTJob *job in _jobArray) {
if (job.jobType == jobType) {
alreadyHaveJobOfThisType = YES;
}
}
return alreadyHaveJobOfThisType;
}
-(BOOL) areJobSlotsAvailable {
return [_activeJob areJobSlotsAvailable];
}
-(int) remainingJobSlots {
return _activeJob.remainingJobSlots;
}
-(void) reduceRemainingJobSlots {
_activeJob.remainingJobSlots--;
}
-(void) increaseRemainingJobSlots {
_activeJob.remainingJobSlots++;
}
-(void) finishOneJobUnit {
[_activeJob finishOneJobUnit];
}
-(int) numberOfJobsInArray {
return (int) _jobArray.count;
}
-(JobType) activeJobType {
return _activeJob.jobType;
}
-(NSMutableArray *)availableJobSlotsForWork {
return _activeJob.availableJobSlots;
}
#pragma mark - Cancel Jobs
-(void) cancelAllJobs {
_activeJob = nil;
[_jobArray removeAllObjects];
self.jobQueueState = JobQueueIdle;
}
-(void) abandonCurrentJob {
if (_activeJob != nil) {
[_activeJob abandonCurrentJob];
[self putActiveJobBackInQueue];
}
}
-(void) putActiveJobBackInQueue {
_activeJob.status = JobInQueue;
[_jobArray addObject:_activeJob];
_activeJob = nil;
}
#pragma mark - NSCoding methods
//omitted for brevity
@end
DTJobStatus.h
typedef NS_ENUM(NSInteger, JobStatus){
JobInQueue = 0,
JobWorkerIsWorking,
JobCompleted
};
DTJob.h
#import <Foundation/Foundation.h>
#import "DTJobTypes.h"
#import "DTJobStatus.h"
@interface DTJob : NSObject <NSCoding>
-(id) initWithType:(JobType)jobType;
@property JobType jobType;
@property JobStatus status;
@property int floorNumber;
@property int jobUnitsToCreate;
@property int jobUnitsNeededToComplete;
@property NSMutableArray *availableJobSlots;
@property int remainingJobSlots;
@property CGPoint jobPosition;
@property NSMutableArray *blocksOnFloor;
@property NSMutableArray *itemsOnFloor;
@property NSMutableArray *enemiesOnFloor;
-(BOOL) areJobSlotsAvailable;
-(void) updateJob;
-(void) startJob;
-(void) abandonCurrentJob;
-(void) finishOneJobUnit;
@end
DTJob.m
#import "DTJob.h"
#import "DTJobUnit.h"
#import "DTGroundBlock.h"
#import "DTItem.h"
#import "DTEnemy.h"
@implementation DTJob {
NSMutableArray *_pendingJobUnits;
NSMutableArray *_completedJobUnits;
int _totalJobSlots;
}
static const int kNumJobSlots = 4;
-(id) initWithType:(JobType)jobType {
self = [super init];
if (self) {
_jobType = jobType;
_jobUnitsToCreate = [self calculateJobsToCreate:jobType];
_jobUnitsNeededToComplete = _jobUnitsToCreate;
_availableJobSlots = [[NSMutableArray alloc]init];
_pendingJobUnits = [[NSMutableArray alloc]init];
_completedJobUnits = [[NSMutableArray alloc]init];
_blocksOnFloor = [[NSMutableArray alloc]init];
_itemsOnFloor = [[NSMutableArray alloc]init];
_enemiesOnFloor = [[NSMutableArray alloc]init];
_totalJobSlots = kNumJobSlots;
_remainingJobSlots = _totalJobSlots;
}
return self;
}
-(int) calculateJobsToCreate:(JobType)jobType {
int numJobsToCreate = 0;
switch (jobType) {
case MiningJob:
numJobsToCreate = 1; //this will be changed to the number of blocks
break;
case LadderJob:
numJobsToCreate = 2;
break;
case BottomBuildJob:
numJobsToCreate = 6;
break;
case WallBuildJob:
numJobsToCreate = 4;
break;
case RoomBuildJob:
numJobsToCreate = 10;
break;
case RoomUpgradeJob:
numJobsToCreate = 1;
break;
case SuperiorWallBuildJob:
numJobsToCreate = 8;
break;
case FightingJob:
numJobsToCreate = 1; //will be changed to number of enemies
break;
case CleaningJob:
numJobsToCreate = 1;
break;
case HaulJob:
numJobsToCreate = 1; //will be changed to number of items
break;
default:
break;
}
return numJobsToCreate;
}
#pragma mark - Update loop
-(void) updateJob {
[self fillJobSlotsWithJobs];
[self resolveFinishedJob];
}
-(void) fillJobSlotsWithJobs {
if (self.availableJobSlots.count < _totalJobSlots) {
int spotsToFill = _totalJobSlots - (int)self.availableJobSlots.count;
for (int i = 0; i < spotsToFill ; i++) {
if (_pendingJobUnits.count > 0) {
DTJobUnit *jobUnit = [_pendingJobUnits lastObject];
jobUnit.status = UnitWaitingForWorker;
[self.availableJobSlots addObject:jobUnit];
[_pendingJobUnits removeLastObject];
}
}
}
}
-(void) resolveFinishedJob {
if (_completedJobUnits.count >= self.jobUnitsNeededToComplete) {
self.status = JobCompleted;
[self clearAllJobUnits];
}
}
-(void) clearAllJobUnits {
[_pendingJobUnits removeAllObjects];
[_availableJobSlots removeAllObjects];
[_completedJobUnits removeAllObjects];
}
#pragma mark - Commands
-(void) startJob {
if (_jobType == MiningJob) {
//make a job unit for each block on the floor
for (int i = 0; i < self.blocksOnFloor.count; i++) {
DTGroundBlock *tempBlock = [self.blocksOnFloor objectAtIndex:i];
DTJobUnit *jobUnit = [[DTJobUnit alloc]init];
jobUnit.position = tempBlock.blockPosition;
jobUnit.status = UnitInQueue;
[_pendingJobUnits addObject:jobUnit];
}
self.jobUnitsNeededToComplete = (int)self.blocksOnFloor.count;
} else if (_jobType == HaulJob) {
//make a job unit for each item on the floor
for (int i = 0; i < self.itemsOnFloor.count; i++) {
DTItem *tempItem = [self.itemsOnFloor objectAtIndex:i];
DTJobUnit *jobUnit = [[DTJobUnit alloc]init];
jobUnit.position = tempItem.position;
jobUnit.status = UnitInQueue;
[_pendingJobUnits addObject:jobUnit];
}
self.jobUnitsNeededToComplete = (int)self.itemsOnFloor.count;
} else if (_jobType == FightingJob) {
//make a job unit for each enemy on the floor
for (int i = 0; i < self.enemiesOnFloor.count; i++) {
DTEnemy *tempEnemy = [self.enemiesOnFloor objectAtIndex:i];
DTJobUnit *jobUnit = [[DTJobUnit alloc]init];
jobUnit.position = tempEnemy.enemyMovement.currentPosition;
jobUnit.status = UnitInQueue;
[_pendingJobUnits addObject:jobUnit];
}
self.jobUnitsNeededToComplete = (int)self.enemiesOnFloor.count;
} else {
//make the preset number of jobs
for (int i = 0; i < self.jobUnitsToCreate; i++) {
DTJobUnit *jobUnit = [[DTJobUnit alloc]init];
jobUnit.position = self.jobPosition;
[_pendingJobUnits addObject:jobUnit];
}
}
}
-(BOOL) areJobSlotsAvailable {
int jobsWaitingForWorkers = 0;
for (DTJobUnit *jobUnit in self.availableJobSlots) {
if (jobUnit.status == UnitWaitingForWorker) {
jobsWaitingForWorkers++;
}
}
if (self.remainingJobSlots > 0 && jobsWaitingForWorkers > 0) {
return YES;
} else {
return NO;
}
}
-(void) finishOneJobUnit {
if (self.availableJobSlots.count > 0) {
DTJobUnit *jobUnit = [self.availableJobSlots firstObject];
jobUnit.status = UnitCompleted;
[_completedJobUnits addObject:jobUnit];
[self.availableJobSlots removeObjectAtIndex:0];
self.remainingJobSlots++;
}
}
-(void) abandonCurrentJob {
int remainingJobsCount = 0;
for (int i = 0; i < _pendingJobUnits.count; i++) {
remainingJobsCount++;
}
for (int i = 0; i < _availableJobSlots.count; i++) {
remainingJobsCount++;
}
self.jobUnitsToCreate = remainingJobsCount;
self.remainingJobSlots = _totalJobSlots;
[self.availableJobSlots removeAllObjects];
[_pendingJobUnits removeAllObjects];
}
#pragma mark - NSCoding methods
//omitted for brevity
@end
DTJobUnitStatus.h
typedef NS_ENUM(NSInteger, JobUnitStatus){
UnitInQueue = 0,
UnitWaitingForWorker,
UnitWorkerIsWorking,
UnitCompleted,
};
DTJobUnit.h
#import <Foundation/Foundation.h>
#import "DTJobUnitStatus.h"
@interface DTJobUnit : NSObject <NSCoding>
@property CGPoint position;
@property JobUnitStatus status;
@end
DTJobUnit.m
#import "DTJobUnit.h"
@implementation DTJobUnit
-(id) init {
self = [super init];
if (self) {
_status = UnitInQueue;
}
return self;
}
#pragma mark - NSCoding methods
//omitted for brevity
@end
And here is the function to add a new job to the queue. This is in the TowerFloor class:
#pragma mark - Job Handling
-(void) addJob: (DTJob *)job {
BOOL addJobToArray = NO;
if (self.floorState == FloorUnderAttack && job.jobType == FightingJob) {
addJobToArray = YES;
}
if (self.floorState == FloorDestroyed && job.jobType == CleaningJob) {
addJobToArray = YES;
}
if (self.floorState != FloorUnderAttack || self.floorState != FloorDestroyed) {
addJobToArray = YES;
}
if (addJobToArray) {
job.floorNumber = self.floorNumber;
job.jobPosition = [self setPositionForJob:job.jobType andFloorNumber:job.floorNumber];
if (job.jobType == MiningJob) {
for (int i = 0; i < _blocksLeftCount; i++) {
CGPoint blockPosition = CGPointMake(-_worldSize.width/2.22 + i * _worldSize.width/10, _floorCenterPosition.y);
[_floorJobQueue.blocksOnFloor addObject:[[DTGroundBlock alloc]initWithPosition:blockPosition]];
}
} else if (job.jobType == HaulJob) {
_floorJobQueue.itemsOnFloor = self.itemArray;
} else if (job.jobType == FightingJob) {
_floorJobQueue.enemiesOnFloor = self.enemyArray;
}
[_floorJobQueue addJob:job];
}
}
-(CGPoint) setPositionForJob:(JobType)jobType andFloorNumber:(int)floorNumber {
CGPoint jobPosition = CGPointZero;
//special case for building a floor above ground
if (jobType == BottomBuildJob && floorNumber >= 0) {
jobPosition = _floorCenterPosition;
} else {
//doing this here because the queue does not know the size of the world or position of the floor center
int randomNumber = arc4random_uniform(4) + 1;
jobPosition = CGPointMake((_worldSize.width/3)/randomNumber, _floorCenterPosition.y);
}
return jobPosition;
}
This is the function that sets the workers working, also in the TowerFloor class:
-(void) assignDwarfMovementForJob:(DTDwarf *)dwarf {
//assign one job at a time out of the queue
int jobsToAssign = 1;
int numJobsAssigned = 0;
for (DTJobUnit *jobUnit in [_floorJobQueue availableJobSlotsForWork]) {
if (jobUnit.status == UnitWaitingForWorker && numJobsAssigned < jobsToAssign) {
dwarf.dwarfMovement.destinationPosition = jobUnit.position;
dwarf.dwarfMovement.dwarfMovementState = DwarfMovingToJobPosition;
jobUnit.status = UnitWorkerIsWorking;
numJobsAssigned++;
[_floorJobQueue reduceRemainingJobSlots];
}
}
}
Finally, this is the method that performs certain actions on the Floor when certain types of job units are completed. This happens when the floor actually registers that the worker is finished working:
-(void) checkDwarvesForWork {
for (DTDwarf *dwarf in self.dwarfArray) {
if (dwarf.dwarfMovement.dwarfMovementState == DwarfInPositionForWork && dwarf.dwarfState != DwarfWorking && dwarf.dwarfState != DwarfFinishedWorking) {
[self startDwarfWorking:dwarf];
}
if (dwarf.dwarfState == DwarfFinishedWorking) {
switch ([_floorJobQueue activeJobType]) {
case MiningJob:
[self doMining];
[self moveDwarfToExit:dwarf];
break;
case HaulJob:
dwarf.currentItem = [self.itemArray lastObject];
[self.itemArray removeLastObject];
dwarf.dwarfMovement.dwarfMovementState = DwarfMovingToFloorExit;
dwarf.dwarfState = DwarfCarryingItemToStockpile;
dwarf.dwarfMovement.destinationPosition = _floorCenterPosition;
break;
case FightingJob:
{
DTEnemy *tempEnemy = [self.enemyArray lastObject];
[dwarf calculateFightDamage:tempEnemy.enemyStrength];
[self.enemyArray removeLastObject];
[self moveDwarfToExit:dwarf];
self.hasFloorChanged = YES;
if (self.enemyArray.count <= 0) {
self.floorState = FloorIdle;
}
break;
}
default:
[self moveDwarfToExit:dwarf];
break;
}
[_floorJobQueue finishOneJobUnit];
}
}
}
This all seem pretty complicated, but is it too complicated? Or rather, is it too complicated to understand, or more complicated than it needs to be? I'm open to any criticism you may provide.