Contents |
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.
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.
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.
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
</creature>
YAML example:
name: zombie type: humanoid hitpoints: 100 attacks: - punch - grapple
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:
with the code, and subtle configuration errors may occur when proper synchronization fails.
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 propose to configure a game through code. This has several benefits:
Thus, configuration errors are caught immediately.
values and the like.
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 read code. Now, assuming that I am not crazy, the main problem to address is legibility 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 ]
end
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#.
One way of doing it could be the member initialization list for the default constructor, like this:
zombie_configuration.cpp:
#include "zombie.h"
Zombie::Zombie() :
name ("zombie"),
type (humanoid),
hitpoints (100),
attacks (2)
{
attacks[0] = punch;
attacks[1] = grapple;
}
However, the vector initialization is a bit ugly, and adding more values requires updating the size of the vector. 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 };
This one is pretty wordy but perhaps less error-prone. Yet another approach uses the aggregate construction syntax; assuming a definition like this:
zombie.cpp:
struct Zombie_configuration {
const char *name;
const Entity_type type;
const int starting_hitpoints;
const Attack_type attacks[2];
};
The designer could edit an initialization like this:
zombie_configuration.cpp:
Zombie_configuration config = {
"zombie", // name
humanoid, // type
100, // starting_hitpoints
{punch, grapple} // attacks
};
This might be more error-prone than the others, and having to specify the maximum size of arrays isn't optimal.