Compare commits
2 Commits
57b350863a
...
6639127bcf
| Author | SHA1 | Date | |
|---|---|---|---|
|
6639127bcf
|
|||
|
320eb29957
|
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BUILD_DIR="build"
|
||||||
|
|
||||||
|
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cpp|hpp)$' | grep -v 'deps/' || true)
|
||||||
|
|
||||||
|
if [ -z "$FILES" ]; then
|
||||||
|
echo "No C/C++ files to check"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Running clang-format..."
|
||||||
|
|
||||||
|
git diff -U0 --cached -- $FILES | \
|
||||||
|
python3 <(curl -s https://raw.githubusercontent.com/llvm/llvm-project/refs/heads/main/clang/tools/clang-format/clang-format-diff.py) -p1 -i
|
||||||
|
|
||||||
|
echo "Running clang-tidy..."
|
||||||
|
|
||||||
|
for file in $FILES; do
|
||||||
|
clang-tidy \
|
||||||
|
"$file" \
|
||||||
|
--header-filter="^(?!.*deps/).*" \
|
||||||
|
-p "$BUILD_DIR"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "pre-commit checks passed"
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BUILD_DIR="build"
|
||||||
|
|
||||||
|
echo "Building..."
|
||||||
|
|
||||||
|
cmake --build "$BUILD_DIR"
|
||||||
|
|
||||||
|
echo "Running tests..."
|
||||||
|
|
||||||
|
ctest --test-dir "$BUILD_DIR" --output-on-failure
|
||||||
|
|
||||||
|
echo "pre-push checks passed"
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#ifndef LIBCATBOY_ECS_COMPONENT_VIEW_HPP
|
||||||
|
#define LIBCATBOY_ECS_COMPONENT_VIEW_HPP
|
||||||
|
|
||||||
|
#include "sparseSet.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <concepts>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <iterator>
|
||||||
|
#include <tuple>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace libcatboy {
|
||||||
|
namespace ecs {
|
||||||
|
|
||||||
|
template <std::integral Entity, typename... Sum>
|
||||||
|
requires(sizeof...(Sum) >= 1)
|
||||||
|
class component_view {
|
||||||
|
friend class iterator;
|
||||||
|
public:
|
||||||
|
class iterator {
|
||||||
|
friend class component_view;
|
||||||
|
private:
|
||||||
|
iterator(component_view* view, std::size_t index)
|
||||||
|
: m_view(view), m_denseMap(view->m_minimal->dense_map()), m_index(index) {
|
||||||
|
find_valid();
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
using value_type = std::tuple<Entity, Sum...>;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
using reference = std::tuple<Entity, Sum&...>;
|
||||||
|
using iterator_category = std::forward_iterator_tag;
|
||||||
|
public:
|
||||||
|
reference operator*() { return std::tuple_cat(std::make_tuple(index()), current_tuple()); }
|
||||||
|
|
||||||
|
iterator& operator++() {
|
||||||
|
++m_index;
|
||||||
|
find_valid();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
iterator operator++(int) {
|
||||||
|
iterator ret = *this;
|
||||||
|
++(*this);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
bool operator==(const iterator& rhs) const noexcept { return m_view == rhs.m_view && m_index == rhs.m_index; }
|
||||||
|
|
||||||
|
bool operator!=(const iterator& rhs) const noexcept { return m_view != rhs.m_view || m_index != rhs.m_index; }
|
||||||
|
public:
|
||||||
|
void swap(iterator& other) noexcept {
|
||||||
|
std::swap(m_view, other.m_view);
|
||||||
|
std::swap(m_denseMap, other.m_denseMap);
|
||||||
|
std::swap(m_index, other.m_index);
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
void find_valid() {
|
||||||
|
while (m_index < m_denseMap.size() && !m_view->valid(index()))
|
||||||
|
++m_index;
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
using component_types = std::tuple<Sum...>;
|
||||||
|
private:
|
||||||
|
auto current_tuple() { return current_tuple(std::make_index_sequence<sizeof...(Sum)>{}); }
|
||||||
|
|
||||||
|
template <std::size_t... Is>
|
||||||
|
auto current_tuple(std::index_sequence<Is...>) {
|
||||||
|
return std::forward_as_tuple(
|
||||||
|
(reinterpret_cast<sparse_set<std::tuple_element_t<Is, component_types>>&>(*m_view->m_sets[Is])
|
||||||
|
.at(index()))...);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto current_tuple() const { return current_tuple(std::make_index_sequence<sizeof...(Sum)>{}); }
|
||||||
|
|
||||||
|
template <std::size_t... Is>
|
||||||
|
auto current_tuple(std::index_sequence<Is...>) const {
|
||||||
|
return std::forward_as_tuple(
|
||||||
|
(reinterpret_cast<const sparse_set<std::tuple_element_t<Is, component_types>>&>(*m_view->m_sets[Is])
|
||||||
|
.at(index()))...);
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
std::size_t index() const noexcept { return m_denseMap[m_index]; }
|
||||||
|
private:
|
||||||
|
component_view* m_view;
|
||||||
|
std::span<std::size_t> m_denseMap;
|
||||||
|
std::size_t m_index = 0;
|
||||||
|
};
|
||||||
|
public:
|
||||||
|
component_view(std::array<isparse_set*, sizeof...(Sum)> sets)
|
||||||
|
: m_sets(sets), m_minimal(sets.front()) {
|
||||||
|
for (std::size_t i = 1; i < sets.size(); ++i) {
|
||||||
|
if (m_minimal->size() > sets[i]->size())
|
||||||
|
m_minimal = sets[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
std::size_t size() const noexcept {
|
||||||
|
std::size_t count = 0;
|
||||||
|
for (auto idx : m_minimal->dense_map()) {
|
||||||
|
if (valid(idx)) ++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
iterator begin() { return { this, 0 }; }
|
||||||
|
iterator end() { return { this, m_minimal->size() }; }
|
||||||
|
private:
|
||||||
|
bool valid(Entity entity) const noexcept {
|
||||||
|
for (isparse_set* set : m_sets) {
|
||||||
|
if (!set->contains(entity)) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
std::array<isparse_set*, sizeof...(Sum)> m_sets;
|
||||||
|
|
||||||
|
isparse_set* m_minimal = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ecs
|
||||||
|
} // namespace libcatboy
|
||||||
|
|
||||||
|
#endif // LIBCATBOY_ECS_COMPONENT_VIEW_HPP
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef LIBCATBOY_ECS_ECS_HPP
|
#ifndef LIBCATBOY_ECS_ECS_HPP
|
||||||
#define LIBCATBOY_ECS_ECS_HPP
|
#define LIBCATBOY_ECS_ECS_HPP
|
||||||
|
|
||||||
|
#include "component_view.hpp"
|
||||||
#include "fwd.hpp"
|
#include "fwd.hpp"
|
||||||
#include "sparseSet.hpp"
|
#include "sparseSet.hpp"
|
||||||
|
|
||||||
@@ -83,6 +84,12 @@ public:
|
|||||||
if (m_alive.find(entity) == m_alive.end()) throw std::runtime_error("entity is dead");
|
if (m_alive.find(entity) == m_alive.end()) throw std::runtime_error("entity is dead");
|
||||||
get_set<T>().erase(entity);
|
get_set<T>().erase(entity);
|
||||||
}
|
}
|
||||||
|
public:
|
||||||
|
template <typename... Sum>
|
||||||
|
requires(sizeof...(Sum) >= 1)
|
||||||
|
component_view<Entity, Sum...> view() {
|
||||||
|
return { { (&get_set<Sum>())... } };
|
||||||
|
}
|
||||||
private:
|
private:
|
||||||
template <typename T>
|
template <typename T>
|
||||||
sparse_set<T>& get_set() {
|
sparse_set<T>& get_set() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef LIBCATBOY_ECS_SPARSE_SET_HPP
|
#ifndef LIBCATBOY_ECS_SPARSE_SET_HPP
|
||||||
#define LIBCATBOY_ECS_SPARSE_SET_HPP
|
#define LIBCATBOY_ECS_SPARSE_SET_HPP
|
||||||
|
|
||||||
|
#include <span>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -18,8 +19,11 @@ public:
|
|||||||
isparse_set(const isparse_set&) = default;
|
isparse_set(const isparse_set&) = default;
|
||||||
isparse_set& operator=(const isparse_set&) = default;
|
isparse_set& operator=(const isparse_set&) = default;
|
||||||
public:
|
public:
|
||||||
virtual bool contains(std::size_t key) const noexcept = 0;
|
virtual bool contains(std::size_t key) const noexcept = 0;
|
||||||
virtual void erase(std::size_t key) noexcept = 0;
|
virtual void erase(std::size_t key) noexcept = 0;
|
||||||
|
virtual std::size_t size() const noexcept = 0;
|
||||||
|
virtual std::size_t sparse_size() const noexcept = 0;
|
||||||
|
virtual std::span<std::size_t> dense_map() noexcept = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
@@ -70,6 +74,14 @@ public:
|
|||||||
m_reverseMap.pop_back();
|
m_reverseMap.pop_back();
|
||||||
set_index(key, TOMBSTONE);
|
set_index(key, TOMBSTONE);
|
||||||
}
|
}
|
||||||
|
public:
|
||||||
|
std::size_t size() const noexcept override { return m_dense.size(); }
|
||||||
|
|
||||||
|
std::size_t sparse_size() const noexcept override { return m_map.size(); }
|
||||||
|
|
||||||
|
std::span<std::size_t> dense_map() noexcept override {
|
||||||
|
return m_reverseMap;
|
||||||
|
}
|
||||||
private:
|
private:
|
||||||
void set_index(std::size_t key, std::size_t dense) {
|
void set_index(std::size_t key, std::size_t dense) {
|
||||||
if (key >= m_map.size()) m_map.resize(key + 1, TOMBSTONE);
|
if (key >= m_map.size()) m_map.resize(key + 1, TOMBSTONE);
|
||||||
|
|||||||
+140
@@ -8,6 +8,7 @@
|
|||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace libcatboy::ecs::test {
|
namespace libcatboy::ecs::test {
|
||||||
|
|
||||||
@@ -939,4 +940,143 @@ TEST_F(RegistryTest, RegistryIsDefaultConstructible) {
|
|||||||
SUCCEED();
|
SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// COMPONENT VIEWS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
TEST_F(RegistryTest, ViewReturnsEntitiesWithAllRequestedComponents) {
|
||||||
|
const auto e1 = create();
|
||||||
|
const auto e2 = create();
|
||||||
|
const auto e3 = create();
|
||||||
|
|
||||||
|
reg.emplace<position>(e1, 1, 2);
|
||||||
|
reg.emplace<velocity>(e1, 10, 20);
|
||||||
|
|
||||||
|
reg.emplace<position>(e2, 3, 4);
|
||||||
|
|
||||||
|
reg.emplace<position>(e3, 5, 6);
|
||||||
|
reg.emplace<velocity>(e3, 50, 60);
|
||||||
|
|
||||||
|
std::vector<entity> result;
|
||||||
|
|
||||||
|
for (auto [e, p, v] : reg.view<position, velocity>()) {
|
||||||
|
result.push_back(e);
|
||||||
|
|
||||||
|
EXPECT_EQ(p, (position{ e == e1 ? 1 : 5, e == e1 ? 2 : 6 }));
|
||||||
|
EXPECT_EQ(v, (velocity{ e == e1 ? 10 : 50, e == e1 ? 20 : 60 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_EQ(result.size(), 2u);
|
||||||
|
EXPECT_NE(std::find(result.begin(), result.end(), e1), result.end());
|
||||||
|
EXPECT_NE(std::find(result.begin(), result.end(), e3), result.end());
|
||||||
|
EXPECT_EQ(std::find(result.begin(), result.end(), e2), result.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RegistryTest, ViewIsEmptyWhenNoEntityHasAllComponents) {
|
||||||
|
const auto e1 = create();
|
||||||
|
const auto e2 = create();
|
||||||
|
|
||||||
|
reg.emplace<position>(e1, 1, 2);
|
||||||
|
reg.emplace<velocity>(e2, 3, 4);
|
||||||
|
|
||||||
|
auto view = reg.view<position, velocity>();
|
||||||
|
|
||||||
|
EXPECT_EQ(view.begin(), view.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RegistryTest, ViewReturnsMutableComponentReferences) {
|
||||||
|
const auto e = create();
|
||||||
|
|
||||||
|
reg.emplace<position>(e, 1, 2);
|
||||||
|
reg.emplace<velocity>(e, 3, 4);
|
||||||
|
|
||||||
|
for (auto [entity, p, v] : reg.view<position, velocity>()) {
|
||||||
|
EXPECT_EQ(entity, e);
|
||||||
|
|
||||||
|
p.x = 100;
|
||||||
|
v.y = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ(reg.at<position>(e), (position{ 100, 2 }));
|
||||||
|
EXPECT_EQ(reg.at<velocity>(e), (velocity{ 3, 200 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RegistryTest, ViewHandlesSparseEntityIDs) {
|
||||||
|
const auto e0 = create();
|
||||||
|
const auto e1 = create();
|
||||||
|
const auto e2 = create();
|
||||||
|
const auto e3 = create();
|
||||||
|
|
||||||
|
reg.emplace<position>(e0, 0, 0);
|
||||||
|
reg.emplace<velocity>(e0, 10, 10);
|
||||||
|
|
||||||
|
reg.emplace<position>(e2, 2, 20);
|
||||||
|
reg.emplace<velocity>(e2, 20, 20);
|
||||||
|
|
||||||
|
reg.emplace<position>(e3, 3, 30);
|
||||||
|
|
||||||
|
std::vector<entity> result;
|
||||||
|
|
||||||
|
for (auto [e, p, v] : reg.view<position, velocity>()) {
|
||||||
|
result.push_back(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_EQ(result.size(), 2u);
|
||||||
|
EXPECT_NE(std::find(result.begin(), result.end(), e0), result.end());
|
||||||
|
EXPECT_NE(std::find(result.begin(), result.end(), e2), result.end());
|
||||||
|
EXPECT_EQ(std::find(result.begin(), result.end(), e1), result.end());
|
||||||
|
EXPECT_EQ(std::find(result.begin(), result.end(), e3), result.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RegistryTest, ViewUsesSmallestComponentSetAsDriver) {
|
||||||
|
// Position exists on many entities while velocity exists on only two.
|
||||||
|
// The result should still contain exactly the intersection.
|
||||||
|
//
|
||||||
|
// This primarily exercises the optimization in component_view that picks
|
||||||
|
// the smallest sparse set as the iteration source.
|
||||||
|
for (int i = 0; i < 100; ++i) {
|
||||||
|
const auto e = create();
|
||||||
|
reg.emplace<position>(e, i, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto e1 = create();
|
||||||
|
const auto e2 = create();
|
||||||
|
|
||||||
|
reg.emplace<position>(e1, 100, 100);
|
||||||
|
reg.emplace<velocity>(e1, 1, 1);
|
||||||
|
|
||||||
|
reg.emplace<position>(e2, 200, 200);
|
||||||
|
reg.emplace<velocity>(e2, 2, 2);
|
||||||
|
|
||||||
|
std::vector<entity> result;
|
||||||
|
|
||||||
|
for (auto [e, p, v] : reg.view<position, velocity>())
|
||||||
|
result.push_back(e);
|
||||||
|
|
||||||
|
ASSERT_EQ(result.size(), 2u);
|
||||||
|
EXPECT_NE(std::find(result.begin(), result.end(), e1), result.end());
|
||||||
|
EXPECT_NE(std::find(result.begin(), result.end(), e2), result.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(RegistryTest, ViewStopsReturningEntityAfterComponentIsErased) {
|
||||||
|
const auto e1 = create();
|
||||||
|
const auto e2 = create();
|
||||||
|
|
||||||
|
reg.emplace<position>(e1, 1, 2);
|
||||||
|
reg.emplace<velocity>(e1, 3, 4);
|
||||||
|
|
||||||
|
reg.emplace<position>(e2, 5, 6);
|
||||||
|
reg.emplace<velocity>(e2, 7, 8);
|
||||||
|
|
||||||
|
reg.erase<velocity>(e1);
|
||||||
|
|
||||||
|
std::vector<entity> result;
|
||||||
|
|
||||||
|
for (auto [e, p, v] : reg.view<position, velocity>())
|
||||||
|
result.push_back(e);
|
||||||
|
|
||||||
|
ASSERT_EQ(result.size(), 1u);
|
||||||
|
EXPECT_EQ(result.front(), e2);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace libcatboy::ecs::test
|
} // namespace libcatboy::ecs::test
|
||||||
|
|||||||
Reference in New Issue
Block a user