Plugin System Guide
The plugin system enables modular, reusable extensions for KeenEyes worlds. Plugins can register systems, provide custom APIs, and encapsulate domain-specific functionality like physics, networking, or audio.
What is a Plugin?
A plugin is a self-contained module that:
- Registers systems - Adds systems to a world during installation
- Provides extensions - Exposes custom APIs via the extension mechanism
- Manages lifecycle - Handles setup on install and cleanup on uninstall
Plugins promote:
- Modularity - Package related functionality together
- Reusability - Share plugins across multiple worlds
- Clean separation - Isolate domain logic from application code
Creating a Plugin
Implement the IWorldPlugin interface:
using KeenEyes;
public class PhysicsPlugin : IWorldPlugin
{
public string Name => "Physics";
public void Install(IPluginContext context)
{
// Register systems
context.AddSystem<GravitySystem>(SystemPhase.FixedUpdate, order: 0);
context.AddSystem<CollisionSystem>(SystemPhase.FixedUpdate, order: 10);
context.AddSystem<IntegrationSystem>(SystemPhase.FixedUpdate, order: 20);
// Expose custom API
context.SetExtension(new PhysicsWorld(context.World));
}
public void Uninstall(IPluginContext context)
{
// Cleanup (systems are auto-removed)
context.RemoveExtension<PhysicsWorld>();
}
}
Plugin Name
The Name property must be unique within a world. Installing two plugins with the same name throws InvalidOperationException.
Installation
During Install(), use IPluginContext to:
- Add systems via
AddSystem<T>()overloads - Add system groups via
AddSystemGroup() - Set extensions via
SetExtension<T>()
All systems registered via the context are automatically tracked and removed when the plugin is uninstalled.
Uninstallation
During Uninstall():
- Systems registered via
context.AddSystem()are automatically removed - Clean up any extensions you set
- Release any resources the plugin owns
Installing Plugins
Via WorldBuilder (Recommended)
using var world = new WorldBuilder()
.WithPlugin<PhysicsPlugin>()
.WithPlugin<AudioPlugin>()
.WithPlugin<NetworkPlugin>()
.Build();
Via World Directly
using var world = new World();
// Install by type
world.InstallPlugin<PhysicsPlugin>();
// Or install by instance
var audioPlugin = new AudioPlugin(config);
world.InstallPlugin(audioPlugin);
// Chaining supported
world
.InstallPlugin<PhysicsPlugin>()
.InstallPlugin<AudioPlugin>();
Querying Plugins
// Check if installed
if (world.HasPlugin<PhysicsPlugin>())
{
// Get plugin instance
var physics = world.GetPlugin<PhysicsPlugin>();
}
// Get by name
if (world.HasPlugin("Physics"))
{
var physics = world.GetPlugin("Physics");
}
// Enumerate all plugins
foreach (var plugin in world.GetPlugins())
{
Console.WriteLine($"Installed: {plugin.Name}");
}
Uninstalling Plugins
// By type
bool removed = world.UninstallPlugin<PhysicsPlugin>();
// By name
bool removed = world.UninstallPlugin("Physics");
When uninstalled:
Uninstall()is called on the plugin- All systems registered via
IPluginContextare removed - The plugin is removed from the world's registry
Extension API
Extensions let plugins expose custom APIs on the world:
Setting Extensions
public void Install(IPluginContext context)
{
// Create and register your API
var physicsWorld = new PhysicsWorld(context.World);
context.SetExtension(physicsWorld);
}
Accessing Extensions
// Get or throw
var physics = world.GetExtension<PhysicsWorld>();
// Safe access
if (world.TryGetExtension<PhysicsWorld>(out var physics))
{
physics.SetGravity(9.8f);
}
// Check existence
if (world.HasExtension<PhysicsWorld>())
{
// ...
}
Generated Typed Access (C# 13+)
Use [PluginExtension] for compile-time typed properties:
[PluginExtension("Physics")]
public class PhysicsWorld
{
private readonly World world;
public PhysicsWorld(World world) => this.world = world;
public void SetGravity(float g) { /* ... */ }
public void Raycast(Vector3 from, Vector3 to) { /* ... */ }
}
This generates an extension property allowing:
// Instead of:
world.GetExtension<PhysicsWorld>().SetGravity(9.8f);
// Write:
world.Physics.SetGravity(9.8f);
For nullable extensions:
[PluginExtension("Physics", Nullable = true)]
public class PhysicsWorld { /* ... */ }
// Generated property returns null if not set
world.Physics?.SetGravity(9.8f);
System Registration
Basic Registration
// With phase and order
context.AddSystem<MovementSystem>(SystemPhase.Update, order: 10);
// With dependencies
context.AddSystem<CollisionSystem>(
SystemPhase.FixedUpdate,
order: 0,
runsBefore: [typeof(DamageSystem)],
runsAfter: [typeof(MovementSystem)]);
System Groups
var physicsGroup = new SystemGroup("Physics");
physicsGroup.AddSystem(new GravitySystem());
physicsGroup.AddSystem(new CollisionSystem());
physicsGroup.AddSystem(new IntegrationSystem());
context.AddSystemGroup(physicsGroup, SystemPhase.FixedUpdate, order: 0);
Plugin Patterns
Configuration Plugin
public class DebugPlugin : IWorldPlugin
{
private readonly DebugConfig config;
public string Name => "Debug";
public DebugPlugin(DebugConfig config) => this.config = config;
public void Install(IPluginContext context)
{
if (config.ShowStats)
{
context.AddSystem<StatsSystem>(SystemPhase.PostRender);
}
if (config.ShowColliders)
{
context.AddSystem<ColliderRenderSystem>(SystemPhase.Render);
}
context.SetExtension(new DebugStats());
}
public void Uninstall(IPluginContext context)
{
context.RemoveExtension<DebugStats>();
}
}
// Usage
var debug = new DebugPlugin(new DebugConfig
{
ShowStats = true,
ShowColliders = false
});
world.InstallPlugin(debug);
Feature Plugin
public class CombatPlugin : IWorldPlugin
{
public string Name => "Combat";
public void Install(IPluginContext context)
{
// Create system group for ordered execution
var combatGroup = new SystemGroup("Combat");
combatGroup.AddSystem(new TargetingSystem());
combatGroup.AddSystem(new AttackSystem());
combatGroup.AddSystem(new DamageSystem());
combatGroup.AddSystem(new DeathSystem());
context.AddSystemGroup(combatGroup, SystemPhase.Update, order: 100);
}
public void Uninstall(IPluginContext context)
{
// Systems auto-cleaned
}
}
Integration Plugin
public class NetworkPlugin : IWorldPlugin
{
private readonly NetworkConfig config;
private NetworkManager? manager;
public string Name => "Network";
public NetworkPlugin(NetworkConfig config) => this.config = config;
public void Install(IPluginContext context)
{
manager = new NetworkManager(config);
context.AddSystem(new NetworkSyncSystem(manager), SystemPhase.LateUpdate);
context.AddSystem(new NetworkReceiveSystem(manager), SystemPhase.EarlyUpdate);
context.SetExtension(manager);
}
public void Uninstall(IPluginContext context)
{
context.RemoveExtension<NetworkManager>();
manager?.Dispose();
manager = null;
}
}
Using Command Buffers in Plugins
Plugins can use command buffers for deferred entity operations through the ICommandBuffer interface, available in KeenEyes.Abstractions:
using KeenEyes;
public class SpawnerSystem : ISystem
{
private readonly ICommandBuffer buffer = new CommandBuffer();
private IWorld? world;
private float timer;
public void Initialize(IWorld world)
{
this.world = world;
}
public void Update(float deltaTime)
{
if (world is null) return;
timer -= deltaTime;
if (timer <= 0)
{
timer = 1.0f;
// Queue entity spawns safely during iteration
foreach (var spawner in world.Query<Position>().With<Spawner>())
{
ref readonly var pos = ref world.Get<Position>(spawner);
buffer.Spawn()
.With(new Position { X = pos.X, Y = pos.Y })
.With(new Velocity { X = 1, Y = 0 });
}
// Execute all queued spawns
buffer.Flush(world);
}
}
public void Dispose() { }
public bool Enabled { get; set; } = true;
}
Benefits for Plugins
- No Core dependency -
ICommandBufferis inKeenEyes.Abstractions - Safe iteration - Prevents iterator invalidation when spawning/despawning during queries
- Batch execution - All queued operations execute atomically via
Flush() - Zero reflection - Uses delegate capture for type-safe, high-performance command execution
See the Command Buffer Guide for detailed usage patterns and the Abstractions Guide for interface documentation.
Capability-Based Architecture
Plugins can access World features through capability interfaces rather than casting to the concrete World type. This enables better testability and explicit dependency declaration.
Why Capabilities?
// ❌ OLD: Casting to World (harder to test)
public void Install(IPluginContext context)
{
var world = (World)context.World; // Requires concrete World
world.RegisterPrefab("Enemy", prefab);
}
// ✅ NEW: Request specific capability (easy to mock)
public void Install(IPluginContext context)
{
if (context.TryGetCapability<IPrefabCapability>(out var prefabs))
{
prefabs.RegisterPrefab("Enemy", prefab);
}
}
Available Capabilities
| Capability | Purpose | Methods |
|---|---|---|
ISystemHookCapability |
Before/after system hooks | AddSystemHook() |
IPersistenceCapability |
World save/load | CreateSnapshot(), RestoreSnapshot() |
IHierarchyCapability |
Entity parent-child relationships | SetParent(), GetChildren(), GetDescendants() |
IValidationCapability |
Component validation | RegisterValidator<T>(), ValidationMode |
ITagCapability |
String-based entity tagging | AddTag(), RemoveTag(), QueryByTag() |
IStatisticsCapability |
Memory profiling | GetMemoryStats() |
IPrefabCapability |
Entity templates | RegisterPrefab(), SpawnFromPrefab() |
Requesting Capabilities
public void Install(IPluginContext context)
{
// Optional capability - gracefully handle absence
if (context.TryGetCapability<IHierarchyCapability>(out var hierarchy))
{
// Use hierarchy features
}
// Required capability - throws if unavailable
var prefabs = context.GetCapability<IPrefabCapability>();
prefabs.RegisterPrefab("Player", playerPrefab);
}
Testing with Mocks
Each capability has a mock implementation in KeenEyes.Testing:
using KeenEyes.Testing;
using KeenEyes.Testing.Capabilities;
[Fact]
public void Plugin_RegistersPrefabs()
{
// Arrange - create mock capability
var mockPrefabs = new MockPrefabCapability();
var mockContext = new MockPluginContext("TestWorld")
.WithCapability<IPrefabCapability>(mockPrefabs);
// Act
var plugin = new EnemyPlugin();
plugin.Install(mockContext);
// Assert - verify behavior without real World
Assert.Contains("Enemy", mockPrefabs.RegistrationOrder);
}
Mock Capability Features
| Mock | Key Properties |
|---|---|
MockHierarchyCapability |
ParentMap, ChildrenMap, OperationLog |
MockValidationCapability |
RegisteredValidators, ValidationMode |
MockTagCapability |
EntityTags, OperationLog |
MockStatisticsCapability |
Configurable TotalAllocatedBytes, EntityCount, etc. |
MockPrefabCapability |
RegistrationOrder, SpawnLog |
See Testing Guide for more testing patterns.
World Isolation
Each world has completely isolated plugins:
using var world1 = new World();
using var world2 = new World();
world1.InstallPlugin<PhysicsPlugin>();
world2.InstallPlugin<AudioPlugin>();
// world1 has Physics, world2 has Audio
// No cross-contamination
Lifecycle
- World created - Empty plugin registry
- InstallPlugin() - Plugin's
Install()called - World.Update() - Plugin systems run normally
- UninstallPlugin() - Plugin's
Uninstall()called, systems removed - World.Dispose() - All plugins uninstalled automatically
Best Practices
Do
- Use
IPluginContext.AddSystem()so systems are auto-tracked - Clean up extensions in
Uninstall() - Use unique, descriptive plugin names
- Provide configuration via constructor parameters
- Document what extensions your plugin provides
Don't
- Register systems directly on
Worldfrom plugins - Leave extensions registered after uninstall
- Use static state within plugins
- Assume plugin installation order
Next Steps
- Systems Guide - System design patterns
- Messaging Guide - Inter-system communication
- Events Guide - Component and entity lifecycle events