From: Mikko Rasa Date: Tue, 18 Oct 2022 20:34:10 +0000 (+0300) Subject: Implement a basic ECS X-Git-Url: http://git.tdb.fi/?p=libs%2Fgame.git;a=commitdiff_plain;h=248d62f7240d342982ade65a510be912b867fe49 Implement a basic ECS --- 248d62f7240d342982ade65a510be912b867fe49 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ba0055 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.config +temp +/libmspgame.a +/libmspgame.so +/mspgame.pc diff --git a/Build b/Build new file mode 100644 index 0000000..beae8cd --- /dev/null +++ b/Build @@ -0,0 +1,22 @@ +package "mspgame" +{ + version "0.1"; + + require "mspcore"; + require "mspmath"; + + build_info + { + standard CXX "c++20"; + }; + + library "mspgame" + { + source "source/game"; + install true; + install_map + { + map "source" "include/msp"; + }; + }; +}; diff --git a/source/game/basicsystem.h b/source/game/basicsystem.h new file mode 100644 index 0000000..12c2725 --- /dev/null +++ b/source/game/basicsystem.h @@ -0,0 +1,54 @@ +#ifndef MSP_GAME_BASICSYSTEM_ +#define MSP_GAME_BASICSYSTEM_ + +#include +#include "pool.h" +#include "stage.h" +#include "system.h" + +namespace Msp::Game { + +template +concept HasPreTick = requires(T x) { x.pre_tick(); }; + +template +concept HasTick = requires(T x) { x.tick(Time::TimeDelta()); }; + +template +concept HasPostTick = requires(T x) { x.post_tick(); }; + +template +class BasicSystem: public System +{ +public: + BasicSystem(Stage &s): System(s) { } + + void pre_tick() override; + void tick(Time::TimeDelta) override; + void post_tick() override; +}; + +template +void BasicSystem::pre_tick() +{ + if constexpr(HasPreTick) + stage.iterate_objects([](T &obj){ obj.pre_tick(); }); +} + +template +void BasicSystem::tick(Time::TimeDelta dt) +{ + if constexpr(HasTick) + stage.iterate_objects([dt](T &obj){ obj.tick(dt); }); +} + +template +void BasicSystem::post_tick() +{ + if constexpr(HasPostTick) + stage.iterate_objects([](T &obj){ obj.post_tick(); }); +} + +} // namespace Msp::Game + +#endif diff --git a/source/game/component.cpp b/source/game/component.cpp new file mode 100644 index 0000000..ae2aba8 --- /dev/null +++ b/source/game/component.cpp @@ -0,0 +1,9 @@ +#include "component.h" + +namespace Msp::Game { + +Component::Component(Handle e): + entity(e) +{ } + +} // namespace Msp::Game diff --git a/source/game/component.h b/source/game/component.h new file mode 100644 index 0000000..6914c15 --- /dev/null +++ b/source/game/component.h @@ -0,0 +1,29 @@ +#ifndef MSP_GAME_COMPONENT_H_ +#define MSP_GAME_COMPONENT_H_ + +#include +#include "handle.h" + +namespace Msp::Game { + +class Entity; + +class Component +{ +protected: + Handle entity; + + Component(Handle); +public: + virtual ~Component() = default; + + Handle get_entity() const { return entity; } + + virtual void pre_tick() { } + virtual void tick(Time::TimeDelta) { } + virtual void post_tick() { } +}; + +} // namespace Msp::Game + +#endif diff --git a/source/game/director.cpp b/source/game/director.cpp new file mode 100644 index 0000000..741965b --- /dev/null +++ b/source/game/director.cpp @@ -0,0 +1,28 @@ +#include "director.h" +#include +#include +#include "stage.h" + +using namespace std; + +namespace Msp::Game { + +Stage &Director::create_stage() +{ + stages.emplace_back(std::make_unique()); + return *stages.back(); +} + +void Director::tick() +{ + Time::TimeStamp now = Time::now(); + Time::TimeDelta dt = (last_tick ? now-last_tick : Time::zero); + last_tick = now; + + backlog = min(backlog+dt, stepsize*max_backlog_steps); + for(unsigned i=0; (i=stepsize); ++i, backlog-=stepsize) + for(const auto &s: stages) + s->tick(stepsize); +} + +} // namespace Msp::Game diff --git a/source/game/director.h b/source/game/director.h new file mode 100644 index 0000000..24ef634 --- /dev/null +++ b/source/game/director.h @@ -0,0 +1,31 @@ +#ifndef MSP_GAME_DIRECTOR_H_ +#define MSP_GAME_DIRECTOR_H_ + +#include +#include +#include +#include + +namespace Msp::Game { + +class Stage; + +class Director +{ +private: + std::vector> stages; + Time::TimeStamp last_tick; + Time::TimeDelta stepsize = Time::sec/60; + Time::TimeDelta backlog; + unsigned max_steps_per_frame = 5; + unsigned max_backlog_steps = 600; + +public: + Stage &create_stage(); + + void tick(); +}; + +} // namespace Msp::Game + +#endif diff --git a/source/game/entity.cpp b/source/game/entity.cpp new file mode 100644 index 0000000..95ad686 --- /dev/null +++ b/source/game/entity.cpp @@ -0,0 +1,39 @@ +#include "entity.h" +#include "component.h" +#include "stage.h" + +using namespace std; + +namespace Msp::Game { + +Entity::Entity(Handle p): + parent(p) +{ } + +void Entity::add_component(Handle comp) +{ + if(comp->get_entity().get()!=this) + throw hierarchy_error(); + + components.push_back(comp); +} + +void Entity::remove_component(Handle comp) +{ + erase(components, comp); +} + +void Entity::add_child(Handle child) +{ + if(child->get_parent().get()!=this) + throw hierarchy_error(); + + children.push_back(child); +} + +void Entity::remove_child(Handle child) +{ + erase(children, child); +} + +} // namespace Msp::Game diff --git a/source/game/entity.h b/source/game/entity.h new file mode 100644 index 0000000..922f552 --- /dev/null +++ b/source/game/entity.h @@ -0,0 +1,48 @@ +#ifndef MSP_GAME_ENTITY_H_ +#define MSP_GAME_ENTITY_H_ + +#include "handle.h" + +namespace Msp::Game { + +class Component; + +class hierarchy_error: public std::logic_error +{ +public: + hierarchy_error(): std::logic_error("hierarchy error") { } +}; + +class Entity +{ +private: + Handle parent; + std::vector> components; + std::vector> children; + +public: + Entity(Handle); + virtual ~Entity(); + + void add_component(Handle); + void remove_component(Handle); + + void add_child(Handle); + void remove_child(Handle); + + Handle get_parent() const { return parent; } + Handle get_root(); +}; + + +inline Handle Entity::get_root() +{ + Handle e = Handle::from_object(this); + while(Handle p = e->get_parent()) + e = p; + return e; +} + +} // namespace Msp::Game + +#endif diff --git a/source/game/handle.cpp b/source/game/handle.cpp new file mode 100644 index 0000000..395395a --- /dev/null +++ b/source/game/handle.cpp @@ -0,0 +1,12 @@ +#include "handle.h" +#include + +using namespace std; + +namespace Msp::Game { + +invalid_handle::invalid_handle(const type_info &ti): + logic_error(Debug::demangle(ti.name())) +{ } + +} // namespace Msp::Game diff --git a/source/game/handle.h b/source/game/handle.h new file mode 100644 index 0000000..d384f35 --- /dev/null +++ b/source/game/handle.h @@ -0,0 +1,43 @@ +#ifndef MSP_GAME_HANDLE_H_ +#define MSP_GAME_HANDLE_H_ + +#include +#include "pool.h" + +namespace Msp::Game { + +class invalid_handle: public std::logic_error +{ +public: + invalid_handle(const std::type_info &); +}; + +template +class Handle +{ + template + friend class Handle; + +protected: + T *ptr = nullptr; + +public: + Handle() = default; + + static Handle from_object(T *o) { Handle h; h.ptr = o; return h; } + + template + requires std::is_base_of_v + Handle(const Handle &other): ptr(other.ptr) { } + + T *get() const { return ptr; } + T &operator*() const { return *ptr; } + T *operator->() const { return ptr; } + explicit operator bool() const { return ptr; } + + bool operator==(const Handle &other) const = default; +}; + +} // namespace Msp::Game + +#endif diff --git a/source/game/owned.h b/source/game/owned.h new file mode 100644 index 0000000..df3cd94 --- /dev/null +++ b/source/game/owned.h @@ -0,0 +1,99 @@ +#ifndef MSP_GAME_OWNED_H_ +#define MSP_GAME_OWNED_H_ + +#include +#include "handle.h" + +namespace Msp::Game { + +class Component; +class Entity; +class Root; +class Stage; + +template +class Owned: public Handle +{ +public: + Owned() = default; + + template + Owned(Handle, Args &&...); + + template + Owned(Entity &parent, Args &&... args): Owned(Handle::from_object(&parent), std::forward(args)...) { } + + Owned(Owned &&other): Handle(other) { other.ptr = nullptr; } + Owned &operator=(Owned &&other); + ~Owned() { destroy(); } + +private: + template + static Stage &get_stage(O &); + + void destroy(); +}; + + +template +template +Owned::Owned(Handle parent, Args &&... args) +{ + if(!parent) + throw std::invalid_argument("Owned::Owned"); + + using DependentEntity = std::conditional_t; + Handle dparent = parent; + + Pool &pool = get_stage(*dparent).get_pools().template get_pool(); + this->ptr = pool.create(parent, std::forward(args)...); + if constexpr(std::is_base_of_v) + dparent->add_component(*this); + else + dparent->add_child(*this); +} + +template +Owned &Owned::operator=(Owned &&other) +{ + destroy(); + + this->ptr = other.ptr; + other.ptr = nullptr; + + return *this; +} + +template +template +Stage &Owned::get_stage(O &obj) +{ + using DependentRoot = std::conditional_t; + if constexpr(std::is_base_of_v) + return get_stage(*obj.get_entity()); + else if constexpr(std::is_base_of_v) + return dynamic_cast(*obj.get_root()).get_stage(); + else + return obj; +} + +template +void Owned::destroy() +{ + T *obj = this->get(); + if(!obj) + return; + + Pool &pool = get_stage(*obj).get_pools().template get_pool(); + + if constexpr(std::is_base_of_v) + obj->get_entity()->remove_component(*this); + else if(auto parent = obj->get_parent().get()) + parent->remove_child(*this); + + pool.destroy(this->ptr); +} + +} // namespace Msp::Game + +#endif diff --git a/source/game/pool.cpp b/source/game/pool.cpp new file mode 100644 index 0000000..901e09e --- /dev/null +++ b/source/game/pool.cpp @@ -0,0 +1,155 @@ +#include "pool.h" +#include +#include + +using namespace std; + +namespace Msp::Game { + +unsigned PoolPool::get_next_id() +{ + static unsigned next_id = 0; + return next_id++; +} + + +PoolBase::PoolBase(uint32_t s, DeleteFunc d): + object_size(s), + deleter(d) +{ } + +PoolBase::PoolBase(PoolBase &&other): + object_size(other.object_size), + blocks(other.blocks), + free_list(other.free_list), + object_count(other.object_count), + capacity(other.capacity), + deleter(other.deleter) +{ + other.blocks = nullptr; + other.free_list = nullptr; + other.object_count = 0; + other.capacity = 0; +} + +PoolBase &PoolBase::operator=(PoolBase &&other) +{ + destroy_all(); + + object_size = other.object_size; + blocks = other.blocks; + free_list = other.free_list; + object_count = other.object_count; + capacity = other.capacity; + deleter = other.deleter; + + other.blocks = nullptr; + other.free_list = nullptr; + other.object_count = 0; + other.capacity = 0; + + return *this; +} + +PoolBase::~PoolBase() +{ + destroy_all(); +} + +void PoolBase::destroy_all() +{ + if(object_count>0) + IO::print(IO::cerr, "Warning: pool is being destroyed but has %d live objects", object_count); + + unsigned block_count = capacity/BLOCK_SIZE; + for(unsigned i=0; i(blocks); +} + +void *PoolBase::prepare_allocate() +{ + if(object_count>=capacity) + add_block(); + + uint32_t full_index = free_list[object_count]; + unsigned block_index = full_index/BLOCK_SIZE; + unsigned object_index = full_index%BLOCK_SIZE; + return blocks[block_index]+object_index*object_size; +} + +void PoolBase::commit_allocate(void *ptr) +{ + uint32_t full_index = free_list[object_count]; + unsigned block_index = full_index/BLOCK_SIZE; + unsigned object_index = full_index%BLOCK_SIZE; + void *expected = blocks[block_index]+object_index*object_size; + if(ptr!=expected) + throw logic_error("PoolBase::commit_allocate does not match prepare_allocate"); + + FlagType *flags = reinterpret_cast(blocks[block_index]+BLOCK_SIZE*object_size); + FlagType bit = 1<<(object_index%FLAG_BITS); + if(flags[object_index/FLAG_BITS]&bit) + throw logic_error("PoolBase::commit_allocate to a not-free index"); + + flags[object_index/FLAG_BITS] |= bit; + ++object_count; +} + +void PoolBase::add_block() +{ + unsigned block_count = capacity/BLOCK_SIZE; + char *new_mem = new alignas(char *) char[(block_count+1)*(sizeof(char *)+BLOCK_SIZE*sizeof(uint32_t))]; + char **new_blocks = reinterpret_cast(new_mem); + uint32_t *new_free = reinterpret_cast(new_mem+(block_count+1)*sizeof(char *)); + + copy(blocks, blocks+block_count, new_blocks); + copy(free_list, free_list+capacity, new_free); + + char *block = new alignas(BLOCK_ALIGNMENT) char[BLOCK_SIZE*object_size+BLOCK_SIZE/8]; + new_blocks[block_count] = block; + FlagType *flags = reinterpret_cast(block+BLOCK_SIZE*object_size); + for(unsigned i=0; i(blocks); + blocks = new_blocks; + free_list = new_free; + capacity += BLOCK_SIZE; +} + +void PoolBase::destroy(void *obj) +{ + unsigned block_index = 0; + unsigned object_index = BLOCK_SIZE; + intptr_t addr = reinterpret_cast(obj); + unsigned block_count = capacity/BLOCK_SIZE; + while(block_index(blocks[block_index]); + intptr_t mem_end = mem+BLOCK_SIZE*object_size; + if(addr>=mem && addr=BLOCK_SIZE) + throw invalid_argument("PoolBase::destroy"); + + FlagType *flags = reinterpret_cast(blocks[block_index]+BLOCK_SIZE*object_size); + FlagType bit = 1<<(object_index%FLAG_BITS); + if(!(flags[object_index/FLAG_BITS]&bit)) + throw invalid_argument("PoolBase::destroy"); + + deleter(obj); + flags[object_index/FLAG_BITS] &= ~bit; + --object_count; + free_list[object_count] = block_index*BLOCK_SIZE+object_index; +} + +} // namespace Msp::Game diff --git a/source/game/pool.h b/source/game/pool.h new file mode 100644 index 0000000..16801f4 --- /dev/null +++ b/source/game/pool.h @@ -0,0 +1,157 @@ +#ifndef MSP_GAME_STORAGE_H_ +#define MSP_GAME_STORAGE_H_ + +#include +#include +#include +#include + +namespace Msp::Game { + +class PoolBase; + +template +class Pool; + +class PoolPool: public NonCopyable +{ +private: + std::vector> pools; + +public: + PoolPool() = default; + PoolPool(PoolPool &&) = default; + PoolPool &operator=(PoolPool &&) = default; + +private: + static unsigned get_next_id(); + +public: + template + static unsigned get_type_id() { static unsigned id = get_next_id(); return id; } + + template + Pool &get_pool(); +}; + + +class PoolBase: public NonCopyable +{ +private: + using DeleteFunc = void(void *); + using FlagType = uint32_t; + + static constexpr std::size_t BLOCK_SIZE = 512; + static constexpr std::size_t BLOCK_ALIGNMENT = 64; + static constexpr std::size_t FLAG_BITS = sizeof(FlagType)*8; + + std::size_t object_size = 0; + char **blocks = nullptr; + std::uint32_t *free_list = nullptr; + std::uint32_t object_count = 0; + std::uint32_t capacity = 0; + DeleteFunc *deleter = nullptr; + +protected: + PoolBase(std::uint32_t, DeleteFunc); +public: + PoolBase(PoolBase &&); + PoolBase &operator=(PoolBase &&); + ~PoolBase(); + +protected: + void destroy_all(); + + template + void iterate_objects(const F &); + + void *prepare_allocate(); + void commit_allocate(void *); + +private: + void add_block(); + +public: + void destroy(void *); +}; + + +template +class Pool: public PoolBase +{ +public: + Pool(): PoolBase(sizeof(T), delete_object) { } + + template + T *create(Args &&...); + + template + void iterate_objects(const F &func) + { PoolBase::iterate_objects([&func](void *ptr){ func(*static_cast(ptr)); }); } + +private: + static void delete_object(void *ptr) { std::destroy_at(static_cast(ptr)); } +}; + + +template +inline Pool &PoolPool::get_pool() +{ + unsigned id = get_type_id(); + if(pools.size()<=id) + pools.resize(id+1); + + std::unique_ptr &ptr = pools[id]; + if(!ptr) + ptr = std::make_unique>(); + + return *static_cast *>(ptr.get()); +} + + +template +inline void PoolBase::iterate_objects(const F &func) +{ + unsigned block_count = capacity/BLOCK_SIZE; + for(unsigned i=0; i(ptr+BLOCK_SIZE*object_size); + for(unsigned j=0; j(~f)) + { + for(; ptr!=end; ptr+=object_size) + func(static_cast(ptr)); + } + else + { + for(; ptr!=end; ptr+=object_size, f>>=1) + if(f&1) + func(static_cast(ptr)); + } + } + else + ptr = end; + ++flags; + } + } +} + + +template +template +inline T *Pool::create(Args &&... args) +{ + void *ptr = prepare_allocate(); + T *obj = std::construct_at(static_cast(ptr), std::forward(args)...); + commit_allocate(ptr); + return obj; +} + +} // namespace Msp::Game + +#endif diff --git a/source/game/root.h b/source/game/root.h new file mode 100644 index 0000000..f4ed18e --- /dev/null +++ b/source/game/root.h @@ -0,0 +1,23 @@ +#ifndef MSP_GAME_ROOT_H_ +#define MSP_GAME_ROOT_H_ + +#include "entity.h" + +namespace Msp::Game { + +class Stage; + +class Root: public Entity +{ +private: + Stage &stage; + +public: + Root(Stage &s): Entity(Handle()), stage(s) { } + + Stage &get_stage() const { return stage; } +}; + +} // namespace Msp::Game + +#endif diff --git a/source/game/stage.cpp b/source/game/stage.cpp new file mode 100644 index 0000000..a6bd19a --- /dev/null +++ b/source/game/stage.cpp @@ -0,0 +1,24 @@ +#include "stage.h" +#include "system.h" + +namespace Msp::Game { + +Stage::Stage(): + root(*this) +{ } + +// Hide ~unique_ptr from the header +Stage::~Stage() +{ } + +void Stage::tick(Time::TimeDelta dt) +{ + for(const auto &s: systems) + s->pre_tick(); + for(const auto &s: systems) + s->tick(dt); + for(const auto &s: systems) + s->post_tick(); +} + +} // namespace Msp::Game diff --git a/source/game/stage.h b/source/game/stage.h new file mode 100644 index 0000000..0607596 --- /dev/null +++ b/source/game/stage.h @@ -0,0 +1,53 @@ +#ifndef MSP_GAME_STAGE_H_ +#define MSP_GAME_STAGE_H_ + +#include +#include +#include "handle.h" +#include "root.h" + +namespace Msp::Game { + +class System; + +class Stage +{ +private: + PoolPool pools; + Root root; + std::vector> systems; + +public: + Stage(); + ~Stage(); + + PoolPool &get_pools() { return pools; } + Handle get_root() { return Handle::from_object(&root); } + + template + void iterate_objects(const F &); + + template + T &add_system(Args &&...); + + const std::vector> &get_systems() const { return systems; } + + void tick(Time::TimeDelta); +}; + +template +void Stage::iterate_objects(const F &func) +{ + pools.get_pool().iterate_objects(func); +} + +template +T &Stage::add_system(Args &&... args) +{ + systems.emplace_back(std::make_unique(*this, std::forward(args)...)); + return static_cast(*systems.back()); +} + +} // namespace Msp::Game + +#endif diff --git a/source/game/system.h b/source/game/system.h new file mode 100644 index 0000000..88ba291 --- /dev/null +++ b/source/game/system.h @@ -0,0 +1,26 @@ +#ifndef MSP_GAME_SYSTEM_H_ +#define MSP_GAME_SYSTEM_H_ + +#include + +namespace Msp::Game { + +class Stage; + +class System +{ +protected: + Stage &stage; + + System(Stage &s): stage(s) { } +public: + virtual ~System() = default; + + virtual void pre_tick() = 0; + virtual void tick(Time::TimeDelta) = 0; + virtual void post_tick() = 0; +}; + +} // namespace Msp::Game + +#endif