| Crates.io | sillyecs |
| lib.rs | sillyecs |
| version | 0.0.6 |
| created_at | 2025-04-01 23:03:07.418175+00 |
| updated_at | 2025-04-16 12:04:02.734351+00 |
| description | A silly little compile-time generated archetype ECS in Rust |
| homepage | https://github.com/sunsided/sillyecs |
| repository | https://github.com/sunsided/sillyecs.git |
| max_upload_size | |
| id | 1615737 |
| size | 189,713 |
A silly little compile-time generated archetype ECS in Rust.
sillyecs is a build-time dependency. To use it, add this to your Cargo.toml:
[build-dependencies]
sillyecs = "0.0.2"
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.
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)
}
}
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();
}