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

View file

@ -12,6 +12,7 @@ internal abstract class EasyWebSocket
private readonly WebSocket _socket; private readonly WebSocket _socket;
private readonly Task _readLoop; private readonly Task _readLoop;
private readonly ArraySegment<byte> _buffer; private readonly ArraySegment<byte> _buffer;
private int _closed;
protected EasyWebSocket(WebSocket socket, ILogger logger, ArraySegment<byte> buffer) protected EasyWebSocket(WebSocket socket, ILogger logger, ArraySegment<byte> buffer)
{ {
@ -33,21 +34,38 @@ internal abstract class EasyWebSocket
await ReceiveAsync(_buffer[..response.Count]); await ReceiveAsync(_buffer[..response.Count]);
} while (_socket.State == WebSocketState.Open); } while (_socket.State == WebSocketState.Open);
await CloseAsync();
} }
protected abstract Task ReceiveAsync(ArraySegment<byte> buffer); 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); _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() public async Task CloseAsync()
{ {
if (Interlocked.Exchange(ref _closed, 1) == 1)
return;
_logger.LogDebug("closing socket"); _logger.LogDebug("closing socket");
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
await _readLoop; await _readLoop;
await ClosingAsync();
_completionSource.SetResult(); _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; namespace TanksServer;
internal class MapDrawer(MapService map) internal class MapDrawer(MapService map):ITickStep
{ {
private const uint GameFieldPixelCount = MapService.PixelsPerRow * MapService.PixelsPerColumn; 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 tileY = 0; tileY < MapService.TilesPerColumn; tileY++)
for (var tileX = 0; tileX < MapService.TilesPerRow; tileX++) 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 data = new byte[10 + GameFieldPixelCount / 8];
var result = new DisplayPixelBuffer(data) var result = new DisplayPixelBuffer(data)
@ -37,4 +37,20 @@ internal class MapDrawer(MapService map)
}; };
return result; 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 app = Configure(args);
var display = app.Services.GetRequiredService<ServicePointDisplay>();
var mapDrawer = app.Services.GetRequiredService<MapDrawer>();
var clientScreenServer = app.Services.GetRequiredService<ClientScreenServer>(); 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")); var clientFileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "client"));
app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = clientFileProvider }); app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = clientFileProvider });
app.UseStaticFiles(new StaticFileOptions { FileProvider = clientFileProvider }); app.UseStaticFiles(new StaticFileOptions { FileProvider = clientFileProvider });
@ -46,9 +40,15 @@ internal static class Program
builder.Services.AddSingleton<ServicePointDisplay>(); builder.Services.AddSingleton<ServicePointDisplay>();
builder.Services.AddSingleton<MapService>(); builder.Services.AddSingleton<MapService>();
builder.Services.AddSingleton<MapDrawer>(); builder.Services.AddSingleton<MapDrawer>();
builder.Services.AddSingleton<ITickStep, MapDrawer>(sp => sp.GetRequiredService<MapDrawer>());
builder.Services.AddSingleton<ClientScreenServer>(); builder.Services.AddSingleton<ClientScreenServer>();
builder.Services.AddHostedService<ClientScreenServer>(sp => sp.GetRequiredService<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(); return builder.Build();
} }