Skip to content

Getting started

GodotECS is a high-performance archetype ECS for Godot 4, written in pure C#. Entities live in dense chunk storage outside the scene tree; bulk systems simulate them and sync only the visible result back to Godot nodes (MultiMesh instances, audio players, signals). This page takes you from an empty project to 1000 simulated entities.

Requirements

  • Godot 4.8-dev (.NET build) — the Godot.NET.Sdk/4.8.0-dev.4 SDK. A standard (non-.NET) Godot build cannot load C#.
  • .NET 10 SDK — the plugin targets net10.0:
    xml
    <Project Sdk="Godot.NET.Sdk/4.8.0-dev.4">
      <PropertyGroup>
        <TargetFramework>net10.0</TargetFramework>
        <EnableDynamicLoading>true</EnableDynamicLoading>
      </PropertyGroup>
    </Project>
  • A C# Godot project (created via New Project → C# in the .NET editor).

Install

  1. Copy the godotecs addon folder into your project so you have addons/godotecs/plugin.cfg and addons/godotecs/plugin.cs.
  2. Open the project in the Godot .NET editor and build once (Build → Build Solution) so the C# assemblies compile.
  3. Enable the plugin: Project → Project Settings → Plugins → GodotECS → Enable. This registers the ECS Debugger dock (left-upper dock slot).
  4. Reference the ECS namespaces from your game code:
    csharp
    using GodotECS.Core;
    using GodotECS.Authoring;
    using GodotECS.Bridge;
    using System;

No NuGet packages or extra configuration are needed.

Your first 1000 entities (C#)

Attach this script to a Node in an otherwise empty scene and press Play. It spawns 1000 entities with a position and a velocity, then moves them every frame:

csharp
using Godot;
using GodotECS.Core;

public partial class Spawn1000 : Node
{
    public override void _Ready()
    {
        World world = World.Default;
        EntityManager em = world.Entities;
        var rng = new EcsRng(1234);

        for (int i = 0; i < 1000; i++)
        {
            Entity e = em.Create();
            em.AddComponent<LocalTransform>(e);
            em.SetComponentData(e, new LocalTransform
            {
                Pos = new Vector3(rng.NextRange(-20f, 20f), 0f, rng.NextRange(-20f, 20f)),
                Rot = Quaternion.Identity,
                Scale = 1f,
            });
            em.AddComponent<Velocity>(e);
            em.SetComponentData(e, new Velocity
            {
                Value = new Vector3(rng.NextRange(-2f, 2f), 0f, rng.NextRange(-2f, 2f)),
            });
            em.AddComponent<Health>(e);
            em.SetComponentData(e, new Health { Value = 100, Max = 100 });
        }
        GD.Print($"Live entities: {em.LiveCount}");
    }

    public override void _PhysicsProcess(double delta)
    {
        float dt = (float)delta;
        EntityManager em = World.Default.Entities;
        // Simple per-entity move. For real games, put this in an ISystem
        // with ScheduleParallel instead of a managed loop.
        var buf = new Entity[em.LiveCount];
        int n = em.CopyLiveEntities(buf);
        for (int i = 0; i < n; i++)
        {
            Entity e = buf[i];
            if (!em.HasComponent<LocalTransform>(e) || !em.HasComponent<Velocity>(e))
                continue;
            LocalTransform t = em.GetComponentData<LocalTransform>(e);
            t.Pos += em.GetComponentData<Velocity>(e).Value * dt;
            em.SetComponentData(e, t);
        }
    }
}

Notes:

  • World.Default is the shared runtime world. World.Preview is a second shared world used by editor tooling — never simulate gameplay in it.
  • Entity is a lightweight struct (Index + Version). Always check Entity.IsNull / EntityManager.IsAlive(e) before touching a stored entity.
  • Never use System.Random in simulation logic; use EcsRng so runs stay deterministic.
  • Structural changes (create/destroy/add/remove) must not happen inside parallel jobs — use state.ECB on the main thread or AsParallelWriter() inside jobs (see ECB). In main-thread code like the example above, direct calls are fine.

The same world from GDScript

Gameplay UI, quests, and menus can stay in GDScript. Add a GdScriptApi node to the scene, then drive the ECS read-only (plus spawn/kill helpers) without writing C#:

gdscript
extends Node

@onready var ecs = $GdScriptApi

func _ready():
    print("Entities: ", ecs.GetEntityCount())
    print("Tick: ", ecs.GetTick())

func _on_spawn_button_pressed():
    var idx = ecs.SpawnScene(preload("res://enemies/bot.tres"), Vector3(10, 0, 5))
    if idx < 0:
        push_warning("Spawn failed: scene empty or null")

func _on_kill_button_pressed(index, version):
    if not ecs.KillEntity(index, version):
        push_warning("Entity already dead")

func _process(_delta):
    $Hud/Count.text = str(ecs.GetEntityCount())

SpawnScene returns the root entity's Index (or -1 on failure); KillEntity(index, version) returns false if the entity is already dead. See Godot bridge for the full interop model and Bridge API for every method.

Where to go next

  • Core concepts — entities, components, queries, ECB, systems.
  • Authoring workflow — place content with EcsAuthoring3D/2D nodes, bake to EntityScene, spawn at runtime.
  • Godot bridge — the sync-point rule, signals, audio, navigation, GDScript facade.
  • Editor tools — debugger dock, inspector, gizmos, bake tool.
  • API reference — exact signatures for every public type.

GodotECS docs — version 0.1.0. All rights reserved until a license is added.