Getting Started
This guide walks you through building a simple simulation with KeenEyes ECS.
Prerequisites
- .NET 10 SDK
- A code editor (VS Code, Visual Studio, Rider)
Installation
Add the KeenEyes packages to your project:
dotnet add package KeenEyes.Core
dotnet add package KeenEyes.Generators.Attributes
Step 1: Define Components
Components are plain data structs. Let's define some for a 2D particle simulation:
using KeenEyes;
// Position in 2D space
public struct Position : IComponent
{
public float X;
public float Y;
}
// Movement velocity
public struct Velocity : IComponent
{
public float X;
public float Y;
}
// Visual appearance
public struct Color : IComponent
{
public float R;
public float G;
public float B;
}
// Particle lifetime
public struct Lifetime : IComponent
{
public float Remaining;
}
Step 2: Create a World and Entities
The World is your ECS container. Use it to spawn entities:
using KeenEyes;
// Create the world
using var world = new World();
// Spawn a particle entity
var particle = world.Spawn()
.With(new Position { X = 0, Y = 0 })
.With(new Velocity { X = 10, Y = 5 })
.With(new Color { R = 1, G = 0, B = 0 })
.With(new Lifetime { Remaining = 2.0f })
.Build();
Console.WriteLine($"Created particle: {particle}");
Step 3: Create Systems
Systems contain the logic that processes entities. Let's create two systems:
Movement System
Updates positions based on velocity:
public class MovementSystem : SystemBase
{
public override void Update(float deltaTime)
{
foreach (var entity in World.Query<Position, Velocity>())
{
ref var pos = ref World.Get<Position>(entity);
ref readonly var vel = ref World.Get<Velocity>(entity);
pos.X += vel.X * deltaTime;
pos.Y += vel.Y * deltaTime;
}
}
}
Lifetime System
Decreases lifetime and removes expired particles:
public class LifetimeSystem : SystemBase
{
private readonly CommandBuffer buffer = new();
public override void Update(float deltaTime)
{
foreach (var entity in World.Query<Lifetime>())
{
ref var lifetime = ref World.Get<Lifetime>(entity);
lifetime.Remaining -= deltaTime;
if (lifetime.Remaining <= 0)
{
// Queue despawn - don't modify during iteration!
buffer.Despawn(entity);
}
}
// Execute all despawns after iteration
buffer.Flush(World);
}
}
Step 4: Register Systems and Run
using KeenEyes;
using var world = new World();
// Spawn some particles
for (int i = 0; i < 100; i++)
{
world.Spawn()
.With(new Position { X = Random.Shared.NextSingle() * 100, Y = 0 })
.With(new Velocity { X = 0, Y = Random.Shared.NextSingle() * 20 })
.With(new Lifetime { Remaining = Random.Shared.NextSingle() * 3 })
.Build();
}
// Register systems
world
.AddSystem<MovementSystem>()
.AddSystem<LifetimeSystem>();
// Game loop
var deltaTime = 0.016f; // ~60 FPS
while (world.EntityCount > 0)
{
world.Update(deltaTime);
Console.WriteLine($"Particles remaining: {world.EntityCount}");
Thread.Sleep(16);
}
Console.WriteLine("All particles expired!");
Step 5: Use Source Generators (Optional)
KeenEyes provides source generators to reduce boilerplate. Add the [Component] attribute:
using KeenEyes.Generators.Attributes;
[Component]
public partial struct Position
{
public float X;
public float Y;
}
This generates a WithPosition(float x, float y) extension method:
// Before (manual)
world.Spawn()
.With(new Position { X = 10, Y = 20 })
.Build();
// After (generated)
world.Spawn()
.WithPosition(x: 10, y: 20)
.Build();
Step 6: Use Component Bundles (Optional)
For entities with many components, use bundles to group related components:
using KeenEyes.Generators.Attributes;
[Component]
public partial struct Position { public float X, Y; }
[Component]
public partial struct Velocity { public float X, Y; }
[Component]
public partial struct Color { public float R, G, B; }
// Define a bundle for particle components
[Bundle]
public partial struct ParticleBundle
{
public Position Position;
public Velocity Velocity;
public Color Color;
}
// Spawn with bundle (concise)
var particle = world.Spawn()
.With(new ParticleBundle
{
Position = new() { X = 0, Y = 0 },
Velocity = new() { X = 10, Y = 5 },
Color = new() { R = 1, G = 0, B = 0 }
})
.With(new Lifetime { Remaining = 2.0f })
.Build();
Bundles are especially useful for commonly-used component combinations like transforms, physics bodies, or character stats. See the Bundles Guide for more details.
Complete Example
Here's the full program:
using KeenEyes;
// Components
public struct Position : IComponent { public float X, Y; }
public struct Velocity : IComponent { public float X, Y; }
public struct Lifetime : IComponent { public float Remaining; }
// Systems
public class MovementSystem : SystemBase
{
public override void Update(float deltaTime)
{
foreach (var entity in World.Query<Position, Velocity>())
{
ref var pos = ref World.Get<Position>(entity);
ref readonly var vel = ref World.Get<Velocity>(entity);
pos.X += vel.X * deltaTime;
pos.Y += vel.Y * deltaTime;
}
}
}
public class LifetimeSystem : SystemBase
{
private readonly CommandBuffer buffer = new();
public override void Update(float deltaTime)
{
foreach (var entity in World.Query<Lifetime>())
{
ref var lifetime = ref World.Get<Lifetime>(entity);
lifetime.Remaining -= deltaTime;
if (lifetime.Remaining <= 0)
buffer.Despawn(entity);
}
buffer.Flush(World);
}
}
// Main program
using var world = new World();
// Spawn particles
for (int i = 0; i < 100; i++)
{
world.Spawn()
.With(new Position { X = Random.Shared.NextSingle() * 100, Y = 0 })
.With(new Velocity { X = 0, Y = Random.Shared.NextSingle() * 20 })
.With(new Lifetime { Remaining = Random.Shared.NextSingle() * 3 })
.Build();
}
// Register and run
world.AddSystem<MovementSystem>().AddSystem<LifetimeSystem>();
while (world.EntityCount > 0)
{
world.Update(0.016f);
Console.WriteLine($"Particles: {world.EntityCount}");
Thread.Sleep(16);
}
Next Steps
- Core Concepts - Understand ECS fundamentals
- Entities Guide - Entity lifecycle and management
- Components Guide - Component patterns
- Bundles Guide - Group components for easier spawning
- Systems Guide - System design patterns
- Command Buffer - Safe entity modification during iteration