A game where you push blocks around and use the power of mathematics to interact with the world around you and solve puzzles.

Play demo here: https://drive.google.com/file/d/114WXCk3p4rPQgUeSWNGvWN9G4C0QcTwZ/view?usp=sharing

I encourage you to play before reading on.

This is a living document that will be added to as development continues and I decide something is interesting enough to write about.

Basic Gameplay

The video above demonstrates the basic mechanics of the game. You move characters around on a 3D grid, pushing blocks around to get all the characters to the exit, which are the yellow tiles you see on the right of the level. The purple block is a Register that holds values and can evaluate expressions. The red tile blocking the exit is a Gate which is tied to the register and requires a specific value (2, in this case) before it will lower itself, clearing the path to the exit. In this level, the player pushes a 1, a +, and another 1 into the register to make 2 which opens the gate.

Engine

The game runs on a custom engine called Workbench written in the Odin programming language with the following major features:

  • Physically-based forward renderer using DirectX 11
  • Cascaded shadow maps
  • Skeletal animation (currently implemented in an old OpenGL backend, still needs to be ported to DirectX)
  • Data-driven material system
  • Entity system and scene editor
  • Extensible asset pipeline

I heard a quote from Casey Muratori a few years ago that went something like this:

If you’re a game developer who only knows Unity and Unity suddenly goes under or disappears, you now don’t know how to make games at all.

This terrified me. The fact that I was resting my career on a specific tool seemed truly absurd, so I started looking into how game engines worked under the hood. While it is a lot more work it is also immensely rewarding and the times where it’s rough aren’t nearly as frustrating. I don’t ever plan on going back to something like Unity or Unreal for any personal project, the amount of control you get over your application with a custom engine is too much to give up.

Level Editor

The developer version of the game has a level editor built into it. This allows for rapid creation of levels and testing puzzle ideas. The video above demonstrates the level editor, making one of the tutorial levels in the game. You can place and rotate entities, change their properties in the inspector, and then save the scene in order to test.

Renderer

The renderer is a physically-based forward renderer built on top of DirectX 11 featuring a data-driven material system, cascaded shadow maps, and tonemapping, with bloom and skeletal animation implemented in an old OpenGL backend still needing to be ported over. The following is an example of a shader definition and a material definition defined in a custom markup language called WBML (WorkBench Markup Language):

// lit.shader
{
    vertex_shader "lit_vertex"
    pixel_shader  "lit_pixel"

    properties [
        { name "base_color"        type Vector4 }
        { name "metallicness"      type Float   }
        { name "roughness"         type Float   }
        { name "ambient"           type Float   }
        { name "do_normal_mapping" type Int     }
    ]

    textures [
        { name "shadow_map0" type Texture2D }
        { name "shadow_map1" type Texture2D }
        { name "shadow_map2" type Texture2D }
        { name "shadow_map3" type Texture2D }

        { name "albedo_map" type Texture2D }
        { name "normal_map" type Texture2D }
        { name "skybox_map" type Cubemap   }
    ]
}

For a shader definition you say which text files the actual source code will come from (lit_vertex and lit_pixel in this case, which are just .hlsl files), as well as any properties and textures the shader takes as inputs. The engine uses this information to generate the actual shader text that will be compiled and sent to the GPU, including generating constant buffer structs and texture slots.

// ground_mtl.material
{
    shader "lit"

    properties [
        { name "base_color"   value .Vector4 [ 1.0 1.0 1.0 1.0 ] }
        { name "metallicness" value .f32 0.0 }
        { name "roughness"    value .f32 1 }
        { name "ambient"      value .f32 0.1 }
    ]

    textures [
        { name "albedo_map" asset "ground_tex" }
    ]
}

A material definition says which shader it is to be used with, and then provides values for any properties/textures that shader uses. The engine will then write the data to a byte array and ship it to the GPU as a constant buffer. In addition to providing values in the material asset on disk, you can also change any values at runtime.

Entity System

I did way too much rat-holing on entity systems before figuring out a structure that I like and is simple. I decided there were 4 main things I needed from the system:

  1. It needs to be easy to add new entities.
  2. It needs to be easy to iterate over all entities.
  3. It needs to be easy to iterate over all entities of a specific type.
  4. The entities need to be easily serializable.

#1. It needs to be easy to add new entities.

Adding a new entity is trivial; you simply define a normal struct and add an @entity tag above the struct and have a base pointer as the first field, like so:

@entity
My_Cool_Entity :: struct {
    using base: ^Entity, // the `using` just makes it so I can do `my_cool_entity.position` rather than `my_cool_entity.base.position`

    some: f32,
    extra: string,
    fields: int,
}

The game has a pre-build step that scans all the source code of the game and looks for all structs with an @entity tag. The builder then generates a tagged union with all the entity types it found, as well as some storage which I will discuss below. Here is the tagged union with all the entity types in the game:

Entity_Kind :: union {
    Empty_Entity,
    Point_Light,
    Directional_Light,
    Camera_Settings,
    Level_Portal,
    Register_Tile,
    Operator,
    Parameter,
    End_Point,
    Gate,
    Ground,
    Block,
    Directional_Ground,
    Multiplexer,
    Player,
    Pressure_Plate,
    World_Text,
    Puzzle_Region,
}

The actual Entity struct has some common fields like position and orientation as well as this tagged union. This tagged union means it is easy to ask an Entity what kind of entity it is, and since the different kinds of entities have a base pointer it is easy to go back the other way from derived to base. This union method means all entities have the same size so I can have all entities live in a single array, which leads me to the next goal of the entity system.

#2. It needs to be easy to iterate over all entities.

Iterating over entities is very easy because all entities are stored in a single array of a large fixed size. The reason it is a fixed size is because I don’t want any resizing to invalidate pointers in gameplay code. The fixed size might feel limiting but in reality you will run into performance problems updating and rendering all the entities before you run into memory problems so I just have a huge array and don’t worry about it. To be totally bulletproof I have considered using a virtual arena that reserves a terabyte of virtual memory or something ridiculous and then only actually commits something like a megabyte at a time, this would keep pointers valid while allowing for “resizing”. Maybe some time in the future I will do that.

Since all the entities are stored in a single array iterating over them all is very easy though a custom iterator is needed to skip over empty slots and disabled entities.

iterator := entity_iterator(&my_scene);
for entity in get_next_entity(&iterator) {
    // do something with the entity
}

#3. It needs to be easy to iterate over all entities of a specific type.

To iterate over all entities of a specific type I decided to do some compile-time code generation to create storage in the Scene for an array of pointers to each entity type. The following structure gets generated:

Entities_By_Type :: struct {
    Empty_Entity: [dynamic]^Empty_Entity,
    Point_Light: [dynamic]^Point_Light,
    Directional_Light: [dynamic]^Directional_Light,
    Camera_Settings: [dynamic]^Camera_Settings,
    Level_Portal: [dynamic]^Level_Portal,
    Register_Tile: [dynamic]^Register_Tile,
    Operator: [dynamic]^Operator,
    Parameter: [dynamic]^Parameter,
    End_Point: [dynamic]^End_Point,
    Gate: [dynamic]^Gate,
    Ground: [dynamic]^Ground,
    Block: [dynamic]^Block,
    Directional_Ground: [dynamic]^Directional_Ground,
    Multiplexer: [dynamic]^Multiplexer,
    Player: [dynamic]^Player,
    Pressure_Plate: [dynamic]^Pressure_Plate,
    World_Text: [dynamic]^World_Text,
    Puzzle_Region: [dynamic]^Puzzle_Region,
}

Just a bunch of dynamic arrays of pointers. Then every frame I clear these arrays, iterate over all entities in the scene, check their type and repopulate them. This means that if you wanted to iterate over all Pressure_Plates for example, you would do the following:

for plate in my_scene.by_type.Pressure_Plate {
    // do something with the plate
}

#4. The entities need to be easily serializable.

Since an entity is just a struct I can leverage the runtime type information facilities in Odin to serialize and deserialize entities using WBML.

@entity
My_Cool_Entity :: struct {
    using base: ^Entity,

    some: f32,
    extra: string,
    fields: int,

    wont_be_serialized: ^Thing "wbml_noserialize", // adding a `wbml_noserialize` tag will stop this field from being written to disk when the scene is saved

    some_renamed_field_456: bool "wbml_oldname=some_renamed_field_123", // adding a `wbml_oldname` tag allows you to rename serialized fields and have the data be pulled from the old name properly
}

When you save the scene, all entities are serialized to disk. Each scene has its own folder and each entity has its own .e file inside that folder which is just a format containing two WBML objects: a header; and the actual entity data. Each entity having its own file in an easy-to-read text format makes it dead-easy to resolve merge conflicts if two people edit the same scene (since each entity has its own file, a merge conflict would only even happen if you change the exact same entity). A binary format would be used in a final release, of course. Here is an example of a .e file for a register entity:

{
    entity_type "Register_Tile"
    id 4294967360
    name "Register_Tile"
}

{
    id 4294967360
    enabled true
    name "Register_Tile"
    version 11
    position [
        0.000
        -1.000
        -1.000
    ]
    scale [
        1.000
        1.000
        1.000
    ]
    orientation quat 1.000 0.000 0.000 0.000
    render_info {
        model_offset [
            0.000
            0.000
            0.000
        ]
        model_scale [
            1.000
            1.000
            1.000
        ]
        model_id "cube_model"
        color [
            1.000
            1.000
            1.000
            1.000
        ]
        material_id "register_tile_mtl"
    }
    user_flags 3
    user_data {
        save_state_in_overworld false
        reset_to_position [
            0.000
            -1.000
            -1.000
        ]
    }
    derived .Register_Tile {
        wire_offset [
            0.000
            1.000
            0.000
        ]
        feeders [
            4294967308
            4294967388
            4294967389
            4294967357
            4294967358
            4294967462
        ]
        listeners [
            8589934657
        ]
    }
}

The format is just two WBML objects. The first being a header containing data for some pre-deserialization work that needs to be done, and the second is that entity’s state in the scene when it was saved. Each entity also has a version number, so if you change an entity structure drastically to require some custom version upgrade code, you can do that just after deserialization with something like this:

do_entity_upgrade :: proc(entity: ^Entity) {
    if entity.version < 4 {
        entity.version = 4;
        // added a new field which is a Vector2, so combine the old x and y values
        entity.some_new_field = Vector2{entity.deprecated_x, entity.deprecated_y};
    }

    if entity.version < 5 {
        entity.version = 5;
        // added a player_name field to the Player entity, so if this entity is a Player, give it a default name
        #partial
        switch kind in &entity.derived {
            case Player: {
                kind.player_name = "Default Player Name";
            }
        }
    }
}