Creating our struct
Before we get started, in our src/lib.rs
file we should add mod archetype;
and then make a
file at src/archetype.rs
. All of the code in this chapter will be inside of this archetype.rs
file
Our archetype struct needs to be able to store any number of component columns. The
way to do this normally would be a Vec<ComponentColumn>
however our component columns
are going to be Vec's which are generic over the type they store. This is a bit problematic
for since we want to store a set of things that aren't the same type
Luckily rust has a way of yeeting type information away- trait objects. We can implement a trait
for every type of Vec and then store a Vec<Box<dyn TypeErasedVec>>
. We can then downcase the
trait object back to a concrete type whenever we need to. e.g. when we add/remove components
#![allow(unused)] fn main() { use std::any::Any; trait ComponentColumn: Any { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; } impl<T: 'static> ComponentColumn for Vec<T> { fn as_any(&self) -> &dyn Any { self } fn as_any_mut(&mut self) -> &mut dyn Any { self } } struct Archetype { entities: Vec<crate::Entity>, columns: Vec<Box<dyn ComponentColumn>>, } }
Here we define our ComponentColumn
trait to be a supertrait of the Any
trait. You might not
have come across the Any
trait before, it doesn't tend to come up very often when writing rust.
The general idea behind the Any
trait is that we cast a type to a trait object and then call
downcast_ref/mut
with a generic to turn it back into a concrete type.
You can see the docs for std::any::Any here
Methods for creating an instance of our Archetype struct
We need some methods for creating instances of our Archetype
struct. There are two situations that
we'll be needing to create an Archetype
in:
- When we add/remove a component we'll have to create the archetype that we need to move the entity to
- When we spawn an entity we'll have to create an archetype matching the component's given
We'll start with the add/remove situation because that's going to be easier to write.
The first thing we want to do is make a method on our ComponentColumn
trait that returns a
Box<dyn ComponentColumn>
of the same type of Vec. This will let us create an archetype with the same
set of columns as an existing one.
#![allow(unused)] fn main() { trait ComponentColumn: Any { fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; fn new_empty_column(&self) -> Box<dyn ComponentColumn>; } impl<T: 'static> ComponentColumn for Vec<T> { fn as_any(&self) -> &dyn Any { self } fn as_any_mut(&mut self) -> &mut dyn Any { self } fn new_empty_column(&self) -> Box<dyn ComponentColumn> { Box::new(Vec::<T>::new()) } } }
Now lets look at the method for creating the archetype we want to move our entity to after an add/remove op:
#![allow(unused)] fn main() { impl Archetype { fn new_from_add<T: 'static>(from_archetype: &Archetype) -> Archetype { let mut columns: Vec<_> = from_archetype .columns .iter() .map(|column| column.new_empty_column()) .collect(); todo!("Actually add the column"); Archetype { entities: Vec::new(), columns, } } fn new_from_remove<T: 'static>(from_archetype: &Archetype) -> Archetype { let mut columns: Vec<_> = from_archetype .columns .iter() .map(|column| column.new_empty_column()) .collect(); todo!("Actually remove the column"); Archetype { entities: Vec::new(), columns, } } } }
We could *technically* merge these into one method and choose whether to add or remove
depending on whether Vec<T>
is already present in the columns. However whenever we call
this method we'll *know* whether it's meant to be an add/remove, so if we make them separate
methods we can add some checks that it's valid to add/remove a component from the archetype.
The code for new_from_add
:
#![allow(unused)] fn main() { impl Archetype { fn new_from_add<T: 'static>(from_archetype: &Archetype) -> Archetype { let mut columns: Vec<_> = from_archetype .columns .iter() .map(|column| column.new_empty_column()) .collect(); /* snip */ assert!(columns .iter() .find(|column| column.as_any().is::<Vec<T>>()) .is_none()); columns.push(Box::new(Vec::<T>::new())); /* snip */ Archetype { entities: Vec::new(), columns, } } } }
We just assert!
here instead of trying to recover because it's going to be a bug in our ECS if
this assert fires and we really want that to happen loudly.
The code for new_from_remove
:
#![allow(unused)] fn main() { fn new_from_remove<T: 'static>(from_archetype: &Archetype) -> Archetype { let mut columns: Vec<_> = from_archetype .columns .iter() .map(|column| column.new_empty_column()) .collect(); /* snip */ let idx = columns .iter() .position(|column| column.as_any().is::<Vec<T>>()).unwrap(); columns.remove(idx); /* snip */ Archetype { entities: Vec::new(), columns, } } }
Same reason as above for why we just panic in here rather than try to recover :)
That should be everything we need for these two functions but before we move on lets add some tests to verify everything is working correctly
Tests for add/remove constructors
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; /* todo tests */ } }
First tests we should add are some simple ones for testing those asserts/unwraps
fire when trying to call new_from_(add/remove)
on incorrect archetypes.
use std::any::Any;
trait ComponentColumn: Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn new_empty_column(&self) -> Box<dyn ComponentColumn>;
}
impl<T: 'static> ComponentColumn for Vec<T> {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn new_empty_column(&self) -> Box<dyn ComponentColumn> {
Box::new(Vec::<T>::new())
}
}
struct Archetype {
entities: Vec<crate::Entity>,
columns: Vec<Box<dyn ComponentColumn>>,
}
impl Archetype {
fn new_from_add<T: 'static>(from_archetype: &Archetype) -> Archetype {
let mut columns: Vec<_> = from_archetype
.columns
.iter()
.map(|column| column.new_empty_column())
.collect();
assert!(columns
.iter()
.find(|column| column.as_any().is::<Vec<T>>())
.is_none());
columns.push(Box::new(Vec::<T>::new()));
Archetype {
entities: Vec::new(),
columns,
}
}
fn new_from_remove<T: 'static>(from_archetype: &Archetype) -> Archetype {
let mut columns: Vec<_> = from_archetype
.columns
.iter()
.map(|column| column.new_empty_column())
.collect();
let idx = columns
.iter()
.position(|column| column.as_any().is::<Vec<T>>())
.unwrap();
columns.remove(idx);
Archetype {
entities: Vec::new(),
columns,
}
}
fn builder() -> ColumnsBuilder {
ColumnsBuilder(Vec::new())
}
fn new_from_columns(columns: ColumnsBuilder) -> Archetype {
Archetype {
entities: Vec::new(),
columns: columns.0,
}
}
}
struct ColumnsBuilder(Vec<Box<dyn ComponentColumn>>);
impl ColumnsBuilder {
fn with_column_type<T: 'static>(mut self) -> Self {
if let Some(_) = self
.0
.iter()
.find(|col| col.as_any().type_id() == std::any::TypeId::of::<Vec<T>>())
{
panic!("Attempted to create invalid archetype");
}
self.0.push(Box::new(Vec::<T>::new()));
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_removes() {
let archetype = Archetype {
entities: Vec::new(),
columns: Vec::new(),
};
let archetype = Archetype::new_from_add::<u32>(&archetype);
assert!(archetype.columns.len() == 1);
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u32>>())
.is_some());
let archetype = Archetype::new_from_add::<u64>(&archetype);
assert!(archetype.columns.len() == 2);
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u32>>())
.is_some());
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u64>>())
.is_some());
let archetype = Archetype::new_from_remove::<u32>(&archetype);
assert!(archetype.columns.len() == 1);
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u64>>())
.is_some());
}
#[test]
#[should_panic]
fn add_preexisting() {
let archetype = Archetype {
entities: Vec::new(),
columns: Vec::new(),
};
let archetype = Archetype::new_from_add::<u32>(&archetype);
let archetype = Archetype::new_from_add::<u32>(&archetype);
}
#[test]
#[should_panic]
fn remove_unpresent() {
let archetype = Archetype {
entities: Vec::new(),
columns: Vec::new(),
};
let archetype = Archetype::new_from_remove::<u32>(&archetype);
}
#[test]
#[should_panic]
fn remove_unpresent_2() {
let archetype = Archetype {
entities: Vec::new(),
columns: Vec::new(),
};
let archetype = Archetype::new_from_add::<u64>(&archetype);
let archetype = Archetype::new_from_remove::<u32>(&archetype);
}
#[test]
fn columns_builder() {
let archetype = Archetype::new_from_columns(
Archetype::builder()
.with_column_type::<u32>()
.with_column_type::<u64>()
.with_column_type::<bool>(),
);
assert!(archetype.columns.len() == 3);
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u32>>())
.is_some());
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u64>>())
.is_some());
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<bool>>())
.is_some());
}
#[test]
#[should_panic]
fn columns_builder_duplicate() {
let archetype = Archetype::new_from_columns(
Archetype::builder()
.with_column_type::<u32>()
.with_column_type::<u32>(),
);
}
}
The next test we want is just something to call new_from_(add/remove)
a bunch and assert
that all the columns that we expect to be present are actually present:
use std::any::Any;
trait ComponentColumn: Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn new_empty_column(&self) -> Box<dyn ComponentColumn>;
}
impl<T: 'static> ComponentColumn for Vec<T> {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn new_empty_column(&self) -> Box<dyn ComponentColumn> {
Box::new(Vec::<T>::new())
}
}
struct Archetype {
entities: Vec<crate::Entity>,
columns: Vec<Box<dyn ComponentColumn>>,
}
impl Archetype {
fn new_from_add<T: 'static>(from_archetype: &Archetype) -> Archetype {
let mut columns: Vec<_> = from_archetype
.columns
.iter()
.map(|column| column.new_empty_column())
.collect();
assert!(columns
.iter()
.find(|column| column.as_any().is::<Vec<T>>())
.is_none());
columns.push(Box::new(Vec::<T>::new()));
Archetype {
entities: Vec::new(),
columns,
}
}
fn new_from_remove<T: 'static>(from_archetype: &Archetype) -> Archetype {
let mut columns: Vec<_> = from_archetype
.columns
.iter()
.map(|column| column.new_empty_column())
.collect();
let idx = columns
.iter()
.position(|column| column.as_any().is::<Vec<T>>())
.unwrap();
columns.remove(idx);
Archetype {
entities: Vec::new(),
columns,
}
}
fn builder() -> ColumnsBuilder {
ColumnsBuilder(Vec::new())
}
fn new_from_columns(columns: ColumnsBuilder) -> Archetype {
Archetype {
entities: Vec::new(),
columns: columns.0,
}
}
}
struct ColumnsBuilder(Vec<Box<dyn ComponentColumn>>);
impl ColumnsBuilder {
fn with_column_type<T: 'static>(mut self) -> Self {
if let Some(_) = self
.0
.iter()
.find(|col| col.as_any().type_id() == std::any::TypeId::of::<Vec<T>>())
{
panic!("Attempted to create invalid archetype");
}
self.0.push(Box::new(Vec::<T>::new()));
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_removes() {
let archetype = Archetype {
entities: Vec::new(),
columns: Vec::new(),
};
let archetype = Archetype::new_from_add::<u32>(&archetype);
assert!(archetype.columns.len() == 1);
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u32>>())
.is_some());
let archetype = Archetype::new_from_add::<u64>(&archetype);
assert!(archetype.columns.len() == 2);
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u32>>())
.is_some());
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u64>>())
.is_some());
let archetype = Archetype::new_from_remove::<u32>(&archetype);
assert!(archetype.columns.len() == 1);
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u64>>())
.is_some());
}
#[test]
#[should_panic]
fn add_preexisting() {
let archetype = Archetype {
entities: Vec::new(),
columns: Vec::new(),
};
let archetype = Archetype::new_from_add::<u32>(&archetype);
let archetype = Archetype::new_from_add::<u32>(&archetype);
}
#[test]
#[should_panic]
fn remove_unpresent() {
let archetype = Archetype {
entities: Vec::new(),
columns: Vec::new(),
};
let archetype = Archetype::new_from_remove::<u32>(&archetype);
}
#[test]
#[should_panic]
fn remove_unpresent_2() {
let archetype = Archetype {
entities: Vec::new(),
columns: Vec::new(),
};
let archetype = Archetype::new_from_add::<u64>(&archetype);
let archetype = Archetype::new_from_remove::<u32>(&archetype);
}
#[test]
fn columns_builder() {
let archetype = Archetype::new_from_columns(
Archetype::builder()
.with_column_type::<u32>()
.with_column_type::<u64>()
.with_column_type::<bool>(),
);
assert!(archetype.columns.len() == 3);
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u32>>())
.is_some());
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<u64>>())
.is_some());
assert!(archetype
.columns
.iter()
.find(|col| col.as_any().is::<Vec<bool>>())
.is_some());
}
#[test]
#[should_panic]
fn columns_builder_duplicate() {
let archetype = Archetype::new_from_columns(
Archetype::builder()
.with_column_type::<u32>()
.with_column_type::<u32>(),
);
}
}
Archetype constructor for when spawning entities
Now that we've finished up the constructors for when we add/remove components on an entity we need to make one for when we spawn an entity and need to build an archetype for it.
This one is a bit tricker because we want to take any # of component types when we create an archetype to spawn our entity into. We'll want some kind of builder struct that we can repeatedly call a function on to add component columns.
Something like this:
#![allow(unused)] fn main() { struct ColumnsBuilder(Vec<Box<dyn ComponentColumn>>); impl ColumnsBuilder { fn with_column_type<T: 'static>(mut self) -> Self { if let Some(_) = self .0 .iter() .find(|col| col.as_any().type_id() == std::any::TypeId::of::<Vec<T>>()) { panic!("Attempted to create invalid archetype"); } self.0.push(Box::new(Vec::<T>::new())); self } } }
We have to check that we don't try to create an archetype with two columns for the same component
type. i.e. an archetype for entities with components [T, T]
is nonsensical as an entity cannot
have the same component added twice. Currently I just panic here if we detect that but it would
be possible to return a Result
here and propagate it up to the user
Now that we have a ColumnsBuilder
lets add some methods to Archetype
to use it
#![allow(unused)] fn main() { impl Archetype { /* snip */ fn builder() -> ColumnsBuilder { ColumnsBuilder(Vec::new()) } fn new_from_columns(columns: ColumnsBuilder) -> Archetype { Archetype { entities: Vec::new(), columns: columns.0, } } } }
An alternative way of implementing this would be to implement a trait for tuples of length 1 to some arbitrary limit and then create the columns in that trait. This has the downside of needing to use macros for the trait impl and we also would have a limit on how many components could be spawned on an entity without adding components separately. (We'll doing something like this later on when we implement iterators over our archetypes)
This should be us done with the method we'll use when creating an Archetype
to spawn an entity
into, we just need to add some tests before moving on :)
Tests for ColumnsBuilder
We'll want a test that our duplicate column checks work and also a general test that we have the
expected columns after building an archetype from the ColumnsBuilder
:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { /* snip */ #[test] fn columns_builder() { let archetype = Archetype::new_from_columns( Archetype::builder() .with_column_type::<u32>() .with_column_type::<u64>() .with_column_type::<bool>(), ); assert!(archetype.columns.len() == 3); assert!(archetype .columns .iter() .find(|col| col.as_any().is::<Vec<u32>>()) .is_some()); assert!(archetype .columns .iter() .find(|col| col.as_any().is::<Vec<u64>>()) .is_some()); assert!(archetype .columns .iter() .find(|col| col.as_any().is::<Vec<bool>>()) .is_some()); } #[test] #[should_panic] fn columns_builder_duplicate() { let archetype = Archetype::new_from_columns( Archetype::builder() .with_column_type::<u32>() .with_column_type::<u32>(), ); } } }
The full source code for this chapter can be viewed here