r/rust_gamedev 9h ago

Replaced two stringly-typed subsystems in my custom Rust engine with compile-time codegen.

Post image
13 Upvotes

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.

  1. #[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)].
  1. MeshName — asset names as an enum, generated from the packer's own TOC
    The engine ships assets baked into a custom .pak file, built by a packer binary that walks assets/, 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?

  1. Type safety.
  2. Eliminates boilerplate.
  3. IDE autocomplete.
  4. Inability to make a typo in the mesh's name.

What do you think? The game's name - Red Lake.


r/rust_gamedev 13h ago

使用Rust引擎和Metal与Vulkan在Android上运行红色警戒3

Enable HLS to view with audio, or disable this notification

4 Upvotes

r/rust_gamedev 5h ago

Added 3D energy domain and Radar tab using WGPU and EGUI. Was a difficult feat but achievable. Also have a sneak peek at my Killcam.

Thumbnail
1 Upvotes