Rustecs is an Entity/Component System written in Rust. It's being used to develop Von Neumann Defense Force.
Please note that while Rustecs works and is usable, it comes with one caveat. I'm currently developing it for the needs of one specific game, which brings a few limitations:
- It's usable but incomplete. I only implement features I actually need and know how to design. Some features you would expect from a generic entity system aren't there yet. For some missing features I use crazy workarounds in my own code because I'm not sure yet how to design a generic API for them yet.
- Performance is good enough but probably not better. I usually go with the simplest solution for a given problem without worrying about performance too much. So far that has worked well for me, but it might not be enough for you.
If you tried Rustecs and find it lacking, feel free to send me a pull request that addresses your concerns!
The documentation in this README is intended to explain the general concepts of Rustecs and how they should be used. To see an actual example, please take a look at the unit tests: https://github.com/hannobraun/rustecs/blob/master/rustecs/tests/
Rustecs is implemented as a compiler plugin that generates the code for your entity system from declarations in a simple DSL.
Here's what the definition for a simple Entity/Component System can look like.
world! {
components Position, Size, Score;
}
We simply declare the three kinds of components we want to use. From that simple declaration, Rustecs generates a lot of useful code for us.
The example is not complete though, since we haven't defined what the components actually are. Components are just pieces of data. They can be any of Rust's data types.
The following type definitions complete the example.
// Regular struct
struct Position {
x: i16,
y: i16,
}
// Tuple struct
struct Size(u16);
// Simple type definition
type Score = u32;
You might ask yourself why we're defining Score
as u32
and not just use
u32
directly in the world definition above. While that should work (I haven't
actually tried it), it's not recommended. As we will see, the name of the
component's type is used to generate other names, for example the names of the
collections the components are stored in.
You might also have different components that are represented by the same type, so you need the type alias to distinguish between them. Here's an example of what this might look like:
world! {
components Score, Health;
}
// Two different component types that are represented by the same Rust type.
type Score = u32;
type Health = u32;
So far we haven't declared anything about entities. Doesn't Rustecs need to know which types of entities we want to work with?
Well, no. Rustecs doesn't care. In Rustecs, entities are just a bunch of components that are identified by a common id number. The "type" of an entity is solely defined by the components it has.
The following example shows what creating entities looks like.
// Here's the world definition from the example above. I'm not going to repeat
// the type declarations, but they have to be in scope for this to work.
world! {
components Position, Size, Score;
}
// The world! macro generates a type called Entities which is a container for
// entities.
let mut entities = Entities::new();
// Let's create a bunch of entites for our player to interact with. Like
// Entities, Entity is a data structure generated by the world! macro.
entities.add(
Entity::new()
.with_position(8, 12)
.with_size(3)
);
entities.add(
Entity::new()
.with_position(-5, 2)
.with_size(5)
);
// Here we create the entity representing the player.
entities.add(
Entity::new()
.with_position(0, 0)
.with_score(0)
);
// The add method returns the id of the created entity. We can use that to
// destroy the entity later.
let entity_id = entities.add(Entity::new().with_position(10, 10));
entities.remove(entity_id);
If you find yourself repeating the same entity creation code everywhere, you can just wrap that in a function.
fn complicated_entity(x: f32, y: f32) -> Entity {
// Here you could generate some of the arguments randomly, for example.
Entity::new()
.with_position(x, y)
.with_a(some_arguments)
.with_b(other_arguments)
.with_c(yet_another_argument)
}
...
entities.add(complicated_entity(5, 10));
We know how to define entity constructors and components, so we have all the tools we need to populate our world with data. Now we actually need to do something with that data.
In an ECS, the logic of a game is implemented in systems. Systems are just functions that operate on a set of entities. Those entities are defined by the components they have.
As I'm writing this Rustecs doesn't have direct support for systems yet (I'm working on it), but of course you can still write systems that operate on your entities (otherwise Rustecs would be pretty much useless).
Let's take a look at a simple ECS.
world! {
components Position, Velocity;
}
type Position = Vector;
type Velocity = Vector;
struct Vector {
x: i16,
y: i16,
}
Let's add a bunch of cars. Maybe this is a racing game?
fn main() {
let mut entities = Entities::new();
// Create the cars.
entities.add(Entity::new().with_position(0, 0).with_velocity(10, 0));
entities.add(Entity::new().with_position(0, 3).with_velocity(10, 0));
entities.add(Entity::new().with_position(0, 6).with_velocity(10, 0));
// Do something with the cars.
...
}
Of course, cars are no good if they don't move, so let's write a system for that.
fn move_cars(positions: Components<Position>, velocities: Components<Velocity>) {
for (entity_id, position) in positions.iter_mut() {
if !velocities.contains_key(entity_id) {
// There might be entities that have a position but no velocity.
// Ignore those.
continue;
}
// If we have both a position and a velocity, it's a car and supposed to
// move!
let velocity = velocities[entity_id];
position.x += velocity.x;
position.y += velocity.y;
}
}
The system iterates over all entities with a Position
component, checks if the
entity also has a Velocity
component and, if it has one, integrates the
position.
At the moment, Components<T>
is simply defined as HashMap<EntityId, T>
. This
might not be ideal for performance, but it is good enough for now and can be
improved later.
So how do we call that system? Let's complete our main function from above.
fn main() {
let mut entities = Entities::new();
// Create the cars.
entities.add(Entity::new().with_position(0, 0).with_velocity(10, 0));
entities.add(Entity::new().with_position(0, 3).with_velocity(10, 0));
entities.add(Entity::new().with_position(0, 6).with_velocity(10, 0));
// Do something with the cars.
loop {
// For each component type we defined, the world has a collection. We
// just pass those to the systems.
move_cars(entities.positions, entities.velocities);
// In a real game, we'd do other stuff in this loop, like gathering
// player input and rendering the cars.
}
}
As you can see, the facilities for using systems are pretty basic. I already have some plans for adding proper system support to Rustecs, but I need some time to finalize the design and implement it.
A note regarding the collection names: The names of the collections are
generated from the names of the component. Position
becomes positions
,
Velocity
becomes velocities
. This pluralizations works according to a few
simple rules (like, if the word ends on y
, replace it with ies
, otherwise
just add s
to the end). Those simple rules might not cover every case. If a
component name is not what you might expect, please open an issue for that!
We've already learned how to add and remove entities. As you might notice,
however, that approach won't always when adding or removing components from a
system. Adding or removing an element from a Components
collection while we're
iterating over it is not safe, and the Rust compiler will prevent you from doing
it. There's one solution however, the Control
struct.
First, let's define a simple world, which contains both players and enemies, both of which have a position.
world! {
components Position, Player, Enemy;
}
struct Position(f32, f32);
// Enemy and Player are just markers that indicate to a system how the entity
// should behave. They contain no data for now.
struct Player;
struct Enemy;
Here's our main function for our game. This time, we'll create a Control
instance in addition to Entities
.
fn main() {
// When we create our entities container, we'll also create a control.
// Please only create one control per entities container, otherwise entities
// created from one control might overwrite those created from another.
let mut entities = Entities::new();
let mut control = Control::new();
// Let's imagine we create a bunch of players and enemies here. I'll not
// write out the code here to keep the example short.
loop {
// There's a simple rule in our game: If a player touches an enemy, they
// die. This is the system that kills the players. It'll need positions,
// players and enemies to know who to kill and the control to then do
// it.
kill_players(
&entities.positions,
&entities.players,
&entities.enemies,
&mut control,
);
// The system will use the control to kill the players, however, any
// changed made through a control need to applied to the entities
// container explicitely.
control.apply(&mut entities);
// Of course, in a real game we would have many more systems for moving
// players and enemies around and such.
}
}
Ok, so that's how the control is used from outside the system. Let's see how it is used by the system to kill the players.
fn kill_players(
positions: &Components<Position>,
players : &Components<Player>,
enemies : &Components<Enemy>,
control : &mut Control
) {
// Compare the positions of all the players to the positions of all the
// enemies.
for (player_id, _) in players.iter() {
for (enemy_id, _) in enemies.iter() {
if positions[player_id] == positions[enemy_id] {
// They are at the same position. Kill the player!
control.remove(player_id);
// So far, nothing has changed. If we iterate over all the
// players again later, the remove player will still be there.
// It will only truely be removed with the call to apply in the
// main loop above.
}
}
}
}
Besides removing, Control
can also add players and more. Please take a look at
Control
's
unit tests
for the full details.
By default, Rustecs doesn't derive any traits for any of the types it generates.
But what if you want to print Entities
to help with debugging? Or if you want
to serialze Entity
to JSON? Rustecs provides the
derived_traits` directive
to help with such cases.
world! {
components One, Two;
derived_traits Clone, Decodable, Encodable, Show;
}
The traits specified by the derived_traits
directive will be derived for all
generated types, like Entities
and Entity
. Please note that this can only
work if all your component types also implement all those traits.
There are some additional feaures I haven't talked about here, like importing and exporting entities, but as far as basic use cases go, that's pretty much it.
If you have any questions, feel free to contact me.
Copyright (c) 2014, Hanno Braun
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.