I posted a question on Stack Overflow about how to implement Decision Tables in Objective-C after reading about them for the first time in chapter 18 of Code Complete. Nobody provided an answer, so after puzzling it out for a while longer, I created the following code for the movement AI of the enemies in my strategy game.
The basic way it works is that there are a number of string constants, one for each value in the enum EnemyMovementState
. The dictionary is then filled with these constants as keys, and the string names of methods in the class as objects. Then I run a switch statement that returns the appropriate string constant for the current state, and pull the object out of the dictionary with that key. This is converted into a selector, and the method is called. This replaced the switch statement that previously had a case for each value in the enum.
I truly do not know if there is any advantage to doing things this way. I think that part of the point of decision tables is to be able to input multiple values and determine an outcome based on the combinations of them. This current implementation only handles one input, the state. I think this could be extended to an intermediate method that could take the current state combined with some other values to retrieve other appropriate methods from the dictionary.
The problems I see are that I need all those string constants, and I still need to run a switch over the states of the enum to return the appropriate string to access the dictionary. I would love it if I could directly convert the name of a value of an enum into a string, but I don't know of a way to do this. So even though this functions as a decision table, it still needs a switch
statement to work, and needs more supporting code to make the whole thing work. If I used the value of the enum directly, I could avoid the switch statement, but then if I moved the order around inside the enum the code would break, and on the whole this seems less extensible and less understandable. I would love to get feedback on this, including feedback that the whole thing is pointless and that switch statements are better.
#import "DTEnemyMovement.h"
#import "DTTowerFloor.h"
/*
Each tick something will happen based on the state, or the state will be processed elsewhere
Towers and Floors both may contain enemies, though they will always be on a floor unless moving between floors
To update the movement, a method converts the current state to a string, and gets the method name to call from a dictionary
This works like a decision table and removes the need for a switch statement
Movement can only be up or down, left or right, no combination of them
*/
//Two options for the decision table, either access the dictionary directly with 0-x, the enum values, or make strings for their names
//the advantage of strings is that it is more extensible, and the position in the enum doesnt matter
NSString* const kEnemyMovementStateJustSpawned = @"enemyMovementStateJustSpawned";
NSString* const kEnemyMovementStateIdle = @"enemyMovementStateIdle";
NSString* const kEnemyMovementStateNeedsMoving = @"enemyMovementStateNeedsMoving";
NSString* const kEnemyMovementStateToFloor = @"enemyMovementStateToFloor";
NSString *const kEnemyMovementStateAtDestinationFloor = @"enemyMovementStateAtDestinationFloor";
NSString* const kEnemyMovementStateToFloorExit = @"enemyMovementStateToFloorExit";
//NSString* const kEnemyMovementStateAtFloorExit = @"enemyMovementStateAtFloorExit"; //tower handles
NSString* const kEnemyMovementStateToAttackWalls = @"enemyMovementStateToAttackWalls";
//NSString* const kEnemyMovementStateAtAttackWalls = @"enemyMovementStateAtAttackWalls"; //floor handles
NSString* const kEnemyMovementStateToAttackFloor = @"enemyMovementStateToAttackFloor";
//NSString* const kEnemyMovementStateAtAttackFloor = @"enemyMovementStateAtAttackFloor"; //floor handles
NSString* const kEnemyMovementStateToAttackRoom = @"enemyMovementStateToAttackRoom";
//NSString* const kEnemyMovementStateAtAttackRoom = @"enemyMovementStateAtAttackRoom"; //floor handles
//NSString* const kEnemyMovementStateStunned = @"enemyMovementStateStunned"; //not yet used
@implementation DTEnemyMovement {
NSMutableArray *_floorList;
int _startingFloor;
NSDictionary *_decisionTable;
}
-(id) initWithWorldSizeForCharacter:(CGSize)worldSize andStartingFloor:(int)startingFloor {
self = [super initWithWorldSize:worldSize];
if (self) {
_floorList = [[NSMutableArray alloc]init];
_startingFloor = startingFloor;
_decisionTable = [[NSDictionary alloc]init];
[self setupDecisionTable];
}
return self;
}
-(void) setupDecisionTable {
//the string objects are the names of methods in the class
_decisionTable = @{kEnemyMovementStateJustSpawned: @"doEnemyJustSpawned",
kEnemyMovementStateIdle: @"doEnemyIdle",
kEnemyMovementStateNeedsMoving: @"doEnemyNeedsMoving",
kEnemyMovementStateToFloorExit: @"doFloorMovement",
kEnemyMovementStateToFloor: @"doVerticalMovement",
kEnemyMovementStateAtDestinationFloor: @"doEnemyAtDestinationFloor",
kEnemyMovementStateToAttackWalls: @"doFloorMovement",
kEnemyMovementStateToAttackFloor: @"doFloorMovement",
kEnemyMovementStateToAttackRoom: @"doFloorMovement"
};
}
-(NSString *) stringForState:(EnemyMovementState)state {
switch (state) {
case EnemyMovementStateJustSpawned:
return kEnemyMovementStateJustSpawned;
case EnemyMovementStateIdle:
return kEnemyMovementStateIdle;
case EnemyMovementStateNeedsMoving:
return kEnemyMovementStateNeedsMoving;
case EnemyMovementStateToFloor:
return kEnemyMovementStateToFloor;
case EnemyMovementStateAtDestinationFloor:
return kEnemyMovementStateAtDestinationFloor;
case EnemyMovementStateToFloorExit:
return kEnemyMovementStateToFloorExit;
case EnemyMovementStateAtFloorExit:
return nil;
case EnemyMovementStateToAttackWalls:
return kEnemyMovementStateToAttackWalls;
case EnemyMovementStateAtAttackWalls:
return nil;
case EnemyMovementStateToAttackFloor:
return kEnemyMovementStateToAttackFloor;
case EnemyMovementStateAtAttackFloor:
return nil;
case EnemyMovementStateToAttackRoom:
return kEnemyMovementStateToAttackRoom;
case EnemyMovementStateAtAttackRoom:
return nil;
default:
break;
}
return nil;
}
#pragma mark - Update Loop
-(void) doMovement {
//the selector is formed from a string inside the decision table dictionary
SEL methodToCallName = NSSelectorFromString([_decisionTable objectForKey:[self stringForState:self.state]]);
if (methodToCallName) {
IMP functionPointer = [self methodForSelector:methodToCallName];
void (*methodToCall)(id, SEL) = (void *)functionPointer;
methodToCall(self, methodToCallName);
}
}
-(void) doEnemyJustSpawned {
if ([self checkFloorsForJobs]) {
self.state = EnemyMovementStateNeedsMoving;
} else {
self.state = EnemyMovementStateIdle;
}
}
-(void) doEnemyIdle {
if ([self checkFloorsForJobs]) {
self.state = EnemyMovementStateNeedsMoving;
} else {
[self doIdleMovement];
}
}
-(void) doEnemyNeedsMoving {
[self calculateFloorExitPositionByFloor];
self.state = EnemyMovementStateToFloorExit;
}
-(void) doEnemyAtDestinationFloor {
BOOL foundADestination = NO;
for (DTTowerFloor *floor in _floorList) {
if (floor.floorNumber == self.currentFloor) {
//attack room first, then walls, then bottom
if (floor.floorBuildState & FloorHasRoom) {
[self moveToAttackRoom];
foundADestination = YES;
} else if (floor.floorBuildState & FloorHasWalls) {
[self moveToAttackWalls];
foundADestination = YES;
} else if (floor.floorBuildState & FloorHasBottom) {
[self moveToAttackBottom];
foundADestination = YES;
}
}
}
if (!foundADestination) {
self.state = EnemyMovementStateIdle;
}
}
-(void) moveToAttackBottom {
[self pickRandomDestinationOnCurrentFloor];
self.state = EnemyMovementStateToAttackFloor;
}
-(void) moveToAttackRoom {
[self pickRandomDestinationOnCurrentFloor];
self.state = EnemyMovementStateToAttackRoom;
}
-(void) moveToAttackWalls {
//decide which wall to move to, the nearest one
int positiveOrNegative = 0;
if (self.currentPosition.x >= 0) {
positiveOrNegative = 1;
} else {
positiveOrNegative = -1;
}
//move to the left or right wall
self.destinationPosition = CGPointMake(([self worldSize].width / 2.1) * positiveOrNegative, self.currentPosition.y);
self.state = EnemyMovementStateToAttackWalls;
}
-(BOOL) checkFloorsForJobs {
NSMutableSet *floorsInRange = [[NSMutableSet alloc]init];
for (DTTowerFloor *floor in _floorList) {
if (floor.floorNumber == _startingFloor || floor.floorNumber == _startingFloor - 1 || floor.floorNumber == _startingFloor + 1) {
if (floor.floorBuildState & FloorHasWalls) {
[floorsInRange addObject:floor];
} else if (floor.floorBuildState & FloorHasBottom) {
[floorsInRange addObject:floor];
} else if (floor.floorBuildState & FloorHasRoomUpgrade) {
[floorsInRange addObject:floor];
}
}
}
if (floorsInRange.count > 0) {
NSMutableArray *tempArray = [[NSMutableArray alloc]init];
for (DTTowerFloor *floor in floorsInRange) {
[tempArray addObject:floor];
}
self.destinationFloor = [self closestFloor:tempArray];
return YES;
}
return NO;
}
#pragma mark - Overrides
-(void) arriveAtDestinationFloor {
self.currentPosition = self.destinationPosition;
self.currentFloor = self.destinationFloor;
self.state = EnemyMovementStateAtDestinationFloor;
//not sure if this conditional is needed but it is good for debugging
if (self.state == EnemyMovementStateToFloor) {
self.state = EnemyMovementStateAtDestinationFloor;
}
}
-(void) doFloorMovement {
//once close enough to the destination, jump to the destination
if ([self isAtFloorDestinationPosition]) {
self.currentPosition = self.destinationPosition;
if (self.state == EnemyMovementStateToFloorExit) {
self.state = EnemyMovementStateAtFloorExit;
} else if (self.state == EnemyMovementStateToAttackWalls) {
self.state = EnemyMovementStateAtAttackWalls;
} else if (self.state == EnemyMovementStateToAttackFloor) {
self.state = EnemyMovementStateAtAttackFloor;
} else if (self.state == EnemyMovementStateToAttackRoom) {
self.state = EnemyMovementStateAtAttackRoom;
}
} else {
if ([self shouldMoveRight]) {
[self moveRight];
} else {
[self moveLeft];
}
}
}
#pragma mark - Floor List
-(void) acceptFloorList:(NSMutableArray *)floorList {
_floorList = floorList;
}
#pragma mark - NSCoding methods
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
_state = [aDecoder decodeIntegerForKey:@"enemyMovementState"];
_floorList = [aDecoder decodeObjectForKey:@"floorList"];
_startingFloor = [aDecoder decodeIntForKey:@"startingFloor"];
_decisionTable = [aDecoder decodeObjectForKey:@"decisionTable"];
}
return self;
}
-(void) encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
[aCoder encodeInteger:self.state forKey:@"enemyMovementState"];
[aCoder encodeObject:_floorList forKey:@"floorList"];
[aCoder encodeInt:_startingFloor forKey:@"startingFloor"];
[aCoder encodeObject:_decisionTable forKey:@"decisionTable"];
}
@end