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
|
||||
#define LIBCATBOY_ECS_ECS_HPP
|
||||
|
||||
#include "component_view.hpp"
|
||||
#include "fwd.hpp"
|
||||
#include "sparseSet.hpp"
|
||||
|
||||
@@ -83,6 +84,12 @@ public:
|
||||
if (m_alive.find(entity) == m_alive.end()) throw std::runtime_error("entity is dead");
|
||||
get_set<T>().erase(entity);
|
||||
}
|
||||
public:
|
||||
template <typename... Sum>
|
||||
requires(sizeof...(Sum) >= 1)
|
||||
component_view<Entity, Sum...> view() {
|
||||
return { { (&get_set<Sum>())... } };
|
||||
}
|
||||
private:
|
||||
template <typename T>
|
||||
sparse_set<T>& get_set() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef LIBCATBOY_ECS_SPARSE_SET_HPP
|
||||
#define LIBCATBOY_ECS_SPARSE_SET_HPP
|
||||
|
||||
#include <span>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -20,6 +21,9 @@ public:
|
||||
public:
|
||||
virtual bool contains(std::size_t key) const 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>
|
||||
@@ -70,6 +74,14 @@ public:
|
||||
m_reverseMap.pop_back();
|
||||
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:
|
||||
void set_index(std::size_t key, std::size_t dense) {
|
||||
if (key >= m_map.size()) m_map.resize(key + 1, TOMBSTONE);
|
||||
|
||||
+140
@@ -8,6 +8,7 @@
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
namespace libcatboy::ecs::test {
|
||||
|
||||
@@ -939,4 +940,143 @@ TEST_F(RegistryTest, RegistryIsDefaultConstructible) {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user