对类型进行编译时操作

Compile time operations on types

提问人:LucioleMaléfique 提问时间:10/16/2023 更新时间:10/17/2023 访问量:85

问:

我正在从头开始重新设计我的 ECS,这次选择了原型。

原型 ECS 的核心思想是,我有一个映射,该映射将原型(组件类型的元组)ID 绑定到包含该原型的所有实体(以及组件数据)的存储桶。

我想要什么:

我虽然认为我的实体是通用的,而不是它们的原型,但要对每个实体具有哪些组件有编译时间知识,并保证方法的结果。get_component

pub struct Entity<Archetype> {
    archetype_marker: PhantomData<Archetype>,
    index: usize,
}

对于 Rust 类型,我认为原型应该是类型的直接元组,因为它是最有意义的。例如,是一个原型。可以使用 TypeId 计算 ID。(String, usize, u32)

我已经有一个基本结构:

struct ComponentTable {
    // for each archetype, store the entities components.
    components: ArchetypeMap,
}

struct ComponentBucket<Archetype> {
    entities: HashMap<usize, Archetype>,
    last_index: usize,
}

这是一个类似于AnyMap板条箱的地图,除了我在mapp中强制执行值之外,我映射到该位置:ArchetypeMapArchetypeComponentBucket<Archetype>AbstractBucket

pub trait AbstractBucket {
    fn query<Archetype>(&self) -> Option<impl Iterator<Type = Archetype>>;
}

这里的目标是能够遍历所有桶进行迭代,即使使用原型映射提供的类型 ereasure。

现在,ECS 需要能够创建/删除实体,这对于这种方法来说是微不足道的。

impl ComponentTable {
    pub fn create_entity<Archetype: 'static>(&mut self, components: Archetype) -> Entity<Archetype> {
        match self.components.get_mut::<ComponentBucket<Archetype>>() {
            Some(bucket) => bucket.create_entity(components),
            None => {
                let mut bucket = ComponentBucket::default();
                let entity = bucket.create_entity(components);
                self.components.insert::<ComponentBucket<Archetype>>(bucket);
                entity
            }
        }
    }

    pub fn remove_entity<Archetype: 'static>(&mut self, entity: Entity<Archetype>) -> Option<Archetype> {
        self.components.get_mut::<ComponentBucket<Archetype>>()?.remove_entity(entity)
    }
}

impl<Archetype> ComponentBucket<Archetype> {
    pub fn create_entity(&mut self, components: Archetype) -> Entity<Archetype> {
        let index = self.last_index;
        self.last_index += 1;
        self.entities.insert(index, components);
        Entity::new(index)
    }

    pub fn remove_entity(&mut self, entity: Entity<Archetype>) -> Option<Archetype> {
        self.entities.remove(&entity.index())
    }
}

添加/删除组件可以通过明确完整的原型元组来完成,但我宁愿有一个适用于每个原型的方法,因为这个实现将需要根据实体的原型大小使用许多不同的名称:

impl ComponentTable {
    pub fn add_component<T1: 'static, T2: 'static, C: 'static>(&mut self, entity: Entity<(T1, T2)>, component: C) -> Option<Entity<(T1, T2, C)>> {
        let archetype = self.remove_entity(entity)?;
        let new_entity = self.create_entity((archetype.0, archetype.1, component));
        Some(new_entity)
    }

    pub fn remove_component<T1: 'static, T2: 'static, C: 'static>(&mut self, entity: Entity<(T1, T2, C)>) -> Option<Entity<(T1, T2)>> {
        let archetype = self.remove_entity(entity)?;
        let new_entity = self.create_entity((archetype.0, archetype.1));
        Some(new_entity)
    }
}

现在这就是我卡住的地方,试图为我的桶实现特征:AbstractBucket

impl<BucketArchetype> AbstractBucket for ComponentBucket<BucketArchetype> {
    fn query<Archetype>(&self) -> Option<()> {
        // ???? 
    }
}

我怎么能有办法将类型“解包”为类型的元组?看起来它没有意义,因为我的类型不一定是元组,但如果它们不是,我们可以将它们视为一个元素的元组。

此函数应: 如果 是 的子原型,则返回原型的迭代器(具有从存储桶原型到请求原型的映射)。否则,返回 .ArchetypeBucketArchetypeNone

有了 Rust 强大的类型系统,我觉得我想要实现的都是可行的,但我要么没有构建它,要么找不到编译器无法理解的示例并证明我的设计无法工作。

我查看了现有原型 ECS 的示例,但它们似乎都在运行时保留了原型值,我试图在编译时完成大部分工作。

我还研究了可泛型,因为这看起来很合适,但它们在 Rust 中不可用。

最后,我尝试在编译时对类型执行操作,例如将类型添加到类型元组并以这种方式构建新类型。这看起来像是在编译时可行的操作,但我正在努力。也许 Rust 不够强大?也许我需要一些更强大的架构来构建我的原型系统?

rust 类型 编译时

评论


答:

0赞 Bion Alex Howard 10/17/2023 #1

如果 Archetype 是 BucketArchetype 的子原型,则返回 原型上的迭代器(使用来自存储桶原型的映射 到询问的原型)。否则,返回 None。

这听起来像是您可能想表达对创建原型的能力的限制:From,也许是这样的:

ecs_contract.rs :

pub struct Entity<Archetype> {
    archetype_marker: PhantomData<Archetype>,
    index: usize,
}

struct ComponentTable {
    // for each archetype, store the entities components.
    components: ArchetypeMap, // you must clarify this type!
}

struct ComponentBucket<Archetype> {
    entities: HashMap<usize, Archetype>,
    last_index: usize,
}

// functionalize the entities to enable more sensible default impl of AbstractBucket
pub trait Entities<A> {
    fn entities(&self) -> HashMap<usize, A>;
}

// note: you probably want to use lifetimes to avoid cloning your underlying bucket data to make this iterator (assuming sub archetypes are subsets of archetypes)
pub trait AbstractBucket<BucketArchetype>: Entities<BucketArchetype> {
    fn query<Archetype>(&self) -> Option<impl Iterator<Type = Archetype>>
    where Archetype: From<BucketArchetype> {
    Some(self.entities().values().map(Archetype::from))
   }
}

ecs_impls.rs :

impl ComponentTable {
    pub fn create_entity<Archetype: 'static>(&mut self, components: Archetype) -> Entity<Archetype> {
        match self.components.get_mut::<ComponentBucket<Archetype>>() {
            Some(bucket) => bucket.create_entity(components),
            None => {
                let mut bucket = ComponentBucket::default();
                let entity = bucket.create_entity(components);
                self.components.insert::<ComponentBucket<Archetype>>(bucket);
                entity
            }
        }
    }

    pub fn remove_entity<Archetype: 'static>(&mut self, entity: Entity<Archetype>) -> Option<Archetype> {
        self.components.get_mut::<ComponentBucket<Archetype>>()?.remove_entity(entity)
    }
}

impl<Archetype> ComponentBucket<Archetype> {
    pub fn create_entity(&mut self, components: Archetype) -> Entity<Archetype> {
        let index = self.last_index;
        self.last_index += 1;
        self.entities.insert(index, components);
        Entity::new(index)
    }

    pub fn remove_entity(&mut self, entity: Entity<Archetype>) -> Option<Archetype> {
        self.entities.remove(&entity.index())
    }
}

// default implementation
impl<BucketArchetype> AbstractBucket for ComponentBucket<BucketArchetype> {}

为了保存多种原型,你可能想要创建一个嵌套结构,其中 ComponentTable 保存一个哈希图的哈希图,每种 Archetype 一个哈希图,你可能需要对 ArchetypeMap 的行为进行更宽松的功能定义。您能否提供有关如何实现多种类型的映射的更多详细信息?

改善这一切的一个简单方法是更多地关注特征,使特征类似于您想要使用的事物的形式,然后制作适合它们的结构;然后,您可以轻松地在以后的不同结构上重新实现这些特征。即原型、实体、组件,这些是特征,那么 TupleArchetype 是一个结构,MapArchetype 是一个结构,等等......函数抽象接口总是允许多个结构具体实现。

一个想法是使原型映射的功能镜头化成为原型类的原型方法。然后,原型将插入和删除它们自己在原型树的分支中,而不是处理各种原型的组件表。对不起,我试图解决这个问题,但我对地图上的类型感到困惑。希望这对您有所帮助!

评论

0赞 LucioleMaléfique 10/17/2023
谢谢你的回答!为了提供有关原型映射的更多详细信息,实际类型是 其中值实际上是任何类型的内存分配。此结构允许将 any 插入到映射中,使用类型 id 作为键,使用类型 data 作为值。特别是,我们可以在那里容纳任何东西。我实际上没有做过任何函数式编程,你会重新建议它来实现这样的架构吗?HashMap<TypeId, Vec<u8>>TArchtypeBucket<A>
0赞 Bion Alex Howard 10/19/2023
是的,我确实推荐函数式风格,在这种情况下,我认为你需要一个 Lens<T> 之类的东西,你可以按类型来处理你的 ArchetypeMap。镜头可以有一个方法将给定类型转换为 TypeId,从原型映射中获取数据,并将数据从字节反序列化到真正的原型 - 尝试 randycoulman.com/blog/2016/07/12/thinking-in-ramda-lenses 基本上你只需要一个 getter 和一个 setter,这里的键是一个类型,所以你可以使用 Archetypes::<T>.get() 样式 Turbofish 来指示你想要的对象类型