hyperion_ec_no_s

Crates.iohyperion_ec_no_s
lib.rshyperion_ec_no_s
version1.0.2
created_at2025-08-19 04:36:28.996921+00
updated_at2025-08-19 04:46:34.689748+00
descriptionA dense ECS without the S
homepage
repositoryhttps://gitlab.com/thatloganguy/hyperion
max_upload_size
id1801360
size102,851
(PartyLogan)

documentation

README

Hyperion

A dense ECS without the S.


Named after:

The eighth-largest moon of Saturn. It is distinguished by its highly irregular shape, chaotic rotation, low density (lol), and its unusual sponge-like appearance. -- Wikipedia

Quick things:

  • No systems
  • No queries
  • No archetypes
  • Simply component storage managers and a world that handles it with generational ID handles
  • Use the command buffer to queue up changes you set up in your loops so they don't crash and burn.
  • A resource registry to store things like textures, sounds etc. in the world for easy access via handles.
  • Unique resources in the resource registry accessible just via their types.

Why:

I wanted a way to easily move around data for rust game dev and didn't want to manually create vectors for new structs everytime. This allows me to have a world in my gamestate that manages it for me and gives me access to the data everywhere.
I did not want a full ECS, go use hecs or something else if you need systems/queries and speed for accessing smaller lego block components.

Test Speed:

Done using the test: 'large_scale_component_iteration_speed_test_separate_timings'
Ran using: 'cargo test --release large_scale_component_iteration_speed_test_separate_timings -- --nocapture'
1.5mill entities tested - Laptop with an AMD Ryzen AI 7 350.

Loop 1: get_all_components_mut time = 13.9909ms, get_component_mut per entity time = 2.07895s
Loop 2: get_all_components_mut time = 13.8826ms, get_component_mut per entity time = 1.9981674s
Loop 3: get_all_components_mut time = 15.2029ms, get_component_mut per entity time = 2.0337959s
Loop 4: get_all_components_mut time = 14.5411ms, get_component_mut per entity time = 1.7639305s
Loop 5: get_all_components_mut time = 14.0716ms, get_component_mut per entity time = 1.762936s
Average time (get_all_components_mut): 14.33782ms
Average time (get_component_mut per entity): 1.92755596s

On the same laptop in release mode version of the bunny mark linked below I get around 240,000 bunnies at 60fps with Y sorting and 270_000ish without.
Please note that the laptop gpu is very bad and just commenting out the texture draw call (not the loop) lets it hit over 2.5 million bunnies still at 60fps.

Docs

https://docs.rs/hyperion_ec_no_s/latest/hyperion/

Examples:

Tetra bunny mark: https://gitlab.com/thatloganguy/hyperion_bunnymark

Basic usage:

use hyperion::prelude::*;

// Component can be derived or impl
#[derive(Component)]
struct Position(u32, u32);

#[derive(Component)]
struct Health(i32);

fn main() {
    let mut world = World::new();

    // Create an entity and give it some components
    let e = world.create_entity();
    world.add_component(e, Position(1, 2));
    world.add_component(e, Health(100));

    // Read mut
    if let Some(pos) = world.get_component_mut::<Position>(e) {
        pos.0 += 10; // mutate in place
    }
    // Non mut
    if let Some(hp) = world.get_component::<Health>(e) {
        println!("entity health = {}", hp.0);
    }

    // Get the owning Entity for a Position component reference
    // I know this example is dumb but it's just show how it works
    if let Some(pos_ref) = world.get_component::<Position>(e) {
        if let Some(owner) = world.get_entity_for::<Position>(pos_ref) {
            println!("owner of pos is {:?}", owner);
        }
    }

    // Iterate over all Position components
    for p in world.get_all_components_mut::<Position>() {
        p.0 += 1;
        p.1 += 1;
    }

    // Get all entities that have a Position component
    let entities_with_position: Vec<_> = world.get_entities_for::<Position>();

    for entity in entities_with_position {
        if let Some(pos) = world.get_component::<Position>(entity) {
            println!("entity {:?} has Position({}, {})", entity, pos.0, pos.1);
        }
    }

    // Note: This is not a query/system. This is just a quicker way to get entities with multiple components.
    // It is not trying to be like other ECS and only exists for qol. There are no archetypes etc.
    // Get all entities that have both Position and Health,
    let entities_with_pos_and_health = world.get_entities_and_components_with_2::<Position, Health>();
    for (entity, pos, hp) in entities_with_pos_and_health {
        println!(
            "entity {:?}: Position({}, {}), Health({})",
            entity, pos.0, pos.1, hp.0
        );
    }

    // Despawn the entity (removes its components too)
    world.destroy_entity(e);
    // Reset the world.
    world.purge();
}

Command Buffer:

use hyperion::prelude::*;

#[derive(Component)]
struct Position(u32, u32);

fn main() {
    let mut world = World::new();
    let mut commands = CommandBuffer::default();

    // Queue: create 100 entities, each with a Position component
    // You can also just queue raw entities if you want for some reason with create_entity
    for _ in 0..100 {
        commands.create_and_add_component(Position(0, 0));
    }

    // Apply queued creates; get back the Entity IDs that were created - this includes ones without components
    let entities = commands.execute(&mut world);

    // Queue: destroy all of them
    for e in entities {
        commands.destroy_entity(e);
    }

    // Apply queued destroys
    commands.execute(&mut world);
}

Resource Registry:

use hyperion::prelude::*;

#[derive(Debug)]
struct Texture {
    id: u32,
}

#[derive(Debug)]
struct Config {
    max_textures: usize,
    app_name: String,
}

fn main() {
    let mut world = World::new();

    // Make some resources and store the handles (generational IDs like entities)
    let mut handles = Vec::new();
    for i in 0..100 {
        let texture = Texture { id: i };
        let handle = world.register_resource::<Texture>(texture);
        handles.push(handle);
    }

    // Easy to get
    for handle in &handles[0..5] {
        if let Some(tex) = world.get_resource::<Texture>(*handle) {
            println!("Got texture with id: {}", tex.id);
        }
    }

    // Easy to change
    if let Some(tex) = world.get_resource_mut::<Texture>(handles[0]) {
        tex.id = 999;
    }

    // Simple to remove, wow.
    for handle in handles {
        let removed = world.remove_resource::<Texture>(handle);
        if let Some(tex) = removed {
            println!("Removed texture with id: {}", tex.id);
        }
    }

    // Register a unique Config resource - only one of a type can be registered.
    // Reregistering will replace it. This actually is the same for components on entities.
    world.register_unique(Config {
        max_textures: 100,
        app_name: "HyperionApp".to_string(),
    });

    // Wow so easy to grab.
    if let Some(config) = world.get_unique::<Config>() {
        println!("Unique Config: max_textures = {}, app_name = {}", config.max_textures, config.app_name);
    }

    // Don't forget mut
    if let Some(config) = world.get_unique_mut::<Config>() {
        config.max_textures = 200;
        config.app_name = "UpdatedHyperionApp".to_string();
    }

    // It's changed crazy.
    if let Some(config) = world.get_unique::<Config>() {
        println!("Updated Unique Config: max_textures = {}, app_name = {}", config.max_textures, config.app_name);
    }

    // Kill it. Gets it back just in case you need it.
    if let Some(removed_config) = world.remove_unique::<Config>() {
        println!("Removed unique Config: {:?}", removed_config);
    }

    // Yeah it's gone.
    assert!(world.get_unique::<Config>().is_none());
    // Purge it all cause why not.
    world.purge();
}

Final notes

AI was used for most of the comments and generating tests because normally I don't write them. So if things aren't quite right please blame the future overlords.

Commit count: 57

cargo fmt