]> git.tdb.fi Git - libs/game.git/commitdiff
Implement a basic ECS
authorMikko Rasa <tdb@tdb.fi>
Tue, 18 Oct 2022 20:34:10 +0000 (23:34 +0300)
committerMikko Rasa <tdb@tdb.fi>
Wed, 19 Oct 2022 06:58:02 +0000 (09:58 +0300)
18 files changed:
.gitignore [new file with mode: 0644]
Build [new file with mode: 0644]
source/game/basicsystem.h [new file with mode: 0644]
source/game/component.cpp [new file with mode: 0644]
source/game/component.h [new file with mode: 0644]
source/game/director.cpp [new file with mode: 0644]
source/game/director.h [new file with mode: 0644]
source/game/entity.cpp [new file with mode: 0644]
source/game/entity.h [new file with mode: 0644]
source/game/handle.cpp [new file with mode: 0644]
source/game/handle.h [new file with mode: 0644]
source/game/owned.h [new file with mode: 0644]
source/game/pool.cpp [new file with mode: 0644]
source/game/pool.h [new file with mode: 0644]
source/game/root.h [new file with mode: 0644]
source/game/stage.cpp [new file with mode: 0644]
source/game/stage.h [new file with mode: 0644]
source/game/system.h [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..9ba0055
--- /dev/null
@@ -0,0 +1,5 @@
+.config
+temp
+/libmspgame.a
+/libmspgame.so
+/mspgame.pc
diff --git a/Build b/Build
new file mode 100644 (file)
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 (file)
index 0000000..12c2725
--- /dev/null
@@ -0,0 +1,54 @@
+#ifndef MSP_GAME_BASICSYSTEM_
+#define MSP_GAME_BASICSYSTEM_
+
+#include <msp/time/timedelta.h>
+#include "pool.h"
+#include "stage.h"
+#include "system.h"
+
+namespace Msp::Game {
+
+template<typename T>
+concept HasPreTick = requires(T x) { x.pre_tick(); };
+
+template<typename T>
+concept HasTick = requires(T x) { x.tick(Time::TimeDelta()); };
+
+template<typename T>
+concept HasPostTick = requires(T x) { x.post_tick(); };
+
+template<typename T>
+class BasicSystem: public System
+{
+public:
+       BasicSystem(Stage &s): System(s) { }
+
+       void pre_tick() override;
+       void tick(Time::TimeDelta) override;
+       void post_tick() override;
+};
+
+template<typename T>
+void BasicSystem<T>::pre_tick()
+{
+       if constexpr(HasPreTick<T>)
+               stage.iterate_objects<T>([](T &obj){ obj.pre_tick(); });
+}
+
+template<typename T>
+void BasicSystem<T>::tick(Time::TimeDelta dt)
+{
+       if constexpr(HasTick<T>)
+               stage.iterate_objects<T>([dt](T &obj){ obj.tick(dt); });
+}
+
+template<typename T>
+void BasicSystem<T>::post_tick()
+{
+       if constexpr(HasPostTick<T>)
+               stage.iterate_objects<T>([](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 (file)
index 0000000..ae2aba8
--- /dev/null
@@ -0,0 +1,9 @@
+#include "component.h"
+
+namespace Msp::Game {
+
+Component::Component(Handle<Entity> e):
+       entity(e)
+{ }
+
+} // namespace Msp::Game
diff --git a/source/game/component.h b/source/game/component.h
new file mode 100644 (file)
index 0000000..6914c15
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef MSP_GAME_COMPONENT_H_
+#define MSP_GAME_COMPONENT_H_
+
+#include <msp/time/timedelta.h>
+#include "handle.h"
+
+namespace Msp::Game {
+
+class Entity;
+
+class Component
+{
+protected:
+       Handle<Entity> entity;
+
+       Component(Handle<Entity>);
+public:
+       virtual ~Component() = default;
+
+       Handle<Entity> 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 (file)
index 0000000..741965b
--- /dev/null
@@ -0,0 +1,28 @@
+#include "director.h"
+#include <stdexcept>
+#include <msp/time/utils.h>
+#include "stage.h"
+
+using namespace std;
+
+namespace Msp::Game {
+
+Stage &Director::create_stage()
+{
+       stages.emplace_back(std::make_unique<Stage>());
+       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<max_steps_per_frame && backlog>=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 (file)
index 0000000..24ef634
--- /dev/null
@@ -0,0 +1,31 @@
+#ifndef MSP_GAME_DIRECTOR_H_
+#define MSP_GAME_DIRECTOR_H_
+
+#include <memory>
+#include <vector>
+#include <msp/time/timedelta.h>
+#include <msp/time/timestamp.h>
+
+namespace Msp::Game {
+
+class Stage;
+
+class Director
+{
+private:
+       std::vector<std::unique_ptr<Stage>> 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 (file)
index 0000000..95ad686
--- /dev/null
@@ -0,0 +1,39 @@
+#include "entity.h"
+#include "component.h"
+#include "stage.h"
+
+using namespace std;
+
+namespace Msp::Game {
+
+Entity::Entity(Handle<Entity> p):
+       parent(p)
+{ }
+
+void Entity::add_component(Handle<Component> comp)
+{
+       if(comp->get_entity().get()!=this)
+               throw hierarchy_error();
+
+       components.push_back(comp);
+}
+
+void Entity::remove_component(Handle<Component> comp)
+{
+       erase(components, comp);
+}
+
+void Entity::add_child(Handle<Entity> child)
+{
+       if(child->get_parent().get()!=this)
+               throw hierarchy_error();
+
+       children.push_back(child);
+}
+
+void Entity::remove_child(Handle<Entity> child)
+{
+       erase(children, child);
+}
+
+} // namespace Msp::Game
diff --git a/source/game/entity.h b/source/game/entity.h
new file mode 100644 (file)
index 0000000..922f552
--- /dev/null
@@ -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<Entity> parent;
+       std::vector<Handle<Component>> components;
+       std::vector<Handle<Entity>> children;
+
+public:
+       Entity(Handle<Entity>);
+       virtual ~Entity();
+
+       void add_component(Handle<Component>);
+       void remove_component(Handle<Component>);
+
+       void add_child(Handle<Entity>);
+       void remove_child(Handle<Entity>);
+
+       Handle<Entity> get_parent() const { return parent; }
+       Handle<Entity> get_root();
+};
+
+
+inline Handle<Entity> Entity::get_root()
+{
+       Handle<Entity> e = Handle<Entity>::from_object(this);
+       while(Handle<Entity> 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 (file)
index 0000000..395395a
--- /dev/null
@@ -0,0 +1,12 @@
+#include "handle.h"
+#include <msp/debug/demangle.h>
+
+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 (file)
index 0000000..d384f35
--- /dev/null
@@ -0,0 +1,43 @@
+#ifndef MSP_GAME_HANDLE_H_
+#define MSP_GAME_HANDLE_H_
+
+#include <stdexcept>
+#include "pool.h"
+
+namespace Msp::Game {
+
+class invalid_handle: public std::logic_error
+{
+public:
+       invalid_handle(const std::type_info &);
+};
+
+template<typename T>
+class Handle
+{
+       template<typename U>
+       friend class Handle;
+
+protected:
+       T *ptr = nullptr;
+
+public:
+       Handle() = default;
+
+       static Handle from_object(T *o) { Handle h; h.ptr = o; return h; }
+
+       template<typename U>
+               requires std::is_base_of_v<T, U>
+       Handle(const Handle<U> &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 (file)
index 0000000..df3cd94
--- /dev/null
@@ -0,0 +1,99 @@
+#ifndef MSP_GAME_OWNED_H_
+#define MSP_GAME_OWNED_H_
+
+#include <stdexcept>
+#include "handle.h"
+
+namespace Msp::Game {
+
+class Component;
+class Entity;
+class Root;
+class Stage;
+
+template<typename T>
+class Owned: public Handle<T>
+{
+public:
+       Owned() = default;
+
+       template<typename... Args>
+       Owned(Handle<Entity>, Args &&...);
+
+       template<typename... Args>
+       Owned(Entity &parent, Args &&... args): Owned(Handle<Entity>::from_object(&parent), std::forward<Args>(args)...) { }
+
+       Owned(Owned &&other): Handle<T>(other) { other.ptr = nullptr; }
+       Owned &operator=(Owned &&other);
+       ~Owned() { destroy(); }
+
+private:
+       template<typename O>
+       static Stage &get_stage(O &);
+
+       void destroy();
+};
+
+
+template<typename T>
+template<typename... Args>
+Owned<T>::Owned(Handle<Entity> parent, Args &&... args)
+{
+       if(!parent)
+               throw std::invalid_argument("Owned::Owned");
+
+       using DependentEntity = std::conditional_t<sizeof(T), Entity, Entity>;
+       Handle<DependentEntity> dparent = parent;
+
+       Pool<T> &pool = get_stage(*dparent).get_pools().template get_pool<T>();
+       this->ptr = pool.create(parent, std::forward<Args>(args)...);
+       if constexpr(std::is_base_of_v<Component, T>)
+               dparent->add_component(*this);
+       else
+               dparent->add_child(*this);
+}
+
+template<typename T>
+Owned<T> &Owned<T>::operator=(Owned &&other)
+{
+       destroy();
+
+       this->ptr = other.ptr;
+       other.ptr = nullptr;
+
+       return *this;
+}
+
+template<typename T>
+template<typename O>
+Stage &Owned<T>::get_stage(O &obj)
+{
+       using DependentRoot = std::conditional_t<sizeof(T), Root, Root>;
+       if constexpr(std::is_base_of_v<Component, O>)
+               return get_stage(*obj.get_entity());
+       else if constexpr(std::is_base_of_v<Entity, O>)
+               return dynamic_cast<DependentRoot &>(*obj.get_root()).get_stage();
+       else
+               return obj;
+}
+
+template<typename T>
+void Owned<T>::destroy()
+{
+       T *obj = this->get();
+       if(!obj)
+               return;
+
+       Pool<T> &pool = get_stage(*obj).get_pools().template get_pool<T>();
+
+       if constexpr(std::is_base_of_v<Component, T>)
+               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 (file)
index 0000000..901e09e
--- /dev/null
@@ -0,0 +1,155 @@
+#include "pool.h"
+#include <stdexcept>
+#include <msp/io/print.h>
+
+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<block_count; ++i)
+               delete[] blocks[i];
+       delete[] reinterpret_cast<char *>(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<FlagType *>(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<char **>(new_mem);
+       uint32_t *new_free = reinterpret_cast<uint32_t *>(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<FlagType *>(block+BLOCK_SIZE*object_size);
+       for(unsigned i=0; i<BLOCK_SIZE; ++i)
+               new_free[capacity+i] = capacity+i;
+       for(unsigned i=0; i<BLOCK_SIZE/FLAG_BITS; ++i)
+               flags[i] = 0;
+
+       delete[] reinterpret_cast<char *>(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<intptr_t>(obj);
+       unsigned block_count = capacity/BLOCK_SIZE;
+       while(block_index<block_count)
+       {
+               intptr_t mem = reinterpret_cast<intptr_t>(blocks[block_index]);
+               intptr_t mem_end = mem+BLOCK_SIZE*object_size;
+               if(addr>=mem && addr<mem_end)
+               {
+                       object_index = (addr-mem)/object_size;
+                       break;
+               }
+               ++block_index;
+       }
+
+       if(object_index>=BLOCK_SIZE)
+               throw invalid_argument("PoolBase::destroy");
+
+       FlagType *flags = reinterpret_cast<FlagType *>(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 (file)
index 0000000..16801f4
--- /dev/null
@@ -0,0 +1,157 @@
+#ifndef MSP_GAME_STORAGE_H_
+#define MSP_GAME_STORAGE_H_
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+#include <msp/core/noncopyable.h>
+
+namespace Msp::Game {
+
+class PoolBase;
+
+template<typename T>
+class Pool;
+
+class PoolPool: public NonCopyable
+{
+private:
+       std::vector<std::unique_ptr<PoolBase>> pools;
+
+public:
+       PoolPool() = default;
+       PoolPool(PoolPool &&) = default;
+       PoolPool &operator=(PoolPool &&) = default;
+
+private:
+       static unsigned get_next_id();
+
+public:
+       template<typename T>
+       static unsigned get_type_id() { static unsigned id = get_next_id(); return id; }
+
+       template<typename T>
+       Pool<T> &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<typename F>
+       void iterate_objects(const F &);
+
+       void *prepare_allocate();
+       void commit_allocate(void *);
+
+private:
+       void add_block();
+
+public:
+       void destroy(void *);
+};
+
+
+template<typename T>
+class Pool: public PoolBase
+{
+public:
+       Pool(): PoolBase(sizeof(T), delete_object) { }
+
+       template<typename... Args>
+       T *create(Args &&...);
+
+       template<typename F>
+       void iterate_objects(const F &func)
+       { PoolBase::iterate_objects([&func](void *ptr){ func(*static_cast<T *>(ptr)); }); }
+
+private:
+       static void delete_object(void *ptr) { std::destroy_at(static_cast<T *>(ptr)); }
+};
+
+
+template<typename T>
+inline Pool<T> &PoolPool::get_pool()
+{
+       unsigned id = get_type_id<T>();
+       if(pools.size()<=id)
+               pools.resize(id+1);
+
+       std::unique_ptr<PoolBase> &ptr = pools[id];
+       if(!ptr)
+               ptr = std::make_unique<Pool<T>>();
+
+       return *static_cast<Pool<T> *>(ptr.get());
+}
+
+
+template<typename F>
+inline void PoolBase::iterate_objects(const F &func)
+{
+       unsigned block_count = capacity/BLOCK_SIZE;
+       for(unsigned i=0; i<block_count; ++i)
+       {
+               char *ptr = blocks[i];
+               const FlagType *flags = reinterpret_cast<FlagType *>(ptr+BLOCK_SIZE*object_size);
+               for(unsigned j=0; j<BLOCK_SIZE; j+=FLAG_BITS)
+               {
+                       char *end = ptr+FLAG_BITS*object_size;
+                       if(FlagType f = *flags)
+                       {
+                               if(!static_cast<FlagType>(~f))
+                               {
+                                       for(; ptr!=end; ptr+=object_size)
+                                               func(static_cast<void *>(ptr));
+                               }
+                               else
+                               {
+                                       for(; ptr!=end; ptr+=object_size, f>>=1)
+                                               if(f&1)
+                                                       func(static_cast<void *>(ptr));
+                               }
+                       }
+                       else
+                               ptr = end;
+                       ++flags;
+               }
+       }
+}
+
+
+template<typename T>
+template<typename... Args>
+inline T *Pool<T>::create(Args &&... args)
+{
+       void *ptr = prepare_allocate();
+       T *obj = std::construct_at(static_cast<T *>(ptr), std::forward<Args>(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 (file)
index 0000000..f4ed18e
--- /dev/null
@@ -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<Entity>()), 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 (file)
index 0000000..a6bd19a
--- /dev/null
@@ -0,0 +1,24 @@
+#include "stage.h"
+#include "system.h"
+
+namespace Msp::Game {
+
+Stage::Stage():
+       root(*this)
+{ }
+
+// Hide ~unique_ptr<System> 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 (file)
index 0000000..0607596
--- /dev/null
@@ -0,0 +1,53 @@
+#ifndef MSP_GAME_STAGE_H_
+#define MSP_GAME_STAGE_H_
+
+#include <memory>
+#include <msp/time/timedelta.h>
+#include "handle.h"
+#include "root.h"
+
+namespace Msp::Game {
+
+class System;
+
+class Stage
+{
+private:
+       PoolPool pools;
+       Root root;
+       std::vector<std::unique_ptr<System>> systems;
+
+public:
+       Stage();
+       ~Stage();
+
+       PoolPool &get_pools() { return pools; }
+       Handle<Root> get_root() { return Handle<Root>::from_object(&root); }
+
+       template<typename T, typename F>
+       void iterate_objects(const F &);
+
+       template<typename T, typename... Args>
+       T &add_system(Args &&...);
+
+       const std::vector<std::unique_ptr<System>> &get_systems() const { return systems; }
+
+       void tick(Time::TimeDelta);
+};
+
+template<typename T, typename F>
+void Stage::iterate_objects(const F &func)
+{
+       pools.get_pool<T>().iterate_objects(func);
+}
+
+template<typename T, typename... Args>
+T &Stage::add_system(Args &&... args)
+{
+       systems.emplace_back(std::make_unique<T>(*this, std::forward<Args>(args)...));
+       return static_cast<T &>(*systems.back());
+}
+
+} // namespace Msp::Game
+
+#endif
diff --git a/source/game/system.h b/source/game/system.h
new file mode 100644 (file)
index 0000000..88ba291
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef MSP_GAME_SYSTEM_H_
+#define MSP_GAME_SYSTEM_H_
+
+#include <msp/time/timedelta.h>
+
+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