• Custom Entity-Component System

  • Rendering System using SDL3's rendering API

  • Particle System

  • Parent-Child Transformations

WHAT I WORKED ON

Language:

C++

Framework:

SDL3

Build System:

CMake

Entity-Component System - WIP

  • I was really interested in Data-Oriented Programming and Entity-Component Systems, so I wanted to give creating one a shot.


  • I used many references online such as EnTT and flecs to understand how the "API" surrounding an ECS should work.


  • For building sparse sets and the ECS itself from scratch, I mostly referred to skypjack's ECS back and forth series:


OVERVIEW

  • My Sparse Set class has a sparse entity array, dense entity array, and dense component array.


  • If N was the max number of entities my scene was going to allow for, my sparse entity array would start out with a size of N, and I would reserve N space for my dense arrays.


  • I have functions for adding and removing components, as well as getters for each of my arrays.


  • My ECS Manager contains querying functions for groups of components, examples of which will be shown in the systems section.


  • The Sparse Sets can definitely be improved, as they function as a simple naive lookup in the sparse arrays. I have researched techniques such as pagination that could be used to improve sparse lookups, and I plan to implement that in the future.


SPARSE SETS

(C++) Sparse Set

#pragma once
#include <cstdint>
#include <vector>
#include "ISparseSet.h"
#include "ECSData.h"

namespace Core::ECS
{
	template<typename ComponentTypeUsedBySparseSet>
	class SparseSet : public ISparseSet
	{
	public:
		SparseSet(const std::uint32_t someMaxEntityCount) : m_maxEntityCount(someMaxEntityCount)
		{
			m_sparseEntityArray.resize(m_maxEntityCount, INVALID_ENTITY_ID);
			m_sparseEntityArray.reserve(m_maxEntityCount/2);
			m_denseEntityArray.reserve(m_maxEntityCount);
			m_denseComponentArray.reserve(m_maxEntityCount);
		}

		~SparseSet() override
		{
			m_sparseEntityArray.clear();
			m_denseEntityArray.clear();
			m_denseComponentArray.clear();

			m_sparseEntityArray.shrink_to_fit();
			m_denseEntityArray.shrink_to_fit();
			m_denseComponentArray.shrink_to_fit();
		}

		void AddComponentToEntity(const std::uint32_t entityID, ComponentTypeUsedBySparseSet&& component)
		{
			if (m_sparseEntityArray.size()-1 < entityID)
			{
				//Resize sparse array to accomodate new elements, and
				//reserve space for future allocations
				m_sparseEntityArray.resize(entityID);
				m_sparseEntityArray.reserve(m_maxEntityCount / 2);
				m_denseEntityArray.reserve(m_maxEntityCount / 2);
				m_denseComponentArray.reserve(m_maxEntityCount / 2);
			}

			if (m_sparseEntityArray[entityID] != INVALID_ENTITY_ID)
			{
				return;
			}

			m_denseEntityArray.push_back(entityID);
			m_denseComponentArray.push_back(std::forward<ComponentTypeUsedBySparseSet>(component));
			m_sparseEntityArray[entityID] = static_cast<std::uint32_t>(m_denseEntityArray.size()-1);
		}

		void RemoveComponentFromEntity(const std::uint32_t entityID) override
		{
			if (m_sparseEntityArray[entityID] == INVALID_ENTITY_ID)
			{
				return;
			}
			const auto swappableLastEntityIndex = m_denseEntityArray.back();

			//Swap component to be removed with the last element in dense array
			std::swap(m_denseEntityArray[m_sparseEntityArray[entityID]], m_denseEntityArray.back());
			std::swap(m_denseComponentArray[m_sparseEntityArray[entityID]], m_denseComponentArray.back());
			std::swap(m_sparseEntityArray[swappableLastEntityIndex], m_sparseEntityArray[entityID]);

			m_denseEntityArray.pop_back();
			m_denseComponentArray.pop_back();
			m_sparseEntityArray[entityID] = INVALID_ENTITY_ID;
		}

		std::vector<std::uint32_t>& GetSparseEntityArray() override
		{
			return m_sparseEntityArray;
		}

		std::vector<std::uint32_t>& GetDenseEntityArray() override
		{
			return m_denseEntityArray;
		}

		std::vector<ComponentTypeUsedBySparseSet>& GetDenseComponentArray()
		{
			return m_denseComponentArray;
		}

	private:
		std::uint32_t m_maxEntityCount;

		std::vector<std::uint32_t> m_sparseEntityArray;
		std::vector<std::uint32_t> m_denseEntityArray = std::vector<uint32_t>(1, 0);
		std::vector<ComponentTypeUsedBySparseSet> m_denseComponentArray =
			std::vector<ComponentTypeUsedBySparseSet>(1, ComponentTypeUsedBySparseSet());
	};
}

  • My Transform Components contain local and world positions, rotations and scales, as well as Up and Right vectors. They also hold a parent entity ID which shows whether it is parented to another transform or not.


  • Currently, it also has an "Owner" GameObject pointer, which I use only for debugging purposes.


  • For the system code, I use a lambda function which is passed to a ForEach function in my ECS Manager, which fetches the corresponding components needed by the system.

RENDERING SYSTEM

(C++) Rendering System

void Core::ECS::Systems::RenderingSystem::UpdateSystem(const float deltaTime)
{
	ECSManager::GetInstance().ForEach<Assets::Components::Transform, Assets::Components::Renderer2D>(
		[&](const Assets::Components::Transform &transform, Assets::Components::Renderer2D &renderer2D)
		{
			SDL_SetRenderDrawColor(Application::GetCoreInstance().GetMainRenderer(),
			                       renderer2D.Color.r, renderer2D.Color.g, renderer2D.Color.b, renderer2D.Color.a);

			glm::vec2 screenCoordinates = ConvertToScreenCoordinates(transform.WorldPosition);
			renderer2D.RenderRectangle.x = screenCoordinates.x - transform.WorldScale.x * 0.5f;
			renderer2D.RenderRectangle.y = screenCoordinates.y - transform.WorldScale.y * 0.5f;
			renderer2D.RenderRectangle.w = transform.WorldScale.x;
			renderer2D.RenderRectangle.h = transform.WorldScale.y;

			SDL_RenderTextureRotated(Application::GetCoreInstance().GetMainRenderer(),
			                         renderer2D.RenderTexture, nullptr, &renderer2D.RenderRectangle,
			                         -glm::degrees(transform.WorldRotation), nullptr, SDL_FLIP_NONE);
		});
}
  • Since I am making Asteroids, I thought that while I am making an ECS, why not make a particle system as well since we are controlling a rocket with thrusters.


  • So I put together a simple and functional particle system with adjustable particle amount, lifetime, speed and randomness.

PARTICLE SYSTEM

(C++) Particle System

void Core::ECS::Systems::ParticleSystem::BeginSystem()
{
	m_maxCartesianLimits = GameScene::GetMaxCartesianLimits();
	m_minCartesianLimits = GameScene::GetMinCartesianLimits();

	ECSManager::GetInstance().ForEach<Assets::Components::Transform, Assets::Components::ParticleEmitter>(
		[&](const Assets::Components::Transform &transform, Assets::Components::ParticleEmitter &particleEmitter)
		{
			std::uniform_int_distribution<int> randomDistribution(
				-particleEmitter.MaxDeviation, particleEmitter.MaxDeviation);

			for (auto &particle: particleEmitter.Particles)
			{
				//Simulation
				//Set particle's initial position according to the initial velocity.
				//The positions are relative to the particle emitter's world position
				particle.CurrentPosition = transform.WorldPosition + particleEmitter.StartingOffset +
					glm::vec2(randomDistribution(m_randomOffsetGenerator),
					randomDistribution(m_randomOffsetGenerator));

				//Rendering
				RenderParticle(particleEmitter, particle);
			}
		});
}

void Core::ECS::Systems::ParticleSystem::UpdateSystem(const float deltaTime)
{
	glm::vec2 particleVelocity = glm::vec2(0.0f);

	ECSManager::GetInstance().ForEach<Assets::Components::Transform, Assets::Components::ParticleEmitter>(
		[&, this](Assets::Components::Transform &transform, Assets::Components::ParticleEmitter &particleEmitter)
		{
			std::uniform_int_distribution<int> randomDistribution(
				-particleEmitter.MaxDeviation, particleEmitter.MaxDeviation);

			particleVelocity = particleEmitter.Velocity * -transform.Up;

			for (auto &particle: particleEmitter.Particles)
			{
				//Simulation
				particle.CurrentPosition += particleVelocity * deltaTime;

				if (particle.CurrentLifeTime < 0.0f)
				{
					//Reset particle position next to particle emitter
					particle.CurrentPosition = transform.WorldPosition + particleEmitter.StartingOffset +
					glm::vec2(randomDistribution(m_randomOffsetGenerator),
					randomDistribution(m_randomOffsetGenerator));

					particle.CurrentLifeTime = particleEmitter.ParticleLifetime;
				}

				particle.CurrentLifeTime -= deltaTime;

				//Rendering
				this->RenderParticle(particleEmitter, particle);
			}
		});
}

gaurdian.kiran@gmail.com

+46 769666977

Contact: