Привет, Хабр! Я продолжаю изучать MS Orleans и делать простенькую онлайн игру с консольным клиентом и сервером работающим с Orleans грейнами. На этот раз я добавил в игру возможность управлять точкой. Ее можно двигать вверх, вниз, влево, вправо. За подробностями добро пожаловать под кат.
Содержание
- Сервер Игры на MS Orleans — часть 1: Что такое Акторы
- Сервер Игры на MS Orleans — часть 2: Делаем управляемую точку
Исходники
MsOrleansOnlineGame
Принцы работы сервера
Клиент игры шлет серверу команды (нажатые игроком клавиши) и его принимает главный актор игры который обрабатывает эти команды. По таймеру обновляет свое состояние. По таймеру шлет клиенту свое состояние. Клиент при получении нового состояния его отображает. По хорошему надо бы сделать интерполяцию между новым состоянием и старым для большей плавности отображения. Состояние игры представляет собой просто набор координат различных игровых объектов. По сути мы через клиент смотрим стрим игры которая происходит на сервере и управляем своим персонажем отправляя на сервер нажатые нами клавиши.
Entities
public enum Direction
{
None,
Left,
Right,
Top,
Bottom
}
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
public Point Move(Direction direction) => direction switch
{
Direction.Bottom => new Point(X, Y + 1),
Direction.Top => new Point(X, Y - 1),
Direction.Right => new Point(X + 1, Y),
Direction.Left => new Point(X - 1, Y),
_ => throw new ApplicationException("Неизвестное направление")
{
Data = { ["direction"] = direction }
}
};
}
public readonly struct GameState
{
public Point Point { get; }
public GameState(Point point)
{
Point = point;
}
}
Grains.Interfaces
public interface IGame : IGrainWithIntegerKey
{
Task Handle(List<ConsoleKey> commands);
}
Grains
public class GameGrain : Grain, IGame
{
private readonly ILogger<GameGrain> _logger;
private readonly IPersistentState<Point> _store;
private IDisposable _timer;
public GameGrain(
ILogger<GameGrain> logger,
//Хранилище для нашего состояния
//В данном проекте у меня все в оперативной памяти лежит но можно в Базу сохранят
// game - Это что-то типо названия таблицы.
// gameState - Это то-то типо названия базы
[PersistentState("game", "gameState")] IPersistentState<Point> store
)
{
_logger = logger;
_store = store;
}
public override Task OnActivateAsync()
{
_logger.LogWarning("ACTIVATED");
// Тут мы регистрируем стрим который будет раз в 100 миллисекунд
//Слать подключенным клиентам текущее состояние игры.
// Вообще хочу в будущем все это перенести на SignalR
var streamProvider = GetStreamProvider("SMSProvider");
var stream = streamProvider.GetStream<GameState>(Guid.Empty, "GAME_STATE");
_timer = RegisterTimer(s =>
{
stream.OnNextAsync(new GameState(_store.State)).Ignore();
_logger.LogWarning("SEND TO STREAM");
return Task.CompletedTask;
}, null, TimeSpan.FromMilliseconds(1000), TimeSpan.FromMilliseconds(100));
return Task.CompletedTask;
}
public override Task OnDeactivateAsync()
{
_logger.LogWarning("DEACTIVATED");
_timer?.Dispose();
_timer = null;
return Task.CompletedTask;
}
public async Task Handle(List<ConsoleKey> commands)
{
var oldState = _store.State;
foreach (var command in commands)
{
var direction = command switch
{
ConsoleKey.LeftArrow => Direction.Left,
ConsoleKey.A => Direction.Left,
ConsoleKey.RightArrow => Direction.Right,
ConsoleKey.D => Direction.Right,
ConsoleKey.UpArrow => Direction.Top,
ConsoleKey.W => Direction.Top,
ConsoleKey.DownArrow => Direction.Bottom,
ConsoleKey.S => Direction.Bottom,
_ => throw new ApplicationException("Не валидная клавища")
{
Data = { ["key"] = command }
}
};
_store.State = _store.State.Move(direction);
}
_logger.LogInformation("Player Moved!");
_logger.LogInformation("{X}:{Y}", _store.State.X, _store.State.Y);
if (_store.State.Y == oldState.Y && _store.State.X == oldState.X)
return;
await _store.WriteStateAsync();
}
}
Client
Следит за приходящим от сервера новым состоянием:
public class StreamObserver : IAsyncObserver<GameState>
{
private const int MARGIN = 5;
private GameState _old;
public Task OnNextAsync(GameState state, StreamSequenceToken token = null)
{
Console.SetCursorPosition(_old.Point.X+MARGIN, _old.Point.Y+ MARGIN);
Console.Write(' ');
Console.SetCursorPosition(0, 0);
Console.WriteLine("Для передвижения используйте клавиши W A S D или стрелочки на клавиатуре");
Console.SetCursorPosition(state.Point.X+ MARGIN, state.Point.Y+ MARGIN);
Console.Write('#');
_old = state;
return Task.CompletedTask;
}
public Task OnCompletedAsync() => Task.CompletedTask;
public Task OnErrorAsync(Exception ex)
{
Console.WriteLine("ERROR");
Console.WriteLine(ex.Message);
return Task.CompletedTask;
}
}
class Program
{
static async Task Main(string[] args)
{
try
{
using (var client = await ConnectClient())
{
var id = Guid.Empty;
var streamProvider = client.GetStreamProvider("SMSProvider");
var stream = streamProvider.GetStream<GameState>(id, "GAME_STATE");
await stream.SubscribeAsync(new StreamObserver());
Console.CursorVisible = false;
Console.Clear();
var game = client.GetGrain<IGame>(1);
await game.Handle(new List<ConsoleKey>());
while (true)
{
var key = Console.ReadKey(true);
await game.Handle(new List<ConsoleKey>() { key.Key });
}
}
}
catch (Exception e)
{
Console.WriteLine($"\nException while trying to run client: {e.Message}");
Console.WriteLine("Make sure the silo the client is trying to connect to is running.");
Console.WriteLine("\nPress any key to exit.");
}
Console.ReadLine();
}
private static async Task<IClusterClient> ConnectClient()
{
IClusterClient client;
client = new ClientBuilder()
.UseLocalhostClustering()
.Configure<ClusterOptions>(options =>
{
options.ClusterId = "dev";
options.ServiceId = "OrleansBasics";
})
.ConfigureLogging(logging => logging.ClearProviders())
.AddSimpleMessageStreamProvider("SMSProvider")
.Build();
await client.Connect();
Console.WriteLine("Client successfully connected to silo host \n");
return client;
}
}
Server
public class Program
{
public static async Task Main(string[] args)
{
try
{
var host = await StartSilo();
Console.WriteLine("\n\n Press Enter to terminate...\n\n");
Console.ReadLine();
await host.StopAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Console.ReadLine();
}
private static async Task<ISiloHost> StartSilo()
{
var builder = new SiloHostBuilder()
.UseLocalhostClustering()
.Configure<ClusterOptions>(options =>
{
options.ClusterId = "dev";
options.ServiceId = "OrleansBasics";
})
.Configure<EndpointOptions>(
options => options.AdvertisedIPAddress = IPAddress.Loopback
)
//Добавляем провайдера для стримов который работает по TCP
.AddSimpleMessageStreamProvider("SMSProvider")
//Добавляем хранилище в которому будут записывается подписчики и публикаторы событий
.AddMemoryGrainStorage("PubSubStore")
.ConfigureApplicationParts(
parts => parts
.AddApplicationPart(typeof(GameGrain).Assembly)
.WithReferences()
)
//Это хранилище в котором будет лежать состояние нашей игры
.AddMemoryGrainStorage("gameState")
.ConfigureLogging(logging => logging.AddConsole());
var host = builder.Build();
await host.StartAsync();
return host;
}
}