22
\$\begingroup\$

My question isn't a coding question; it applies to all of game engine design in general.

How do you avoid hard coding?

This question is a lot deeper than it seems. Say, if you want to run a game that loads the files necessary for operation, how do you avoid saying something like load specificfile.wad in the engine's code? Also, when the file is loaded, how do you avoid saying load aspecificmap in specificfile.wad?

This question applies to pretty much all of engine design, and as little as possible of the engine should be hard coded. What is the best way to achieve that?

\$\endgroup\$
42
\$\begingroup\$

Data-driven coding

Every thing you mention is something that can be specified in data. Why are you loading aspecificmap ? Because the game configuration says that is first level when a player starts a new game, or because that's the name of the current save point in the player's save file they just loaded, etc.

How do you find aspecificmap ? Because it's in a data file that lists map ids and their on-disk resources.

There need only be a particularly small set of "core" resources which are legitimately hard or impossible to avoid hard-coding. With a bit of work, this can be limited to a single hard-coded default asset name like main.wad or the like. This file can potentially be changed at runtime by passing a command-line argument to the game, aka game.exe -wad mymain.wad.

Writing data-driven code relies on a few other principles. For instance, one can avoid having systems or modules ask for a particular resource and instead invert those dependencies. That is, don't make DebugDrawer load up debug.font in its initialization code; instead, have DebugDrawer take a resource handle in its initialization code. That handle might be loaded from the main game configuration file.

As a concrete examples from our codebase, we have a "global data" object that is loaded from the resource database (which itself is by default the ./resources folder but can be overloaded with a command line argument). The resource databse ID of this global data is the only necessary hard-coded resource name in the codebase (we have others because sometimes programmers get lazy, but we generally end up fixing/removing those eventually). This global data object is full of components whose sole purpose is to provide configuration data. One of the component is the UI Global Data component which contains resource handles to all the main UI resources (fonts, Flash files, icons, localization data, etc.) among a number of other configuration items. When a UI developer decides to rename the main UI asset from /ui/mainmenu.swf to /ui/lobby.swf they just update that global data reference; no engine code needs to change at all.

We use this global data for everything. All the playable characters, all the levels, UI, audio, core assets, network configuration, everything. (well, not everything, but those other things are bugs to be fixed.)

This approach has a lot of other advantages. For one, it makes resource packing and bundling integral to the whole process. Hard-coding paths in the engine also tends to be mean that those same paths have to be hard-coded in whatever scripts or tools package up game assets, and those paths can then get out of sync. Relying instead on a single core asset and reference chains from there, we can build an asset bundle with a single command like bundle.exe -root config.data -out main.wad and know that it will include all the assets we require. Further, since the bundler would just be following resource references, we know that it will include only the assets that we require and skip all the left-over fluff that inevitably accumulates over the life of a project (plus we can automatically generate lists of that fluff for pruning).

A tricky corner case of this whole thing is in scripts. Making the engine data-driven is easy conceptually, but I've seen so very many projects (hobby to AAA) where scripts are considered data and hence are "allowed" to just use resource paths indiscriminately. Don't do that. If a Lua file needs a resource and it just calls a function like textures.lua("/path/to/texture.png") then the asset pipeline is going to have a lot of trouble knowing that the script requires /path/to/texture.png to operate correctly and might consider that texture to be unused and unnecessary. Scripts should be treated like any other code: any data they need including resources or tables should be specified in a configuration entry that the engine and resource pipeline can inspect for dependencies. The data that says "load script foo.lua" instead should say "load foo.lua and give it these parameters" where the parameters include any resources necessary. If a script randomly spawns enemies for example, pass the list of possible enemies into the script from that configuration file. The engine can then pre-load the enemies with the level (since it knows the complete list of possible spawns) and the resource pipeline knows to bundle all the enemies with the game (since they're definitively referenced by configuration data). If the scripts generates strings of path names and just calls a load function then neither the engine nor resource pipeline have any way of knowing specifically which assets the script might try to load.

\$\endgroup\$
  • \$\begingroup\$ Good answer, very practical, and also explains the pitfalls and errors people make when implementing this! +1 \$\endgroup\$ – opa Oct 6 '17 at 20:46
  • \$\begingroup\$ +1. Would add that following the pattern of pointing to resources which contain configuration data is also very helpful if you want to enable modding. It's soooo much harder and riskier to modify games which require you to change the original data files rather than create your own and point at them. Even better if you can point at multiple files with a defined order of priority. \$\endgroup\$ – Jeutnarg Oct 6 '17 at 22:45
12
\$\begingroup\$

The same way you avoid hardcoding in general functions.

You pass parameters and you keep your information in configuration files.

In that situation, there is absolutely no difference in software engineering between writing an engine and writing a class.

MgrAssets
public:
  errorCode loadAssetFromDisk( filePath )
  errorCode getMap( mapName, map& )

private:
  maps[name, map]

Then your client code reads a "master" configuration file (this one is either hard coded or passed as a command line argument) which contains the information that tells where the assets files are and what map they contain.

From there, everything is driven by the "master" configuration file.

\$\endgroup\$
  • 1
    \$\begingroup\$ Yep, that plus some sort of mechanism to bring in custom logic. Might be by embedding a language like C#, python etc. in order to extend the engine's core features by user defined functionality \$\endgroup\$ – qCring Oct 6 '17 at 18:04
3
\$\begingroup\$

I like the other answers so I'm going to be a little bit contrary. ;)

You can't avoid coding knowledge about your data into your engine. Wherever the information comes from, the engine must know to look for it. However, you can avoid encoding the actual information itself into your engine.

A "pure" data driven approach would have you start the executable with the command line parameter(s) necessary for it to load the initial configuration but the engine will have to be coded to know how to interpret that information. E.g. if your configuration files are JSON, you need to hard code the variables you look for, e.g. the engine will have to know to look for "intro_movies" and "level_list" and so forth.

However, a "well-constructed" engine can work for many different games just by swapping out the configuration data and the data that it references.

So the mantra isn't so much to avoid hard coding as it is to ensure that you can make changes with the least amount of effort possible.

To contrast against the data files approach (which I wholeheartedly support), it may be that you're ok compiling the data into your engine. If the "cost" of doing so is lower, then there's no real harm; if you're the only one working on it then you can defer filehandling for a later date and not necessarily screw yourself. My first few game projects had big tables of data hardcoded into the game itself, e.g. a list of weapons and their assorted data:

struct Weapon
{
    enum IconID icon;
    enum ModelID model;
    int damage;
    int rateOfFire;
    // etc...
};

const struct Weapon g_weapons[] =
{
    { ICON_PISTOL, MODEL_PISTOL, 5, 6 },
    { ICON_RIFLE, MODEL_RIFLE, 10, 20 },
    // etc...
};

So you put this data somewhere easy to reference and it's easy to edit as necessary. The ideal would be to put this stuff into a config file of some sort but then you need to do parsing and translation and all of that jazz, plus hooking up inter-structure references might become an additional pain that you really don't want to deal with.

\$\endgroup\$
  • \$\begingroup\$ It's not terribly difficult to parse json. The only "cost" involved is learning. (Specifically, learning to use the appropriate module or library. Go has good json support, for example.) \$\endgroup\$ – Wildcard Oct 7 '17 at 2:00
  • \$\begingroup\$ It's not terribly difficult but it requires doing it beyond just learning. E.g. I know how to technically parse JSON, I've written parsers for many other file formats, but I'd either need to find and install a third party solution (and figure out dependencies and how to build it) or roll my own. Takes more time than not doing it. \$\endgroup\$ – dash-tom-bang Oct 7 '17 at 2:06
  • 4
    \$\begingroup\$ Everything takes more time than not doing it. But the tools you need have already been written. Just like you don't have to design a compiler to write a game, or fiddle around with machine code, but you do have to learn a language for the platform you're working with. So, learn to use a json parser, also. \$\endgroup\$ – Wildcard Oct 7 '17 at 2:10
  • \$\begingroup\$ I'm not sure what your argument is. In this answer I'm advocating YAGNI; if you don't need to spend/waste the time doing something that won't help yout then don't. If you want to spend time on that then great. Maybe you'll need to spend the time later, maybe you won't, but doing it up front is only distracting you from the task of actually making the game. Game development is trivial; every single task that goes into making a game is simple. It's just that most games have a million simple tasks and a responsible dev chooses the ones that get to that goal fastest. \$\endgroup\$ – dash-tom-bang Oct 7 '17 at 21:14
  • 2
    \$\begingroup\$ Actually, I upvoted your answer; no real argument as such. I just wanted to note that JSON is not difficult to parse. Reading again, I suppose I was mostly responding to the snippet "but then you need to do parsing and translation and all of that jazz." But I agree that for personal project games and such, YAGNI. :) \$\endgroup\$ – Wildcard Oct 8 '17 at 5:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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