From Schmid.wiki
Jump to: navigation, search

Game Design: Configuration Approaches and the Object Oriented Game World

This article addresses two related issues in the field of game design: a) game configuration, i.e. tweaking gameplay by changing constants defining the rules of the game, and b) defining functionality of game world entities.

However, this article is not directed towards game designers, but discusses how the programmer should facilitate and implement changes issued by game designers.

Game Configuration Approaches

When the core implementation of a game is completed, the game designer has the important task of tweaking the rules of the gameplay to create a satisfying playing experience. This task is usually accomplished iteratively:

  1. testing the gameplay, noting anything that isn't optimal,
  2. tweaking rule constants in the game, changing suboptimal values,
  3. and returning to 1. to test the gameplay again.

To streamline this process, the programmer should ensure that step 2 is as painless as possible. Basically, there are three ways to allow the game designer to change the rules of the game, henceforth named the game configuration:

  1. Configuration through integrated editing.
  2. Configuration using external data files.
  3. Configuration through code.

Configuration Through Integrated Editing

If sufficient resources are available to create a dedicated editor for the game configuration, the first option is always the best choice for the game designer. The configuration flow is faster, and the work is more intuitive.

If the game development system has a graphical IDE like Unity, allowing the game designer to edit the configuration may be as simple as making values public. A Configuration singleton class with public attributes could be available. In Unity, the attributes of prefabs should be public (in Unity, a prefab is a game world entity template from which entities are created). Thus, the configuration values are conveniently grouped by entity types. However, sometimes the graphical approach is impractical and a graphical IDE is not available.

Configuration Using External Data Files

During my employment with the now defunct Pollux Gamelabs, I spent some time refactoring game configuration components. The game Lost Empire: Immortals had a lot of configuration, and it was decided to store the configuration as XML files. For each configurable class, XML reader and writer methods were manually written. These were trivial to write, but of course, each time a new value was added to the configuration, the reader and writer methods had to be changed.

This wasn't optimal. When refactoring such classes, I strove to make them XmlSerializable, i.e. automatically serializable by the .NET framework XML functionality. Generic Dictionaries were not automatically serializable at the time, so I wrote a generic dictionary wrapper class, that extended their functionality with serialization. However, even though using the wrapper was very easy, fixing bugs in the serialization code was a nightmare. The errors reported by Visual Studio were not helpful at all, and I spent a lot of time just staring at the code, trying to find a particularly subtle bug.

Another issue was synchronizing the configuration files with the code. We were often confused by values in the configuration file that didn't have a corresponding field in the configurable class, either because that particular functionality had been removed, or because the value had been renamed in the class but not in the configuration file. Failure to add a new field to the configuration file resulted in the value taking a default value like 0 or the empty string "", which in certain cases wasn't immediately detectable when playing the game.

To ensure synchronization, a possibility is always saving the configuration after loading it when running the game. If a version control system is in use, the saved configuration should then be committed together with the code. However, wrongly specified configuration values will be lost in the process. Renaming configuration constants also result in losing the original value. Of course, a version control system would make it possible to find such lost values.

Using XML for serializaing game configuration ought to be easy, and extending the configuration should be relatively painless. But is XML really the best choice for editing by human game designers? XML is designed to be "relatively human-legible", which in my book isn't good enough, when there are alternatives.

YAML (Yet Another Markup Language) is designed to be human-legible, and by comparison to XML, this is clear:

XML example:

<creature
 name="zombie"
 type="humanoid"
 hitpoints="100">
  <attacks>
      <attack name="punch" />
      <attack name="grapple" />
  </attacks
  <target_selection>
      <target_weight type="humanoid" weight="0.8" />
      <target_weight type="canine"   weight="0.2" />
  </target_selection>
</creature>

YAML example:

name: zombie
type: humanoid
hitpoints: 100
attacks:
 - punch
 - grapple
target_selection:
 - type:   humanoid
   weight: 0.8
 - type:   canine
   weight: 0.2

So YAML should always be preferable to XML when the data should be edited by a human. However, YAML-support is not pervasive through programming languages, and often it must be added through an external library.

To sum up:

  • a graphical editor is always preferable, but may be too expensive to produce,
  • and external configuration files may be a hassle to update and synchronize with the code, and subtle configuration errors may occur when proper synchronization fails.

Configuration Through Code

It would be nice if there were an easy way to configure a game without redundancy, where erroneous values would be detected, and where the configuration still would be human-legible.

I will investigate whether it is beneficial to configure a game through code. Several benefits are immediately recognized:

  • Redundancy is avoided.
  • The configuration is automatically checked for errors in syntax or semantics. Thus, configuration errors are caught immediately.
  • When edited in an IDE, auto-completion may facilitate remembering enumerated values and the like.
  • Easy and fast to implement.

Of course, all programmers will clap their little hands in excitement, and all game designers will say that I'm crazy in expecting game designers to edit code. Now, assuming that I am not crazy, the main problem to address is making the configuration legible by non-programmers. I will try to outline some methods for achieving this goal in the following:

If we are making a game in Ruby, configuration through code is pretty easy to implement:

module Zombie_configuration
    NAME             = "zombie"
    TYPE             = :humanoid
    HITPOINTS        = 100
    ATTACKS          = [ :punch, :grapple ]
    TARGET_SELECTION = { :humanoid => 0.8, :canine => 0.2 }
end

This is almost as legible as the YAML example, and even more legible than the XML example. Very nice.

However, most games are not implemented in Ruby, and other languages are not as easy to handle. I think, that if I have solved a language technical problem in C++, I have sufficient understanding to implement a solution in most other languages. So, if configuration through code is doable in C++, it should certainly be doable in less syntax-heavy languages such as C#.

Through experimentation, I have discovered that the bit that causes the most problems with configuration through code in C++ is variable-length collections of values. In the example, the 'attacks' list is the culprit. Let's look at a few example approaches.

The aggregate construction syntax is very concise, enabling a configuration looking like this:

Zombie_configuration config = {
    "zombie",                                // name
    humanoid,                                // type
    100,                                     // starting_hitpoints
    {punch, grapple},                        // attacks
    { { humanoid, 0.8f }, { canine, 0.2f } } // target_selection
};

However, this requires specifying the maximum array size in advance, which isn't optimal; also it requires replacing the std::map's with arrays of structs, or alternatively arrays of std::pairs, which is less useful than a std::map :

struct Zombie_configuration {
    // ...
    const Attack_type attacks[MAX_ATTACK_TYPES];

    // one of these:
    // const Target_weight target_selection[MAX_TARGET_TYPES];
    // const pair<Entity_type> target_selection[MAX_TARGET_TYPES];
};

This approach might also be quite error-prone, as reordering to variables with the same type would result in the values being switched.

A better way of doing it could be the member initialization list for the default constructor, like this:

#include "zombie.h"

Zombie::Zombie() :
    name      ("zombie"),
    type      (humanoid),
    hitpoints (100),
{
    attacks.push_back(punch);
    attacks.push_back(grapple);
    target_selection[humanoid] = 0.8f;
    target_selection[canine]   = 0.2f;
}

However, the vector and map initializations are not very legible. Using Boost.Assign could make it a bit more legible (even though the overloading of operator() still creeps me out):

#include "zombie.h"

Zombie::Zombie() :
    name      ("zombie"),
    type      (humanoid),
    hitpoints (100),
{
    attacks += punch, grapple;
    insert(target_selection)
        (humanoid, 0.8f)
        (canine,   0.2f);
}

An alternative uses static constant member variables:

zombie_configuration.cpp:

#include "zombie.h"

const char *        Zombie::name               = "zombie";
const Entity_type   Zombie::type               = humanoid;
const int           Zombie::starting_hitpoints = 100;
const Attack_type   Zombie::attacks[]          = { punch, grapple };
const Target_weight Zombie::target_selection[] =
    { { humanoid, 0.8f }, { canine, 0.2f } };

This one is pretty wordy but at least the potential confusion stays on the left side of the assignments. Also, we are still forced to replace vectors and maps with an array and an array of structs (or pairs), respectively.

The whole core of the problem is, as mentioned, variable-length collections of values. The only ways to assign to such collections in a single statement are:

  • aggregate initalization, which requires either declaring the type in the same statement,
  • recursively initializing it with through aggregate initialization of the class that contains the collection, or
  • through operator overloading magic, which requires a non-declaration statement.

The Object Oriented Game World

In games, we will often have a lot of game world entities with similar capabilities, e.g. different kinds of enemies. One way of accomplishing this is to create a single "super"-entity with all the possible capabilities, and configuring the different actual entities to have a subset of the capabilities and different stats. It would not be uncommon to make something like this

class Weapon {
public:
    Weapon(std::string name, int damage);
    ...
}
class Enemy : public Attackable {
public:
    Enemy(std::string name, int hitpoints, int strength);
    void attack(Attackable *target);

    std::vector<Weapon *> weapons;
    ...
};

...

Enemy enemy = new Enemy("orc", 100, 10);
enemy->weapons.push_back(new Weapon("axe", 6));
...
enemy->attack(player);

Every single enemy, perhaps except bosses, would be an instance of class Enemy, and their peculiarities would have to fit this frame, or new functionality would have to be added to class Enemy.

Of course, this is not the object oriented way of doing things. If it is done like this, it is in consideration of being able to reconfigure the stats of the different enemy types. In line with the preceding discussion ... FIXME

  • Adding functionality inheritance and virtual methods.
  • No changes to base classes is necessary when adding new derived classes.