I guess I’m renaming this series to TCP now, for “The Current Project.” I’ve updated the tags on the old posts accordingly.

Anyways! Work on that project I talked about last week is going pretty great. While it’s indev, I’ve code-named1 it “Robot Language” after the meganeko album. Starting fresh has been really good for me, I think; I have gotten way more done in a week and a bit than I could have on Foxfire. I’ve implemented rendering, collision, FOV calculations … lots and lots of things.

Here’s a screenshot of the game as it is so far.

An at sign stands in a dining hall drawn in characters, featuring some scattered tables and chars, and a kitchenette. 
        There's a subtle CRT effect applied over the screen. A panel on the right says "Sink: Muddy waste pops up from the drain."
That better CRT effect is courtesy of a video by Acerola. The FOV calculations use Adam Milazzo's algorithm.

It’s a mystery game, so I won’t reveal too much … but I will say you wake up on a spacecraft with no memory, and have to figure out what is going on. I’ve been playing Outer Wilds recently, and I’m really inspired by the idea of a metroidvania where your “keys” are knowledge.

I don’t think I’ve heard of a game that mixes metroidvanias with Zelda-like dungeons. To me, a Zelda dungeon feels kinda like a metroidvania in minature.2 The plan is to have the larger challenges in the game be unlocked with knowledge, and smaller, self-contained challenges styled after Zelda dungeons with a key item that unlocks new areas in the ship.

But we’ll see as I design it more.

Prefabs#

But anyways this post is not really about Robot Language; it’s about a (technology? idea? algorithm?) I’ve developed as I write the game.

Robot Language’s world layout will have no randomization, given it’s a metroidvania-like. So, I need some way to make the world, and doing it from code would be a pain in the ass.

So, in short, what I do is I draw out a map in RexPaint, a kinda crufty but very useful tool for drawing ASCII art. Then, the game loads the file at runtime using the aptly-named rexpaint crate. Each character and its foreground color is then mapped to a prefab. That mapping is written in KDL, a language like XML but not horrible to write which Lemma has totally hooked me on. Then, those prefabs are mapped to actual collections of components in code.

I realize that makes no sense, so here’s some pictures to show you what I’m talking about.

A zoomed out view of the previous screenshot, revealing a large area of mostly hallways. Doors are marked in red and green, and walls
        that used to have connected textures are just hashtags.
Here's the map file for the area of the ship the previous screenshot is of. It's not cut off at the edge; this is a rotating spaceship! This zone is a ring is connected at the left and right.

Here’s part of prefabs.kdl, which is loaded by the game on startup.

"#" color=0xffffff {
    prefab "wall"
    type "inner"
}
"#" color=0x3333ff {
    prefab "wall"
    type "outer"
}

"+" color=0xff0000 pass-color=true {
    prefab "door"
    closed true
}
"+" color=0x00ff00 pass-color=true {
    prefab "door"
    closed false
}

// --- Furniture ---

// This orangey color is for food
"T" color=0xff4000 { prefab "table"; type "dining"; }
"T" color=0x6666ff { prefab "table"; type "stainless"; }

"c" color=0xff4000 {
    prefab "widget"
    char 0xe3 // pi
    fg "dark-yellow"
    bg null
    z-level 300
    collider false
    occluder false
    description "chair?plastic"
}

// Angstrom for any and all misc furniture
"0x86" color=0xff4000 { prefab "microwave"; }
"0x86" color=0xd900d9 { prefab "sink"; }

Every character in one of those maps has to have an entry in here where the character and foreground color both match. By marking pass-color=true, they can also pass their background color as an argument, which I use for things like notes that need a whole bunch of indices. (A white with background color 0x000000 is note #0, a white with background color 0x000001 is note #1 … etc.) Then, the prefab string is matched against a bank of strings in the game code and used to spawn it with the given arguments.

Here’s part of that code:

pub fn spawn_prefab(
    world: &mut World,
    globals: &Globals,
    prefab_name: &str,
    color_arg: Option<u32>,
    args: &AHashMap<SmolStr, KdlLiteral>,
    pos: Position,
) -> Entity {
    if let Some(color_arg) = color_arg {
        match prefab_name {
            "door" => {
                let closed = args["closed"].as_bool().unwrap();
                door(world.spawn(), pos, closed, color_arg).build()
            }
            "note" => note(world.spawn(), pos, color_arg).build(),
            _ => panic!(
                "unknown prefab {:?} (color argument {:#x})",
                prefab_name, color_arg
            ),
        }
    } else {
        match prefab_name {
            "player" => {
                let player = player(world.spawn(), pos).build();
                world.insert_resource(ThePlayerEntity(player));
                player
            }

            "wall" => {
                let ty = &args["type"].as_string().unwrap();
                wall(world.spawn(), pos, ty).build()
            }

            "table" => {
                let ty = &args["type"].as_string().unwrap();
                table(world.spawn(), pos, ty).build()
            }
            "sink" => sink(world.spawn(), pos).build(),
            "microwave" => microwave(world.spawn(), pos).build(),

            _ => panic!("unknown prefab {:?} (with no color arg)", prefab_name),
        }
    }
}

fn player<B: EntityBuilder>(builder: B, pos: Position) -> B {
    builder
        .with(Positioned(pos))
        .with(ZLevel(u16::MAX))
        .with(IsDrawable)
        .with(HasSimpleDrawable(Drawable::simple(
            Color::Magenta,
            None,
            b'@',
        )))
        .with(Describable(SmolStr::new("player")))
}

fn wall<B: EntityBuilder>(builder: B, pos: Position, ty: &str) -> B {
    let (desc, fg, bg) = match ty {
        "inner" => ("inner-wall", Color::Gray, Color::DarkGray),
        "outer" => ("outer-wall", Color::White, Color::Gray),
        _ => panic!("unknown wall type {:?}", ty),
    };
    builder
        .with(Positioned(pos))
        .with(Collider)
        .with(Occluder)
        .with(ZLevel(200))
        .with(IsDrawable)
        .with(HasSimpleDrawable(Drawable::box_drawing(
            fg,
            Some(bg),
            true,
            "walls",
        )))
        .with(Describable(SmolStr::new(desc)))
}

fn table<B: EntityBuilder>(builder: B, pos: Position, ty: &str) -> B {
    let (desc, fg, bg) = match ty {
        "dining" => ("table?dining", Color::DarkYellow, None),
        "stainless" => ("table?stainless", Color::DarkBlue, Some(Color::Gray)),
        _ => panic!("unknown table type {:?}", ty),
    };
    builder
        .with(Positioned(pos))
        .with(ZLevel(200))
        .with(Collider)
        .with(IsDrawable)
        .with(HasSimpleDrawable(Drawable::box_drawing(
            fg, bg, true, "table",
        )))
        .with(Describable(SmolStr::new(desc)))
}

// many other spawner funcs elided ...

Let’s look at the lifecycle of one of the orange Ts in the dining hall. “T with background color #ff4000” is mapped against eack entry in prefabs.kdl, and it matches this one:

"T" color=0xff4000 { prefab "table"; type "dining"; }

Then, the game calls spawn_prefab, looking for a prefab named "table" and passing it the arguments {type: "dining"}. The match statement matches it to the table function, which is called and spawns the entity. Et voila!


I stand on the shoulders of giants here:

  • I first saw the idea of reading prefabs from a file at all from Caves of Qud. I have a bit of an intermediary layer here; Qud’s prefabs (called “blueprints”) directly put components on the entities, whereas I have this abstraction in the middle. It’s less flexible, but it does save me some serde work. (However! I just finished writing serde support for palkia this morning, so a future version of this might deserialize components directly from the prefab file.)
  • Kyzrati/Josh Ge, the person behind rexpaint and cogmind, has this blog post about how they use rexpaint to add prefabs to dungeon maps. Their use of “prefab” is a bit different than mine; they mean specific rooms or features. Their implementation is a lot more regimented; each layer has a specific job, tiles are matched by their color alone on layer 1, and entities are matched by their character alone on layers 3 and 4. But that works for their game, because they have a lot of random generation to spruce things up (where I have to draw it all myself.)
  • The idea of KDL as a prefab language goes to Lemma and its Minecraft mod Kdly Content, which lets you add blocks and items using kdl. Also, Caves of Qud uses XML, and KDL more or less maps 1-to-1 to XML, so I figured it would be a good shot. (Early versions of this idea I had used EDN, which would have been … fraught.)

So yeah! This system lets me prototype really rapidly, which I think is REALLY important for game development.3 I’m also really glad to have figured KDL out; it really is a joy to write. (All the descriptions for tiles in the game are also written in KDL, and I’m planning for all the misc. strings and things to be KDL as well.)

I’ve been working hard making Palkia more useable as I figure out where the rough edges are by working on Robot Language. Next week will probably be a more game-design-y article, as I bootstrap Robot Language enough that I can start working on the game as opposed to the engine.

So I’ll see you all next week …


  1. “Code-named” (haven’t thought of a real name yet) ↩︎

  2. The ones with lots of backtracking, at least. ↩︎

  3. If I were being really fancy, I could probably set up hot-reloading of the map, with some marker component that indicates it’s OK to clear it when reloading. ↩︎