game tick service

This commit is contained in:
Vinzenz Schroeter 2024-04-06 21:15:26 +02:00
parent 154539488a
commit 1a20fa1fb6
5 changed files with 120 additions and 35 deletions

View file

@ -7,8 +7,9 @@ namespace TanksServer;
internal sealed class ClientScreenServer(
ILogger<ClientScreenServer> logger,
ILoggerFactory loggerFactory
) : IHostedLifecycleService
ILoggerFactory loggerFactory,
MapDrawer drawer
) : IHostedLifecycleService, ITickStep
{
private readonly List<ClientScreenServerConnection> _connections = new();
@ -16,45 +17,57 @@ internal sealed class ClientScreenServer(
{
logger.LogDebug("HandleClient");
var connection =
new ClientScreenServerConnection(socket, loggerFactory.CreateLogger<ClientScreenServerConnection>());
new ClientScreenServerConnection(socket, loggerFactory.CreateLogger<ClientScreenServerConnection>(), this);
_connections.Add(connection);
return connection.Done;
}
public Task Send(DisplayPixelBuffer buf)
{
logger.LogDebug("Sending buffer to {} clients", _connections.Count);
return Task.WhenAll(_connections.Select(c => c.Send(buf)));
}
public Task StoppingAsync(CancellationToken cancellationToken)
{
logger.LogInformation("closing connections");
return Task.WhenAll(_connections.Select(c => c.CloseAsync()));
}
public Task TickAsync()
{
logger.LogTrace("Sending buffer to {} clients", _connections.Count);
return Task.WhenAll(_connections.Select(c => c.SendAsync(drawer.LastFrame)));
}
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class ClientScreenServerConnection(WebSocket webSocket, ILogger<ClientScreenServerConnection> logger)
: EasyWebSocket(webSocket, logger, ArraySegment<byte>.Empty)
{
private bool _wantsNewFrame = true;
public Task Send(DisplayPixelBuffer buf)
private void Remove(ClientScreenServerConnection connection) => _connections.Remove(connection);
private sealed class ClientScreenServerConnection(
WebSocket webSocket,
ILogger<ClientScreenServerConnection> logger,
ClientScreenServer server
) : EasyWebSocket(webSocket, logger, ArraySegment<byte>.Empty)
{
if (!_wantsNewFrame)
private bool _wantsNewFrame = true;
public Task SendAsync(DisplayPixelBuffer buf)
{
if (!_wantsNewFrame)
return Task.CompletedTask;
_wantsNewFrame = false;
return TrySendAsync(buf.Data);
}
protected override Task ReceiveAsync(ArraySegment<byte> buffer)
{
_wantsNewFrame = true;
return Task.CompletedTask;
return SendAsync(buf.Data);
}
}
protected override Task ReceiveAsync(ArraySegment<byte> buffer)
{
_wantsNewFrame = true;
return Task.CompletedTask;
protected override Task ClosingAsync()
{
server.Remove(this);
return Task.CompletedTask;
}
}
}

View file

@ -12,6 +12,7 @@ internal abstract class EasyWebSocket
private readonly WebSocket _socket;
private readonly Task _readLoop;
private readonly ArraySegment<byte> _buffer;
private int _closed;
protected EasyWebSocket(WebSocket socket, ILogger logger, ArraySegment<byte> buffer)
{
@ -33,21 +34,38 @@ internal abstract class EasyWebSocket
await ReceiveAsync(_buffer[..response.Count]);
} while (_socket.State == WebSocketState.Open);
await CloseAsync();
}
protected abstract Task ReceiveAsync(ArraySegment<byte> buffer);
protected abstract Task ClosingAsync();
protected Task SendAsync(byte[] data)
protected async Task TrySendAsync(byte[] data)
{
if (_socket.State != WebSocketState.Open)
await CloseAsync();
_logger.LogTrace("sending {} bytes of data", _buffer.Count);
return _socket.SendAsync(data, WebSocketMessageType.Binary, true, CancellationToken.None);
try
{
await _socket.SendAsync(data, WebSocketMessageType.Binary, true, CancellationToken.None);
}
catch (WebSocketException wsEx)
{
_logger.LogDebug(wsEx, "send failed");
}
}
public async Task CloseAsync()
{
if (Interlocked.Exchange(ref _closed, 1) == 1)
return;
_logger.LogDebug("closing socket");
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
await _readLoop;
await ClosingAsync();
_completionSource.SetResult();
}
}

View file

@ -0,0 +1,38 @@
using System.Threading;
using Microsoft.Extensions.Hosting;
namespace TanksServer;
public class GameTickService(IEnumerable<ITickStep> steps) : IHostedService
{
private readonly CancellationTokenSource _cancellation = new();
private readonly List<ITickStep> _steps = steps.ToList();
private Task? _run;
public Task StartAsync(CancellationToken cancellationToken)
{
_run = RunAsync();
return Task.CompletedTask;
}
private async Task RunAsync()
{
while (!_cancellation.IsCancellationRequested)
{
foreach (var step in _steps)
await step.TickAsync();
await Task.Delay(1000 / 250);
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await _cancellation.CancelAsync();
if (_run != null) await _run;
}
}
public interface ITickStep
{
Task TickAsync();
}

View file

@ -1,10 +1,10 @@
namespace TanksServer;
internal class MapDrawer(MapService map)
internal class MapDrawer(MapService map):ITickStep
{
private const uint GameFieldPixelCount = MapService.PixelsPerRow * MapService.PixelsPerColumn;
public void DrawInto(DisplayPixelBuffer buf)
private void DrawInto(DisplayPixelBuffer buf)
{
for (var tileY = 0; tileY < MapService.TilesPerColumn; tileY++)
for (var tileX = 0; tileX < MapService.TilesPerRow; tileX++)
@ -23,7 +23,7 @@ internal class MapDrawer(MapService map)
}
}
public DisplayPixelBuffer CreateGameFieldPixelBuffer()
private DisplayPixelBuffer CreateGameFieldPixelBuffer()
{
var data = new byte[10 + GameFieldPixelCount / 8];
var result = new DisplayPixelBuffer(data)
@ -37,4 +37,20 @@ internal class MapDrawer(MapService map)
};
return result;
}
private DisplayPixelBuffer? _lastFrame;
public DisplayPixelBuffer LastFrame
{
get => _lastFrame ?? throw new InvalidOperationException("first frame not yet drawn");
private set => _lastFrame = value;
}
public Task TickAsync()
{
var buffer = CreateGameFieldPixelBuffer();
DrawInto(buffer);
LastFrame = buffer;
return Task.CompletedTask;
}
}

View file

@ -12,14 +12,8 @@ internal static class Program
{
var app = Configure(args);
var display = app.Services.GetRequiredService<ServicePointDisplay>();
var mapDrawer = app.Services.GetRequiredService<MapDrawer>();
var clientScreenServer = app.Services.GetRequiredService<ClientScreenServer>();
var buffer = mapDrawer.CreateGameFieldPixelBuffer();
mapDrawer.DrawInto(buffer);
await display.Send(buffer);
var clientFileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "client"));
app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = clientFileProvider });
app.UseStaticFiles(new StaticFileOptions { FileProvider = clientFileProvider });
@ -46,10 +40,16 @@ internal static class Program
builder.Services.AddSingleton<ServicePointDisplay>();
builder.Services.AddSingleton<MapService>();
builder.Services.AddSingleton<MapDrawer>();
builder.Services.AddSingleton<ITickStep, MapDrawer>(sp => sp.GetRequiredService<MapDrawer>());
builder.Services.AddSingleton<ClientScreenServer>();
builder.Services.AddHostedService<ClientScreenServer>(sp => sp.GetRequiredService<ClientScreenServer>());
builder.Services.AddSingleton<ITickStep, ClientScreenServer>(sp => sp.GetRequiredService<ClientScreenServer>());
builder.Services.AddHostedService<GameTickService>();
return builder.Build();
}
}