Skip to content

Commit 169c463

Browse files
authored
Implement generation-safe entity registry (#51)
1 parent 51f543a commit 169c463

5 files changed

Lines changed: 232 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,16 @@ endfunction()
3535
set(SAFECROWD_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
3636

3737
add_library(ecs_engine STATIC
38+
src/engine/Entity.h
39+
src/engine/EntityRegistry.h
3840
src/engine/EngineConfig.h
3941
src/engine/EngineRuntime.h
4042
src/engine/EngineState.h
4143
src/engine/EngineStats.h
4244
src/engine/EngineStepContext.h
4345
src/engine/EngineSystem.h
4446
src/engine/FrameClock.h
47+
src/engine/EntityRegistry.cpp
4548
src/engine/EngineRuntime.cpp
4649
src/engine/FrameClock.cpp
4750
)
@@ -74,6 +77,7 @@ if (BUILD_TESTING)
7477
add_executable(safecrowd_tests
7578
tests/TestMain.cpp
7679
tests/TestSupport.h
80+
tests/EngineRegistryTests.cpp
7781
tests/FrameClockTests.cpp
7882
tests/EngineRuntimeTests.cpp
7983
tests/SafeCrowdDomainTests.cpp

src/engine/Entity.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#pragma once
2+
3+
#include <compare>
4+
#include <cstdint>
5+
#include <limits>
6+
#include <ostream>
7+
8+
namespace safecrowd::engine {
9+
10+
using EntityIndex = std::uint32_t;
11+
using EntityGeneration = std::uint32_t;
12+
13+
struct Entity {
14+
static constexpr EntityIndex invalidIndex = std::numeric_limits<EntityIndex>::max();
15+
16+
EntityIndex index{invalidIndex};
17+
EntityGeneration generation{0};
18+
19+
[[nodiscard]] constexpr bool isValid() const noexcept {
20+
return index != invalidIndex;
21+
}
22+
23+
[[nodiscard]] static constexpr Entity invalid() noexcept {
24+
return {};
25+
}
26+
27+
auto operator<=>(const Entity&) const = default;
28+
};
29+
30+
inline std::ostream& operator<<(std::ostream& stream, const Entity& entity) {
31+
stream << "Entity{index=" << entity.index << ", generation=" << entity.generation << "}";
32+
return stream;
33+
}
34+
35+
} // namespace safecrowd::engine

src/engine/EntityRegistry.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#include "engine/EntityRegistry.h"
2+
3+
#include <sstream>
4+
#include <stdexcept>
5+
#include <utility>
6+
7+
namespace safecrowd::engine {
8+
namespace {
9+
10+
std::string describeEntity(Entity entity) {
11+
std::ostringstream stream;
12+
stream << entity;
13+
return stream.str();
14+
}
15+
16+
} // namespace
17+
18+
EntityRegistry::EntityRegistry(std::size_t maxEntityCount)
19+
: entries_(maxEntityCount) {
20+
freeIndices_.resize(maxEntityCount);
21+
22+
for (std::size_t index = 0; index < maxEntityCount; ++index) {
23+
freeIndices_[index] = static_cast<EntityIndex>(index);
24+
}
25+
}
26+
27+
Entity EntityRegistry::allocate() {
28+
if (freeIndices_.empty()) {
29+
throw std::overflow_error("EntityRegistry capacity exhausted.");
30+
}
31+
32+
const EntityIndex index = freeIndices_.front();
33+
freeIndices_.pop_front();
34+
35+
Entry& entry = entries_[index];
36+
entry.alive = true;
37+
entry.signature.reset();
38+
39+
return Entity{index, entry.generation};
40+
}
41+
42+
void EntityRegistry::release(Entity entity) {
43+
Entry& entry = entryFor(entity);
44+
entry.alive = false;
45+
entry.signature.reset();
46+
++entry.generation;
47+
freeIndices_.push_back(entity.index);
48+
}
49+
50+
bool EntityRegistry::isAlive(Entity entity) const noexcept {
51+
if (!entity.isValid()) {
52+
return false;
53+
}
54+
55+
const auto index = static_cast<std::size_t>(entity.index);
56+
if (index >= entries_.size()) {
57+
return false;
58+
}
59+
60+
const Entry& entry = entries_[index];
61+
return entry.alive && entry.generation == entity.generation;
62+
}
63+
64+
void EntityRegistry::setSignature(Entity entity, Signature signature) {
65+
Entry& entry = entryFor(entity);
66+
entry.signature = signature;
67+
}
68+
69+
Signature EntityRegistry::signatureOf(Entity entity) const {
70+
return entryFor(entity).signature;
71+
}
72+
73+
const EntityRegistry::Entry& EntityRegistry::entryFor(Entity entity) const {
74+
if (!entity.isValid()) {
75+
throw std::invalid_argument("Invalid entity handle.");
76+
}
77+
78+
const auto index = static_cast<std::size_t>(entity.index);
79+
if (index >= entries_.size()) {
80+
throw std::out_of_range("Entity index is out of range.");
81+
}
82+
83+
const Entry& entry = entries_[index];
84+
if (!entry.alive || entry.generation != entity.generation) {
85+
throw std::invalid_argument("Stale or dead entity handle: " + describeEntity(entity));
86+
}
87+
88+
return entry;
89+
}
90+
91+
EntityRegistry::Entry& EntityRegistry::entryFor(Entity entity) {
92+
return const_cast<Entry&>(std::as_const(*this).entryFor(entity));
93+
}
94+
95+
} // namespace safecrowd::engine

src/engine/EntityRegistry.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#pragma once
2+
3+
#include <bitset>
4+
#include <cstddef>
5+
#include <deque>
6+
#include <vector>
7+
8+
#include "engine/Entity.h"
9+
10+
namespace safecrowd::engine {
11+
12+
inline constexpr std::size_t kMaxComponentTypes = 64;
13+
using Signature = std::bitset<kMaxComponentTypes>;
14+
15+
class EntityRegistry {
16+
public:
17+
explicit EntityRegistry(std::size_t maxEntityCount = 4096);
18+
19+
[[nodiscard]] Entity allocate();
20+
void release(Entity entity);
21+
[[nodiscard]] bool isAlive(Entity entity) const noexcept;
22+
void setSignature(Entity entity, Signature signature);
23+
[[nodiscard]] Signature signatureOf(Entity entity) const;
24+
25+
private:
26+
struct Entry {
27+
EntityGeneration generation{0};
28+
bool alive{false};
29+
Signature signature{};
30+
};
31+
32+
[[nodiscard]] const Entry& entryFor(Entity entity) const;
33+
[[nodiscard]] Entry& entryFor(Entity entity);
34+
35+
std::vector<Entry> entries_;
36+
std::deque<EntityIndex> freeIndices_;
37+
};
38+
39+
} // namespace safecrowd::engine

tests/EngineRegistryTests.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#include <stdexcept>
2+
3+
#include "TestSupport.h"
4+
5+
#include "engine/EntityRegistry.h"
6+
7+
SC_TEST(EntityRegistryReusesIndexWithNewGeneration) {
8+
safecrowd::engine::EntityRegistry registry(1);
9+
10+
const auto first = registry.allocate();
11+
SC_EXPECT_TRUE(registry.isAlive(first));
12+
13+
registry.release(first);
14+
SC_EXPECT_TRUE(!registry.isAlive(first));
15+
16+
const auto second = registry.allocate();
17+
SC_EXPECT_EQ(second.index, first.index);
18+
SC_EXPECT_TRUE(second.generation > first.generation);
19+
SC_EXPECT_TRUE(registry.isAlive(second));
20+
}
21+
22+
SC_TEST(EntityRegistryRejectsStaleEntityHandles) {
23+
safecrowd::engine::EntityRegistry registry(1);
24+
25+
const auto entity = registry.allocate();
26+
registry.release(entity);
27+
28+
bool threwOnRelease = false;
29+
try {
30+
registry.release(entity);
31+
} catch (const std::invalid_argument&) {
32+
threwOnRelease = true;
33+
}
34+
35+
SC_EXPECT_TRUE(threwOnRelease);
36+
37+
bool threwOnSignatureRead = false;
38+
try {
39+
static_cast<void>(registry.signatureOf(entity));
40+
} catch (const std::invalid_argument&) {
41+
threwOnSignatureRead = true;
42+
}
43+
44+
SC_EXPECT_TRUE(threwOnSignatureRead);
45+
}
46+
47+
SC_TEST(EntityRegistryStoresSignaturesPerLiveEntity) {
48+
safecrowd::engine::EntityRegistry registry(1);
49+
50+
const auto entity = registry.allocate();
51+
safecrowd::engine::Signature signature;
52+
signature.set(1);
53+
signature.set(5);
54+
55+
registry.setSignature(entity, signature);
56+
57+
const auto stored = registry.signatureOf(entity);
58+
SC_EXPECT_EQ(stored, signature);
59+
}

0 commit comments

Comments
 (0)