Blue Moon Games

Stable pointer dynamic array - part 1

2016. november 02.

Egy ECS-ben fontos, hogy hol és hogyan tároljuk a komponenseket, főleg, ha játékokról van szó, ahol fontos a sebesség.

Az első megközelítés

Az entity-component kapcsolat nagy vonalakban nézve annyit mond, hogy egy entitást a komponensei határoznak meg, azaz maga az entity egy buta objektum, ami a komponenseit "tárolja". Gondoljunk a Unity GameObject osztályára: adott egy objektum, le tudjuk kérni a hozzá tartozó komponenseket - többek között - a GetComponent függvényével.

Tekintsük általánosságban a következő, nagyon egyszerű komponens osztály(oka)t:

class Component
{
public:
	Component(GameObject* obj) : object(obj) { }
	virtual ~Component() { }
	
protected:
	GameObject* const object;
};

class Transform : public Component { /* ... */ }
class Renderer : public Component { /* ... */ }
// ...

 Ekkor, ha azt mondjuk, hogy az entity tárolja a komponenseket, akkor az valahogy így néz ki:

class GameObject
{
public:
	template <class T, typename... Args>
	T* Add(Args&&... args)
	{
		// static_assert: base_of<Component, T>

		T* result = new T(this, std::forward<Args>(args)...);
		components.push_back(result);
		return result;
	}

private:
	typedef std::vector<Component*> Components;
	
	Components components;
};

Mi is a probléma ezzel? Az objektumtól le tudjuk kérni a hozzá tartozó komponenseket, szóval mindenki boldog. Pontosabban csak egy valaki boldog, mégpedig a komponenst lekérdező fél, sajnos senki más nem igazán.

A komponenseken való végigiterálás egy igen gyakori jelenség futás közben: a mozgó objektumoknak ki kell számolni a world mátrixát, ütközést kell számolni a fizikai komponensekkel, stb. Azonban, ha az entitás tárolja a komponenseket (főleg ilyen általános módon), akkor az iterálás igen problémás. Minden objektumon végig kell iterálni, le kell kérdezni az adott típusú komponensét (pl. Transform), és azon elvégezni a műveleteket.

Egy kicsivel jobb, ha az objektumok néhány előre definiált, jól ismert komponenst külön tárolnak, és csak a user-defined komponenseket tárolják egy ilyen általános tárolóban, valahogy így:

Transform* transform;
Renderer* renderer;
Components otherComponents;

Ekkor az adott típusú built-in komponens elérése már gyorsabb, de továbbra is végig kell iterálni az összes objektumon, és azon keresztül lekérni az adott komponenst. Magyarul a memóriában oda kell ugrani az objektum címére, kiolvasni a komponens címét, majd odaugrani a memóriában. Ráadásul le kell ellenőriznünk, hogy van-e ilyen komponense az adott objektumnak. Ez úgy nem tűnik annyira hatékonynak.

Egy kicsit jobb

Tegyük félre, hogy az entitás hozza létre a komponenseket, szimplán csak tároljon egy pointert. De akkor ki hozza létre és tárolja magát a komponenst? Egyszerű a válasz: az adott system, vagy akár még egy szinttel feljebb, a systemeket vezérlő manager/system.

A system egy olyan osztály, amely egy adott tevékenységet végez, általában egy típusú komponenshez tartozik. Például a TransformSystem az objektumok transzformálásáért (térbeli elhelyezéséért) felel, és a Transform típusú komponenseket használja. A PhysicSystem a fizikához használatos komponens(eke)t, és így tovább. Természetesen vannak olyan esetek is, amikor egy systemnek többféle komponensre is szüksége van, például a RendererSystem: szüksége van az objektumok térbeli elhelyezkedésére (world mátrix) és magára a Renderer komponensre, amely a kirajzolandó dolgokat tárolja.

Tehát a komponensek létrehozása egy másik szintre kerül. Személy szerint azt a megközelítést választottam, hogy a Scene tárolja a komponenseket, a System pedig csak egy referenciát kap a megfelelő komponens listára/listákra.

class GameObject
{
public:
	void StoreComponent(Component* component) { components.push_back(component); }

private:
	typedef std::vector<Component*> Components;
	Components components;
};

class Scene
{
public:
	template <class T, typename... Args>
	T* AddComponent(GameObject* object, Args&&... args)
	{
		// static_assert...

		T* result = new T(object, std::forward<Args>(args)...);

		object->StoreComponent(result);
		AddToPool<T>(result);

		return result;
	}
	
	void Update()
	{
		transformSystem->DoJob(transforms);
		// ...
	}

private:
	template <class T>
	using ComponentPool = std::vector<T>;
	
	ComponentPool<Transform*> transforms;
	ComponentPool<Renderer*> renderers;
	ComponentPool<Component*> otherComponents;
	// ...
	
	TransformSystem* transformSystem;
	RendererSystem* rendererSystem;
	// ...
	
	template <class T>
	void AddToPool(T* component) { otherComponents.push_back(component); }
	
	// specializations....

	template <>
	void AddToPool<Transform>(Transform* component) { transforms.push_back(component); }
	
	// ...
};

Mit is nyertünk ezzel? Komponensenként eggyel kevesebb memória-ugrást, nomeg ellenőriznünk sem kell, hogy van-e ilyen komponens vagy sem, hiszen ott van a listában, és kész. Mindezt azzal értük el, hogy nem az objektumokon kell végigiterálni, hanem az adott komponens listán.

Most már örülhetnénk, mert minden jó. Alapvetően igen, kivéve a szegény memóriát. Mivel heap alloc történik, így nem tudjuk pontosan, hogy a memóriában hol kapnak helyet a létrehozott komponensek. Ráadásul alapvetően a komponensek viszonylag kicsik, így könnyen előfordulhat, hogy a memóriában össze-vissza helyeken lesznek a komponenseink (ahol éppen helyet talált nekik az oprendszer). Miután a komponenseket felszabadítjuk, az általuk lefoglalt memória szabaddá válik. Azonban, ha sok ilyen kicsi memóriaterületet foglaltunk le "össze-vissza", akkor előfordulhat, hogy egy nagyobb allokáció nem fog sikerülni, mert nem maradt egyetlen egybefüggő memóriaterület.

Érdemes utánanézni a "memory fragmentation" fogalomnak. A linken található ábrán látható az előbbiekben tárgyalt probléma, amikor egy "közbezárt" memóriaterület felszabadul, a helyére nem foglalható egy annál nagyobb objektum.

Freed block B. Notice that the memory that B used cannot be included for an allocation larger than B's size.

PC-n ez nem okoz túl nagy gondot, ott a virtuális memória, meg eleve viszonylag nagy az elérhető RAM, de gondoljunk egy zárt platformra, bármely konzol, telefon vagy tablet. Ezeknek a memóriája véges, ráadásul abban sem vagyok biztos, hogy lenne (például) az xbox-nak virtuális memória-kezelése, hiszen - tudtommal - a HDD nem szükséges tartozéka. No persze PC-n sem elhanyagolható a probléma, az erőforrás-pazarlás sosem jó ötlet.

Mi a megoldás?

Saját memóriakezelés: foglaljunk le egy nagyobb egybefüggő területet előre, és placement new segítségével hozzuk létre az objektumokat. Bővebben később... :)

A bejegyzés trackback címe:

https://bluemoongames.blog.hu/api/trackback/id/tr1311923649

Kommentek:

A hozzászólások a vonatkozó jogszabályok  értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai  üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a  Felhasználási feltételekben és az adatvédelmi tájékoztatóban.

Nincsenek hozzászólások.
süti beállítások módosítása