I have been thinking about this aspect of my game for a little while, and I have come up with the following solution. I am sure that there are problems with the way I have implemented it so any kind of feedback would be very helpful.
The idea is that there are a number of different variables in the game that together have an effect on the balance of the difficulty as well as the potential "fun" of playing the game. These variables will need to be tweaked repeatedly on the way to finalizing the game balance. Unfortunately, they are scattered around in different classes based on the scope of their effect, so some of them have to be in the Game class, some of them have to be in the Tower class, and some might even be in the Dwarf class or any other class that might be created. I don't see a way around this in an OOP context.
At first I was simply declaring constants at the top of the class, so at least they were all in one place inside the file. But trying to remember where I have the value of food and where I have the value of the cost of building a farm can be troubling. I would rather look at a list of all the variables that will potentially change for balance reasons and see/change their values all together in one place.
To achieve that I have built a GameBalance class. It provides access to the variables stored in a Settings dictionary that contains other dictionaries for the GameSettings, TowerSettings, RoomCostSettings, and any other settings that might be needed. It loads all of those values from a property list (very much like XML if you don't know Objective-C) and then the objects that need various settings make calls to the GameBalance object asking for whatever setting value they may need. That way, I can make any variable changes directly inside the property list and they will be automatically loaded into the game.
Because of the number of properties total, I have omitted some of them for this post.
DTGameBalance.h
#import <Foundation/Foundation.h>
#import "DTJobType.h"
@interface DTGameBalance : NSObject <NSCoding>
#pragma mark - Game Settings
@property (readonly) int startingFood;
@property (readonly) int startingCommonResources;
@property (readonly) int foodStockpileSize;
@property (readonly) int percentChanceDrawCard;
#pragma mark - Tower Settings
-(int) foodValue:(BOOL)aboveGround;
@property (readonly) int enemySpawnChance;
@property (readonly) float stockpileBonus;
@property (readonly) float outpostPenalty;
@property (readonly) int foodValueAnimal;
#pragma mark - Job Costs
-(int) costForJob:(JobType)jobType;
@end
DTGameBalance.m
#import "DTGameBalance.h"
//Game Settings Names//
NSString* const kStartingCommonResources = @"startingCommonResources";
NSString* const kStartingFood = @"startingFood";
NSString* const kFoodStockpileSize = @"foodStockpileSize";
NSString* const kPercentChanceDrawCard = @"percentChanceDrawCard";
//Tower Settings Names//
NSString* const kFoodValue = @"foodValue";
NSString* const kFoodValueAboveGround = @"foodValueAboveGround";
NSString* const kEnemySpawnChance = @"enemySpawnChance";
NSString* const kStockpileBonus = @"stockpileBonus";
NSString* const kOutpostPenalty = @"outpostPenalty";
NSString* const kFoodValueAnimal = @"foodValueAnimal";
//Job Cost Settings Names//
NSString* const kLadderCost = @"ladderCost";
NSString* const kBottomCost = @"floorCost";
NSString* const kWallCost = @"wallCost";
NSString* const kRoomCost = @"roomCost";
NSString* const kSuperiorWallCost = @"superiorWallCost";
@implementation DTGameBalance {
NSDictionary *_listOfSettings;
NSDictionary *_gameSettings;
NSDictionary *_towerSettings;
NSDictionary *_jobCostSettings;
}
#pragma mark - Initialization
-(id) init {
self = [super init];
if (self) {
_listOfSettings = settingsList();
_gameSettings = [_listOfSettings objectForKey:@"gameSettings"];
_towerSettings = [_listOfSettings objectForKey:@"towerSettings"];
_jobCostSettings = [_listOfSettings objectForKey:@"jobCostSettings"];
}
return self;
}
NSDictionary* settingsList() {
NSString *plistCatPath = [[NSBundle mainBundle] pathForResource:@"DTGameBalance" ofType:@"plist"];
NSDictionary *settingsList = [[NSDictionary alloc] initWithContentsOfFile:plistCatPath];
return settingsList[@"Settings"];
}
#pragma mark - Game Settings
-(int) startingCommonResources {
return [[_gameSettings objectForKey:kStartingCommonResources]intValue];
}
-(int) startingFood {
return [[_gameSettings objectForKey:kStartingFood]intValue];
}
-(int) foodStockpileSize {
return [[_gameSettings objectForKey:kFoodStockpileSize]intValue];
}
-(int) percentChanceDrawCard {
return [[_gameSettings objectForKey:kPercentChanceDrawCard]intValue];
}
#pragma mark - Tower Settings
-(int) foodValue:(BOOL)aboveGround {
if (aboveGround) {
return [[_towerSettings objectForKey:kFoodValueAboveGround]intValue];
} else {
return [[_towerSettings objectForKey:kFoodValue]intValue];
}
}
-(int) enemySpawnChance {
return [[_towerSettings objectForKey:kEnemySpawnChance]intValue];
}
-(float) stockpileBonus {
return [[_towerSettings objectForKey:kStockpileBonus]floatValue];
}
-(float) outpostPenalty {
return [[_towerSettings objectForKey:kOutpostPenalty]floatValue];
}
-(int) foodValueAnimal {
return [[_towerSettings objectForKey:kFoodValueAnimal]intValue];
}
#pragma mark - Job Costs
-(int) costForJob:(JobType)jobType {
switch (jobType) {
case LadderJob:
return [[_jobCostSettings objectForKey:kLadderCost]intValue];
case BottomBuildJob:
return [[_jobCostSettings objectForKey:kBottomCost]intValue];
case WallBuildJob:
return [[_jobCostSettings objectForKey:kWallCost]intValue];
case RoomBuildJob:
return [[_jobCostSettings objectForKey:kRoomCost]intValue];
case SuperiorWallBuildJob:
return [[_jobCostSettings objectForKey:kSuperiorWallCost]intValue];
default:
return 0;
}
}
#pragma mark - NSCoding Methods
-(id) initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
_gameSettings = [aDecoder decodeObjectForKey:@"gameSettings"];
_towerSettings = [aDecoder decodeObjectForKey:@"towerSettings"];
_jobCostSettings = [aDecoder decodeObjectForKey:@"jobCostSettings"];
_roomCostSettings = [aDecoder decodeObjectForKey:@"roomCostSettings"];
}
return self;
}
-(void) encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_gameSettings forKey:@"gameSettings"];
[aCoder encodeObject:_towerSettings forKey:@"towerSettings"];
[aCoder encodeObject:_jobCostSettings forKey:@"jobCostSettings"];
[aCoder encodeObject:_roomCostSettings forKey:@"roomCostSettings"];
}
@end
Now I have an instance variable of a GameBalance object in the Game and Tower classes like this:
_balanceSettings = [[DTGameBalance alloc]init];
And I use it like this:
_currentCommonResources = _balanceSettings.startingCommonResources;
_currentFoodResources = _balanceSettings.startingFood;
Here is the guts of the Property List file for reference:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Settings</key>
<dict>
<key>gameSettings</key>
<dict>
<key>startingCommonResources</key>
<integer>1500</integer>
<key>startingFood</key>
<integer>300</integer>
<key>foodStockpileSize</key>
<integer>400</integer>
<key>percentChanceDrawCard</key>
<integer>5</integer>
</dict>
<key>towerSettings</key>
<dict>
<key>foodValue</key>
<integer>6</integer>
<key>foodValueAboveGround</key>
<integer>10</integer>
<key>enemySpawnChance</key>
<integer>990</integer>
<key>stockpileBonus</key>
<integer>2</integer>
<key>outpostPenalty</key>
<real>0.66</real>
<key>foodValueAnimal</key>
<integer>50</integer>
</dict>
<key>jobCostSettings</key>
<dict>
<key>ladderCost</key>
<integer>20</integer>
<key>floorCost</key>
<integer>40</integer>
<key>wallCost</key>
<integer>60</integer>
<key>roomCost</key>
<integer>100</integer>
<key>superiorWallCost</key>
<integer>400</integer>
</dict>
</dict>
</dict>
</plist>
Is this an acceptable approach? Is there a way to get rid of the large number of NSString constants at the top of the file? I need them because everything is set up using dictionaries and I think this makes the code more self documenting, but there might be a better way to go about this.