After posting my previous question about this Job Queue, I decided I wasn't actually very happy with it. I am embarrassed to admit that upon further testing it did not function properly in all situations. I received some awesome feedback about it, and I have made extensive changes to the classes as well as fixing all of the bugs that I could find. I believe the code is much simpler and easier to understand, so I would like to get some feedback on it.
One of the key differences between this and the previous version is that now the workers are assigned a JobUnit from the JobQueue, and derive their destination positions based on that. Then when they are finished moving, they are sent to the JobQueue to work on that JobUnit. I don't know if this is a good way to handle the problem, but it is working.
DTJobQueue.h:
#import <Foundation/Foundation.h>
#import "DTDwarf.h"
#import "DTJob.h"
#import "DTJobQueueState.h"
@interface DTJobQueue : NSObject <NSCoding>
@property JobQueueState state;
@property NSMutableArray *completedJobsForPickup;
-(void) updateJobQueue;
#pragma mark - Job Handling
-(void) addJob: (DTJob *)job;
-(BOOL) alreadyHaveJobOfThisType:(JobType)jobType;
-(BOOL) areJobsAvailableForWork;
-(JobType) activeJobType;
#pragma mark - Dwarf Handling
-(void) addDwarfToQueue:(DTDwarf *)dwarf;
-(DTJobUnit *)jobUnitForDwarf;
#pragma mark - Pause and Cancel Jobs
-(void) cancelAllJobs;
//nothing currently uses pause
-(void) pauseJobQueue;
-(void) unpauseJobQueue;
#pragma mark - Info for Rendering
-(int) numberOfJobsInArray;
-(NSMutableArray *) listOfJobs;
@end
DTJobQueue.m:
#import "DTJobQueue.h"
/*
The JobQueue handles the jobs of the floor it is on.
One job is active at a time, and only one job of each type is allowed at once.
Each update if a job is not active, the queue tries to make one active and starts it if successful.
If a job is currently active, it updates it based on workloads received from the workers.
Once complete it resolves the job and clears workers out of the queue.
It also has methods to cancel jobs in the queue.
*/
@implementation DTJobQueue {
DTJob *_activeJob;
NSMutableArray *_activeJobUnits;
NSMutableArray *_workerSlots;
NSMutableArray *_jobQueue;
}
static const int kNumWorkerSlots = 4;
-(id) init {
self = [super init];
if (self) {
_state = JobQueueStateIdle;
_jobQueue = [[NSMutableArray alloc]init];
_workerSlots = [[NSMutableArray alloc]init];
_activeJobUnits = [[NSMutableArray alloc]init];
_completedJobsForPickup = [[NSMutableArray alloc]init];
}
return self;
}
#pragma mark - Update Loop
-(void) updateJobQueue {
switch (self.state) {
case JobQueueStateIdle:
//jobs are started in the idle state and only if one is not already active
if ([self chooseAnActiveJob]) {
[self startActiveJob];
self.state = JobQueueStateWorking;
}
break;
case JobQueueStateWorking:
//this case happens when a job is active
//first it loads as many job units as it can, then it checks if the workers have finished any
//then it resolves the job if all of its units are completed
[_activeJob updateJob];
//dont create more new job units than the max minus the number of already loaded units
int numAvailableJobUnits = kNumWorkerSlots - (int)_activeJobUnits.count;
//first check if more job units can be loaded
if ((int)_activeJobUnits.count < kNumWorkerSlots) {
//if so, load the previously determined number of them, not more than the total number that will be needed
while ((int)_activeJobUnits.count < _activeJob.jobUnitsNeededToComplete && numAvailableJobUnits > 0) {
[self fillJobUnitSlot];
numAvailableJobUnits--;
}
}
[self checkForFinishedJobUnits];
if (_activeJob.status == JobStateCompleted) {
[self resolveFinishedJob];
}
break;
default:
break;
}
}
#pragma mark - Idle Update
-(BOOL) chooseAnActiveJob {
if (_jobQueue.count > 0 && _activeJob == nil) {
_activeJob = [_jobQueue firstObject];
[_jobQueue removeObjectAtIndex:0];
return YES;
} else {
return NO;
}
}
-(void) startActiveJob {
//creates the job units
[_activeJob startJob];
}
#pragma mark - Working Update
-(void) fillJobUnitSlot {
//returns nil if there are no available job units
DTJobUnit *jobUnit = [_activeJob jobUnitWaitingForWorker];
if (jobUnit != nil) {
jobUnit.status = UnitWaitingForWorker;
[_activeJobUnits addObject:jobUnit];
}
}
-(void) checkForFinishedJobUnits {
//dwarves are removed from their worker slot if their unit is finished
//they are not passed back and forth, just a reference kept while they are working
NSMutableArray *dwarvesStaying = [[NSMutableArray alloc]init];
for (DTDwarf *dwarf in _workerSlots) {
if (dwarf.dwarfState == DwarfFinishedWorking) {
dwarf.dwarfState = DwarfReadyForFloorPickup;
[self completeJobUnit:dwarf.dwarfJobUnit];
} else {
[dwarvesStaying addObject:dwarf];
}
}
_workerSlots = dwarvesStaying;
}
-(void) completeJobUnit:(DTJobUnit *)jobUnit {
[_activeJob completeJobUnit:jobUnit];
[_activeJobUnits removeObject:jobUnit];
}
-(void) resolveFinishedJob {
[self.completedJobsForPickup addObject:_activeJob];
_activeJob = nil;
self.state = JobQueueStateIdle;
}
#pragma mark - Dwarf Handling
-(void) addDwarfToQueue:(DTDwarf *)dwarf {
//the dwarf is only added to the queue once in position to work the job
dwarf.dwarfState = DwarfWorking;
[_workerSlots addObject:dwarf];
for (DTJobUnit *jobUnit in _activeJobUnits) {
//this is a sanity check to make sure the dwarf has the right job unit
if (jobUnit == dwarf.dwarfJobUnit) {
jobUnit.status = UnitWorkerIsWorking;
}
}
}
-(DTJobUnit *)jobUnitForDwarf {
//the dwarf is assigned a job unit when it reaches the floor and is assigned a job destination
DTJobUnit *jobUnit = nil;
for (int i = 0; i < (int)_activeJobUnits.count; i++) {
DTJobUnit *tempUnit = [_activeJobUnits objectAtIndex:i];
if (tempUnit.status == UnitWaitingForWorker) {
return tempUnit;
}
}
return jobUnit;
}
#pragma mark - Job Handling
-(void) addJob: (DTJob *)job {
//at this point all other validation has already taken place
if (self.state != JobQueueStateClosed) {
[_jobQueue addObject:job];
}
}
-(BOOL) alreadyHaveJobOfThisType:(JobType)jobType {
//saving a lot of execution time by returning early
if (_activeJob.jobType == jobType) {
return YES;
} else {
for (DTJob *job in _jobQueue) {
if (job.jobType == jobType) {
return YES;
}
}
return NO;
}
}
-(BOOL) areJobsAvailableForWork {
//this method is called by the dwarf movement AI also
if ([self jobSlotsOpen] && [self jobUnitsAvailableForWork] && self.state == JobQueueStateWorking) {
return YES;
} else {
return NO;
}
}
-(BOOL) jobSlotsOpen {
//part of this is a sanity check to prevent too many workers from trying to enter the queue
if ((int)_workerSlots.count < kNumWorkerSlots && (int)_workerSlots.count < (int)_activeJobUnits.count) {
return YES;
} else {
return NO;
}
}
-(BOOL) jobUnitsAvailableForWork {
for (DTJobUnit *jobUnit in _activeJobUnits) {
if (jobUnit.status == UnitWaitingForWorker || jobUnit.status == UnitWorkerAssigned) { //temporary workaround
return YES;
}
}
return NO;
}
-(JobType) activeJobType {
return _activeJob.jobType;
}
#pragma mark - Pause and Cancel Jobs
-(void) cancelAllJobs {
[self removeDwarvesFromJobQueue];
[_activeJobUnits removeAllObjects];
_activeJob = nil;
[_jobQueue removeAllObjects];
self.state = JobQueueStateIdle;
}
-(void) removeDwarvesFromJobQueue {
for (DTDwarf *dwarf in _workerSlots) {
dwarf.dwarfState = DwarfStateAbandoningJob;
dwarf.dwarfJobUnit = nil;
}
[_workerSlots removeAllObjects];
}
//currently pausing is not being used by anything
-(void) pauseJobQueue {
[self removeDwarvesFromJobQueue];
[self putJobUnitsBackInJob];
[_activeJobUnits removeAllObjects];
self.state = JobQueueStatePaused;
}
-(void) unpauseJobQueue {
if ((int)_jobQueue.count > 0) {
_activeJob = [_jobQueue firstObject];
[_jobQueue removeObject:[_jobQueue firstObject]];
self.state = JobQueueStateWorking;
} else {
self.state = JobQueueStateIdle;
}
}
-(void) putJobUnitsBackInJob {
for (DTJobUnit *jobUnit in _activeJobUnits) {
jobUnit.status = UnitInQueue;
}
}
-(void) putActiveJobBackInQueue {
if (_activeJob != nil) {
[_jobQueue insertObject:_activeJob atIndex:0];
_activeJob = nil;
}
}
#pragma mark - Info for Rendering
-(NSMutableArray *) listOfJobs {
NSMutableArray *listOfJobs = [[NSMutableArray alloc]init];
if (_activeJob != nil) {
[listOfJobs addObject:_activeJob];
}
for (int i = 0; i < (int)_jobQueue.count; i++) {
[listOfJobs addObject:[_jobQueue objectAtIndex:i]];
}
return listOfJobs;
}
-(int) numberOfJobsInArray {
return (int) _jobQueue.count;
}
#pragma mark - NSCoding methods
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
_state = [aDecoder decodeIntegerForKey:@"jobQueueState"];
_completedJobsForPickup = [aDecoder decodeObjectForKey:@"completedJobsForPickup"];
_activeJob = [aDecoder decodeObjectForKey:@"activeJob"];
_activeJobUnits = [aDecoder decodeObjectForKey:@"activeJobUnits"];
_workerSlots = [aDecoder decodeObjectForKey:@"workerSlots"];
_jobQueue = [aDecoder decodeObjectForKey:@"jobQueue"];
}
return self;
}
-(void) encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeInteger:self.state forKey:@"jobQueueState"];
[aCoder encodeObject:self.completedJobsForPickup forKey:@"completedJobsForPickup"];
[aCoder encodeObject:_activeJob forKey:@"activeJob"];
[aCoder encodeObject:_activeJobUnits forKey:@"activeJobUnits"];
[aCoder encodeObject:_workerSlots forKey:@"workerSlots"];
[aCoder encodeObject:_jobQueue forKey:@"jobQueue"];
}
@end
DTJob.h
#import <Foundation/Foundation.h>
#import "DTJobTypes.h"
#import "DTJobStatus.h"
#import "DTJobUnit.h"
@interface DTJob : NSObject <NSCoding>
-(id) initWithType:(JobType)jobType;
@property JobType jobType;
@property JobState status;
@property CGPoint jobPosition;
@property int floorNumber;
@property int jobUnitsNeededToComplete;
-(void) completeJobUnit:(DTJobUnit *)jobUnit;
-(DTJobUnit *) jobUnitWaitingForWorker;
-(BOOL) jobsAvailable;
-(void) updateJob;
-(void) startJob;
-(void) pauseJob;
@property NSMutableArray *blocksOnFloor;
@property NSMutableArray *itemsOnFloor;
@property NSMutableArray *enemiesOnFloor;
@end
DTJob.m:
#import "DTJob.h"
#import "DTJobUnit.h"
#import "DTGroundBlock.h"
#import "DTItem.h"
#import "DTEnemy.h"
/*
Jobs are created by the Floors and passed to the JobQueue once validated.
At this time the Floor gives the job an array of blocks, enemies, or items if the job is Mining, Fighting, or Hauling.
The JobQueue starts the job, and the Job creates the proper number of JobUnits.
The JobQueue asks for a valid JobUnit, and then hands it to a dwarf.
Once completed the JobQueue tells the Job to complete the correct JobUnit.
Once all JobUnits are completed, the Job changes its state to finished and the JobQueue handles it.
*/
@implementation DTJob {
int _jobUnitsToCreate;
NSMutableArray *_pendingJobUnits;
NSMutableArray *_completedJobUnits;
}
-(id) initWithType:(JobType)jobType {
self = [super init];
if (self) {
_jobType = jobType;
_jobUnitsToCreate = [self calculateJobsToCreate:jobType];
_pendingJobUnits = [[NSMutableArray alloc]init];
_completedJobUnits = [[NSMutableArray alloc]init];
_blocksOnFloor = [[NSMutableArray alloc]init];
_itemsOnFloor = [[NSMutableArray alloc]init];
_enemiesOnFloor = [[NSMutableArray alloc]init];
}
return self;
}
-(int) calculateJobsToCreate:(JobType)jobType {
switch (jobType) {
case MiningJob: //this will be changed to the number of blocks
case RoomUpgradeJob: //different rooms could require more workers but default is 1
case FightingJob: //this will be changed to the number of enemies
case HaulJob: //this will be changed to the number of items
case CleaningJob:
return 1;
case LadderJob:
return 2;
case BottomBuildJob:
return 6;
case WallBuildJob:
return 4;
case RoomBuildJob:
return 10;
case SuperiorWallBuildJob:
return 8;
default:
break;
}
return 0;
}
#pragma mark - Update loop
-(void) updateJob {
[self checkIfJobIsFinished];
}
-(void) checkIfJobIsFinished {
if (_completedJobUnits.count >= self.jobUnitsNeededToComplete) {
self.status = JobStateCompleted;
[self clearAllJobUnits];
}
}
-(void) clearAllJobUnits {
[_pendingJobUnits removeAllObjects];
[_completedJobUnits removeAllObjects];
}
#pragma mark - Start and Pause Jobs
-(void) startJob {
if (_jobType == MiningJob) {
//make a job unit for each block on the floor
for (int i = 0; i < (int)self.blocksOnFloor.count; i++) {
DTGroundBlock *tempBlock = [self.blocksOnFloor objectAtIndex:i];
DTJobUnit *jobUnit = [[DTJobUnit alloc]initWithJobType:_jobType];
jobUnit.position = tempBlock.blockPosition;
jobUnit.unitGroundBlock = tempBlock;
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 < (int)self.itemsOnFloor.count; i++) {
DTItem *tempItem = [self.itemsOnFloor objectAtIndex:i];
DTJobUnit *jobUnit = [[DTJobUnit alloc]initWithJobType:_jobType];
jobUnit.position = tempItem.position;
jobUnit.unitItem = tempItem;
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 < (int)self.enemiesOnFloor.count; i++) {
DTEnemy *tempEnemy = [self.enemiesOnFloor objectAtIndex:i];
DTJobUnit *jobUnit = [[DTJobUnit alloc]initWithJobType:_jobType];
jobUnit.position = tempEnemy.enemyMovement.currentPosition;
jobUnit.unitEnemy = tempEnemy;
jobUnit.status = UnitInQueue;
[_pendingJobUnits addObject:jobUnit];
}
self.jobUnitsNeededToComplete = (int)self.enemiesOnFloor.count;
} else {
//if not a special job case, make the preset number of jobs
for (int i = 0; i < _jobUnitsToCreate; i++) {
DTJobUnit *jobUnit = [[DTJobUnit alloc]initWithJobType:_jobType];
jobUnit.position = self.jobPosition;
jobUnit.status = UnitInQueue;
//need to make a method that randomly moves this along the x axis for each copy
[_pendingJobUnits addObject:jobUnit];
}
self.jobUnitsNeededToComplete = _jobUnitsToCreate;
}
}
//pause not currently used by anything
-(void) pauseJob {
int remainingJobsCount = 0;
for (int i = 0; i < _pendingJobUnits.count; i++) {
remainingJobsCount++;
}
_jobUnitsToCreate = remainingJobsCount;
[_pendingJobUnits removeAllObjects];
}
#pragma mark - Job Unit Handling
-(void) completeJobUnit:(DTJobUnit *)jobUnit {
if (jobUnit != nil) {
jobUnit.status = UnitCompleted;
[_completedJobUnits addObject:jobUnit];
[_pendingJobUnits removeObject:jobUnit];
}
}
-(DTJobUnit *) jobUnitWaitingForWorker {
DTJobUnit *jobUnitForWorker = nil;
for (DTJobUnit *jobUnit in _pendingJobUnits) {
if (jobUnit.status == UnitInQueue) {
jobUnitForWorker = jobUnit;
return jobUnitForWorker;
}
}
return jobUnitForWorker;
}
-(BOOL) jobsAvailable {
for (DTJobUnit *jobUnit in _pendingJobUnits) {
if (jobUnit.status == UnitInQueue) {
return YES;
}
}
return NO;
}
#pragma mark - NSCoding methods
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
//basic job properties
_status = [aDecoder decodeIntegerForKey:@"status"];
_jobType = [aDecoder decodeIntegerForKey:@"jobType"];
_jobPosition = [aDecoder decodeCGPointForKey:@"jobPosition"];
_floorNumber = [aDecoder decodeIntForKey:@"floorNumber"];
//job unit management
_jobUnitsToCreate = [aDecoder decodeIntForKey:@"jobUnitsToCreate"];
_jobUnitsNeededToComplete = [aDecoder decodeIntForKey:@"jobUnitsNeededToComplete"];
_pendingJobUnits = [aDecoder decodeObjectForKey:@"pendingJobUnits"];
_completedJobUnits = [aDecoder decodeObjectForKey:@"completedJobUnits"];
//job objects
_blocksOnFloor = [aDecoder decodeObjectForKey:@"blocksOnFloor"];
_itemsOnFloor = [aDecoder decodeObjectForKey:@"itemsOnFloor"];
_enemiesOnFloor = [aDecoder decodeObjectForKey:@"enemiesOnFloor"];
}
return self;
}
-(void) encodeWithCoder:(NSCoder *)aCoder {
//basic job properties
[aCoder encodeInteger:self.status forKey:@"status"];
[aCoder encodeInteger:self.jobType forKey:@"jobType"];
[aCoder encodeInt:self.floorNumber forKey:@"floorNumber"];
[aCoder encodeCGPoint:self.jobPosition forKey:@"jobPosition"];
//job unit management
[aCoder encodeInt:_jobUnitsToCreate forKey:@"jobUnitsToCreate"];
[aCoder encodeInt:self.jobUnitsNeededToComplete forKey:@"jobUnitsNeededToComplete"];
[aCoder encodeObject:_pendingJobUnits forKey:@"pendingJobUnits"];
[aCoder encodeObject:_completedJobUnits forKey:@"completedJobUnits"];
//job objects
[aCoder encodeObject:self.blocksOnFloor forKey:@"blocksOnFloor"];
[aCoder encodeObject:self.itemsOnFloor forKey:@"itemsOnFloor"];
[aCoder encodeObject:self.enemiesOnFloor forKey:@"enemiesOnFloor"];
}
@end
DTJobUnit.h:
#import <Foundation/Foundation.h>
#import "DTJobUnitStatus.h"
#import "DTJobTypes.h"
#import "DTGroundBlock.h"
#import "DTItem.h"
#import "DTEnemy.h"
@interface DTJobUnit : NSObject <NSCoding>
-(id) initWithJobType:(JobType)jobType;
@property JobUnitStatus status;
@property JobType jobType;
@property CGPoint position;
//these are set by the Job when the JobUnit is created if it is a Mining, Hauling, or Fighting job
@property DTGroundBlock *unitGroundBlock;
@property DTItem *unitItem;
@property DTEnemy *unitEnemy;
@end
DTJobUnit.m:
#import "DTJobUnit.h"
@implementation DTJobUnit
/*
JobUnits are created by the Job that handles them.
The Job also sets and checks their status.
They contain position information to give to dwarves.
They will contain a reference to a block, item, or enemy if a Mining, Hauling, or Fighting job.
*/
-(id) initWithJobType:(JobType)jobType {
self = [super init];
if (self) {
_status = UnitInQueue;
_jobType = jobType;
}
return self;
}
#pragma mark - NSCoding methods
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
//basic unit properties
_status = [aDecoder decodeIntegerForKey:@"status"];
_position = [aDecoder decodeCGPointForKey:@"position"];
_jobType = [aDecoder decodeIntegerForKey:@"jobType"];
//things a unit can reference
_unitGroundBlock = [aDecoder decodeObjectForKey:@"unitGroundBlock"];
_unitItem = [aDecoder decodeObjectForKey:@"unitItem"];
_unitEnemy = [aDecoder decodeObjectForKey:@"unitEnemy"];
}
return self;
}
-(void) encodeWithCoder:(NSCoder *)aCoder {
//basic unit properties
[aCoder encodeInteger:self.status forKey:@"status"];
[aCoder encodeCGPoint:self.position forKey:@"position"];
[aCoder encodeInteger:self.jobType forKey:@"jobType"];
//things a unit can reference
[aCoder encodeObject:self.unitGroundBlock forKey:@"unitGroundBlock"];
[aCoder encodeObject:self.unitItem forKey:@"unitItem"];
[aCoder encodeObject:self.unitEnemy forKey:@"unitEnemy"];
}
@end
Here is the method that first assigns a JobUnit to a worker:
-(void) assignDwarfJobUnit:(DTDwarf *)dwarf {
DTJobUnit *jobUnit = [_floorJobQueue jobUnitForDwarf];
if (jobUnit != nil) {
dwarf.dwarfJobUnit = jobUnit;
dwarf.dwarfMovement.destinationPosition = jobUnit.position;
dwarf.dwarfMovement.dwarfMovementState = DwarfMovingToJobPosition;
jobUnit.status = UnitWorkerAssigned;
}
}
And here is the method that sends the worker to the JobQueue when it is in position:
-(void) putDwarvesToWork {
for (DTDwarf *dwarf in self.dwarfArray) {
if (dwarf.dwarfMovement.dwarfMovementState == DwarfAtDestinationFloor || dwarf.dwarfMovement.dwarfMovementState == DwarfAtFloorExit) {
if (dwarf.dwarfState != DwarfCarryingItemToStockpile) {
if ([_floorJobQueue areJobsAvailableForWork]) {
if ([dwarf isJobAllowed:[_floorJobQueue activeJobType]]) {
[self assignDwarfJobUnit:dwarf];
} else {
dwarf.dwarfMovement.dwarfMovementState = DwarfIdleMovement;
}
} else {
dwarf.dwarfMovement.dwarfMovementState = DwarfIdleMovement;
}
}
}
}
}
I am open to any kind of criticism, so please don't hold back. This is the first time I have tried to extensively comment my code, especially big picture comments that describe what the overall point of a class is, so I would love to know how I did. I did not include the enums this time (this is already a lot of code), and I realized as I posted this that the names still do not perfectly conform to Apple standards, but I will be fixing that next.