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.
Examples:
Tetra bunny mark: https://gitlab.com/thatloganguy/hyperion_bunnymark
Basic usage:
use hyperion::Component;
use hyperion::World;
#[derive(Component)]
struct Position(u32, u32);
#[derive(Component)]
struct Health(i32);
fn main() {
let mut world = World::new();
let e = world.create_entity();
world.add_component(e, Position(1, 2));
world.add_component(e, Health(100));
if let Some(pos) = world.get_component_mut::<Position>(e) {
pos.0 += 10; }
if let Some(hp) = world.get_component::<Health>(e) {
println!("entity health = {}", hp.0);
}
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);
}
}
for p in world.get_all_components_mut::<Position>() {
p.0 += 1;
p.1 += 1;
}
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);
}
}
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
);
}
world.destroy_entity(e);
world.purge();
}
Command Buffer:
use hyperion::{World, CommandBuffer, Component};
#[derive(Component)]
struct Position(u32, u32);
fn main() {
let mut world = World::new();
let mut commands = CommandBuffer::default();
for _ in 0..100 {
commands.create_and_add_component(Position(0, 0));
}
let entities = commands.execute(&mut world);
for e in entities {
commands.destroy_entity(e);
}
commands.execute(&mut world);
}
Resource Registry:
use hyperion::{World, ResourceHandle};
#[derive(Debug)]
struct Texture {
id: u32,
}
#[derive(Debug)]
struct Config {
max_textures: usize,
app_name: String,
}
fn main() {
let mut world = World::new();
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);
}
for handle in &handles[0..5] {
if let Some(tex) = world.get_resource::<Texture>(*handle) {
println!("Got texture with id: {}", tex.id);
}
}
if let Some(tex) = world.get_resource_mut::<Texture>(handles[0]) {
tex.id = 999;
}
for handle in handles {
let removed = world.remove_resource::<Texture>(handle);
if let Some(tex) = removed {
println!("Removed texture with id: {}", tex.id);
}
}
world.register_unique(Config {
max_textures: 100,
app_name: "HyperionApp".to_string(),
});
if let Some(config) = world.get_unique::<Config>() {
println!("Unique Config: max_textures = {}, app_name = {}", config.max_textures, config.app_name);
}
if let Some(config) = world.get_unique_mut::<Config>() {
config.max_textures = 200;
config.app_name = "UpdatedHyperionApp".to_string();
}
if let Some(config) = world.get_unique::<Config>() {
println!("Updated Unique Config: max_textures = {}, app_name = {}", config.max_textures, config.app_name);
}
if let Some(removed_config) = world.remove_unique::<Config>() {
println!("Removed unique Config: {:?}", removed_config);
}
assert!(world.get_unique::<Config>().is_none());
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.