#ecs #gamedev #archetypes #ecs-code

sillyecs

A silly little compile-time generated archetype ECS in Rust

6 releases

Uses new Rust 2024

0.0.6 Apr 16, 2025
0.0.5 Apr 5, 2025

#231 in Game dev

28 downloads per month

EUPL-1.2

72KB
1.5K SLoC

sillyecs

A silly little compile-time generated archetype ECS in Rust.

Crates.io License

Table of Contents

Installation

sillyecs is a build-time dependency. To use it, add this to your Cargo.toml:

[build-dependencies]
sillyecs = "0.0.2"

Usage

Use sillyecs in your build.rs:

use sillyecs::EcsCode;
use std::fs::File;
use std::io::BufReader;

fn main() -> eyre::Result<()> {
    println!("cargo:rerun-if-changed=ecs.yaml");

    let file = File::open("ecs.yaml").expect("Failed to open ecs.yaml");
    let reader = BufReader::new(file);
    EcsCode::generate(reader)?.write_files()?;
    Ok(())
}

Define your ECS components and systems in a YAML file:

# ecs.yaml
states:
  - name: WgpuRender
    description: The WGPU render state; will be initialized in the Render phase hooks

components:
  - name: Position
  - name: Velocity
  - name: Health
  - name: Collider

archetypes:
  - name: Particle
    description: A particle system particle.
    components:
      - Position
      - Velocity

  - name: Player
    components:
      - Position
      - Velocity
      - Health

  - name: ForegroundObject
    components:
      - Position
      - Collider
    promotions:
      - BackgroundObject

  - name: BackgroundObject
    components:
      - Position
    promotions:
      - ForegroundObject

phases:
  - name: Startup
  - name: FixedUpdate
    fixed: 60 Hz  # or "0.01666 s"
  - name: Update
  - name: Render
    states:
      - use: WgpuRender  # Use state in phase begin/end hooks
        write: true

systems:
  - name: Physics
    phase: FixedUpdate
    context: true
    run_after: []  # optional
    inputs:
      - Velocity
    outputs:
      - Position
  - name: Render
    phase: Render
    manual: true        # Require manual call to world.apply_system_phase_render()
    # on_request: true  # call world.request_render_phase() to allow execution (resets atomically)
    states:
      - use: WgpuRender
        write: false  # optional
    inputs:
      - Position

worlds:
  - name: Main
    archetypes:
      - Particle
      - Player
      - ForegroundObject
      - BackgroundObject

# Optional, if you're feeling lucky
allow_unsafe: true

Include the compile-time generated files:

include!(concat!(env!("OUT_DIR"), "/components_gen.rs"));
include!(concat!(env!("OUT_DIR"), "/archetypes_gen.rs"));
include!(concat!(env!("OUT_DIR"), "/systems_gen.rs"));
include!(concat!(env!("OUT_DIR"), "/world_gen.rs"));

The compiler will tell you which traits and functions to implement.

Command Queue

You will have to implement a command queue. Below is an example for a queue based on crossbeam-channel:

struct CommandQueue {
    sender: crossbeam_channel::Sender<WorldCommand>,
    receiver: crossbeam_channel::Receiver<WorldCommand>,
}

impl CommandQueue {
    pub fn new() -> Self {
        let (sender, receiver) = crossbeam_channel::unbounded();
        Self {
            sender,
            receiver,
        }
    }
}

impl WorldCommandReceiver for CommandQueue {
    type Error = TryRecvError;

    fn recv(&self) -> Result<Option<WorldCommand>, Self::Error> {
        match self.receiver.try_recv() {
            Ok(cmd) => Ok(Some(cmd)),
            Err(TryRecvError::Empty) => Ok(None),
            Err(err) => Err(err),
        }
    }
}

impl WorldCommandSender for CommandQueue {
    type Error = crossbeam_channel::SendError<WorldCommand>;

    fn send(&self, command: WorldCommand) -> Result<(), Self::Error> {
        self.sender.send(command)
    }
}

Examples

WGPU Shader Compilation

Define the WgpuRender state, the WgpuShader component, a WgpuShader archetype that holds it, a WgpuReinit system phase and a WgpuInitShader system that uses the state to update the component:


allow_unsafe: true

states:
  - name: WgpuRender
    description: The WGPU render state (e.g. device, queue, ...)

components:
  - name: WgpuShader

worlds:
  - name: Main
    archetypes:
      - WgpuShader

archetypes:
  - name: WgpuShader
    components:
      - WgpuShader

phases:
  - name: WgpuReinit
    manual: true

systems:
  - name: WgpuInitShader
    phase: WgpuReinit
    states:
      - use: WgpuRender
        write: true
    outputs:
      - WgpuShader

Implement WgpuShaderData to hold shader definitions and the handle:

use wgpu::{ShaderModule, ShaderModuleDescriptor, ShaderSource};

#[derive(Debug, Clone)]
pub struct WgpuShaderData {
    pub descriptor: ShaderModuleDescriptor<'static>,
    pub module: Option<ShaderModule>
}

impl WgpuShaderData {
    pub const fn new(descriptor: ShaderModuleDescriptor<'static>) -> Self {
        Self { descriptor, module: None }
    }
}

Implement the WgpuInitShaderSystem to compile and upload the shader:

use std::convert::Infallible;
use wgpu_resource_manager::{DeviceAndQueue, DeviceId};
use crate::engine::{ApplyWgpuInitShaderSystem, CreateSystem, PhaseEvents, SystemFactory, SystemWgpuReinitPhaseEvents, WgpuInitShaderSystem, WgpuShaderComponent};
use crate::engine::phases::render::WgpuRenderState;

#[derive(Debug, Default)]
pub struct WgpuInitShaderSystemData {
    device_id: DeviceId
}

impl CreateSystem<WgpuInitShaderSystem> for SystemFactory {
    fn create(&self) -> WgpuInitShaderSystem {
        WgpuInitShaderSystem(WgpuInitShaderSystemData::default())
    }
}

impl ApplyWgpuInitShaderSystem for WgpuInitShaderSystem {
    type Error = Infallible;

    fn is_ready(&self, gpu: &mut WgpuRenderState) -> bool {
        gpu.is_ready()
    }

    fn apply_many(&mut self, gpu: &mut WgpuRenderState, shaders: &mut [WgpuShaderComponent]) {
        let Ok(device) = gpu.device() else {
            return;
        };

        let device_changed = device.id() != self.device_id;
        self.device_id = device.id();

        for shader in shaders {
            // Skip over all already initialized shaders.
            if shader.module.is_some() && !device_changed {
                continue;
            }

            let module = device.device().create_shader_module(shader.descriptor.clone());
            shader.module = Some(module);
        }
    }
}

In your world, you can now instantiate shaders and get them back:

fn register_example_shader<E, Q>(world: &MainWorld<E, Q>) -> WgpuShaderEntityRef {
    let entity_id = world.spawn_wgpu_shader(WgpuShaderEntityData {
        wgpu_shader: WgpuShaderData::new(include_wgsl!("shader.wgsl")).into(),
    });

    // Get it back
    self.get_wgpu_shader(entity_id).unwrap()
}

Since the phase is marked manual, it has to be called explicitly:

fn initialize_gpu_resources<E, Q>(world: &MainWorld<E, Q>) {
    world.apply_system_phase_wgpu_reinit();
}

Dependencies

~3.5MB
~68K SLoC