Last active
October 11, 2017 19:13
-
-
Save wortelstoemp/75c83e28b270fe6a2546de4165c77231 to your computer and use it in GitHub Desktop.
Data-Oriented Entity Component System
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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(); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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(); | |
| }; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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); | |
| }; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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