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.4SDK. 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
- Copy the
godotecsaddon folder into your project so you haveaddons/godotecs/plugin.cfgandaddons/godotecs/plugin.cs. - Open the project in the Godot .NET editor and build once (Build → Build Solution) so the C# assemblies compile.
- Enable the plugin: Project → Project Settings → Plugins → GodotECS → Enable. This registers the ECS Debugger dock (left-upper dock slot).
- 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:
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.Defaultis the shared runtime world.World.Previewis a second shared world used by editor tooling — never simulate gameplay in it.Entityis a lightweightstruct(Index+Version). Always checkEntity.IsNull/EntityManager.IsAlive(e)before touching a stored entity.- Never use
System.Randomin simulation logic; useEcsRngso runs stay deterministic. - Structural changes (create/destroy/add/remove) must not happen inside parallel jobs — use
state.ECBon the main thread orAsParallelWriter()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#:
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/2Dnodes, bake toEntityScene, 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.