Microsoft Orleans patterns for distributed game servers, Grains, Silos, persistence, and multiplayer game architecture
Creates distributed game servers using Microsoft Orleans virtual actor patterns for multiplayer architecture.
npx claudepluginhub davincidreams/atlas-agent-teamsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Look for: .sln with Orleans NuGet packages, *Grain*.cs, *Silo*.cs, Microsoft.Orleans.* in .csproj, ISiloBuilder, IClusterClient
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
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();
}
}
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);
}
}
// 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;
});
// 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
});
// 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
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
This skill should be used when the user wants to "create a skill", "add a skill to plugin", "write a new skill", "improve skill description", "organize skill content", or needs guidance on skill structure, progressive disclosure, or skill development best practices for Claude Code plugins.