A while ago I posted this question about worker AI and Job Queues for a strategy game. I have posted other questions about the Job Queue, and now I would like to post this revised question about specifically the movement class for the workers in the game. I think it would make for a good code review.
The basic idea is that each turn, the update method is triggered which computes the destination floor of a worker based on its current state and the available jobs on different floors. The DwarfMovement
class computes the floor to move to, and the Movement class handles the basic movement itself. Enemy, animal, and dwarf movement classes all inherit from the Movement class.
I've spent a lot of time cleaning up this code, but I am sure there are still things that I can improve. I know it is a lot of code, but please feel free to comment on small things. Those types of answers are very helpful too.
DTMovement.h:
#import <Foundation/Foundation.h>
@interface DTMovement : NSObject <NSCoding>
-(id) initWithWorldSize:(CGSize)worldSize;
#pragma mark - Position Properties
@property CGPoint currentPosition;
@property CGPoint destinationPosition;
@property int currentFloor;
@property int destinationFloor;
#pragma mark - Update
-(void) doMovement;
#pragma mark - Advanced Movement
-(void) doIdleMovement;
-(void) doVerticalMovement;
-(void) doFloorMovement;
-(void) pickRandomDestinationOnCurrentFloor;
#pragma mark - Simple Movement
-(void) moveUp;
-(void) moveDown;
-(void) moveLeft;
-(void) moveRight;
#pragma mark - Postion Calculations
-(int) currentFloorByPosition;
-(int) closestFloor:(NSMutableArray *)possibleFloors;
-(void) calculateFloorExitPositionByFloor;
-(void) calculateDestinationPositionByFloor;
-(BOOL) isAtVerticalDestinationPosition;
-(BOOL) isAtFloorDestinationPosition;
-(BOOL) shouldMoveUp;
-(BOOL) shouldMoveRight;
#pragma mark - Tower Change
-(void) transitionToNewTower;
@end
DTMovement.m:
#import "DTMovement.h"
#import "DTTowerFloor.h"
static int const kMinVerticalStep = 18;
static int const kMinHorizontalStep = 18;
@implementation DTMovement {
CGSize _worldSize;
}
-(id) initWithWorldSize:(CGSize)worldSize {
self = [super init];
if (self) {
_worldSize = worldSize;
}
return self;
}
#pragma mark - Update
-(void) doMovement {
//overridden by subclasses
}
#pragma mark - Vertical Movement
-(void) doVerticalMovement {
//the destination floor could change so this needs to update each tick
[self calculateDestinationPositionByFloor];
//once close enough to the destination, jump to the destination
if ([self isAtVerticalDestinationPosition]) {
[self arriveAtDestinationFloor];
} else {
if ([self shouldMoveUp]) {
[self moveUp];
} else {
[self moveDown];
}
}
}
-(BOOL) isAtVerticalDestinationPosition {
int distanceFromDestination = self.currentPosition.y - self.destinationPosition.y;
int minimumDistance = kMinVerticalStep;
return (!(distanceFromDestination > minimumDistance || distanceFromDestination < -minimumDistance));
}
-(BOOL) shouldMoveUp {
int distanceFromDestination = self.currentPosition.y - self.destinationPosition.y;
return distanceFromDestination < 0;
}
-(void) arriveAtDestinationFloor {
//overridden by subclasses
}
#pragma mark - Horizontal Movement
-(void) doFloorMovement {
//overridden by subclasses
}
-(BOOL) isAtFloorDestinationPosition {
int distanceFromDestination = self.currentPosition.x - self.destinationPosition.x;
int minimumDistance = kMinHorizontalStep;
return (!(distanceFromDestination > minimumDistance || distanceFromDestination < -minimumDistance));
}
-(BOOL) shouldMoveRight {
int distanceFromDestination = self.currentPosition.x - self.destinationPosition.x;
return distanceFromDestination < 0;
}
#pragma mark - Idle Movement
-(void) doIdleMovement {
//move randomly left and right, prevent moving past the walls of the floor
CGPoint tempPosition;
int moveLeftOrRight = arc4random_uniform(2);
if (moveLeftOrRight == 0) {
tempPosition = CGPointMake(self.currentPosition.x + kMinHorizontalStep, self.currentPosition.y);
} else if (moveLeftOrRight == 1) {
tempPosition = CGPointMake(self.currentPosition.x - kMinHorizontalStep, self.currentPosition.y);
}
//bounds checking
if (tempPosition.x < - _worldSize.width/2) {
tempPosition.x = -_worldSize.width/2 + kMinHorizontalStep;
}
if (tempPosition.x > _worldSize.width/2){
tempPosition.x = _worldSize.width/2 - kMinHorizontalStep;
}
self.currentPosition = tempPosition;
}
#pragma mark - Simple Movement
-(void) moveUp {
self.currentPosition = CGPointMake(0, self.currentPosition.y + _worldSize.height / kMinVerticalStep);
}
-(void) moveDown {
self.currentPosition = CGPointMake(0, self.currentPosition.y - _worldSize.height / kMinVerticalStep);
}
-(void) moveLeft {
self.currentPosition = CGPointMake(self.currentPosition.x - _worldSize.width / kMinHorizontalStep, self.currentPosition.y);
}
-(void) moveRight {
self.currentPosition = CGPointMake(self.currentPosition.x + _worldSize.width / kMinHorizontalStep, self.currentPosition.y);
}
#pragma mark - Position Calculations
-(int) closestFloor:(NSMutableArray *)possibleFloors {
int currentFloor = [self currentFloorByPosition];
int destinationFloor = 0;
int bestCount = 1000; //to guarantee it gets assigned on the first try
int currentCount = 0;
for (DTTowerFloor *floor in possibleFloors) {
//find out what floor has the lowest count of distance
if (currentFloor > floor.floorNumber) {
for (int i = currentFloor; i > floor.floorNumber; i--) {
currentCount++;
}
} else {
for (int i = currentFloor; i < floor.floorNumber; i++) {
currentCount++;
}
}
//set the destination to the floor with lowest count
if (currentCount < bestCount) {
destinationFloor = floor.floorNumber;
bestCount = currentCount;
}
currentCount = 0;
}
return destinationFloor;
}
-(int) currentFloorByPosition {
int floorSize = _worldSize.height/6;
return self.currentPosition.y / floorSize;
}
-(void) calculateDestinationPositionByFloor {
self.destinationPosition = CGPointMake(0, self.destinationFloor * _worldSize.height/6 - _worldSize.height/6/3);
}
-(void) calculateFloorExitPositionByFloor {
int currentFloor = [self currentFloorByPosition];
int floorY = currentFloor * _worldSize.height/6 - _worldSize.height/6/3;
self.destinationPosition = CGPointMake(0, floorY);
}
-(void) pickRandomDestinationOnCurrentFloor {
int randomNumber = arc4random_uniform(4) + 1;
int randomXPosition = (_worldSize.width/3)/randomNumber;
self.destinationPosition = CGPointMake(randomXPosition, self.currentPosition.y);
}
#pragma mark - Tower Change
-(void) transitionToNewTower {
//overridden by subclasses
}
#pragma mark - NSCoding methods
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
_currentPosition = [aDecoder decodeCGPointForKey:@"currentPosition"];
_destinationPosition = [aDecoder decodeCGPointForKey:@"destinationPosition"];
_currentFloor = [aDecoder decodeIntForKey:@"currentFloor"];
_destinationFloor = [aDecoder decodeIntForKey:@"destinationFloor"];
_worldSize = [aDecoder decodeCGSizeForKey:@"worldSize"];
}
return self;
}
-(void) encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeCGPoint:self.currentPosition forKey:@"currentPosition"];
[aCoder encodeCGPoint:self.destinationPosition forKey:@"destinationPosition"];
[aCoder encodeInt:self.currentFloor forKey:@"currentFloor"];
[aCoder encodeInt:self.destinationFloor forKey:@"destinationFloor"];
[aCoder encodeCGSize:_worldSize forKey:@"worldSize"];
}
@end
DTDwarfMovement.h:
#import <Foundation/Foundation.h>
#import "DTMovement.h"
#import "DTDwarfMovementState.h"
@interface DTDwarfMovement : DTMovement <NSCoding>
-(id) initWithWorldSizeForCharacter:(CGSize)worldSize;
@property DwarfMovementState movementState;
@property BOOL isTiredOrHungry;
-(void) acceptFloorList:(NSMutableArray *)floorList;
-(void) acceptFilteredFloorListForWork:(NSMutableArray *)filteredFloorList;
@end
This is most of the AI of the worker.
DTDwarfMovement.m:
#import "DTDwarfMovement.h"
#import "DTTowerFloor.h"
/*
Each turn if the dwarf is in a correct state, position is updated in the doMovement method
Lists of valid floors are loaded in before the destination is computed
There are different computations for special cases such as hunger, idle, playing
Movement is computed in the super class, and it is restricted to up down left and right
Typically when at a destination, state changes
Some states are advanced by the Tower or TowerFloor
*/
@implementation DTDwarfMovement {
NSMutableArray *_floorList;
NSMutableArray *_floorListForWork;
}
-(id) initWithWorldSizeForCharacter:(CGSize)worldSize {
self = [super initWithWorldSize:worldSize];
if (self) {
_floorList = [[NSMutableArray alloc]init];
_floorListForWork = [[NSMutableArray alloc]init];
}
return self;
}
#pragma mark - Floor List Handling
-(void) acceptFloorList:(NSMutableArray *)floorList {
_floorList = floorList;
}
-(void) acceptFilteredFloorListForWork:(NSMutableArray *)filteredFloorList {
_floorListForWork = filteredFloorList;
}
#pragma mark - Checking For Jobs
-(BOOL) checkFloorsForJobs {
for (DTTowerFloor *floor in _floorListForWork) {
if ([floor areJobSlotsAvailable]) {
return YES;
}
}
return NO;
}
#pragma mark - Update Movement
-(void) doMovement {
switch (self.movementState) {
//in the idle state, if not hungry or tired, try to choose a job, if no job found, idle
case DwarfMovementStateIdle:
if (!self.isTiredOrHungry) {
[self tryToChooseAJobWhileIdle];
}
break;
//once at the floor, if no jobs available, idle
case DwarfMovementStateAtDestinationFloor:
if (![self checkFloorsForJobs]) {
self.movementState = DwarfMovementStateIdle;
}
break;
//this is the state when a dwarf first finds a job
case DwarfMovementStateNeedsMoving:
[self calculateFloorExitPositionByFloor];
self.movementState = DwarfMovementStateToFloorExit;
break;
//basic movement between floors and to positions on floors
case DwarfMovementStateAtFloorExit:
self.movementState = DwarfMovementStateToFloor;
break;
case DwarfMovementStateToFloor:
[self doVerticalMovement];
break;
case DwarfMovementStateToJobPosition:
[self doFloorMovement];
break;
case DwarfMovementStateToFloorExit:
[self doFloorMovement];
break;
case DwarfMovementStateToRandomPositionForEating:
[self doFloorMovement];
break;
case DwarfMovementStateToRandomPositionForResting:
[self doFloorMovement];
break;
//just randomly moves around, mostly up and down as the destination floor constantly changes
case DwarfMovementStatePlaying:
[self handlePlayingMovement];
break;
//deal with specific movement states
case DwarfMovementStateToStockpile:
[self pickDestinationFloorForStockpile];
[self doMovementForMovementState:DwarfMovementStateToStockpile];
break;
case DwarfMovementStateToSlaughterhouse:
[self pickDestinationFloorForSlaughterhouse];
[self doMovementForMovementState:DwarfMovementStateToSlaughterhouse];
break;
case DwarfMovementStateToPasture:
[self pickDestinationFloorForPasture];
[self doMovementForMovementState:DwarfMovementStateToPasture];
break;
case DwarfMovementStateHunger:
[self pickDestinationFloorForEating];
[self doMovementForMovementState:DwarfMovementStateHunger];
break;
case DwarfMovementStateResting:
[self pickDestinationFloorForResting];
[self doMovementForMovementState:DwarfMovementStateResting];
break;
case DwarfMovementStateFleeingFromEnemy:
{
int currentFloor = [self currentFloorByPosition];
self.destinationFloor = currentFloor + 1; //makes the dwarf flee one floor up
[self doMovementForMovementState:DwarfMovementStateFleeingFromEnemy];
break;
}
//special case for setting up moving to a new tower
case DwarfMovementStateToNewTower:
[self handleMovingToNewTower];
break;
default:
break;
}
}
-(void) tryToChooseAJobWhileIdle {
[self pickDestinationFloorForWork];
[self calculateDestinationPositionByFloor];
if (self.currentFloor != self.destinationFloor) {
self.movementState = DwarfMovementStateNeedsMoving;
} else {
if ([self checkFloorsForJobs]) {
self.movementState = DwarfMovementStateNeedsMoving;
} else {
[self doIdleMovement];
}
}
}
-(void) handlePlayingMovement {
[self pickDestinationFloorForPlaying];
[self calculateDestinationPositionByFloor];
if (self.currentFloor != self.destinationFloor) {
[self doVerticalMovement];
} else {
[self doIdleMovement];
}
}
-(void) handleMovingToNewTower {
[self pickDestinationFloorForGateway];
[self calculateDestinationPositionByFloor];
if (![self isAtFloorDestinationPosition]) {
[self doFloorMovement];
} else if (![self isAtVerticalDestinationPosition]) {
[self doVerticalMovement];
} else {
self.movementState = DwarfMovementStateReadyToMoveToNewTower;
}
}
-(void) doMovementForMovementState:(DwarfMovementState)movementState {
//the destination floor has already been chosen
[self calculateDestinationPositionByFloor];
if ([self doBasicMovementToFloor]) {
switch (movementState) {
case DwarfMovementStateToStockpile:
self.movementState = DwarfMovementStateAtStockpile;
break;
case DwarfMovementStateToSlaughterhouse:
self.movementState = DwarfMovementStateAtSlaughterhouse;
break;
case DwarfMovementStateToPasture:
self.movementState = DwarfMovementStateAtPasture;
break;
case DwarfMovementStateHunger:
self.movementState = DwarfMovementStateToRandomPositionForEating;
[self pickRandomDestinationOnCurrentFloor];
break;
case DwarfMovementStateResting:
self.movementState = DwarfMovementStateToRandomPositionForResting;
[self pickRandomDestinationOnCurrentFloor];
break;
case DwarfMovementStateFleeingFromEnemy:
//do nothing yet
break;
default:
break;
}
}
}
-(BOOL) doBasicMovementToFloor {
if (![self isAtFloorDestinationPosition]) {
[self doFloorMovement];
} else if (![self isAtVerticalDestinationPosition]) {
[self doVerticalMovement];
} else {
[self arriveAtDestinationFloor];
return YES;
}
return NO;
}
#pragma mark - Overrides
-(void) arriveAtDestinationFloor {
//these have to be set this way to make sure movement remains aligned
self.currentPosition = self.destinationPosition;
self.currentFloor = self.destinationFloor;
self.movementState = DwarfMovementStateAtDestinationFloor;
}
-(void) doFloorMovement {
//once close enough to the destination, jump to the destination
//all of these special cases specifically route the dwarf based on their movement state
//once the state switches the appropriate logic will run on the next tick
if ([self isAtFloorDestinationPosition]) {
if (self.movementState == DwarfMovementStateToFloorExit) {
self.currentPosition = self.destinationPosition;
self.movementState = DwarfMovementStateAtFloorExit;
}
if (self.movementState == DwarfMovementStateToJobPosition) {
self.currentPosition = self.destinationPosition;
self.movementState = DwarfMovementStateInPositionForWork;
}
if (self.movementState == DwarfMovementStateToRandomPositionForEating) {
self.currentPosition = self.destinationPosition;
self.movementState = DwarfMovementStateInPositionForEating;
}
if (self.movementState == DwarfMovementStateToRandomPositionForResting) {
self.currentPosition = self.destinationPosition;
self.movementState = DwarfMovementStateInPositionForResting;
}
} else {
if ([self shouldMoveRight]) {
[self moveRight];
} else {
[self moveLeft];
}
}
}
#pragma mark - Pick Destination Floors
-(void) pickDestinationFloorForWork {
NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
for (DTTowerFloor *floor in _floorListForWork) {
if ([floor areJobSlotsAvailable]) {
[possibleFloorsArray addObject:floor];
}
}
if (possibleFloorsArray.count > 0) {
self.destinationFloor = [self closestFloor:possibleFloorsArray];
}
}
-(void) pickDestinationFloorForEating {
NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
for (DTTowerFloor *floor in _floorList) {
if (floor.room.roomType == Foodhall) {
[possibleFloorsArray addObject:floor];
}
if (floor.room.roomType == GatewayExit) {
[possibleFloorsArray addObject:floor];
}
}
if (possibleFloorsArray.count > 0) {
self.destinationFloor = [self closestFloor:possibleFloorsArray];
} else {
self.destinationFloor = 0;
}
}
-(void) pickDestinationFloorForResting {
NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
for (DTTowerFloor *floor in _floorList ) {
if (floor.room.roomType == Dormitory) {
[possibleFloorsArray addObject:floor];
}
if (floor.room.roomType == GatewayExit) {
[possibleFloorsArray addObject:floor];
}
}
if (possibleFloorsArray.count > 0) {
self.destinationFloor = [self closestFloor:possibleFloorsArray];
} else {
self.destinationFloor = 0;
}
}
-(void) pickDestinationFloorForGateway {
NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
for (DTTowerFloor *floor in _floorList) {
if (floor.room.roomType == Gateway) {
[possibleFloorsArray addObject:floor];
}
}
if (possibleFloorsArray.count > 0) {
self.destinationFloor = [self closestFloor:possibleFloorsArray];
} else {
self.destinationFloor = 0;
}
}
-(void) pickDestinationFloorForStockpile {
NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
for (DTTowerFloor *floor in _floorList) {
BOOL foundAStockpile = NO;
if (floor.room.roomType == RoomTypeStockpile) {
foundAStockpile = YES;
[possibleFloorsArray addObject:floor];
}
if (!foundAStockpile) {
if (floor.room.roomType == GatewayExit) {
[possibleFloorsArray addObject:floor];
}
}
}
if (possibleFloorsArray.count > 0) {
self.destinationFloor = [self closestFloor:possibleFloorsArray];
} else {
self.destinationFloor = 0;
}
}
-(void) pickDestinationFloorForSlaughterhouse {
NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
for (DTTowerFloor *floor in _floorList) {
BOOL foundASlaughterhouse = NO;
if (floor.room.roomType == Slaughterhouse) {
foundASlaughterhouse = YES;
[possibleFloorsArray addObject:floor];
}
if (!foundASlaughterhouse) {
if (floor.room.roomType == GatewayExit) {
[possibleFloorsArray addObject:floor];
}
}
}
if (possibleFloorsArray.count > 0) {
self.destinationFloor = [self closestFloor:possibleFloorsArray];
} else {
self.destinationFloor = 0;
}
}
-(void) pickDestinationFloorForPasture {
NSMutableArray *possibleFloorsArray = [[NSMutableArray alloc]init];
for (DTTowerFloor *floor in _floorList) {
BOOL foundAPasture = NO;
if (floor.room.roomType == RoomTypePasture) {
foundAPasture = YES;
[possibleFloorsArray addObject:floor];
}
if (!foundAPasture) {
if (floor.room.roomType == GatewayExit) {
[possibleFloorsArray addObject:floor];
}
}
}
if (possibleFloorsArray.count > 0) {
self.destinationFloor = [self closestFloor:possibleFloorsArray];
} else {
self.destinationFloor = 0;
}
}
-(void) pickDestinationFloorForPlaying {
if (_floorList.count > 0) {
int randomNumber = arc4random_uniform((int)_floorList.count);
DTTowerFloor *floor = [_floorList objectAtIndex:randomNumber];
self.destinationFloor = floor.floorNumber;
} else {
BOOL gatewayExitPresent = NO;
for (DTTowerFloor *floor in _floorList) {
if (floor.room.roomType == GatewayExit) {
gatewayExitPresent = YES;
self.destinationFloor = floor.floorNumber;
}
}
if (!gatewayExitPresent) {
self.destinationFloor = 0;
}
}
}
#pragma mark - Tower Transition
-(void) transitionToNewTower {
[self calculateDestinationPositionByFloor];
[self arriveAtDestinationFloor];
}
#pragma mark - NSCoding methods
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
_movementState = [aDecoder decodeIntegerForKey:@"dwarfMovementState"];
_isTiredOrHungry = [aDecoder decodeBoolForKey:@"isTiredOrHungry"];
_floorList = [[NSMutableArray alloc]init];
_floorListForWork = [[NSMutableArray alloc]init];
}
return self;
}
-(void) encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
[aCoder encodeInteger:self.movementState forKey:@"dwarfMovementState"];
[aCoder encodeBool:self.isTiredOrHungry forKey:@"isTiredOrHungry"];
}
@end