feat(ecs): implement basic ECS
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
#ifndef LIBCATBOY_ECS_ECS_HPP
|
||||
#define LIBCATBOY_ECS_ECS_HPP
|
||||
|
||||
#include "fwd.hpp"
|
||||
#include "sparseSet.hpp"
|
||||
|
||||
#include <concepts>
|
||||
#include <queue>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <typeindex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace libcatboy {
|
||||
namespace ecs {
|
||||
|
||||
template <std::integral Entity>
|
||||
class basic_registry {
|
||||
public:
|
||||
using entity_type = Entity;
|
||||
public:
|
||||
entity_type create_entity() {
|
||||
if (m_graveyard.empty()) {
|
||||
entity_type entity = m_entityCounter++;
|
||||
m_alive.insert(entity);
|
||||
return entity;
|
||||
}
|
||||
entity_type entity = m_graveyard.front();
|
||||
m_alive.insert(entity);
|
||||
m_graveyard.pop();
|
||||
return entity;
|
||||
}
|
||||
|
||||
void erase_entity(entity_type entity) {
|
||||
if (!m_alive.contains(entity)) return;
|
||||
m_alive.erase(entity);
|
||||
for (const auto& [type, set] : m_components) {
|
||||
set->erase(entity);
|
||||
}
|
||||
m_graveyard.push(entity);
|
||||
}
|
||||
public:
|
||||
template <typename T>
|
||||
void register_component() {
|
||||
get_set<T>();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void unregister_component() {
|
||||
auto it = m_components.find(typeid(T));
|
||||
if (it == m_components.end()) return;
|
||||
delete reinterpret_cast<sparse_set<T>*>(it->second);
|
||||
m_components.erase(it);
|
||||
}
|
||||
public:
|
||||
template <typename T>
|
||||
void insert(entity_type entity, T&& component) {
|
||||
emplace<std::remove_cvref_t<T>>(entity, std::forward<T>(component));
|
||||
}
|
||||
|
||||
template <typename T, typename... Args>
|
||||
T& emplace(entity_type entity, Args&&... args) {
|
||||
if (m_alive.find(entity) == m_alive.end()) throw std::runtime_error("entity is dead");
|
||||
return get_set<T>().emplace(entity, std::forward<Args>(args)...);
|
||||
}
|
||||
public:
|
||||
template <typename T>
|
||||
T& at(entity_type entity) {
|
||||
if (m_alive.find(entity) == m_alive.end()) throw std::runtime_error("entity is dead");
|
||||
return get_set<T>().at(entity);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
const T& at(entity_type entity) const {
|
||||
if (m_alive.find(entity) == m_alive.end()) throw std::runtime_error("entity is dead");
|
||||
return get_set<T>().at(entity);
|
||||
}
|
||||
public:
|
||||
template <typename T>
|
||||
void erase(entity_type entity) {
|
||||
if (m_alive.find(entity) == m_alive.end()) throw std::runtime_error("entity is dead");
|
||||
get_set<T>().erase(entity);
|
||||
}
|
||||
private:
|
||||
template <typename T>
|
||||
sparse_set<T>& get_set() {
|
||||
auto it = m_components.find(typeid(T));
|
||||
if (it != m_components.end()) return dynamic_cast<sparse_set<T>&>(*it->second);
|
||||
sparse_set<T>* set = new sparse_set<T>();
|
||||
m_components.emplace(typeid(T), set);
|
||||
return *set;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
const sparse_set<T>& get_set() const {
|
||||
auto it = m_components.find(typeid(T));
|
||||
if (it != m_components.end()) return dynamic_cast<sparse_set<T>&>(*it->second);
|
||||
throw std::runtime_error("error");
|
||||
}
|
||||
private:
|
||||
std::unordered_map<std::type_index, isparse_set*> m_components;
|
||||
|
||||
std::unordered_set<entity_type> m_alive;
|
||||
std::queue<entity_type> m_graveyard;
|
||||
entity_type m_entityCounter = 0;
|
||||
};
|
||||
|
||||
} // namespace ecs
|
||||
} // namespace libcatboy
|
||||
|
||||
#endif // LIBCATBOY_ECS_ECS_HPP
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef LIBCATBOY_ECS_FWD_HPP
|
||||
#define LIBCATBOY_ECS_FWD_HPP
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
|
||||
namespace libcatboy {
|
||||
namespace ecs {
|
||||
|
||||
using entity = std::uint32_t;
|
||||
|
||||
template <std::integral Entity = entity>
|
||||
class basic_registry;
|
||||
|
||||
using registry = basic_registry<>;
|
||||
|
||||
} // namespace ecs
|
||||
} // namespace libcatboy
|
||||
|
||||
#endif // LIBCATBOY_ECS_FWD_HPP
|
||||
@@ -0,0 +1,89 @@
|
||||
#ifndef LIBCATBOY_ECS_SPARSE_SET_HPP
|
||||
#define LIBCATBOY_ECS_SPARSE_SET_HPP
|
||||
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace libcatboy {
|
||||
namespace ecs {
|
||||
|
||||
class isparse_set {
|
||||
public:
|
||||
isparse_set() = default;
|
||||
virtual ~isparse_set() = default;
|
||||
|
||||
isparse_set(isparse_set&&) noexcept = default;
|
||||
isparse_set& operator=(isparse_set&&) noexcept = default;
|
||||
isparse_set(const isparse_set&) = default;
|
||||
isparse_set& operator=(const isparse_set&) = default;
|
||||
public:
|
||||
virtual bool contains(std::size_t key) const noexcept = 0;
|
||||
virtual void erase(std::size_t key) noexcept = 0;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class sparse_set : public isparse_set {
|
||||
public:
|
||||
using value_type = T;
|
||||
using reference = T&;
|
||||
using const_reference = const T&;
|
||||
using pointer = T*;
|
||||
using const_pointer = const T*;
|
||||
|
||||
static constexpr std::size_t TOMBSTONE = -1;
|
||||
public:
|
||||
void insert(std::size_t key, const T& value) { emplace(key, value); }
|
||||
void insert(std::size_t key, T&& value) { emplace(key, std::move(value)); }
|
||||
|
||||
template <typename... Args, typename = std::enable_if_t<std::is_constructible_v<T, Args...>>>
|
||||
T& emplace(std::size_t key, Args&&... args) {
|
||||
std::size_t index = get_index(key);
|
||||
if (index == TOMBSTONE) {
|
||||
index = m_dense.size();
|
||||
set_index(key, index);
|
||||
m_reverseMap.push_back(key);
|
||||
return m_dense.emplace_back(std::forward<Args>(args)...);
|
||||
}
|
||||
m_reverseMap[index] = key;
|
||||
return *m_dense.emplace(m_dense.cbegin() + index, std::forward<Args>(args)...);
|
||||
}
|
||||
public:
|
||||
T& operator[](std::size_t key) { return m_dense[get_index(key)]; }
|
||||
const T& operator[](std::size_t key) const { return m_dense[get_index(key)]; }
|
||||
|
||||
T& at(std::size_t key) { return m_dense.at(get_index(key)); }
|
||||
const T& at(std::size_t key) const { return m_dense.at(get_index(key)); }
|
||||
|
||||
bool contains(std::size_t key) const noexcept override { return get_index(key) != TOMBSTONE; }
|
||||
public:
|
||||
void erase(std::size_t key) noexcept override {
|
||||
std::size_t index = get_index(key);
|
||||
if (index == TOMBSTONE) return;
|
||||
if (index != m_dense.size() - 1) {
|
||||
set_index(m_reverseMap.back(), index);
|
||||
std::swap(m_dense[index], m_dense.back());
|
||||
std::swap(m_reverseMap[index], m_reverseMap.back());
|
||||
}
|
||||
|
||||
m_dense.pop_back();
|
||||
m_reverseMap.pop_back();
|
||||
set_index(key, TOMBSTONE);
|
||||
}
|
||||
private:
|
||||
void set_index(std::size_t key, std::size_t dense) {
|
||||
if (key >= m_map.size()) m_map.resize(key + 1, TOMBSTONE);
|
||||
m_map[key] = dense;
|
||||
}
|
||||
|
||||
std::size_t get_index(std::size_t key) const { return key >= m_map.size() ? TOMBSTONE : m_map[key]; }
|
||||
private:
|
||||
std::vector<std::size_t> m_map;
|
||||
std::vector<std::size_t> m_reverseMap;
|
||||
std::vector<T> m_dense;
|
||||
};
|
||||
|
||||
} // namespace ecs
|
||||
} // namespace libcatboy
|
||||
|
||||
#endif // LIBCATBOY_ECS_SPARSE_SET_HPP
|
||||
Reference in New Issue
Block a user