game tick service
This commit is contained in:
parent
154539488a
commit
1a20fa1fb6
|
@ -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,40 +17,45 @@ 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 sealed class ClientScreenServerConnection(
|
||||||
|
WebSocket webSocket,
|
||||||
|
ILogger<ClientScreenServerConnection> logger,
|
||||||
|
ClientScreenServer server
|
||||||
|
) : EasyWebSocket(webSocket, logger, ArraySegment<byte>.Empty)
|
||||||
{
|
{
|
||||||
private bool _wantsNewFrame = true;
|
private bool _wantsNewFrame = true;
|
||||||
|
|
||||||
public Task Send(DisplayPixelBuffer buf)
|
public Task SendAsync(DisplayPixelBuffer buf)
|
||||||
{
|
{
|
||||||
if (!_wantsNewFrame)
|
if (!_wantsNewFrame)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
return SendAsync(buf.Data);
|
_wantsNewFrame = false;
|
||||||
|
return TrySendAsync(buf.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task ReceiveAsync(ArraySegment<byte> buffer)
|
protected override Task ReceiveAsync(ArraySegment<byte> buffer)
|
||||||
|
@ -57,4 +63,11 @@ internal sealed class ClientScreenServerConnection(WebSocket webSocket, ILogger<
|
||||||
_wantsNewFrame = true;
|
_wantsNewFrame = true;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override Task ClosingAsync()
|
||||||
|
{
|
||||||
|
server.Remove(this);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
38
TanksServer/GameTickService.cs
Normal file
38
TanksServer/GameTickService.cs
Normal 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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue