EnTT源码分析【二】:Entity

本文分析了开源项目EnTT v3.12.2的原理和实现。述说了ECS中的Entity部分。

Entity

Entity的本质

EnTT中的Entity是正整数,或者更严谨一点,是enum class

1
2
3
4
5
6
7
8
enum class entity : id_type {}; //> src/entt/entity/fwd.hpp
using id_type = ENTT_ID_TYPE;   //> src/entt/core/fwd.hpp

//> src/entt/config/config.h
#ifndef ENTT_ID_TYPE
#    include <cstdint>
#    define ENTT_ID_TYPE std::uint32_t
#endif

所以总的来说就是正整数类型的强枚举。

之所以使用强枚举是因为这样可以避免用户拿到Entity之后胡乱当做整数进行运算。每次操作Entity的时候其实都会将其强转到id_type的,本质上还是当整数去操作。

entity traits

traits用于限制Entity的类型,并且定义一些字段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//> src/entt/entity/entity.hpp

namespace internal {

...

/*(1)*/
template<typename, typename = void>
struct entt_traits;

/*(2)*/
template<typename Type>
struct entt_traits<Type, std::enable_if_t<std::is_enum_v<Type>>>
    : entt_traits<std::underlying_type_t<Type>> {
    using value_type = Type;
};

/*(3)*/
template<typename Type>
struct entt_traits<Type, std::enable_if_t<std::is_class_v<Type>>>
    : entt_traits<typename Type::entity_type> {
    using value_type = Type;
};

/*(4)*/
template<>
struct entt_traits<std::uint32_t> {
    using value_type = std::uint32_t;

    using entity_type = std::uint32_t;
    using version_type = std::uint16_t;

    static constexpr entity_type entity_mask = 0xFFFFF;
    static constexpr entity_type version_mask = 0xFFF;
};

template<>
struct entt_traits<std::uint64_t> {
    using value_type = std::uint64_t;

    using entity_type = std::uint64_t;
    using version_type = std::uint32_t;

    static constexpr entity_type entity_mask = 0xFFFFFFFF;
    static constexpr entity_type version_mask = 0xFFFFFFFF;
};

...

}

EnTT中有很多类似这样的操作:首先(1)处声明一个模板,但是不实现它。等到后面对其进行特化。这样只有满足特化的模板参数才可以通过编译,其余的一律是不支持的模板参数,变相地限制了模板参数(C++20 concept我想你了555)。

(2)(3)是在做如下事情:

  • 如果Type是类,那它要求Type中有一个entity_type,并且这个entity_type也必须是类或枚举,然后将这个类型递归地进行萃取
  • 如果Type是枚举,得到他对应的数字类型(EnTT中就是id_typeuint32_t)并且通过继承聚合此类型相关的信息

(4)处开始真正的Entity信息定义。这里通过全特化指定只有uint32_tuint64_t能够有类型,其余的数字类型一律编译失败。

Entity由两部分组成:id部分和version部分。version部分主要是为了复用entity。

那么我们可以看到,Entity应该有如下信息:

  • value_type:Entity真正的数字类型
  • entity_type:id部分的类型
  • version_type:version部分的类型
  • entity_mask:id部分的掩码
  • version_mask:version部分的掩码

比如对于uint32_t类型来说,其Entity组成如下:

version id
0000 0000 0001 000 0000 0000 0000 0001

version在高位id在低位。

接下来的basic_entt_traits则通过组合的方式增加了一些对Entity的操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename Traits>
class basic_entt_traits {
static constexpr auto length = internal::popcount(Traits::entity_mask);
    /*(1)*/
    static_assert(Traits::entity_mask && ((typename Traits::entity_type{1} << length) == (Traits::entity_mask + 1)), "Invalid entity mask");
    static_assert((typename Traits::entity_type{1} << internal::popcount(Traits::version_mask)) == (Traits::version_mask + 1), "Invalid version mask");

public:
    using value_type = typename Traits::value_type;
    using entity_type = typename Traits::entity_type;
    using version_type = typename Traits::version_type;

    static constexpr entity_type entity_mask = Traits::entity_mask;
    static constexpr entity_type version_mask = Traits::version_mask;

    [[nodiscard]] static constexpr entity_type to_integral(const value_type value) noexcept { ... }

    [[nodiscard]] static constexpr entity_type to_entity(const value_type value) noexcept { ... }

    [[nodiscard]] static constexpr version_type to_version(const value_type value) noexcept { ... }

    ...
}

这里的模板参数Traits就是上面的entt_traits。然后类里面重新using了traits里的类型。

增加的一些操作也很好懂,在这里就不分析了,注释写的很详细。主要是得到Entity的版本号/ID号,通过版本号&ID好拼一个Entity,通过两个Entity拼一个Entity等等。

这里稍微看一下(1)处的两个static_assert,要求entity_maskversion_mask必须满足所有位全为1的条件。我也是第一次知道static_assert可以直接放在类里面。

最后,通过entt命名空间中的entt_traits继承basic_entt_traits得到最后的traits:

1
2
3
4
5
template<typename Type>
struct entt_traits: basic_entt_traits<internal::entt_traits<Type>> {
    using base_type = basic_entt_traits<internal::entt_traits<Type>>;
    static constexpr std::size_t page_size = ENTT_SPARSE_PAGE;
};

并且在下面将basic_entt_traitsstatic函数封装成全局函数以便于调用。

小结一下:

  1. 首先通过entt_traits进行类型萃取,要求Entity的类型必须是枚举,并且枚举的底层数字类型必须是uint32_t/uint64_t(但可通过增加全特化版本来扩展支持类型)
  2. 然后通过basic_entt_traits在原本的信息上增加控制这些数据的函数
  3. 最后使用全局函数封装basic_entt_traits以方便函数调用(basic_entt_traits需要一个entt_traits作为模板参数。entt_traits需要一个Entity作为模板参数。封装成全局函数可直接通过Entity模板参数调用函数)

null和tombstone

null代表空,而tombstone代表“死了”,两者虽然实现非常相似,但不是一个东西(之后的文章会说用法)。

这里对nulltombstone的实现也很有意思,可以学习学习。这两者实现几乎一样,而且也非常地好懂:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//> src/entt/entity/entity.hpp

struct null_t {
    template<typename Entity>
    [[nodiscard]] constexpr operator Entity() const noexcept {
        using traits_type = entt_traits<Entity>;
        constexpr auto value = traits_type::construct(traits_type::entity_mask, traits_type::version_mask);
        return value;
    }

    [[nodiscard]] constexpr bool operator==([[maybe_unused]] const null_t other) const noexcept {
        return true;
    }

    [[nodiscard]] constexpr bool operator!=([[maybe_unused]] const null_t other) const noexcept {
        return false;
    }

    template<typename Entity>
    [[nodiscard]] constexpr bool operator==(const Entity entity) const noexcept {
        using traits_type = entt_traits<Entity>;
        return traits_type::to_entity(entity) == traits_type::to_entity(*this);
    }

    template<typename Entity>
    [[nodiscard]] constexpr bool operator!=(const Entity entity) const noexcept {
        return !(entity == *this);
    }
};

template<typename Entity>
[[nodiscard]] constexpr bool operator==(const Entity entity, const null_t other) noexcept {
    return other.operator==(entity);
}

template<typename Entity>
[[nodiscard]] constexpr bool operator!=(const Entity entity, const null_t other) noexcept {
    return !(other == entity);
}

实现很简单。首先,这个类是一个空类。其次有如下三种函数:

  • 隐式转换到Entity
  • 和任意的null_t类型比较,总是为true
  • Entity比较,只有特定情况(Entity所有位全是1)的时候为true,其他的全为false

真正的做到了0开销抽象原则。

updatedupdated2023-08-102023-08-10