game design, code, writing, art and music

English По-русски

Game code that programs itself

I've made and released four games on Steam so far, all of which are written in Haxe. I'm a big fan of automating as much of my work as possible, and I'm going to share some of the tricks I'm using to program my games.

For the uninitiated: Haxe is both a programming language and a cross-compiler. This means that you can write a game in Haxe, and it is automatically "translated" into a different programming language based on your chosen target platform (e.g. C++ for Windows, JavaScript for Web, etc.), and then compiled into a game that you can run on that platform.

The language has some useful metaprogramming features, which can be used for writing code that basically changes itself. Note that this isn't a tutorial or a guide, so you won't find any code here, but rather a few examples of how these tricks can be used in practice when making games.

By the way, some of these features are also present in other languages, although they might be called something else. So you can probably make use of these ideas even if you're not a Haxe programmer.

Conditional compilation

This is probably the simplest way to affect the compilation process. You have the option to only compile certain parts of the code by providing and checking compiler flags.

For example, when developing my games I'm always using a level editor that's built right into my game. With the exception of Speebot, this editor is only available to me, and not included in the release version. This is achieved by wrapping all of the code that's responsible for the level editor into a conditional, which checks for the "dev" compiler flag. If the flag is not there — the editor is "erased" from the source code before the actual native compilation even begins.

The internal map editor in <a href='/piliepals' target='_blank'>Pilie Pals</a>, which is only available in dev mode.
The internal map editor in Pilie Pals, which is only available in dev mode.

This feature is also used for separating assets between "demo" and "prod" builds. The demo versions of my games usually include a few of the early levels of that game, so I'm using compiler flags to determine which assets need to be included in the build. This way the unused levels, music files, etc. are not included in demo versions of the games.

Additionally, I am using compiler flags to enable or disable some of the optimization features of my game engine. Some of the 3D object batching functionality is disabled in the renderer in dev mode, because this optimization gets in the way when editing a level. In other words, the 3D renderer is optimized for performance when building levels in dev mode. In production builds, the renderer is optimized for performance when playing the game.

Metadata

Haxe has several metadata features, which can be used for annotating, reading and manipulating parts of the code that would normally be inaccessible.

In my case I have a Settings class, which contains variables of all the different options the player can change in the settings menu. The user's settings are stored in a separate file in the game's directory. The file is generated automatically by cycling through all of the fields in the Settings class, and using that information to either save or load the data from the file. This is done using Haxe's reflection API.

Not all of the fields in the Settings class need to be serialized and saved to the file, though. There are some variables which are constant, and they're marked with a custom metadata annotation "@ignore(true)", which the code detects and doesn't include in the saved file.

Macros

This is the most complicated metaprogramming feature in Haxe, and also the coolest. Macros let me actually run real Haxe code during compilation, which directly modifies the source code of the game.

The simplest use case for this: including the date and time of compilation in the build. The build number and time of compilation is used instead of version numbers in my games. They are always updated automatically, so I never have to remember to increment anything myself.

The biggest benefit of this feature for me is the ability to move code from run-time to compile-time.

In Speebot there's a level selection screen, which shows how many collectibles there are in each level, and how many of them the player has already found. In order to count the number of collectibles in a level, the game needs to load the level's file, parse it, then count all of the collectible objects, and return that number.

This logic works fine, until you need to display this information for 200 levels at once. The game has to load and parse 200 level files, which not only uses up a lot of memory at once, but can also take several seconds to process. When implemented this way, the level selection screen would freeze each time the player opened it.

There are 4 worlds, containing 50 levels each. The completion percentage is calculated based on the number of completed levels and collected crystals in each level of that world.
There are 4 worlds, containing 50 levels each. The completion percentage is calculated based on the number of completed levels and collected crystals in each level of that world.

Of course, I could manually count the numbers of collectibles and hardcode them into the game, but I'm too lazy, and this could actually lead to errors if I update a level and forget to change a number.

The solution was to write a macro, which loads all 200 levels of the game (macros have access to the file system), calculates all the required data, then stores the numbers in an array. The game no longer needs to calculate anything at run-time, as it simply needs to show the numbers that were automatically "hardcoded" into the game's source code by the macro.

A similar approach is used in Phantom Path. In the game, the user can find an item that displays how many secrets, lore items, treasure etc. can be found in each area of the world. Instead of loading the game data of each area at run-time, this is done at compile-time using macros.

As a result, there's no lag when showing the map. The game also uses less memory this way, because it doesn't need to load all of the levels at once at run-time.

Next Article

Speebot update: Steam Achievements, Russian localization

Subscribe

Receive a notification on your device whenever there's a new blog post available, in order to:

Follow the development process of my next game.

Get notified of articles about the art and tech of designing games and engines.

Receive updates about changes to my games.

Subscription is free and takes just one click!