game tick service
This commit is contained in:
parent
154539488a
commit
1a20fa1fb6
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue