Table of Contents

KeenEyes.Abstractions

The KeenEyes.Abstractions package provides lightweight interfaces and types for authoring plugins and extensions without depending on the full KeenEyes.Core runtime.

Philosophy: Swappable Subsystems

KeenEyes is designed as a fully-featured game engine that remains completely customizable. The abstractions layer is the foundation that makes this possible.

The Problem with Monolithic Engines

Traditional game engines often tightly couple their subsystems:

❌ Monolithic: Everything depends on everything
┌─────────────────────────────────────────┐
│   Rendering ←→ Physics ←→ Audio ←→ ECS │
│        ↕          ↕         ↕          │
│      Input ←→ Networking ←→ Assets     │
└─────────────────────────────────────────┘

This creates problems:

  • Can't use a different physics engine without rewriting rendering code
  • Testing requires the entire engine
  • Updates to one subsystem can break others

The KeenEyes Solution

KeenEyes uses abstraction boundaries that allow any subsystem to be swapped:

✅ Modular: Subsystems are independent
┌──────────┐  ┌──────────┐  ┌──────────┐
│ Graphics │  │  Audio   │  │ Physics  │  ← Swappable implementations
└────┬─────┘  └────┬─────┘  └────┬─────┘
     │             │             │
     └─────────────┼─────────────┘
                   ↓
          ┌───────────────┐
          │  KeenEyes.Core │  ← ECS runtime
          └───────┬───────┘
                  ↓
          ┌───────────────────┐
          │ Abstractions      │  ← Stable interfaces
          └───────────────────┘

Real-world example:

// Default: Use built-in implementations
using var world = new WorldBuilder()
    .WithPlugin<SilkGraphicsPlugin>()    // OpenGL via Silk.NET
    .WithPlugin<OpenALAudioPlugin>()     // OpenAL audio
    .Build();

// Alternative: Swap in your preferred libraries
using var world = new WorldBuilder()
    .WithPlugin<MonoGameGraphicsPlugin>() // MonoGame rendering
    .WithPlugin<FmodAudioPlugin>()        // FMOD audio
    .WithPlugin<JoltPhysicsPlugin>()      // Jolt physics
    .Build();

The ECS core and your game logic don't change - only the plugins differ.

Even the Core is Replaceable

The abstraction runs all the way down. IWorld itself is an interface - if you need a different ECS implementation (sparse sets instead of archetypes, SIMD-optimized queries, distributed entity storage), you can build your own:

public class MySparseSetWorld : IWorld
{
    // Your custom ECS implementation
    // All plugins targeting IWorld still work
}

KeenEyes.Core is the default implementation, not a requirement. This ensures the ecosystem of plugins and tools remains useful even if you outgrow the default core.

Purpose

When building plugins, libraries, or extensions for KeenEyes, you often want to:

  • Keep dependencies minimal for faster compilation and smaller packages
  • Avoid coupling to implementation details
  • Enable testing with mock implementations
  • Support multiple KeenEyes versions with a stable interface
  • Allow users to swap your implementation for alternatives

The Abstractions package solves this by providing only the essential interfaces and types needed to define systems, components, and plugins.

Installation

Reference only KeenEyes.Abstractions in your plugin project:

<PackageReference Include="KeenEyes.Abstractions" Version="1.0.0" />

Applications consuming your plugin will reference KeenEyes.Core, which implements all the abstractions.

Core Interfaces

IWorld

The IWorld interface defines essential world operations that systems and plugins need:

public interface IWorld : IDisposable
{
    // Entity spawning
    IEntityBuilder Spawn();
    IEntityBuilder Spawn(string? name);

    // Entity operations
    bool IsAlive(Entity entity);
    bool Despawn(Entity entity);

    // Component operations
    ref T Get<T>(Entity entity) where T : struct, IComponent;
    bool Has<T>(Entity entity) where T : struct, IComponent;
    bool Remove<T>(Entity entity) where T : struct, IComponent;
    void Add<T>(Entity entity, T component) where T : struct, IComponent;
    void Set<T>(Entity entity, T component) where T : struct, IComponent;

    // Query operations
    IEnumerable<Entity> Query<T1>() where T1 : struct, IComponent;
    IEnumerable<Entity> Query<T1, T2>() /* ... */;
    IEnumerable<Entity> Query<T1, T2, T3>() /* ... */;
    IEnumerable<Entity> Query<T1, T2, T3, T4>() /* ... */;

    // Extension operations
    T GetExtension<T>() where T : class;
    bool TryGetExtension<T>(out T? extension) where T : class;
    bool HasExtension<T>() where T : class;
}

The interface provides complete functionality for most plugin needs, including entity spawning and component operations.

IWorldPlugin

Plugins encapsulate related systems, components, and functionality:

public interface IWorldPlugin
{
    string Name { get; }
    void Install(IPluginContext context);
    void Uninstall(IPluginContext context);
}

IPluginContext

The plugin context provides access to system registration and extension APIs:

public interface IPluginContext
{
    IWorld World { get; }
    IWorldPlugin Plugin { get; }

    // System registration
    T AddSystem<T>(SystemPhase phase = SystemPhase.Update, int order = 0)
        where T : ISystem, new();

    // Extension management
    void SetExtension<T>(T extension) where T : class;
    bool RemoveExtension<T>() where T : class;
}

ISystem

Systems contain the logic that operates on entities:

public interface ISystem : IDisposable
{
    bool Enabled { get; set; }
    void Initialize(IWorld world);
    void Update(float deltaTime);
}

ISystemLifecycle

Optional lifecycle hooks for systems needing before/after update callbacks:

public interface ISystemLifecycle
{
    void OnBeforeUpdate(float deltaTime);
    void OnAfterUpdate(float deltaTime);
}

IComponent / ITagComponent

Marker interfaces for components:

public interface IComponent;
public interface ITagComponent : IComponent;

ICommandBuffer

Interface for queuing deferred entity operations:

public interface ICommandBuffer
{
    int Count { get; }

    // Entity spawning
    EntityCommands Spawn();
    EntityCommands Spawn(string? name);

    // Entity operations
    void Despawn(Entity entity);
    void Despawn(int placeholderId);

    // Component operations
    void AddComponent<T>(Entity entity, T component) where T : struct, IComponent;
    void AddComponent<T>(int placeholderId, T component) where T : struct, IComponent;
    void RemoveComponent<T>(Entity entity) where T : struct, IComponent;
    void RemoveComponent<T>(int placeholderId) where T : struct, IComponent;
    void SetComponent<T>(Entity entity, T component) where T : struct, IComponent;
    void SetComponent<T>(int placeholderId, T component) where T : struct, IComponent;

    // Execution
    Dictionary<int, Entity> Flush(IWorld world);
    void Clear();
}

IEntityBuilder

Interface for building entities with components:

public interface IEntityBuilder
{
    IEntityBuilder With<T>(T component) where T : struct, IComponent;
    IEntityBuilder WithTag<T>() where T : struct, ITagComponent;
    Entity Build();
}

// Generic version for type-safe fluent chaining
public interface IEntityBuilder<out TSelf> : IEntityBuilder
    where TSelf : IEntityBuilder<TSelf>
{
    new TSelf With<T>(T component) where T : struct, IComponent;
    new TSelf WithTag<T>() where T : struct, ITagComponent;
}

The generic version enables type-safe method chaining while the non-generic version allows usage through the interface.

Types

Entity

A lightweight identifier for entities:

public readonly record struct Entity(int Id, int Version)
{
    public static readonly Entity Null = new(-1, 0);
    public bool IsValid => Id >= 0;
}

EntityCommands

A fluent builder for queuing entity spawns in command buffers:

public sealed class EntityCommands : IEntityBuilder<EntityCommands>
{
    public int PlaceholderId { get; }
    public string? Name { get; }

    public EntityCommands With<T>(T component) where T : struct, IComponent;
    public EntityCommands WithTag<T>() where T : struct, ITagComponent;

    // Build() not supported - must use CommandBuffer.Flush()
    Entity Build();
}

Used via CommandBuffer.Spawn() to queue entity creation with components.

SystemGroup

Groups multiple systems for organized execution:

var physicsGroup = new SystemGroup("Physics")
    .Add<BroadphaseSystem>(order: 0)
    .Add<NarrowphaseSystem>(order: 10)
    .Add<SolverSystem>(order: 20);

Writing a Plugin

Here's a complete example of a plugin using only Abstractions:

using KeenEyes;

namespace MyPlugin;

// Define components using the marker interface
[Component]
public partial struct Velocity : IComponent
{
    public float X;
    public float Y;
}

// Define systems using the ISystem interface
public class MovementSystem : ISystem
{
    private IWorld? world;

    public bool Enabled { get; set; } = true;

    public void Initialize(IWorld world)
    {
        this.world = world;
    }

    public void Update(float deltaTime)
    {
        if (world is null) return;

        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 void Dispose() { }
}

// Define the plugin
public class MovementPlugin : IWorldPlugin
{
    public string Name => "Movement";

    public void Install(IPluginContext context)
    {
        context.AddSystem<MovementSystem>(SystemPhase.Update, order: 0);
    }

    public void Uninstall(IPluginContext context)
    {
        // Systems registered via context are automatically cleaned up
    }
}

Exposing Custom APIs

Plugins can expose custom APIs through extensions:

// Define the API interface
public interface IPhysicsWorld
{
    bool Raycast(Vector3 origin, Vector3 direction, out RaycastHit hit);
    IEnumerable<Entity> QuerySphere(Vector3 center, float radius);
}

// Implementation (in your plugin)
internal class PhysicsWorld : IPhysicsWorld
{
    private readonly IWorld world;

    public PhysicsWorld(IWorld world)
    {
        this.world = world;
    }

    public bool Raycast(Vector3 origin, Vector3 direction, out RaycastHit hit)
    {
        // Implementation...
    }

    public IEnumerable<Entity> QuerySphere(Vector3 center, float radius)
    {
        // Implementation...
    }
}

// Register in plugin
public class PhysicsPlugin : IWorldPlugin
{
    public string Name => "Physics";

    public void Install(IPluginContext context)
    {
        context.AddSystem<BroadphaseSystem>(SystemPhase.FixedUpdate, order: 0);
        context.AddSystem<NarrowphaseSystem>(SystemPhase.FixedUpdate, order: 10);
        context.AddSystem<SolverSystem>(SystemPhase.FixedUpdate, order: 20);

        // Expose the physics API
        context.SetExtension<IPhysicsWorld>(new PhysicsWorld(context.World));
    }

    public void Uninstall(IPluginContext context)
    {
        context.RemoveExtension<IPhysicsWorld>();
    }
}

// Usage in application code
var physics = world.GetExtension<IPhysicsWorld>();
if (physics.Raycast(origin, direction, out var hit))
{
    Console.WriteLine($"Hit entity: {hit.Entity}");
}

System Groups

Organize related systems using SystemGroup:

public class RenderingPlugin : IWorldPlugin
{
    public string Name => "Rendering";

    public void Install(IPluginContext context)
    {
        var renderGroup = new SystemGroup("Render Pipeline")
            .Add<CullingSystem>(order: 0)
            .Add<ShadowMapSystem>(order: 10)
            .Add<OpaquePassSystem>(order: 20)
            .Add<TransparentPassSystem>(order: 30)
            .Add<PostProcessSystem>(order: 40);

        context.AddSystemGroup(renderGroup, SystemPhase.Render, order: 0);
    }

    public void Uninstall(IPluginContext context) { }
}

When to Use Abstractions vs Core

Use Case Package
Writing plugins/extensions KeenEyes.Abstractions
Writing unit tests with mocks KeenEyes.Abstractions
Building applications KeenEyes.Core
Advanced world operations KeenEyes.Core
Entity spawning/building KeenEyes.Core

Advanced: Accessing Core Features

When you need features beyond the abstractions (like entity spawning), cast to the concrete type:

public void Initialize(IWorld world)
{
    this.world = world;

    // Cast to access full API when needed
    if (world is World concreteWorld)
    {
        var prefab = concreteWorld.CreatePrefab()
            .With(new Position { X = 0, Y = 0 })
            .With(new Velocity { X = 1, Y = 0 })
            .Build();
    }
}

This approach keeps your plugin's core logic decoupled while still allowing access to advanced features when necessary.

Using Command Buffers in Plugins

Plugins can use command buffers for deferred entity operations without depending on KeenEyes.Core:

using KeenEyes;

public class SpawnerSystem : ISystem
{
    private readonly ICommandBuffer buffer = new CommandBuffer();
    private IWorld? world;

    public void Initialize(IWorld world)
    {
        this.world = world;
    }

    public void Update(float deltaTime)
    {
        if (world is null) return;

        // Queue spawns
        buffer.Spawn()
            .With(new Position { X = 0, Y = 0 })
            .With(new Velocity { X = 1, Y = 0 });

        // Queue component additions
        foreach (var entity in world.Query<Health>())
        {
            ref readonly var health = ref world.Get<Health>(entity);
            if (health.Current <= 0)
            {
                buffer.AddComponent(entity, new Dead());
            }
        }

        // Execute all queued operations
        buffer.Flush(world);
    }

    public void Dispose() { }
    public bool Enabled { get; set; } = true;
}

See the Command Buffer Guide for detailed usage patterns and best practices.

Generated Extension Methods

Component generator creates extension methods that work seamlessly with IEntityBuilder:

[Component]
public partial struct Position
{
    public float X;
    public float Y;
}

// Generated methods work with both generic and non-generic builders:

// Generic version (type-safe chaining)
public static TSelf WithPosition<TSelf>(this TSelf builder, float x, float y)
    where TSelf : IEntityBuilder<TSelf>

// Non-generic version (interface usage)
public static IEntityBuilder WithPosition(this IEntityBuilder builder, float x, float y)

This dual-generation enables:

  • Type-safe chaining with concrete types (EntityBuilder, EntityCommands)
  • Interface compatibility for plugin usage through IWorld.Spawn() and CommandBuffer.Spawn()

Creating Swappable Subsystems

When building a subsystem (physics, audio, rendering, etc.) that users might want to swap, follow this pattern:

1. Define the Interface

Create an interface in a shared abstractions package:

// In MyPhysics.Abstractions
public interface IPhysicsWorld
{
    void SetGravity(float x, float y, float z);
    bool Raycast(Vector3 origin, Vector3 direction, float maxDistance, out RaycastHit hit);
    void AddForce(Entity entity, Vector3 force);
}

public interface IPhysicsPlugin : IWorldPlugin
{
    IPhysicsWorld PhysicsWorld { get; }
}

2. Implement the Plugin

Create one or more implementations:

// In MyPhysics.Jolt (uses Jolt physics)
public class JoltPhysicsPlugin : IPhysicsPlugin
{
    public string Name => "Physics.Jolt";
    private JoltPhysicsWorld? physicsWorld;

    public IPhysicsWorld PhysicsWorld => physicsWorld
        ?? throw new InvalidOperationException("Plugin not installed");

    public void Install(IPluginContext context)
    {
        physicsWorld = new JoltPhysicsWorld(context.World);
        context.SetExtension<IPhysicsWorld>(physicsWorld);
        context.AddSystem<JoltPhysicsSystem>(SystemPhase.FixedUpdate);
    }

    public void Uninstall(IPluginContext context)
    {
        context.RemoveExtension<IPhysicsWorld>();
        physicsWorld?.Dispose();
    }
}

// In MyPhysics.Bullet (alternative implementation)
public class BulletPhysicsPlugin : IPhysicsPlugin
{
    public string Name => "Physics.Bullet";
    // ... same interface, different implementation
}

3. Consume via Interface

Game code depends only on the interface:

public class PlayerController : SystemBase
{
    public override void Update(float deltaTime)
    {
        // Works with ANY physics implementation
        var physics = World.GetExtension<IPhysicsWorld>();

        foreach (var entity in World.Query<Player, Position>())
        {
            if (physics.Raycast(position, Vector3.Down, 1.0f, out var hit))
            {
                // Player is grounded
            }
        }
    }
}

4. User Chooses Implementation

Users pick the implementation at startup:

// User prefers Jolt physics
using var world = new WorldBuilder()
    .WithPlugin<JoltPhysicsPlugin>()
    .Build();

// Another user prefers Bullet
using var world = new WorldBuilder()
    .WithPlugin<BulletPhysicsPlugin>()
    .Build();

// Game code works identically with both!

Key Principles

  1. Interface in abstractions - The contract lives in a package with no implementation dependencies
  2. Implementation in separate packages - Each backend is its own package
  3. Register via extension - Use SetExtension<IInterface>() so consumers use the interface type
  4. Same plugin name category - Use names like "Physics.Jolt", "Physics.Bullet" for clarity

This pattern enables the KeenEyes ecosystem where users can mix and match subsystems freely.

Package Contents

The KeenEyes.Abstractions package includes:

  • Interfaces: IWorld, IWorldPlugin, IPluginContext, ISystem, ISystemLifecycle, IComponent, ITagComponent, ICommandBuffer, IEntityBuilder, IEntityBuilder<TSelf>
  • Types: Entity, EntityCommands, SystemGroup
  • Enums: SystemPhase (via KeenEyes.Generators.Attributes dependency)
  • Internal: ICommand (command execution interface)

The package has minimal dependencies, making it ideal for library authors who want to keep their dependency footprint small.