Obmyślam strukturę zarządzania danymi w silniku gry, pisanym w C++. Z jednej strony chciałbym, aby całość była dosyć elastyczna i nadawała się do różnych projektów (nie chcę silnika zaprojektowanego z myślą o jednej grze, czy jednym typie gier), ale też nie porywam się z motyką na słońce i chcę go używać do małych, hobbystycznych gierek, więc wolę trzymać się zwykłej obiektówki, bez systemów w stylu ECS.
Tyle tytułem wstępu. Obecnie zamysł na całość mam taki. Jest główna klasa abstrakcyjna ObjectBase
:
class ObjectBase
{
protected:
virtual void Update() = 0;
virtual void Render() = 0;
};
Każdy taki obiekt może istnieć samodzielnie, jako Object
, stanowiący wrapper na ObjectBase
:
template<typename T> requires std::is_base_of_v<ObjectBase, T>
class Object
{
private:
T* Data = nullptr;
ObjectBase** Link = nullptr;
public:
T* operator->();
void Allocate();
void Destroy();
};
Albo też jako część grupy obiektów ObjectList
:
template<typename T> requires std::is_base_of_v<ObjectBase, T>
class ObjectList
{
private:
T* List = nullptr;
ObjectBase** Link = nullptr;
public:
T& operator[](const uint16_t& uiIndex);
void Allocate(const uint16_t& uiIndex);
void Destroy(const uint16_t& uiIndex);
void AllocateAll();
void DestroyAll();
};
Z tych komponentów można potem zbudować szkielet poziomu gry, np.:
class LevelBase
{
private:
ObjectBase** List;
public:
void AllocateAll();
void DestroyAll();
};
class Level : public LevelBase
{
public:
Object<Player> Player;
struct NPC
{
ObjectList<Merchant> Merchant;
} NPC;
struct Enemies
{
ObjectList<Orc> Orc;
ObjectList<Skeleton> Skeleton;
} Enemies;
struct Weapons
{
ObjectList<Sword> Sword;
ObjectList<Dagger> Dagger;
} Weapons;
};
Dzięki temu obiekty są dosyć wygodnie posortowane i można uzyskać łatwy dostęp do rzeczywistych typów bez konieczności rzutowania, np.:
Level level[4];
level[0].Enemies.Skeleton[24].Damage = 10;
Dodatkowo wszystkie obiekty są spięte w jedną listę wskaźników ObjectBase** List
, do której łatwo może podpiąć się globalny dla silnika ObjectBase** World
, z którego dane o obiektach świata gry czerpią wątki odpowiedzialne za odświeżanie logiki i renderowanie. Z tego względu oba wrappery, tj. Object
i ObjectList
mają wskaźnik ObjectBase** Link
, który muszą pozyskać z klasy Level
. Dzięki temu wywołując metodę Allocate()
i jej pochodne, nie tylko następuje alokacja pamięci dla obiektu, ale zostaje też on podpięty pod ObjectBase** List
i tym samym staje się częścią świata (inaczej silnik go nie "zobaczy").
Minusy są takie, że każdy Level
wymaga wielgaśniego konstruktora, bo każdy wrapper musi jakoś uzyskać dostęp do ObjectBase** List
, żeby móc podpinać do niej nowe obiekty. Po drugie jest to dosyć nieelastyczne, bo jak chcę dodać nowy element do poziomu albo nową grupę elementów, to muszę przerabiać klasę i konstruktor (zwłaszcza gdybym chciał dla każdego poziomu mieć indywidualną strukturę).
Mógłbym mieć po prostu jedną, wielką listę obiektów, a potem uzyskiwać do nich dostęp przez rzutowanie, np.:
class World
{
private:
ObjectBase** List = nullptr;
public:
template<typename T>
T& Get(const uint16_t uiIndex)
{
return *static_cast<T*>(List[uiIndex]);
}
};
World world;
world.Get<Orc>(12).Health = 76;
Tylko wtedy musiałbym wiedzieć, że np. obiekt o indeksie 1276 jest np. dokładnie tym mieczem, którego akurat szukam, co byłoby raczej mało praktyczne. Z tego względu najbardziej by mi odpowiadało, gdyby dało się to zrobić dynamiczne i modularnie. Na przykład tworzę sobie obiekt od szablonu:
World<Orc, Sword, Dragon> world;
A potem mogę się odwoływać do poszczególnych typów obiektów, gdzie każdy typ ma własną listę, z własnymi indeksami:
world.Get<Orc>(0).Health = 11; //Pierwszy Orc z listy Orców
world.Get<Dragon>(0).AoEDamage = 76.21f; //Pierwszy smok z listy smoków.
To też nie idealne rozwiązanie, ale już w miarę sensowne do ogarnięcia. No ale to wymagałoby (?) specjalizacji szablonów, a tego chyba nie da się (?) zrobić modularnie, przez dziedziczenie.
Ktoś ma jakieś pomysły albo sugestie jak taki system zbudować, żeby był wygodny i w miarę elastyczny?