Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ endfunction()
set(SAFECROWD_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")

add_library(ecs_engine STATIC
src/engine/Entity.h
src/engine/EntityRegistry.h
src/engine/EngineConfig.h
src/engine/EngineRuntime.h
src/engine/EngineState.h
src/engine/EngineStats.h
src/engine/EngineStepContext.h
src/engine/EngineSystem.h
src/engine/FrameClock.h
src/engine/EntityRegistry.cpp
src/engine/EngineRuntime.cpp
src/engine/FrameClock.cpp
)
Expand Down Expand Up @@ -74,6 +77,7 @@ if (BUILD_TESTING)
add_executable(safecrowd_tests
tests/TestMain.cpp
tests/TestSupport.h
tests/EngineRegistryTests.cpp
tests/FrameClockTests.cpp
tests/EngineRuntimeTests.cpp
tests/SafeCrowdDomainTests.cpp
Expand Down
35 changes: 35 additions & 0 deletions src/engine/Entity.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <compare>
#include <cstdint>
#include <limits>
#include <ostream>

namespace safecrowd::engine {

using EntityIndex = std::uint32_t;
using EntityGeneration = std::uint32_t;

struct Entity {
static constexpr EntityIndex invalidIndex = std::numeric_limits<EntityIndex>::max();

EntityIndex index{invalidIndex};
EntityGeneration generation{0};

[[nodiscard]] constexpr bool isValid() const noexcept {
return index != invalidIndex;
}

[[nodiscard]] static constexpr Entity invalid() noexcept {
return {};
}

auto operator<=>(const Entity&) const = default;
};

inline std::ostream& operator<<(std::ostream& stream, const Entity& entity) {
stream << "Entity{index=" << entity.index << ", generation=" << entity.generation << "}";
return stream;
}

} // namespace safecrowd::engine
95 changes: 95 additions & 0 deletions src/engine/EntityRegistry.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#include "engine/EntityRegistry.h"

#include <sstream>
#include <stdexcept>
#include <utility>

namespace safecrowd::engine {
namespace {

std::string describeEntity(Entity entity) {
std::ostringstream stream;
stream << entity;
return stream.str();
}

} // namespace

EntityRegistry::EntityRegistry(std::size_t maxEntityCount)
: entries_(maxEntityCount) {
freeIndices_.resize(maxEntityCount);

for (std::size_t index = 0; index < maxEntityCount; ++index) {
freeIndices_[index] = static_cast<EntityIndex>(index);
}
}

Entity EntityRegistry::allocate() {
if (freeIndices_.empty()) {
throw std::overflow_error("EntityRegistry capacity exhausted.");
}

const EntityIndex index = freeIndices_.front();
freeIndices_.pop_front();

Entry& entry = entries_[index];
entry.alive = true;
entry.signature.reset();

return Entity{index, entry.generation};
}

void EntityRegistry::release(Entity entity) {
Entry& entry = entryFor(entity);
entry.alive = false;
entry.signature.reset();
++entry.generation;
freeIndices_.push_back(entity.index);
}

bool EntityRegistry::isAlive(Entity entity) const noexcept {
if (!entity.isValid()) {
return false;
}

const auto index = static_cast<std::size_t>(entity.index);
if (index >= entries_.size()) {
return false;
}

const Entry& entry = entries_[index];
return entry.alive && entry.generation == entity.generation;
}

void EntityRegistry::setSignature(Entity entity, Signature signature) {
Entry& entry = entryFor(entity);
entry.signature = signature;
}

Signature EntityRegistry::signatureOf(Entity entity) const {
return entryFor(entity).signature;
}

const EntityRegistry::Entry& EntityRegistry::entryFor(Entity entity) const {
if (!entity.isValid()) {
throw std::invalid_argument("Invalid entity handle.");
}

const auto index = static_cast<std::size_t>(entity.index);
if (index >= entries_.size()) {
throw std::out_of_range("Entity index is out of range.");
}

const Entry& entry = entries_[index];
if (!entry.alive || entry.generation != entity.generation) {
throw std::invalid_argument("Stale or dead entity handle: " + describeEntity(entity));
}

return entry;
}

EntityRegistry::Entry& EntityRegistry::entryFor(Entity entity) {
return const_cast<Entry&>(std::as_const(*this).entryFor(entity));
}

} // namespace safecrowd::engine
39 changes: 39 additions & 0 deletions src/engine/EntityRegistry.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#pragma once

#include <bitset>
#include <cstddef>
#include <deque>
#include <vector>

#include "engine/Entity.h"

namespace safecrowd::engine {

inline constexpr std::size_t kMaxComponentTypes = 64;
using Signature = std::bitset<kMaxComponentTypes>;

class EntityRegistry {
public:
explicit EntityRegistry(std::size_t maxEntityCount = 4096);

[[nodiscard]] Entity allocate();
void release(Entity entity);
[[nodiscard]] bool isAlive(Entity entity) const noexcept;
void setSignature(Entity entity, Signature signature);
[[nodiscard]] Signature signatureOf(Entity entity) const;

private:
struct Entry {
EntityGeneration generation{0};
bool alive{false};
Signature signature{};
};

[[nodiscard]] const Entry& entryFor(Entity entity) const;
[[nodiscard]] Entry& entryFor(Entity entity);

std::vector<Entry> entries_;
std::deque<EntityIndex> freeIndices_;
};

} // namespace safecrowd::engine
59 changes: 59 additions & 0 deletions tests/EngineRegistryTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#include <stdexcept>

#include "TestSupport.h"

#include "engine/EntityRegistry.h"

SC_TEST(EntityRegistryReusesIndexWithNewGeneration) {
safecrowd::engine::EntityRegistry registry(1);

const auto first = registry.allocate();
SC_EXPECT_TRUE(registry.isAlive(first));

registry.release(first);
SC_EXPECT_TRUE(!registry.isAlive(first));

const auto second = registry.allocate();
SC_EXPECT_EQ(second.index, first.index);
SC_EXPECT_TRUE(second.generation > first.generation);
SC_EXPECT_TRUE(registry.isAlive(second));
}

SC_TEST(EntityRegistryRejectsStaleEntityHandles) {
safecrowd::engine::EntityRegistry registry(1);

const auto entity = registry.allocate();
registry.release(entity);

bool threwOnRelease = false;
try {
registry.release(entity);
} catch (const std::invalid_argument&) {
threwOnRelease = true;
}

SC_EXPECT_TRUE(threwOnRelease);

bool threwOnSignatureRead = false;
try {
static_cast<void>(registry.signatureOf(entity));
} catch (const std::invalid_argument&) {
threwOnSignatureRead = true;
}

SC_EXPECT_TRUE(threwOnSignatureRead);
}

SC_TEST(EntityRegistryStoresSignaturesPerLiveEntity) {
safecrowd::engine::EntityRegistry registry(1);

const auto entity = registry.allocate();
safecrowd::engine::Signature signature;
signature.set(1);
signature.set(5);

registry.setSignature(entity, signature);

const auto stored = registry.signatureOf(entity);
SC_EXPECT_EQ(stored, signature);
}
Loading