Skip to content

Instantly share code, notes, and snippets.

@kvark
Last active March 31, 2016 14:08
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kvark/168b132818aa6f6ef4db to your computer and use it in GitHub Desktop.
Save kvark/168b132818aa6f6ef4db to your computer and use it in GitHub Desktop.
Future ECS experiments in Rust

See https://github.com/amethyst/amethyst/wiki/ECS-Design-Crossroads

Proposed approach

  • Use "in-place modification" with "generational ID".
  • Synchronize between systems using Future concept.

Systems are scheduled in the same order they are added/registered. Actual execution is delayed until the requested components are available. Obviously, the modified components require nothing else to be working with them at this moment.

Previous design was aimed at constraining the systems interface on the type level:

trait System {
    type Inputs: ComponentList;
    type Extras: ComponentList;
    type Output: Component;
    
    fn update(inputs: Self::Inputs::Data, extras: Self::Extras::ExtraData) -> Self::Output;
}

The next step

I figured there is no point in forcing the compile time interface on the systems. Instead, they can just schedule updates as free as they need:

impl System for MySystem {
    fn process(&mut self, sh: &mut Schedule) {
        sh.get_mut::<Position>().with(sh.get::<Speed>()).iter(|(pos, speed)| {
            *pos += *speed;
        });
    }
}

The juice of this approach is contained within the Schedule interface. It takes care of providing the component data and synchronizing workloads:

impl Schedule {
  fn get<T>(&mut self) -> FutureData<&T> {...}
  fn get_mut<T>(&mut self) -> FutureData<&mut T> {...}
}
impl FutureData<T> {
  fn with<Q>(self, other: FutureData<Q>) -> FutureData<(T, Q)> {...}
  fn iter<F>(self, fun: F) where F: Fn<T> {
    // records a new workload
    // hooks up all the signals to make it kick in after the dependencies are available
    // updates the dependent futures to wait for this operation for the next systems that need them
    //   note: this requires the `Schedule` itself, not sure how to get it nicely
    // returns immediately
  }
}

Advantages:

  1. This is simple, both from the API point of view and the implementation. There is no need to translate compile-time information about inputs/outputs to the run-time and schedule it. This is done automatically now with signals/futures.
  2. More freedom for the systems. They can now access the components conditionally (thus, no need for the "optional component" concept). They can aggregate sub-systems easily, and even schedule different independent workloads.
@HeroesGrave
Copy link

  1. Collection of component IDs. Sub-variants:
    1. Optional indices = (Option, Option, ...)
      • rather inefficient, considering that Option doubles the size of each index

If you special-case the 0 index to be a 'null' index, You can store it as a plain u64 (or if you want type-safety and have nightly, Option<NonZero<u64>>)

@OvermindDL1
Copy link

For the
Systems are scheduled in the same order they are added/registered
in your gist, perhaps let them register with an arbitrary integer for a priority so they can be sorted on adding, any with same priority are sorted in order. That is something I wish I did in mine instead of manual implacement.

And yes, Entity ID 0 should always be the 'null'/empty Entity, used as the empty placeholder. You can either disallow components on it or you could allow components, say for global settings or so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment