Last active
August 29, 2023 13:30
-
-
Save kanerogers/1454789b544a0196018a7291a340a370 to your computer and use it in GitHub Desktop.
Evil Component links in Hecs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use hecs::{Component, ComponentError, ComponentRef, DynamicBundle, QueryBorrow, World}; | |
use std::{marker::PhantomData, ops::Deref, ptr::NonNull}; | |
/// Wrapper around [World] to conceal evil-doing. | |
#[derive(Default)] | |
pub struct ECS { | |
world: World, | |
} | |
impl ECS { | |
/// Forward to [World] | |
pub fn spawn(&mut self, bundle: impl DynamicBundle) -> hecs::Entity { | |
self.world.spawn(bundle) | |
} | |
/// Forward to [World] | |
pub fn query<Q: hecs::Query>(&self) -> QueryBorrow<'_, Q> { | |
self.world.query() | |
} | |
/// Create a link to the component `T` on entity `target`. | |
/// Does not check if the entity exists or has component `T` | |
pub fn link<T: Component>(&self, target: hecs::Entity) -> Link<T> { | |
let world = NonNull::from(&self.world); | |
Link { | |
world, | |
target, | |
_component_type: PhantomData, | |
} | |
} | |
} | |
/// I mean, sure. | |
unsafe impl<T> Send for Link<T> {} | |
unsafe impl<T> Sync for Link<T> {} | |
/// A link to the component `T` on entity `target`. | |
pub struct Link<T> { | |
world: NonNull<World>, | |
target: hecs::Entity, | |
_component_type: PhantomData<T>, | |
} | |
impl<'a, T> Link<T> | |
where | |
T: ComponentRef<'a>, | |
{ | |
/// This would be the safer, better version. But it's boring! | |
pub fn get(&'a self) -> Result<T::Ref, ComponentError> { | |
let world = unsafe { self.world.as_ref() }; | |
world.get::<T>(self.target) | |
} | |
} | |
/// Cool, fun and evil. No checks, no fucks given. | |
impl<T> Deref for Link<T> | |
where | |
T: Component, | |
{ | |
type Target = T; | |
fn deref(&self) -> &Self::Target { | |
let world = unsafe { self.world.as_ref() }; | |
// We use query here to get around `hecs` doing things to prevent evil. | |
// Specifically, because `world.get<'a, T>` is `T: ComponentRef<'a>`, | |
// it's very, very annoying to specify that constraint in the `deref` | |
// implementation without Shenanigans. | |
let mut query = world.query_one::<&T>(self.target).unwrap(); | |
let component = query.get().unwrap(); | |
// *lightning strikes, cackles* | |
let component_ptr = NonNull::from(component); | |
unsafe { component_ptr.as_ref() } | |
} | |
} | |
pub struct Transform { | |
pub position: [f32; 3], | |
} | |
pub struct Info { | |
pub name: String, | |
} | |
pub struct Target { | |
pub info: Link<Info>, | |
pub transform: Link<Transform>, | |
} | |
impl Target { | |
pub fn new(info: Link<Info>, transform: Link<Transform>) -> Self { | |
Self { info, transform } | |
} | |
} | |
#[cfg(test)] | |
mod tests { | |
use super::*; | |
#[test] | |
pub fn test_magic_component() { | |
let mut ecs = ECS::default(); | |
let a = ecs.spawn(( | |
Info { name: "Bob".into() }, | |
Transform { | |
position: [1., 1., 1.], | |
}, | |
)); | |
let _ = ecs.spawn(( | |
Info { name: "Sam".into() }, | |
Transform { | |
position: [3., 2., 1.], | |
}, | |
Target::new(ecs.link(a), ecs.link(a)), | |
)); | |
for (_, (info, target)) in ecs.query::<(&Info, &Target)>().iter() { | |
assert_eq!(&info.name, "Sam"); | |
assert_eq!(&target.info.name, "Bob"); | |
assert_eq!(target.transform.position, [1., 1., 1.]); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment