Skip to content

Instantly share code, notes, and snippets.

@wortelstoemp
Last active October 11, 2017 19:13
Show Gist options
  • Select an option

  • Save wortelstoemp/75c83e28b270fe6a2546de4165c77231 to your computer and use it in GitHub Desktop.

Select an option

Save wortelstoemp/75c83e28b270fe6a2546de4165c77231 to your computer and use it in GitHub Desktop.
Data-Oriented Entity Component System
#include "core.h"
namespace tqECS
{
tqECS::TransformComponentSystem::TransformComponentSystem()
{
std::srand(std::time(NULL));
}
void tqECS::TransformComponentSystem::Allocate(unsigned int allocatedCount)
{
ComponentData newData;
const unsigned int instanceDataSize = (sizeof(Entity) + 2 * sizeof(Vec3) + sizeof(Quaternion));
const unsigned int bytes = allocatedCount * instanceDataSize;
newData.buffer = std::malloc(bytes);
newData.instanceCount = data.instanceCount;
newData.allocatedCount = allocatedCount;
newData.entity = (Entity*)(newData.buffer);
newData.position = (Vec3*)(newData.entity + allocatedCount);
newData.scaling = (Vec3*)(newData.position + allocatedCount);
newData.orientation = (Quaternion*)(newData.scaling + allocatedCount);
memcpy(newData.entity, data.entity, data.instanceCount * sizeof(Entity));
memcpy(newData.position, data.position, data.instanceCount * sizeof(Vec3));
memcpy(newData.scaling, data.scaling, data.instanceCount * sizeof(Vec3));
memcpy(newData.orientation, data.orientation, data.instanceCount * sizeof(Quaternion));
std::free(data.buffer);
data = newData;
}
Component tqECS::TransformComponentSystem::Lookup(Entity e)
{
return this->table[e];
}
void tqECS::TransformComponentSystem::GarbageCollect(const EntitySystem& es)
{
const unsigned int maxAliveInRowCount = 4;
unsigned int aliveInRowCount = 0;
while (data.instanceCount > 0 && aliveInRowCount < maxAliveInRowCount) {
// Get random entity: index in [0, data.instanceCount - 1]
unsigned int index = std::rand() % data.instanceCount;
// If this entity is alive: skip it
if (es.IsEntityAlive(data.entity[index])) {
aliveInRowCount++;
continue;
}
// If this entity is not alive: destroy its component
aliveInRowCount = 0;
this->Destroy(index);
}
}
void tqECS::TransformComponentSystem::Destroy(Component index)
{
// Deleting a component is the same as replacing it by a copy of the last component
// and leaving the original last component behind.
Component last = data.instanceCount - 1;
Entity e = data.entity[index];
Entity lastEntity = data.entity[last];
data.entity[index] = data.entity[last];
data.position[index] = data.position[last];
data.scaling[index] = data.scaling[last];
data.orientation[index] = data.orientation[last];
// In lookup table connect overwritten last entity to index of removed component
table[lastEntity] = index;
// Remove entity component pair from lookup table
// and leave the original last component behind
table.erase(e);
data.instanceCount--;
}
Vec3 TransformComponentSystem::Position(Component index) const
{
return data.position[index];
}
void tqECS::TransformComponentSystem::SetPosition(Component index, const Vec3& position)
{
data.position[index] = position;
}
Vec3 tqECS::TransformComponentSystem::Scaling(Component index) const
{
return data.scaling[index];
}
void tqECS::TransformComponentSystem::SetScaling(Component index, const Vec3& scaling)
{
data.scaling[index] = scaling;
}
Quaternion tqECS::TransformComponentSystem::Orientation(Component index) const
{
return data.orientation[index];
}
void tqECS::TransformComponentSystem::SetOrientation(Component index, const Quaternion& orientation)
{
data.orientation[index] = orientation;
}
void TransformComponentSystem::TranslateTowards(Component index, const Vec3 & direction, const float amount)
{
this->data.position[index] = this->data.position[index] + (Normalized(direction) * amount);
}
void TransformComponentSystem::Rotate(Component index, const Quaternion & amount)
{
this->data.orientation[index] = amount * this->data.orientation[index];
}
void TransformComponentSystem::Rotate(Component index, const Vec3 & axis, const float angle)
{
this->data.orientation[index] = Rotated(this->data.orientation[index], axis, angle);
}
Matrix4x4 TransformComponentSystem::CalculateModel(Component index) const
{
const Matrix4x4 translation = Translate(this->data.position[index]);
const Matrix4x4 rotation = CreateMatrix4x4(this->data.orientation[index]);
const Matrix4x4 scale = Scale(this->data.scaling[index]);
return translation * rotation * scale;
}
Matrix4x4 TransformComponentSystem::CalculateMVP(Component index, const Camera & camera)
{
return camera.viewProjection * this->CalculateModel(index);
}
//--------------------------------------------------------------------------
// Camera
//--------------------------------------------------------------------------
void Camera::MakePerspective(const Vec3& position, const Quaternion& orientation,
const float fovy, const float aspect, const float zNear, const float zFar)
{
this->position = position;
this->orientation = orientation;
this->viewProjection =
Perspective(fovy, aspect, zNear, zFar) *
ViewMatrix4x4(this->position, this->orientation);
this->cameraType = CAMERA_PERSPECTIVE;
this->fovy = fovy;
this->aspect = aspect;
this->zNear = zNear;
this->zFar = zFar;
}
void Camera::MakeOrtho(const Vec3 & position, const Quaternion & orientation,
const float width, const float height, const float zNear, const float zFar)
{
this->position = position;
this->orientation = orientation;
this->viewProjection =
Ortho(0, width, 0, height, zNear, zFar) *
ViewMatrix4x4(this->position, this->orientation);
this->cameraType = CAMERA_ORTHO;
this->width = width;
this->height = height;
this->zNear = zNear;
this->zFar = zFar;
}
void Camera::Update()
{
switch (this->cameraType) {
case CAMERA_PERSPECTIVE: {
this->viewProjection = Perspective(this->fovy, this->aspect, this->zNear, this->zFar) *
ViewMatrix4x4(this->position, this->orientation);
} break;
case CAMERA_ORTHO: {
this->viewProjection = Ortho(0, this->width, 0, this->height, this->zNear, this->zFar) *
ViewMatrix4x4(this->position, this->orientation);
} break;
}
}
void CameraSystem::AddCamera(const Camera & camera)
{
const unsigned int index = this->availableCameras.size();
this->availableCameras.push_back(camera);
this->table[camera.name] = index;
// If this is the only camera, then this will become the main camera
}
void CameraSystem::RemoveCamera(const char * name)
{
const unsigned int index = this->table[name];
// One cannot remove the main camera while in use
if (index == MAIN_CAMERA_INDEX) {
return;
}
// Removing a camera is the same as replacing it with a copy of the last camera
// and removing the original last camera
Camera lastCamera = this->availableCameras.back();
this->availableCameras[index] = lastCamera;
this->table[lastCamera.name] = index;
this->availableCameras.pop_back();
this->table.erase(name);
}
Camera* CameraSystem::MainCamera()
{
if (this->availableCameras.empty()) {
return NULL;
}
return &this->availableCameras[0];
}
void CameraSystem::ChangeMainCamera(const char* newMainCamera)
{
// Changing the main camera is the same as swapping the new main camera with the old one
const unsigned int newMainIndex = this->table[newMainCamera];
const unsigned int oldMainIndex = MAIN_CAMERA_INDEX;
const char* oldMainCamera = this->availableCameras[oldMainIndex].name;
Camera tmp = this->availableCameras[oldMainIndex];
this->availableCameras[oldMainIndex] = this->availableCameras[newMainIndex];
this->availableCameras[newMainIndex] = tmp;
this->table[newMainCamera] = oldMainIndex;
this->table[oldMainCamera] = newMainIndex;
}
void CameraSystem::Update()
{
for (int i = 0; i < this->availableCameras.size(); i++) {
this->availableCameras[i].Update();
}
}
}
#pragma once
// TransformComponentSystem:
// Allocate 1 system per world.
// An entity can create transform components in multiple worlds.
// Coordinate systems are exclusively right-handed!
// This system does not support scene graphs and is instead flat.
// A scene graph can be build in the future on top of the arrays without affecting the API.
//
// Author: Tom Quareme
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <map>
#include <queue>
#include <vector>
#include "ecs.h"
#include "math.h"
namespace tqECS
{
using namespace tqMath;
class TransformComponentSystem
{
struct ComponentData
{
unsigned int instanceCount;
unsigned int allocatedCount;
void* buffer;
Entity* entity;
Vec3* position;
Vec3* scaling;
Quaternion* orientation;
};
private:
ComponentData data;
std::map<Entity, Component> table;
// Checks if entities are destroyed and when they are their components get destroyed
void GarbageCollect(const EntitySystem& es);
// Destroys a certain component
void Destroy(Component index);
public:
TransformComponentSystem();
// Allocates a certain amount of components
void Allocate(unsigned int allocatedCount);
// Looks up component index from the entity
Component Lookup(Entity e);
Vec3 Position(Component index) const;
void SetPosition(Component index, const Vec3& position);
Vec3 Scaling(Component index) const;
void SetScaling(Component index, const Vec3& scaling);
Quaternion Orientation(Component index) const;
void SetOrientation(Component index, const Quaternion& orientation);
// Translate a vector towards a direction by a certain amount
void TranslateTowards(Component index, const Vec3& direction, const float amount);
// Rotate the transform by a quaternion
void Rotate(Component index, const Quaternion& amount);
// Rotate the transform by an axis-angle
void Rotate(Component index, const Vec3& axis, const float angle);
// Calculate the Model matrix (also known as World matrix)
Matrix4x4 CalculateModel(Component index) const;
// Calculate the Model-View-Projection matrix (also known as World-View-Projection matrix)
Matrix4x4 CalculateMVP(Component index, const Camera& camera);
};
union Camera
{
friend class CameraSystem;
enum CameraType
{
CAMERA_PERSPECTIVE,
CAMERA_ORTHO,
};
// Perspective
struct
{
Vec3 position;
Quaternion orientation;
Matrix4x4 viewProjection;
CameraType cameraType;
const char* name;
float fovy;
float aspect;
float zNear;
float zFar;
};
// Ortho
struct
{
Vec3 position;
Quaternion orientation;
Matrix4x4 viewProjection;
CameraType cameraType;
const char* name;
float width;
float height;
float zNear;
float zFar;
};
Camera(const char* name) : name(name) {}
// Creates perspective camera
void MakePerspective(const Vec3& position, const Quaternion& orientation,
const float fovy, const float aspect, const float zNear, const float zFar);
// Creates orthographic camera
void MakeOrtho(const Vec3& position, const Quaternion& orientation,
const float width, const float height, const float zNear, const float zFar);
private:
// Updates the viewProjection matrix
// This will be done by the CameraSystem.
void Update();
};
class CameraSystem
{
private:
std::vector<Camera> availableCameras;
std::map<const char*, unsigned int> table;
static const unsigned int MAIN_CAMERA_INDEX = 0;
public:
// The first added camera will be the default main camera at first
void AddCamera(const Camera& camera);
void RemoveCamera(const char* name);
Camera* MainCamera();
void ChangeMainCamera(const char* newMainCamera);
void Update();
};
}
#include "ecs.h"
namespace tqECS
{
Entity EntitySystem::CreateEntity()
{
unsigned int index;
unsigned int generation;
if (this->freeIndices.size() > MINIMUM_FREE_INDICES) {
// Take an index and mark as not free anymore
index = this->freeIndices.front();
this->freeIndices.pop();
} else {
this->generation.push_back(0);
index = this->generation.size() - 1;
}
generation = (this->generation[index] << ENTITY_INDEX_BITS);
return Entity{ generation | index };
}
void EntitySystem::DestroyEntity(Entity e)
{
const Entity index = EntityIndex(e);
this->generation[index]++;
this->freeIndices.push(index);
}
bool EntitySystem::IsEntityAlive(Entity e) const
{
return this->generation[EntityIndex(e)] == EntityGeneration(e);
}
unsigned int EntitySystem::EntityIndex(Entity e) const
{
return e & ENTITY_INDEX_MASK;
}
unsigned int EntitySystem::EntityGeneration(Entity e) const
{
return (e >> ENTITY_INDEX_BITS) & ENTITY_GENERATION_MASK;
}
//----------------------------------------------------------------------------
// ExampleComponentSystem
//----------------------------------------------------------------------------
ExampleComponentSystem::ExampleComponentSystem()
{
std::srand(std::time(NULL));
}
void ExampleComponentSystem::Allocate(unsigned int allocatedCount)
{
ComponentData newData;
const unsigned int instanceDataSize = (sizeof(Entity) + sizeof(float) + sizeof(float));
const unsigned int bytes = allocatedCount * instanceDataSize;
newData.buffer = std::malloc(bytes);
newData.instanceCount = data.instanceCount;
newData.allocatedCount = allocatedCount;
newData.entity = (Entity*)(newData.buffer);
newData.x = (float*)(newData.entity + allocatedCount);
newData.y = (float*)(newData.x + allocatedCount);
memcpy(newData.entity, data.entity, data.instanceCount * sizeof(Entity));
memcpy(newData.x, data.x, data.instanceCount * sizeof(float));
memcpy(newData.y, data.y, data.instanceCount * sizeof(float));
std::free(data.buffer);
data = newData;
}
void ExampleComponentSystem::Update(const EntitySystem& es, float dt)
{
const float velocity = 100.0f;
for (unsigned i = 0; i < this->data.instanceCount; i++) {
this->data.x[i] += velocity * dt;
}
for (unsigned i = 0; i < this->data.instanceCount; i++) {
this->data.y[i] += velocity * dt;
}
this->GarbageCollect(es);
}
void ExampleComponentSystem::GarbageCollect(const EntitySystem& es)
{
const unsigned int maxAliveInRowCount = 4;
unsigned int aliveInRowCount = 0;
while (data.instanceCount > 0 && aliveInRowCount < maxAliveInRowCount) {
// Get random entity: index in [0, data.instanceCount - 1]
unsigned int index = std::rand() % data.instanceCount;
// If this entity is alive: skip it
if (es.IsEntityAlive(data.entity[index])) {
aliveInRowCount++;
continue;
}
// If this entity is not alive: destroy its component
aliveInRowCount = 0;
this->Destroy(index);
}
}
void ExampleComponentSystem::Destroy(Component index)
{
// Deleting a component is the same as replacing it by a copy of the last component
// and leaving the original last component behind.
Component last = data.instanceCount - 1;
Entity e = data.entity[index];
Entity lastEntity = data.entity[last];
data.entity[index] = data.entity[last];
data.x[index] = data.x[last];
data.y[index] = data.y[last];
// In lookup table connect overwritten last entity to index of removed component
table[lastEntity] = index;
// Remove entity component pair from lookup table
// and leave the original last component behind
table.erase(e);
data.instanceCount--;
}
Component ExampleComponentSystem::Lookup(Entity e)
{
return this->table[e];
}
float ExampleComponentSystem::X(Component index) const
{
return this->data.x[index];
}
void ExampleComponentSystem::SetX(Component index, const float x)
{
data.x[index] = x;
}
float ExampleComponentSystem::Y(Component index) const
{
return data.y[index];
}
void ExampleComponentSystem::SetY(Component index, const float y)
{
data.y[index] = y;
}
}
#pragma once
// Data Oriented Entity Component System
// This module is mainly inspired by Bitsquid.
// Author: Tom Quareme
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <map>
#include <queue>
#include <vector>
namespace tqECS
{
const unsigned int ENTITY_INDEX_BITS = 22;
const unsigned int ENTITY_GENERATION_BITS = 8;
const unsigned int ENTITY_LUA_LIGHT_USERDATA = 2;
// Set all bits to 1 for mask
const unsigned int ENTITY_INDEX_MASK = (1 << ENTITY_INDEX_BITS) - 1;
const unsigned int ENTITY_GENERATION_MASK = (1 << ENTITY_GENERATION_BITS) - 1;
const unsigned int MINIMUM_FREE_INDICES = 1024;
typedef unsigned int Entity; // TODO: long is minimal 32 bits
typedef unsigned int Component;
class EntitySystem
{
private:
std::vector<unsigned char> generation; // for each used index
std::queue<unsigned int> freeIndices;
public:
Entity CreateEntity();
void DestroyEntity(Entity e);
bool IsEntityAlive(Entity e) const;
// The index part gives us the index of the entity in a lookup array
unsigned int EntityIndex(Entity e) const;
// The generation part is used to distinguish entities created at the same index slot
// Indices of the lookup array get reused
// Changing the generation part will ensure unique entity id's
unsigned int EntityGeneration(Entity e) const;
};
//----------------------------------------------------------------------------
// ExampleComponentSystem
//----------------------------------------------------------------------------
class ExampleComponentSystem
{
struct ComponentData
{
unsigned int instanceCount;
unsigned int allocatedCount;
void* buffer;
Entity* entity;
float* x;
float* y;
};
private:
ComponentData data;
std::map<Entity, Component> table;
// Checks if entities are destroyed and when they are their components get destroyed
void GarbageCollect(const EntitySystem& es);
// Destroys a certain component
void Destroy(Component index);
public:
ExampleComponentSystem();
// Allocates a certain amount of components
void Allocate(unsigned int allocatedCount);
// Updates all components
void Update(const EntitySystem& es, float dt);
// Looks up component index from the entity
Component Lookup(Entity e);
float X(Component index) const;
void SetX(Component index, const float x);
float Y(Component index) const;
void SetY(Component index, const float y);
};
}
#include <stdio.h>
#include "ecs.h"
int main(void)
{
tqECS::EntitySystem entitySystem;
tqECS::Entity shark0 = entitySystem.CreateEntity();
tqECS::Entity shark1 = entitySystem.CreateEntity();
tqECS::Entity shark2 = entitySystem.CreateEntity();
tqECS::Entity shark3 = entitySystem.CreateEntity();
entitySystem.DestroyEntity(shark0);
tqECS::Entity shark4 = entitySystem.CreateEntity();
printf("Entity: %d!\n", shark0);
printf("Entity: %d!\n", shark1);
printf("Entity: %d!\n", shark2);
printf("Entity: %d!\n", shark3);
printf("Entity: %d!\n", shark4);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment