3
\$\begingroup\$

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.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

If you feel that the massive list of NSString * const declarations at the top of the file makes the file feel cluttered, you can always put them in their own file(s). You could even do one file for each category of constants.

My projects will frequently have a handful of files that are nothing more than NSString * const declarations that I use for keys into NSUserDefaults and other dictionaries I might make use of. I even also define every storyboard segue identifier as an NSString * const. There's an entire SegueNames.h file in almost every one of my iOS projects. There's usually an UserDefaultKeys.h file in my projects as well.

There's no reason why you can't have a setup like this:

DTGameBalanceKeys.h

#import "DTGameBalanceGameSettingsKeys.h"
#import "DTGameBalanceTowerSettingsKeys.h"
#import "DTGameBalanceJobCostsKeys.h"

And however many other constant files are needed. Define all your keys in those three files. Now DTGameBalance.h is nothing more than importing those three files, and then your DTGameBalance.h just imports DTGameBalanceKeys.h.


What I would definitely recommend is providing a way for this class to fetch a file provided by you online so the app checks for this file say once a day or so. This way, if you need to update the game balance, you can do so without going through the app store. If you need to change executable code, you of course need to go through the app store, but tweaking a few numbers in game balance shouldn't require going through the app store, and providing your app the ability to look online for these tweaks means your end users can get the balance changes a lot faster than having to wait for an average 5-8 days for Apple to review your update.

When you do make these server-side tweaks, I still suggest sending an update through the app store with these changes in the property list so that eventually new downloaders of your app will have the correct settings in the property list on first download without checking online for tweaks.

\$\endgroup\$
2
  • \$\begingroup\$ Both great ideas. I will have to figure out how to pull down a file from a server from inside my game! \$\endgroup\$
    – bazola
    Aug 14, 2014 at 22:30
  • \$\begingroup\$ Fetching from a server should be easy. It's just a matter of getting a server set up to fetch to and then some simple ObjC code, especially since you're just fetching a static file. \$\endgroup\$
    – nhgrif
    Aug 14, 2014 at 22:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.