I'm trying to implement a basic ECS in Rust. I want a data structure storing, for each component, a storage of that particular component. Because some components are common while others are rare, I want different types of storage policies such as VecStorage<T>
and HashMapStorage<T>
.
As components are unknown to the game engine's ECS, I came up with:
trait AnyStorage: Debug {
fn new() -> Self
where
Self: Sized;
}
#[derive(Default, Debug)]
struct StorageMgr {
storages: HashMap<TypeId, Box<AnyStorage>>,
}
with VecStorage
and HashMapStorage<T>
implementing the AnyStorage
trait. Since AnyStorage
doesn't know T
, I added one more trait implemented by both concrete storages: ComponentStorage<T>
.
While I was able to register new components (i.e. add a new Box<AnyStorage>
in StorageMgr
's storages
), I didn't find a way to insert components.
Here is the erroneous code:
pub fn add_component_to_storage<C: Component>(&mut self, component: C) {
let storage = self.storages.get_mut(&TypeId::of::<C>()).unwrap();
// storage is of type: &mut Box<AnyStorage + 'static>
println!("{:?}", storage); // Prints "VecStorage([])"
storage.insert(component); // This doesn't work
// This neither:
// let any_stor: &mut Any = storage;
// let storage = any_stor.downcast_ref::<ComponentStorage<C>>();
}
I know that my problem comes from the fact that storage
's type is &mut Box<AnyStorage>
; can I obtain the concrete VecStorage
from it?
The whole point of doing all this is that I want components to be contiguous in memory and to have different storage for each component type. I can not resolve myself to use Box<Component>
, or I don't see how.
I reduced my problem to a minimal code on Rust Playground.
I wasn't sure if something like this was possible but I've finally figured it out. There are a couple things to note as to why your posted example was failing.
- Trait
AnyStorage
in your example did not implement ComponentStorage<T>
, therefore because you were storing your "storage"s in a HashMap<TypeId, Box<AnyStorage>>
, Rust could not guarantee that every stored type implemented ComponentStorage<T>::insert()
because it only knew that they were AnyStorage
s.
- If you did combine the two traits into one simply called
Storage<T>
and stored them in a HashMap<TypeId, Box<Storage<T>>
, every version of Storage
would have to store the same type because of the single T
. Rust doesn't have a way to dynamically type the values of a map based on the TypeId of the key, as a solution like this would require. Also, you can't replace T
with Any
because Any
isn't Sized
, which Vec
and all other storage types require. I'm guessing you knew all of this which is why you used two different traits in your original example.
The solution I ended up using stored the Storage<T>
s as Any
s in a HashMap<TypeId, Box<Any>>
, and then I downcasted the Any
s into Storage<T>
s inside the implementation functions for StorageMgr
. I've put a short example below, and a full version is on Rust Playground here
.
trait Component: Debug + Sized + Any {
type Storage: Storage<Self>;
}
trait Storage<T: Debug>: Debug + Any {
fn new() -> Self
where
Self: Sized;
fn insert(&mut self, value: T);
}
struct StorageMgr {
storages: HashMap<TypeId, Box<Any>>,
}
impl StorageMgr {
pub fn new() -> Self {
Self {
storages: HashMap::new(),
}
}
pub fn get_storage_mut<C: Component>(&mut self) -> &mut <C as Component>::Storage {
let type_id = TypeId::of::<C>();
// Add a storage if it doesn't exist yet
if !self.storages.contains_key(&type_id) {
let new_storage = <C as Component>::Storage::new();
self.storages.insert(type_id, Box::new(new_storage));
}
// Get the storage for this type
match self.storages.get_mut(&type_id) {
Some(probably_storage) => {
// Turn the Any into the storage for that type
match probably_storage.downcast_mut::<<C as Component>::Storage>() {
Some(storage) => storage,
None => unreachable!(), // <- you may want to do something less explosive here
}
}
None => unreachable!(),
}
}
}