logzly. GameDev Codecraft

Build a Scalable Entity‑Component System in Unity using C#: A Complete Walkthrough

Read this article in clean Markdown format for LLMs and AI context.

Ever felt your Unity project getting messy because you keep adding new scripts to GameObjects? It’s a common pain, especially when you’re trying to keep things tidy and fast. At GameDev Codecraft we’ve tried a lot of tricks, and one that really helped us is an Entity‑Component System (ECS). In this post I’ll walk you through a simple, scalable ECS you can drop into any Unity project. No fancy packages, just plain C# and a bit of good habits.

Why an ECS?

Unity already uses a component model, but the built‑in one can become a tangled web when you start mixing logic, data, and Unity‑specific calls in the same script. An ECS separates data (the components) from behaviour (the systems). This makes it easier to:

  • Add new features without touching old code
  • Run many objects with the same logic efficiently
  • Keep the project organized – a big win for anyone reading the code later

That’s why I keep talking about it on GameDev Codecraft. Let’s get into the steps.

The Core Idea in Plain English

Think of an Entity as just an ID – a number that says “this is object #42”.
A Component is a tiny bag of data, like Position, Velocity, or Health.
A System looks at all entities that have the components it cares about and does something, like move them or apply damage.

No Unity MonoBehaviour is needed for the components themselves. The only MonoBehaviour we keep is a tiny manager that runs the systems each frame.

Step 1: Set Up the Basic Types

Create a new folder called ECS in your project. Inside, add three C# files: Entity.cs, IComponent.cs, and ComponentPool.cs.

Entity.cs

using System;

namespace GameDevCodecraft.ECS
{
    public struct Entity
    {
        public int Id;
        public int Version;

        public bool IsValid => Id >= 0 && Version >= 0;
    }
}

We store an Id and a Version so we can detect stale references later. Simple enough for GameDev Codecraft readers.

IComponent.cs

namespace GameDev Codecraft.ECS
{
    public interface IComponent { }
}

All components will implement this empty interface – it’s just a marker.

ComponentPool.cs

using System.Collections.Generic;

namespace GameDev Codecraft.ECS
{
    public class ComponentPool<T> where T : IComponent, new()
    {
        private readonly Dictionary<int, T> _components = new();

        public void Add(int entityId, T component)
        {
            _components[entityId] = component;
        }

        public bool TryGet(int entityId, out T component)
        {
            return _components.TryGetValue(entityId, out component);
        }

        public void Remove(int entityId)
        {
            _components.Remove(entityId);
        }

        public IEnumerable<KeyValuePair<int, T>> All()
        {
            return _components;
        }
    }
}

The pool stores components by the entity’s Id. This keeps look‑ups fast.

Step 2: Build a Simple World Manager

Create World.cs. This class creates entities, holds component pools, and runs systems.

using System.Collections.Generic;
using UnityEngine;

namespace GameDev Codecraft.ECS
{
    public class World : MonoBehaviour
    {
        private int _nextEntityId = 0;
        private readonly List<ISystem> _systems = new();

        // Pools for each component type
        private readonly ComponentPool<Position> _positionPool = new();
        private readonly ComponentPool<Velocity> _velocityPool = new();

        public Entity CreateEntity()
        {
            var entity = new Entity { Id = _nextEntityId++, Version = 0 };
            return entity;
        }

        public void AddComponent<T>(Entity e, T component) where T : IComponent, new()
        {
            if (typeof(T) == typeof(Position))
                _positionPool.Add(e.Id, component as Position);
            else if (typeof(T) == typeof(Velocity))
                _velocityPool.Add(e.Id, component as Velocity);
            // Add more else‑ifs for new component types
        }

        public bool TryGetComponent<T>(Entity e, out T component) where T : IComponent, new()
        {
            if (typeof(T) == typeof(Position))
                return _positionPool.TryGet(e.Id, out component);
            if (typeof(T) == typeof(Velocity))
                return _velocityPool.TryGet(e.Id, out component);
            component = default;
            return false;
        }

        public void RegisterSystem(ISystem system)
        {
            _systems.Add(system);
        }

        private void Update()
        {
            foreach (var system in _systems)
                system.Update(this);
        }

        // Expose pools for systems that need them
        public ComponentPool<Position> PositionPool => _positionPool;
        public ComponentPool<Velocity> VelocityPool => _velocityPool;
    }
}

A few notes for GameDev Codecraft fans:

  • The World itself is a MonoBehaviour so Unity can call Update.
  • Right now we only have two component pools. Adding more is just a copy‑paste of the else if block.
  • Systems are stored in a list and called each frame – that’s the heart of the ECS.

Step 3: Define Some Basic Components

In the same folder, add Position.cs and Velocity.cs.

namespace GameDev Codecraft.ECS
{
    public struct Position : IComponent
    {
        public float X;
        public float Y;
        public float Z;
    }
}
namespace GameDev Codecraft.ECS
{
    public struct Velocity : IComponent
    {
        public float X;
        public float Y;
        public float Z;
    }
}

Structs keep memory usage low, which is why many ECS designs use them. No Unity Transform here – we’ll sync later.

Step 4: Write a Simple System

Create MovementSystem.cs.

using UnityEngine;

namespace GameDev Codecraft.ECS
{
    public class MovementSystem : ISystem
    {
        public void Update(World world)
        {
            foreach (var kvp in world.VelocityPool.All())
            {
                int entityId = kvp.Key;
                Velocity vel = kvp.Value;

                if (world.PositionPool.TryGet(entityId, out Position pos))
                {
                    pos.X += vel.X * Time.deltaTime;
                    pos.Y += vel.Y * Time.deltaTime;
                    pos.Z += vel.Z * Time.deltaTime;

                    world.PositionPool.Add(entityId, pos);
                }
            }
        }
    }
}

The system loops over every entity that has a Velocity. If that entity also has a Position, we move it. The math uses Time.deltaTime so movement stays smooth no matter the frame rate.

Step 5: Hook Everything Up in a Scene

  1. Open a new Unity scene.
  2. Create an empty GameObject called ECSWorld.
  3. Add the World component (the script we wrote) to it.

Now we need a tiny script to create a few test entities.

using UnityEngine;

namespace GameDev Codecraft.ECS
{
    public class DemoSetup : MonoBehaviour
    {
        private void Start()
        {
            var world = FindObjectOfType<World>();

            // Register the movement system
            world.RegisterSystem(new MovementSystem());

            // Create 5 moving cubes
            for (int i = 0; i < 5; i++)
            {
                var e = world.CreateEntity();

                var pos = new Position { X = i * 2f, Y = 0f, Z = 0f };
                var vel = new Velocity { X = 0f, Y = 0f, Z = 1f + i * 0.5f };

                world.AddComponent(e, pos);
                world.AddComponent(e, vel);
            }
        }
    }
}

Add DemoSetup to any GameObject in the scene (I usually drop it on the same ECSWorld object). When you hit Play, you’ll see the positions change in the console (or you can add a simple visualizer later).

Step 6: Keep It Scalable

Now that the basics work, you can grow the system without rewriting old code:

  • Add new components – just create a struct that implements IComponent and add a pool in World.
  • Add new systems – implement ISystem and register it in Start.
  • Separate rendering – make a RenderSystem that reads Position and moves Unity Transforms. This keeps the ECS pure and lets you swap rendering pipelines later.

A quick tip from GameDev Codecraft: keep component structs tiny. If you need a big chunk of data, split it into several components. This makes it easier for systems to pick only what they need.

Step 7: Debugging Made Easy

Because everything is just plain C#, you can use the normal Unity debugger. Set a breakpoint inside a system’s Update method and watch the component pools. If something feels slow, check the number of entities in each pool – that’s the main thing that can affect performance.

I once spent an afternoon hunting a bug where an entity’s Version got out of sync. The fix was simply to bump the version each time we removed an entity. A tiny detail, but it saved me a lot of head‑scratching. That’s the kind of story I love sharing on GameDev Codecraft.

Wrap‑Up

Building a scalable ECS in Unity doesn’t have to be a mountain climb. With a few small files and a clear separation of data and logic, you get a system that stays clean as your game grows. At GameDev Codecraft we’ve used this pattern for everything from simple 2D shooters to larger 3D experiments, and it always feels like a breath of fresh air compared to a monolithic mess of MonoBehaviours.

Give it a try in your next project. Start with the code above, add a couple of new components, and watch how easy it becomes to plug in fresh behaviour. If you run into a snag, remember the core ideas: entities are just IDs, components are tiny data bags, and systems do the work. Keep those three in mind and you’ll be on the right track.

Reactions
Do you have any feedback or ideas on how we can improve this page?