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.
All data in froql is stored in a World.
Froql does not use globals behind the scenes.
You can use multiple different Worlds without issue, if you want to.
use froql::world::World;
letmut world = World::new();
An Entity is just an unique identifier.
You can copy it or store it in other data structures.
use froql::world::World;
letmut world = World::new();
let my_entity = world.create_entity();
assert!(world.is_alive(my_entity));
world.destroy(my_entity); // destroy entityassert!(!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;
letmut world = World::new();
let my_entity = world.create_entity(); // create entityassert!(world.is_alive(my_entity));
world.destroy(my_entity); // destroy entitylet new_entity = world.create_entity();
// old id is reusedassert_eq!(new_entity.id, my_entity.id);
// but the old entity is still dead - because of the generationassert!(!world.is_alive(my_entity));
assert_ne!(new_entity.generation, my_entity.generation);
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;
letmut world = World::new();
structMyStruct(u32);
world.register_component::<MyStruct>();
let e = world.create_entity();
world.add_component(e, MyStruct(42)); // add data// mutation
{
letmut 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).
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;
structMyStruct(u32);
fncreate_world() -> World {
letmut world = World::new();
world.register_component::<MyStruct>();
world
}
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.
use froql::world::World;
enumMyRelation {}
letmut 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;
enumMyRelation {}
letmut 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));
use froql::world::World;
use froql::component::EXCLUSIVE;
enumChildOf {}
letmut 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 anymoreassert!(!world.has_relation::<ChildOf>(a,b));
assert!(world.has_relation::<ChildOf>(a,c));
use froql::world::World;
use froql::component::TRANSITIVE;
enumInsideOf {}
letmut 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));
use froql::world::World;
use froql::component::SYMMETRIC;
enumFriends {}
letmut 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));
When A in A->B gets destroyed, B also gets destroyed
use froql::world::World;
use froql::component::CASCADING_DESTRUCT;
enumCleanup {}
letmut 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));
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;
enumBestFriends {}
letmut 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));
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;
structName(&'staticstr);
structAge(u32);
structPlayer{}
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.
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;
structName(&'staticstr);
structAge(u32);
structPlayer{}
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
}
Relations are expressed in the form <Type>(<variable>, <variable>).
use froql::component::TRANSITIVE;
use froql::query;
use froql::world::World;
structName(&'staticstr);
enumIsA {}
letmut 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.
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;
structName(&'staticstr);
enumIsA {}
letmut 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);
}
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;
structHP(i32);
letmut 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 calledassert!(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.
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;
structName(&'staticstr);
enumFoes {}
letmut 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);
letmut 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);
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;
structName(&'staticstr);
enumFoes {}
letmut 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);
letmut 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
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;
structName(&'staticstr);
enumFoes {}
letmut 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);
letmut 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);
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.
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;
enumLikes {}
structName(&'staticstr);
structAge(i32);
letmut 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);
letSome(oldest) = iterator
.max_by_key(|(_, age)| age.0)
.map(|(name, _)| name.0)
else { panic!() };
assert_eq!(oldest, "Paul");