Introduction to Froql
Froql is a proc_macro based DSL for dealing with graph-like state in Rust. It allows an user to input data, define relations between data objects and then query the data back out in whatever shape is needed at the usage site.
If you are familiar with ECS you might want to jump straight to the Queries chapter to see what froql is all about. Froql isn't really an ECS because it lacks systems but a lot of concepts overlap.
Otherwise skim the book from front to back, experiment and come back when you need to know something.
Install and setup
Running cargo add froql
is enough to add the crate as a dependency.
For improved compilation speed during iterative development it's recommended to add the following to your Cargo.toml
:
[profile.dev.build-override]
opt-level = 3
This compiles proc_macros (including froql's query macro) in release mode.
World, Entities and Components
The World
ゴゴゴゴゴゴゴ -- famous mangaka
All data in froql is stored in a World
.
Froql does not use globals behind the scenes.
You can use multiple different World
s without issue, if you want to.
use froql::world::World;
let mut world = World::new();
Entities
An Entity
is just an unique identifier.
You can copy it or store it in other data structures.
use froql::world::World;
let mut world = World::new();
let my_entity = world.create_entity();
assert!(world.is_alive(my_entity));
world.destroy(my_entity); // destroy entity
assert!(!world.is_alive(my_entity));
let my_entity = world.create_entity();
Use after free and the ABA problem are solved via generation checks.
use froql::world::World;
let mut world = World::new();
let my_entity = world.create_entity(); // create entity
assert!(world.is_alive(my_entity));
world.destroy(my_entity); // destroy entity
let new_entity = world.create_entity();
// old id is reused
assert_eq!(new_entity.id, my_entity.id);
// but the old entity is still dead - because of the generation
assert!(!world.is_alive(my_entity));
assert_ne!(new_entity.generation, my_entity.generation);
Components
To associate data with an Entity
you add to the Entity
as a component.
A component can be any T: 'static
, there are no traits that must be implemented.
use froql::world::World;
let mut world = World::new();
struct MyStruct(u32);
world.register_component::<MyStruct>();
let e = world.create_entity();
world.add_component(e, MyStruct(42)); // add data
// mutation
{
let mut mutable_ref = world.get_component_mut::<MyStruct>(e);
mutable_ref.0 += 1;
}
// immutable reference
{
let imm_ref = world.get_component::<MyStruct>(e);
assert_eq!(43, imm_ref.0);
}
// remove (and drop) component
world.remove_component::<MyStruct>(e);
assert!(!world.has_component::<MyStruct>(e));
Components in froql use interior mutability via RefCell
.
This allows for finegrained access, but may panic at runtime on misuse (violating the aliasing xor mutation rule).
Registering components
Froql needs to know about what types of components it manages. Before a component can be used, it therefore must be registered.
Registration happens automatically when adding a component to an entity.
But it does not happen with methods that borrow World
non mutably, they panic instead
when they encounter an unregistered component.
The autoregistration exists for prototyping purposes and can be disabled by enabling the feature flag manual_registration
.
For larger projects it is recommended to register everything upfront.
use froql::world::World;
struct MyStruct(u32);
fn create_world() -> World {
let mut world = World::new();
world.register_component::<MyStruct>();
world
}
Mutable Entity Views
EntityViewMut
is a helper struct to reduce boilerplate when mutating entities.
It can be used similarly to a builder.
use froql::world::World;
use froql::entity_store::Entity;
struct MyStruct(u32);
struct Name(&'static str);
struct Age(u32);
let mut world = World::new();
world.register_component::<MyStruct>();
world.register_component::<Name>();
world.register_component::<Age>();
let e: Entity = world.create()
.add(MyStruct(42))
.add(Name("Bob"))
.add(Age(25))
.entity;
Relations
A Relation is always between two entities.
You can think of Entities being nodes on a directed graph with Relations being the edges.
Relations are distinguished with a Rust type via its TypeId
.
To prevent accidentally adding a Relation as a Component it is recommended to use uninhabited types for them.
For example an enum with no variants.
A Relation always has an origin and a target.
Registration
Like for Components, it is recommended to register Relations before use.
use froql::world::World;
enum MyRelation {}
fn create_world() -> World {
let mut world = World::new();
world.register_relation::<MyRelation>();
world
}
Adding and removing relations between entities
use froql::world::World;
enum MyRelation {}
let mut world = World::new();
world.register_relation::<MyRelation>();
let a = world.create_entity();
let b = world.create_entity();
world.add_relation::<MyRelation>(a,b);
assert!(world.has_relation::<MyRelation>(a,b));
world.remove_relation::<MyRelation>(a,b);
assert!(!world.has_relation::<MyRelation>(a,b));
In the EntityView the vocubulary is relate
and unrelate
.
use froql::world::World;
enum MyRelation {}
let mut world = World::new();
world.register_relation::<MyRelation>();
let b = world.create_entity();
let a = world.create().relate_to::<MyRelation>(b).entity;
assert!(world.has_relation::<MyRelation>(a,b));
world.view_mut(a).unrelate_to::<MyRelation>(b);
assert!(!world.has_relation::<MyRelation>(a,b));
Relation Flags
By default relations are directed, many-to-many and non-transitive. But this behavior can be changed when registering the relation using flags.
Exclusive Relations
A -> B
implies there is no A->C
use froql::world::World;
use froql::component::EXCLUSIVE;
enum ChildOf {}
let mut world = World::new();
world.register_relation_flags::<ChildOf>(EXCLUSIVE);
let a = world.create_entity();
let b = world.create().relate_from::<ChildOf>(a).entity;
assert!(world.has_relation::<ChildOf>(a,b));
let c = world.create().relate_from::<ChildOf>(a).entity;
// a is not in a ChildOf relation to b anymore
assert!(!world.has_relation::<ChildOf>(a,b));
assert!(world.has_relation::<ChildOf>(a,c));
Transitive Relations
A -> B -> C
implies A->C
use froql::world::World;
use froql::component::TRANSITIVE;
enum InsideOf {}
let mut world = World::new();
world.register_relation_flags::<InsideOf>(TRANSITIVE);
let house = world.create_entity();
let room = world.create().relate_to::<InsideOf>(house).entity;
let guy = world.create().relate_to::<InsideOf>(room).entity;
assert!(world.has_relation::<InsideOf>(guy, room));
assert!(world.has_relation::<InsideOf>(guy, house));
Symmetric Relations
A -> B
implies B->A
use froql::world::World;
use froql::component::SYMMETRIC;
enum Friends {}
let mut world = World::new();
world.register_relation_flags::<Friends>(SYMMETRIC);
let anna = world.create_entity();
let otto = world.create().relate_to::<Friends>(anna).entity;
assert!(world.has_relation::<Friends>(anna, otto));
assert!(world.has_relation::<Friends>(otto, anna));
Cascading deletion
When A
in A->B
gets destroyed, B
also gets destroyed
use froql::world::World;
use froql::component::CASCADING_DESTRUCT;
enum Cleanup {}
let mut world = World::new();
world.register_relation_flags::<Cleanup>(CASCADING_DESTRUCT);
let resource = world.create_entity();
let container = world.create().relate_to::<Cleanup>(resource).entity;
let outer_container = world.create().relate_to::<Cleanup>(container).entity;
// destruction is propagated
world.destroy(outer_container);
assert!(!world.is_alive(outer_container));
assert!(!world.is_alive(container));
assert!(!world.is_alive(resource));
Multiple Flags
You can pass multiple flags when registering a relation by xoring them together.
use froql::world::World;
use froql::component::SYMMETRIC;
use froql::component::EXCLUSIVE;
enum BestFriends {}
let mut world = World::new();
world.register_relation_flags::<BestFriends>(SYMMETRIC | EXCLUSIVE);
let mustadir = world.create_entity();
let asif = world.create_entity();
world.add_relation::<BestFriends>(asif, mustadir);
let salman = world.create_entity();
// friendship ended with mustadir, now salman is my best friend
world.add_relation::<BestFriends>(asif, salman);
assert!(!world.has_relation::<BestFriends>(asif, mustadir));
assert!(world.has_relation::<BestFriends>(salman, asif));
Singletons
Singletons are components that only exist once in the World
. They are attached to the singleton Entity
.
use froql::world::World;
struct DeltaTime(f32);
let mut world = World::new();
// create singleton
world.singleton_add(DeltaTime(1./60.));
// access singleton
assert_eq!(world.singleton::<DeltaTime>().0, 1./60.);
// mutate singleton
world.singleton_mut::<DeltaTime>().0 = 1.;
assert_eq!(world.singleton::<DeltaTime>().0, 1.);
// remove singleton
world.singleton_remove::<DeltaTime>();
Queries
Queries in froql are proc-macros.
A query!
always needs a reference to a World
as first argument.
After the World
a comma separated list of terms follows, which define the output of the query.
Query for Components
Components can be queried by writing their type name as terms.
use froql::world::World;
use froql::entity_store::Entity;
use froql::query;
struct Name(&'static str);
struct Age(u32);
let world = &mut World::new();
world.register_component::<Name>();
world.register_component::<Age>();
world.create()
.add(Name("Bob"))
.add(Age(25));
world.create()
.add(Name("Anna"))
.add(Age(32));
let mut check = 0;
for (name, age) in query!(world, Name, Age) {
println!("{} is {} years old.", name.0, age.0);
check += age.0;
}
assert_eq!(57, check);
This prints:
Bob is 25 years old.
Anna is 32 years old.
Ignoring components in the result
If you only care that a component exists but don't care about its value you can ignore by prefixing the term with _
.
Note that the space is not optional, since typenames in Rust can start with an underscore.
Example:
use froql::world::World;
use froql::entity_store::Entity;
use froql::query;
struct Name(&'static str);
struct Age(u32);
struct Player{}
let world = &mut World::new();
world.register_component::<Name>();
world.register_component::<Age>();
world.register_component::<Player>();
// ...
world.create()
.add(Name("Bob"))
.add(Age(25))
.add(Player{});
world.create()
.add(Name("Anna"))
.add(Age(32));
for (name, age) in query!(world, Name, Age, _ Player) {
// ...
}
The query here only matches Bob, since he is the only one tagged as Player
.
But the result tuple is not modified.
Component sources
Components always have a source, called a variable.
If no variable is given this
is used as default.
The source is specified in parenthesis after the component name.
So the query in the previous example is equivalent to:
use froql::world::World;
use froql::entity_store::Entity;
use froql::query;
struct Name(&'static str);
struct Age(u32);
struct Player{}
let world = &mut World::new();
world.register_component::<Name>();
world.register_component::<Age>();
world.register_component::<Player>();
world.create()
.add(Name("Bob"))
.add(Age(25))
.add(Player{});
world.create()
.add(Name("Anna"))
.add(Age(32));
for (name, age) in query!(world, Name(this), Age(this), _ Player(this)) {
// ... only matches Bob
}
Mutating Components
Components can be mutably borrowed in a query by prefixing the term with mut
.
So the query in the previous example is equivalent to:
use froql::world::World;
use froql::entity_store::Entity;
use froql::query;
struct Name(&'static str);
struct Age(u32);
let world = &mut World::new();
world.register_component::<Name>();
world.register_component::<Age>();
world.create()
.add(Name("Bob"))
.add(Age(25));
world.create()
.add(Name("Anna"))
.add(Age(32));
let mut check = 0;
// a year passes
for (mut age,) in query!(world, mut Age) {
age.0 += 1;
}
for (name, age) in query!(world, Name, Age) {
println!("{} is {} years old.", name.0, age.0);
check += age.0;
}
assert_eq!(59, check);
Now it prints:
Bob is 26 years old.
Anna is 33 years old.
Query for Relations
Relations are expressed in the form <Type>(<variable>, <variable>)
.
use froql::component::TRANSITIVE;
use froql::query;
use froql::world::World;
struct Name(&'static str);
enum IsA {}
let mut world = World::new();
world.register_relation_flags::<IsA>(TRANSITIVE);
let food = world.create().add(Name("Food")).entity;
let fruit = world.create().add(Name("Fruit")).relate_to::<IsA>(food).entity;
world.create().add(Name("Tomato")).relate_to::<IsA>(fruit);
world.create().add(Name("Bread")).relate_to::<IsA>(food);
for (a, b) in query!(world, Name(a), Name(b), IsA(a, b)) {
println!("{} is a {}", a.0, b.0);
}
If you only care about an entity being a relation target or origin you can use the form <Type>(<variable>, _)
or <Type>(_, <variable>)
.
This will only match the entity in question once.
Outvars: getting matched Entities
Sometimes you want to get the Entity behind a variable.
For this you can use a term of the form &<variable>
.
use froql::component::TRANSITIVE;
use froql::query;
use froql::world::World;
struct Name(&'static str);
enum IsA {}
let mut world = World::new();
world.register_relation_flags::<IsA>(TRANSITIVE);
let food = world.create().add(Name("Food")).entity;
let fruit = world.create().add(Name("Fruit")).relate_to::<IsA>(food).entity;
world.create().add(Name("Tomato")).relate_to::<IsA>(fruit);
world.create().add(Name("Bread")).relate_to::<IsA>(food);
for (entity_a, a, b) in query!(world, &a, Name(a), Name(b), IsA(a, b)) {
dbg!(entity_a);
println!("{} is a {}", a.0, b.0);
}
Modifying entities during a query
The returned entity is wrapped in an EntityViewDeferred
.
Like the name implies structural changes on this entity are deferred until world.process()
is called, so as to not invalidate our iterator.
use froql::query;
use froql::world::World;
struct HP(i32);
let mut world = World::new();
let e = world.create().add(HP(-5)).entity;
for (entity, hp) in query!(world, &this, HP) {
if hp.0 <= 0 {
entity.destroy();
}
}
// entity is only destroyed once world.process() is called
assert!(world.is_alive(e));
world.process();
assert!(!world.is_alive(e));
You can also use this to add/remove components or relationships to an entity during query iteration.
You can also spawn entities using world.create_deferred()
and directly use them as normal.
Invars: setting a query variable to a fixed value
It's often necessary to fix a query variable to an Entity coming from an outer scope.
A variable counts as invar if it is prefixed with a star (*
) at least once in the query.
use froql::query;
use froql::world::World;
use froql::component::SYMMETRIC;
struct Name(&'static str);
enum Foes {}
let mut world = World::new();
world.register_relation_flags::<Foes>(SYMMETRIC);
let player = world.create().add(Name("Player")).entity;
let goblin = world.create().add(Name("Goblin")).relate_to::<Foes>(player).entity;
world.create().add(Name("Villager")).relate_to::<Foes>(goblin);
let mut counter = 0;
for (name,) in query!(world, Name, Foes(this, *player)) {
println!("{} is an enemy of the player", name.0);
counter += 1;
}
assert_eq!(1, counter);
Unrelations: negative Relation constraints
Prefix a relation type with !
to match entities that don't have that relation.
use froql::query;
use froql::world::World;
use froql::component::SYMMETRIC;
struct Name(&'static str);
enum Foes {}
let mut world = World::new();
world.register_relation_flags::<Foes>(SYMMETRIC);
let player = world.create().add(Name("Player")).entity;
let goblin = world.create().add(Name("Goblin")).relate_to::<Foes>(player).entity;
world.create().add(Name("Villager")).relate_to::<Foes>(goblin);
let mut counter = 0;
for (name,) in query!(world, Name, ! Foes(this, *player)) {
println!("{} is not an enemy of the player", name.0);
counter += 1;
}
assert_eq!(2, counter);
This prints:
Player is not an enemy of the player
Villager is not an enemy of the player
Uncomponents: negative Component constraints
Prefix a component type with !
to match entities that don't have that component.
use froql::world::World;
use froql::entity_store::Entity;
use froql::query;
struct Name(&'static str);
struct Age(u32);
struct Player{}
let world = &mut World::new();
world.register_component::<Name>();
world.register_component::<Age>();
world.register_component::<Player>();
// ...
world.create()
.add(Name("Bob"))
.add(Age(25))
.add(Player{});
world.create()
.add(Name("Anna"))
.add(Age(32));
let mut counter = 0;
for (name, age) in query!(world, Name, Age, ! Player) {
// ... only matches Anna
assert_eq!(name.0, "Anna");
counter += 1;
}
assert_eq!(1, counter);
Unequalities
The term <variable_a> != <variable_b>
makes sure that the two variables don't have the same entity as value.
This is especially useful for preventing dynamic borrowing errors when mutably borrowed components.
use froql::query;
use froql::world::World;
use froql::component::SYMMETRIC;
struct Name(&'static str);
enum Foes {}
let mut world = World::new();
world.register_relation_flags::<Foes>(SYMMETRIC);
let player = world.create().add(Name("Player")).entity;
let goblin = world.create().add(Name("Goblin")).relate_to::<Foes>(player).entity;
world.create().add(Name("Villager")).relate_to::<Foes>(goblin);
let mut counter = 0;
for (name,) in query!(world, Name, ! Foes(this, *player), this != player) {
println!("{} is not an enemy of the player", name.0);
counter += 1;
}
assert_eq!(1, counter);
This prints:
Villager is not an enemy of the player
Matching singletons
Singletons can be accessed through the world API.
For convenience they also can be accessed via query, by prefixing their typename with $
.
use froql::query;
use froql::world::World;
use froql::component::SYMMETRIC;
struct DeltaTime(f32);
struct Animation {time_left: f32}
let mut world = World::new();
world.create().add(Animation {time_left: 5.});
world.singleton_add(DeltaTime(1./60.));
let mut counter = 0;
for (dt, mut animation, animation_e)
in query!(world, $ DeltaTime, mut Animation, &this) {
animation.time_left -= dt.0;
if animation.time_left < 0.0 {
animation_e.destroy();
}
counter += 1;
}
world.process();
assert_eq!(1, counter);
Query limitations
Out joins in queries are not allowed.
So a query like query!(world, Name(a), Name(b), a != b)
will not compile.
This limitation is put in place intentionally, so that the user does not get O(n^2) scaling on accident.
If an outerjoin is desired you can nest queries.
Nested queries
use froql::query;
use froql::world::World;
enum Likes {}
struct Name(&'static str);
let mut world = World::new();
world.register_relation::<Likes>();
world.create().add(Name("Jack"));
world.create().add(Name("Paul"));
world.create().add(Name("Fred"));
for (a,) in query!(world, &this, _ Name) {
for (b,) in query!(world, &this, _ Name, this != *a) {
a.relate_to::<Likes>(*b);
}
}
world.process(); // don't forget this part !
let mut counter = 0;
for (a,b) in query!(world, Name(a), Name(b), Likes(a,b)) {
println!("{} likes {}.", a.0, b.0);
counter += 1;
}
assert_eq!(6, counter);
Outputs:
Jack likes Paul.
Fred likes Paul.
Paul likes Jack.
Fred likes Jack.
Jack likes Fred.
Paul likes Fred.
Queries are iterators
Something that may not be obvious from the examples so far is that query!(..)
returns an iterator.
So you can use all the normal iterator methods on them.
use froql::query;
use froql::world::World;
enum Likes {}
struct Name(&'static str);
struct Age(i32);
let mut world = World::new();
world.create().add(Name("Jack")).add(Age(32));
world.create().add(Name("Paul")).add(Age(42));
world.create().add(Name("Fred")).add(Age(21));
let iterator = query!(world, Name, Age);
let Some(oldest) = iterator
.max_by_key(|(_, age)| age.0)
.map(|(name, _)| name.0)
else { panic!() };
assert_eq!(oldest, "Paul");
Multihop queries
The other examples haven't shown this, but its possible to have far more than two variables in a query.
So a query like
query!(world, Comp(a), Comp(e), Rel1(a,b), Rel2(b,c), Rel3(c,d), Rel(d,e))
is possible.
Glossary
term | explanation | example |
---|---|---|
entity | something that can have components and relationships | |
component | a struct attached to an entity | Health (Health is a normal Rust type) |
relation | a connection between two entities | Friends(a,b) (Friends is a normal Rust type) |
variable | a standin for an entity in a query | Health(a) <- a is a variable |
component access | ||
mut component access | ||
singleton | something that only exists once in a World | world.singleton::<GameTicks>() |
outvar | entity variable that should be returned by the query | &this |
invar | a value for an entity that is passed into a query | Health(\*me) |
constraint | something that filters out results from a query | this != that |
uncomponent | negative component constraint, filters out results where var has component | !Health |
unrelation | negative relation constraint, filters out results where Relation is present | !ChildOf(this, other) |
create | creates an entity or entityview | let e = world.create() |
destroy | removes an entity and cleans up its relations and components | e.destroy() |
add | adds a component to an entity | e.add(Comp{}) |
remove | removes a component from an entity | e.remove::<Comp>() |
relate | creates a relation between two entities | a.relate<sub>to</sub>::<Friend>(b) |
unrelate | removes a relation between two entities | a.unrelate_to_::<Friend>(b) |
immediate | a change of entities, components or relations is immediately executed | e.add(Comp{}); (with a mutable EntityView) |
deferred | a change is queued up until World::process() is called | e.add(Comp{}); (with a EntityViewDeferred) |
exclusive | Rel(a,b) gets removed when Rel(a,c) is created | |
reflexive | Rel(a,b) also means Rel(b,a) | |
transitive | Rel(a,b) and Rel(b,c) means Rel(a,c) implicitly | |
cascading delete | when a from Rel(a,b) gets destroyed, then b also gets destroyed |