orleans
Installation
SKILL.md
Microsoft Orleans Game Server Skill
Engine Detection
Look for: .sln with Orleans NuGet packages, *Grain*.cs, *Silo*.cs, Microsoft.Orleans.* in .csproj, ISiloBuilder, IClusterClient
Project Structure
GameServer/
GameServer.sln
src/
GameServer.Grains.Interfaces/ # Grain interfaces (shared)
IPlayerGrain.cs
IRoomGrain.cs
IMatchGrain.cs
ILeaderboardGrain.cs
GameServer.Grains/ # Grain implementations
PlayerGrain.cs
RoomGrain.cs
MatchGrain.cs
LeaderboardGrain.cs
GameServer.Silo/ # Silo host configuration
Program.cs
SiloConfig.cs
GameServer.Client/ # Client SDK / API gateway
Program.cs
Controllers/
GameController.cs
GameServer.Shared/ # Shared types and DTOs
Models/
PlayerState.cs
MatchState.cs
GameAction.cs
tests/
GameServer.Tests/
PlayerGrainTests.cs
MatchGrainTests.cs
Grain Pattern (Virtual Actor)
Grains are the core abstraction. Each grain has a unique identity and is single-threaded:
// Interface - GameServer.Grains.Interfaces/IPlayerGrain.cs
public interface IPlayerGrain : IGrainWithStringKey
{
Task<PlayerState> GetState();
Task JoinRoom(string roomId);
Task LeaveRoom();
Task<bool> TakeDamage(float amount, string attackerId);
Task UpdatePosition(Vector3 position, Quaternion rotation);
}
// Implementation - GameServer.Grains/PlayerGrain.cs
public class PlayerGrain : Grain, IPlayerGrain
{
private readonly IPersistentState<PlayerState> _state;
private readonly ILogger<PlayerGrain> _logger;
private IDisposable? _heartbeatTimer;
public PlayerGrain(
[PersistentState("player", "gameStore")]
IPersistentState<PlayerState> state,
ILogger<PlayerGrain> logger)
{
_state = state;
_logger = logger;
}
public override async Task OnActivateAsync(CancellationToken ct)
{
_logger.LogInformation("Player {Id} activated", this.GetPrimaryKeyString());
_heartbeatTimer = this.RegisterGrainTimer(
Heartbeat, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
await base.OnActivateAsync(ct);
}
public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken ct)
{
_heartbeatTimer?.Dispose();
await _state.WriteStateAsync();
await base.OnDeactivateAsync(reason, ct);
}
public Task<PlayerState> GetState() => Task.FromResult(_state.State);
public async Task JoinRoom(string roomId)
{
var room = GrainFactory.GetGrain<IRoomGrain>(roomId);
await room.AddPlayer(this.GetPrimaryKeyString());
_state.State.CurrentRoomId = roomId;
await _state.WriteStateAsync();
}
public async Task<bool> TakeDamage(float amount, string attackerId)
{
_state.State.Health -= amount;
if (_state.State.Health <= 0)
{
_state.State.Health = 0;
_state.State.IsAlive = false;
await _state.WriteStateAsync();
// Notify the room
if (_state.State.CurrentRoomId is not null)
{
var room = GrainFactory.GetGrain<IRoomGrain>(_state.State.CurrentRoomId);
await room.OnPlayerDeath(this.GetPrimaryKeyString(), attackerId);
}
return true; // Player died
}
await _state.WriteStateAsync();
return false;
}
private Task Heartbeat()
{
_state.State.LastHeartbeat = DateTime.UtcNow;
return _state.WriteStateAsync();
}
}
Room/Match Grain (Game Session)
public interface IRoomGrain : IGrainWithStringKey
{
Task AddPlayer(string playerId);
Task RemovePlayer(string playerId);
Task<RoomState> GetState();
Task BroadcastAction(GameAction action);
Task OnPlayerDeath(string playerId, string killerId);
}
public class RoomGrain : Grain, IRoomGrain
{
private readonly IPersistentState<RoomState> _state;
private readonly HashSet<string> _activePlayers = new();
public RoomGrain(
[PersistentState("room", "gameStore")]
IPersistentState<RoomState> state)
{
_state = state;
}
public async Task AddPlayer(string playerId)
{
if (_activePlayers.Count >= _state.State.MaxPlayers)
throw new InvalidOperationException("Room is full");
_activePlayers.Add(playerId);
_state.State.PlayerIds = _activePlayers.ToList();
await _state.WriteStateAsync();
// Notify all players
await BroadcastAction(new GameAction
{
Type = "player_joined",
PlayerId = playerId,
Timestamp = DateTime.UtcNow
});
}
public async Task BroadcastAction(GameAction action)
{
var tasks = _activePlayers.Select(async id =>
{
var player = GrainFactory.GetGrain<IPlayerGrain>(id);
// Push via stream or polling
});
await Task.WhenAll(tasks);
}
}
Orleans Streams (Real-Time Updates)
// Producer (in RoomGrain)
public async Task BroadcastGameState()
{
var streamProvider = this.GetStreamProvider("GameStream");
var stream = streamProvider.GetStream<GameStateUpdate>(
StreamId.Create("room", this.GetPrimaryKeyString()));
await stream.OnNextAsync(new GameStateUpdate
{
RoomId = this.GetPrimaryKeyString(),
Players = _state.State.PlayerIds,
Timestamp = DateTime.UtcNow
});
}
// Consumer (in client or another grain)
var stream = streamProvider.GetStream<GameStateUpdate>(
StreamId.Create("room", roomId));
await stream.SubscribeAsync((update, token) =>
{
// Handle real-time game state update
return Task.CompletedTask;
});
Silo Configuration
// Program.cs - Silo Host
var builder = Host.CreateDefaultBuilder(args)
.UseOrleans((context, siloBuilder) =>
{
if (context.HostingEnvironment.IsDevelopment())
{
siloBuilder.UseLocalhostClustering();
siloBuilder.AddMemoryGrainStorage("gameStore");
}
else
{
siloBuilder.UseAzureStorageClustering(options =>
options.ConfigureTableServiceClient(connectionString));
siloBuilder.AddAzureTableGrainStorage("gameStore", options =>
options.ConfigureTableServiceClient(connectionString));
}
siloBuilder.AddMemoryStreams("GameStream");
siloBuilder.UseDashboard(); // Orleans Dashboard for monitoring
});
Persistence Patterns
// State class
[GenerateSerializer]
public class PlayerState
{
[Id(0)] public string PlayerId { get; set; } = "";
[Id(1)] public float Health { get; set; } = 100f;
[Id(2)] public bool IsAlive { get; set; } = true;
[Id(3)] public string? CurrentRoomId { get; set; }
[Id(4)] public DateTime LastHeartbeat { get; set; }
[Id(5)] public Dictionary<string, int> Inventory { get; set; } = new();
}
// Use [GenerateSerializer] and [Id(n)] for Orleans serialization
// Write state explicitly after mutations: await _state.WriteStateAsync()
// State is automatically loaded on grain activation
Key Rules
- Grains are single-threaded - No locks needed, but avoid blocking calls
- Use Task/async everywhere - Never block with .Result or .Wait()
- Keep grain state small - Large state = slow activation/persistence
- Use grain timers, not Task.Delay - Timers are grain-lifecycle aware
- Dispose timers in OnDeactivateAsync - Prevent leaks
- Use streams for real-time communication - Not polling
- Persist state explicitly - Call WriteStateAsync after mutations
- Use [GenerateSerializer] - Not JSON serialization for grain state
- Design for grain deactivation - Grains can deactivate at any time
- Use string or Guid keys - Choose based on your identity model
Common Anti-Patterns
- Storing client connections in grain state (not serializable)
- Calling .Result on Tasks inside grains (deadlock risk)
- Large grain state with full game world (partition into multiple grains)
- Not handling grain deactivation/reactivation cycles
- Using static state instead of grain state (lost on silo restart)
Scaling Patterns
- One grain per player - Player state, inventory, progression
- One grain per room/match - Game session state, player list
- One grain per world zone - Spatial partitioning for large worlds
- Stateless worker grains - For CPU-intensive calculations (pathfinding, matchmaking)
- Observer pattern - Use IGrainObserver for push notifications to clients
Related skills
More from davincidreams/agent-team-plugins
blender
Blender interface, workflows, and 3D production pipeline
222rigging
Rigging fundamentals, skeleton setup, and animation controls
16animation
Animation principles, techniques, and best practices for 3D animation
13vroid
Vroid Studio, VRM format, and VTuber avatar creation
10technical-writing
Technical writing principles and best practices for creating clear, accurate documentation
9unreal
Unreal Engine patterns, Actor/Component model, Blueprints vs C++, and best practices
8