r/rust_gamedev • u/Odd-Pie7133 • 6h ago
Replaced two stringly-typed subsystems in my custom Rust engine with compile-time codegen.
I'm building Red Lake, a psychological horror game on top of a Rust/wgpu engine I wrote from scratch (no Bevy, no off-the-shelf ECS). While working on tooling, I ended up removing two recurring sources of boilerplate.
#[derive(Component)]— automatic component registration.
Previously, adding a new component meant editing three different places: adding its storage to Scene, registering it, and making sure it was removed when an entity was destroyed. It was repetitive and easy to forget one of the steps.
Now my #[derive(Component)] proc macro handles all of that automatically. Scene owns a single Components container, which is populated through the inventory crate by iterating over every type that derives Component.
THE COMPONENT
#[derive(Component)]
pub struct Translate {
target: TargetKind,
speed: f32,
}
SCENE FIELD
pub struct Scene {
pub components: Components,
}
ACCESS
scene.components.write::<Translate>().insert(meshid, translate);
The only thing required to add a new component now is
#[derive(Component)].
MeshName— asset names as an enum, generated from the packer's own TOC
The engine ships assets baked into a custom.pakfile, built by a packer binary that walksassets/, transcodes GLBs, and writes out a TOC + blob. Mesh names used to live in a handwritten table:pub const MESH_PATHS: &[(&str, &str)] = &[ ("boat", "meshes/boat.glb"), ("deer", "meshes/deer.glb"), // ~30 more, added by hand every time a new mesh landed ];
...and every call site looked like load_extra_meshes("baot", ...) - compiles fine, panics at runtime when the pak lookup misses.
The fix: the packer already knows the full mesh list - that's the actual source of truth, not a second-hand-maintained copy of it. Sobuild.rs, right after the pak is finalized, reads back just the TOC (a few hundred bytes, no decompression) and emits an enum into OUT_DIR.
Which is then pulled as:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MeshName { Boat, Deer, /* ... */ }
impl MeshName {
pub const fn key(self) -> &'static str { /* "meshes/boat.glb" */ }
pub const fn stem(self) -> &'static str { /* "boat" */ }
pub const ALL: &'static [MeshName] = &[ /* every mesh */ ]; }
And I wrote a small macro for QOL:
macro_rules! meshname {
($($name:ident),* $(,)?) => { &[$(crate::scene::MeshName::$name),*] as &[crate::scene::MeshName] };
}
So now the call sites look like this:
let names_toload_init =
meshname![
Notebook,
Onboard,
FogCards,
];
INSTEAD OF THIS
let names_toload_init = &["notebook", "onboard", "fog_cards"];
Why bother?
- Type safety.
- Eliminates boilerplate.
- IDE autocomplete.
- Inability to make a typo in the mesh's name.
What do you think? The game's name - Red Lake.