From a3bd582b2ea1f5e20cb8e8f3f870b6f40695f6ce Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 7 Apr 2024 13:02:49 +0200 Subject: [PATCH] tanks can spawn and get rendered --- TanksServer/ClientScreenServer.cs | 2 +- TanksServer/ControlsServer.cs | 10 +- TanksServer/Helpers/AppSerializerContext.cs | 5 +- TanksServer/Helpers/DisplayPixelBuffer.cs | 4 +- TanksServer/Helpers/EasyWebSocket.cs | 10 +- TanksServer/Models/PixelPosition.cs | 3 + TanksServer/Models/Player.cs | 19 ++++ TanksServer/Models/Tank.cs | 8 ++ TanksServer/Models/TilePosition.cs | 3 + TanksServer/PlayerServer.cs | 29 ++--- TanksServer/Program.cs | 28 +++-- TanksServer/Services/GameTickService.cs | 8 +- TanksServer/Services/MapDrawer.cs | 56 ---------- TanksServer/Services/MapService.cs | 8 +- TanksServer/Services/PixelDrawer.cs | 112 ++++++++++++++++++++ TanksServer/Services/ServicePointDisplay.cs | 14 ++- TanksServer/Services/SpawnQueue.cs | 51 +++++++++ TanksServer/Services/TankManager.cs | 20 ++++ TanksServer/TanksServer.csproj | 4 + TanksServer/assets/tank.png | Bin 0 -> 355 bytes 20 files changed, 286 insertions(+), 108 deletions(-) create mode 100644 TanksServer/Models/PixelPosition.cs create mode 100644 TanksServer/Models/Player.cs create mode 100644 TanksServer/Models/Tank.cs create mode 100644 TanksServer/Models/TilePosition.cs delete mode 100644 TanksServer/Services/MapDrawer.cs create mode 100644 TanksServer/Services/PixelDrawer.cs create mode 100644 TanksServer/Services/SpawnQueue.cs create mode 100644 TanksServer/Services/TankManager.cs create mode 100644 TanksServer/assets/tank.png diff --git a/TanksServer/ClientScreenServer.cs b/TanksServer/ClientScreenServer.cs index 8c9b4ce..685ad19 100644 --- a/TanksServer/ClientScreenServer.cs +++ b/TanksServer/ClientScreenServer.cs @@ -9,7 +9,7 @@ namespace TanksServer; internal sealed class ClientScreenServer( ILogger logger, ILoggerFactory loggerFactory, - MapDrawer drawer + PixelDrawer drawer ) : IHostedLifecycleService, ITickStep { private readonly List _connections = new(); diff --git a/TanksServer/ControlsServer.cs b/TanksServer/ControlsServer.cs index dfa520a..770e46b 100644 --- a/TanksServer/ControlsServer.cs +++ b/TanksServer/ControlsServer.cs @@ -2,6 +2,7 @@ using System.Net.WebSockets; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using TanksServer.Helpers; +using TanksServer.Models; namespace TanksServer; @@ -35,8 +36,9 @@ internal sealed class ControlsServer(ILogger logger, ILoggerFact _connections.Remove(connection); } - private sealed class ControlsServerConnection(WebSocket socket, ILogger logger, ControlsServer server, - Player player) + private sealed class ControlsServerConnection( + WebSocket socket, ILogger logger, + ControlsServer server, Player player) : EasyWebSocket(socket, logger, new byte[2]) { private enum MessageType : byte @@ -58,8 +60,8 @@ internal sealed class ControlsServer(ILogger logger, ILoggerFact { var type = (MessageType)buffer[0]; var control = (InputType)buffer[1]; - - logger.LogTrace("player input {} {} {}", player.Id, type, control); + + Logger.LogTrace("player input {} {} {}", player.Id, type, control); var isEnable = type switch { diff --git a/TanksServer/Helpers/AppSerializerContext.cs b/TanksServer/Helpers/AppSerializerContext.cs index c02d085..c048633 100644 --- a/TanksServer/Helpers/AppSerializerContext.cs +++ b/TanksServer/Helpers/AppSerializerContext.cs @@ -1,6 +1,7 @@ using System.Text.Json.Serialization; +using TanksServer.Models; -namespace TanksServer; +namespace TanksServer.Helpers; [JsonSerializable(typeof(Player))] -internal partial class AppSerializerContext: JsonSerializerContext; +internal sealed partial class AppSerializerContext: JsonSerializerContext; diff --git a/TanksServer/Helpers/DisplayPixelBuffer.cs b/TanksServer/Helpers/DisplayPixelBuffer.cs index 6f06445..a2b4f10 100644 --- a/TanksServer/Helpers/DisplayPixelBuffer.cs +++ b/TanksServer/Helpers/DisplayPixelBuffer.cs @@ -1,6 +1,4 @@ -using TanksServer.Helpers; - -namespace TanksServer; +namespace TanksServer.Helpers; internal sealed class DisplayPixelBuffer(byte[] data) { diff --git a/TanksServer/Helpers/EasyWebSocket.cs b/TanksServer/Helpers/EasyWebSocket.cs index 390885a..cb7d4bc 100644 --- a/TanksServer/Helpers/EasyWebSocket.cs +++ b/TanksServer/Helpers/EasyWebSocket.cs @@ -10,7 +10,7 @@ internal abstract class EasyWebSocket { private readonly TaskCompletionSource _completionSource = new(); - private readonly ILogger _logger; + protected readonly ILogger Logger; private readonly WebSocket _socket; private readonly Task _readLoop; private readonly ArraySegment _buffer; @@ -19,7 +19,7 @@ internal abstract class EasyWebSocket protected EasyWebSocket(WebSocket socket, ILogger logger, ArraySegment buffer) { _socket = socket; - _logger = logger; + Logger = logger; _buffer = buffer; _readLoop = ReadLoopAsync(); } @@ -46,7 +46,7 @@ internal abstract class EasyWebSocket if (_socket.State != WebSocketState.Open) await CloseAsync(); - _logger.LogTrace("sending {} bytes of data", _buffer.Count); + Logger.LogTrace("sending {} bytes of data", _buffer.Count); try { @@ -54,7 +54,7 @@ internal abstract class EasyWebSocket } catch (WebSocketException wsEx) { - _logger.LogDebug(wsEx, "send failed"); + Logger.LogDebug(wsEx, "send failed"); } } @@ -65,7 +65,7 @@ internal abstract class EasyWebSocket { if (Interlocked.Exchange(ref _closed, 1) == 1) return; - _logger.LogDebug("closing socket"); + Logger.LogDebug("closing socket"); await _socket.CloseAsync(status, description, CancellationToken.None); await _readLoop; await ClosingAsync(); diff --git a/TanksServer/Models/PixelPosition.cs b/TanksServer/Models/PixelPosition.cs new file mode 100644 index 0000000..116ac0e --- /dev/null +++ b/TanksServer/Models/PixelPosition.cs @@ -0,0 +1,3 @@ +namespace TanksServer.Models; + +internal record struct PixelPosition(int X, int Y); diff --git a/TanksServer/Models/Player.cs b/TanksServer/Models/Player.cs new file mode 100644 index 0000000..10a7d59 --- /dev/null +++ b/TanksServer/Models/Player.cs @@ -0,0 +1,19 @@ +namespace TanksServer.Models; + +internal sealed class Player(string name) +{ + public string Name => name; + + public Guid Id { get; } = Guid.NewGuid(); + + public PlayerControls Controls { get; } = new(); +} + +internal sealed class PlayerControls +{ + public bool Forward { get; set; } + public bool Backward { get; set; } + public bool TurnLeft { get; set; } + public bool TurnRight { get; set; } + public bool Shoot { get; set; } +} diff --git a/TanksServer/Models/Tank.cs b/TanksServer/Models/Tank.cs new file mode 100644 index 0000000..71eef56 --- /dev/null +++ b/TanksServer/Models/Tank.cs @@ -0,0 +1,8 @@ +namespace TanksServer.Models; + +internal sealed class Tank(Player player, PixelPosition spawnPosition) +{ + public Player Owner { get; } = player; + public int Rotation { get; set; } + public PixelPosition Position { get; set; } = spawnPosition; +} diff --git a/TanksServer/Models/TilePosition.cs b/TanksServer/Models/TilePosition.cs new file mode 100644 index 0000000..d68d44a --- /dev/null +++ b/TanksServer/Models/TilePosition.cs @@ -0,0 +1,3 @@ +namespace TanksServer.Models; + +internal record struct TilePosition(int X, int Y); diff --git a/TanksServer/PlayerServer.cs b/TanksServer/PlayerServer.cs index eafb381..06e6c2f 100644 --- a/TanksServer/PlayerServer.cs +++ b/TanksServer/PlayerServer.cs @@ -1,16 +1,18 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; +using TanksServer.Models; +using TanksServer.Services; namespace TanksServer; -internal sealed class PlayerServer(ILogger logger) +internal sealed class PlayerServer(ILogger logger, SpawnQueue spawnQueue) { private readonly ConcurrentDictionary _players = new(); public Player GetOrAdd(string name) { - var player = _players.GetOrAdd(name, _ => new Player(name)); + var player = _players.GetOrAdd(name, AddAndSpawn); logger.LogInformation("player {} (re)joined", player.Id); return player; } @@ -28,22 +30,11 @@ internal sealed class PlayerServer(ILogger logger) foundPlayer = null; return false; } -} - -internal sealed class Player(string name) -{ - public string Name => name; - - public Guid Id { get; } = Guid.NewGuid(); - public PlayerControls Controls { get; } = new(); -} - -internal sealed class PlayerControls -{ - public bool Forward { get; set; } - public bool Backward { get; set; } - public bool TurnLeft { get; set; } - public bool TurnRight { get; set; } - public bool Shoot { get; set; } + private Player AddAndSpawn(string name) + { + var player = new Player(name); + spawnQueue.SpawnTankForPlayer(player); + return player; + } } diff --git a/TanksServer/Program.cs b/TanksServer/Program.cs index a50658e..17e13fa 100644 --- a/TanksServer/Program.cs +++ b/TanksServer/Program.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; +using TanksServer.Helpers; using TanksServer.Services; namespace TanksServer; @@ -26,7 +27,7 @@ internal static class Program app.UseStaticFiles(new StaticFileOptions { FileProvider = clientFileProvider }); app.MapGet("/player", playerService.GetOrAdd); - + app.Map("/screen", async context => { if (!context.WebSockets.IsWebSocketRequest) @@ -46,7 +47,7 @@ internal static class Program if (!playerService.TryGet(playerId, out var player)) return Results.NotFound(); - + using var ws = await context.WebSockets.AcceptWebSocketAsync(); await controlsServer.HandleClient(ws, player); return Results.Empty; @@ -73,18 +74,23 @@ internal static class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - - builder.Services.AddSingleton(); - builder.Services.AddHostedService(sp => sp.GetRequiredService()); - builder.Services.AddSingleton(sp => sp.GetRequiredService()); - - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddHostedService(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => sp.GetRequiredService()); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => sp.GetRequiredService()); + + builder.Services.AddSingleton(); + builder.Services.AddHostedService(sp => sp.GetRequiredService()); + builder.Services.AddSingleton(sp => sp.GetRequiredService()); + + builder.Services.AddSingleton(); + builder.Services.AddHostedService(sp => sp.GetRequiredService()); + builder.Services.AddSingleton(); return builder.Build(); diff --git a/TanksServer/Services/GameTickService.cs b/TanksServer/Services/GameTickService.cs index 07dfb80..8c39497 100644 --- a/TanksServer/Services/GameTickService.cs +++ b/TanksServer/Services/GameTickService.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Hosting; namespace TanksServer.Services; -internal sealed class GameTickService(IEnumerable steps) : IHostedService +internal sealed class GameTickService(IEnumerable steps) : IHostedService, IDisposable { private readonly CancellationTokenSource _cancellation = new(); private readonly List _steps = steps.ToList(); @@ -29,6 +29,12 @@ internal sealed class GameTickService(IEnumerable steps) : IHostedSer await _cancellation.CancelAsync(); if (_run != null) await _run; } + + public void Dispose() + { + _cancellation.Dispose(); + _run?.Dispose(); + } } public interface ITickStep diff --git a/TanksServer/Services/MapDrawer.cs b/TanksServer/Services/MapDrawer.cs deleted file mode 100644 index ee41489..0000000 --- a/TanksServer/Services/MapDrawer.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace TanksServer.Services; - -internal class MapDrawer(MapService map):ITickStep -{ - private const uint GameFieldPixelCount = MapService.PixelsPerRow * MapService.PixelsPerColumn; - - private void DrawInto(DisplayPixelBuffer buf) - { - for (var tileY = 0; tileY < MapService.TilesPerColumn; tileY++) - for (var tileX = 0; tileX < MapService.TilesPerRow; tileX++) - { - if (!map.IsCurrentlyWall(tileX, tileY)) - continue; - - var absoluteTilePixelY = tileY * MapService.TileSize; - for (var pixelInTileY = 0; pixelInTileY < MapService.TileSize; pixelInTileY++) - { - var absoluteRowStartPixelIndex = (absoluteTilePixelY + pixelInTileY) * MapService.PixelsPerRow - + tileX * MapService.TileSize; - for (var pixelInTileX = 0; pixelInTileX < MapService.TileSize; pixelInTileX++) - buf.Pixels[absoluteRowStartPixelIndex + pixelInTileX] = pixelInTileX % 2 == pixelInTileY % 2; - } - } - } - - private DisplayPixelBuffer CreateGameFieldPixelBuffer() - { - var data = new byte[10 + GameFieldPixelCount / 8]; - var result = new DisplayPixelBuffer(data) - { - Magic1 = 0, - Magic2 = 19, - X = 0, - Y = 0, - WidthInTiles = MapService.TilesPerRow, - HeightInPixels = MapService.PixelsPerColumn - }; - 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; - } -} diff --git a/TanksServer/Services/MapService.cs b/TanksServer/Services/MapService.cs index be93d3d..25606e7 100644 --- a/TanksServer/Services/MapService.cs +++ b/TanksServer/Services/MapService.cs @@ -1,6 +1,8 @@ +using TanksServer.Models; + namespace TanksServer.Services; -internal class MapService +internal sealed class MapService { public const int TilesPerRow = 44; public const int TilesPerColumn = 20; @@ -35,8 +37,8 @@ internal class MapService private char this[int tileX, int tileY] => _map[tileX + tileY * TilesPerRow]; - public bool IsCurrentlyWall(int tileX, int tileY) + public bool IsCurrentlyWall(TilePosition position) { - return this[tileX, tileY] == '#'; + return this[position.X, position.Y] == '#'; } } diff --git a/TanksServer/Services/PixelDrawer.cs b/TanksServer/Services/PixelDrawer.cs new file mode 100644 index 0000000..b5fed90 --- /dev/null +++ b/TanksServer/Services/PixelDrawer.cs @@ -0,0 +1,112 @@ +using System.Diagnostics; +using System.Net.Mime; +using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using TanksServer.Helpers; +using TanksServer.Models; + +namespace TanksServer.Services; + +internal sealed class PixelDrawer : ITickStep +{ + private const uint GameFieldPixelCount = MapService.PixelsPerRow * MapService.PixelsPerColumn; + private DisplayPixelBuffer? _lastFrame; + private readonly MapService _map; + private readonly TankManager _tanks; + private readonly bool[] _tankSprite; + private readonly int _tankSpriteWidth; + + public PixelDrawer(MapService map, TankManager tanks, ILogger logger) + { + _map = map; + _tanks = tanks; + + using var tankImage = Image.Load("assets/tank.png"); + _tankSprite = new bool[tankImage.Height * tankImage.Width]; + + var whitePixel = new Rgba32(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); + var i = 0; + for (var y = 0; y < tankImage.Height; y++) + for (var x = 0; x < tankImage.Width; x++, i++) + { + _tankSprite[i] = tankImage[x, y] == whitePixel; + } + + _tankSpriteWidth = tankImage.Width; + } + + public DisplayPixelBuffer LastFrame + { + get => _lastFrame ?? throw new InvalidOperationException("first frame not yet drawn"); + private set => _lastFrame = value; + } + + public Task TickAsync() + { + var buffer = CreateGameFieldPixelBuffer(); + DrawMap(buffer); + DrawTanks(buffer); + LastFrame = buffer; + return Task.CompletedTask; + } + + private void DrawMap(DisplayPixelBuffer buf) + { + for (var tileY = 0; tileY < MapService.TilesPerColumn; tileY++) + for (var tileX = 0; tileX < MapService.TilesPerRow; tileX++) + { + var tile = new TilePosition(tileX, tileY); + if (!_map.IsCurrentlyWall(tile)) + continue; + + var absoluteTilePixelY = tileY * MapService.TileSize; + for (var pixelInTileY = 0; pixelInTileY < MapService.TileSize; pixelInTileY++) + { + var absoluteRowStartPixelIndex = (absoluteTilePixelY + pixelInTileY) * MapService.PixelsPerRow + + tileX * MapService.TileSize; + for (var pixelInTileX = 0; pixelInTileX < MapService.TileSize; pixelInTileX++) + buf.Pixels[absoluteRowStartPixelIndex + pixelInTileX] = pixelInTileX % 2 == pixelInTileY % 2; + } + } + } + + private void DrawTanks(DisplayPixelBuffer buf) + { + foreach (var tank in _tanks) + { + for (var dy = 0; dy < MapService.TileSize; dy++) + { + var rowStartIndex = (tank.Position.Y + dy) * MapService.PixelsPerRow; + + for (var dx = 0; dx < MapService.TileSize; dx++) + { + var i = rowStartIndex + tank.Position.X + dx; + buf.Pixels[i] = TankSpriteAt(dx, dy, tank.Rotation); + } + } + } + } + + private bool TankSpriteAt(int dx, int dy, int tankRotation) + { + var x = tankRotation % 4 * (MapService.TileSize + 1); + var y = tankRotation / 4 * (MapService.TileSize + 1); + return _tankSprite[(y + dy) * _tankSpriteWidth + x + dx]; + } + + private static DisplayPixelBuffer CreateGameFieldPixelBuffer() + { + var data = new byte[10 + GameFieldPixelCount / 8]; + var result = new DisplayPixelBuffer(data) + { + Magic1 = 0, + Magic2 = 19, + X = 0, + Y = 0, + WidthInTiles = MapService.TilesPerRow, + HeightInPixels = MapService.PixelsPerColumn + }; + return result; + } +} diff --git a/TanksServer/Services/ServicePointDisplay.cs b/TanksServer/Services/ServicePointDisplay.cs index 250fc55..ebb5502 100644 --- a/TanksServer/Services/ServicePointDisplay.cs +++ b/TanksServer/Services/ServicePointDisplay.cs @@ -3,13 +3,21 @@ using Microsoft.Extensions.Options; namespace TanksServer.Services; -internal sealed class ServicePointDisplay(IOptions options) +internal sealed class ServicePointDisplay( + IOptions options, + PixelDrawer drawer +) : ITickStep, IDisposable { private readonly UdpClient _udpClient = new(options.Value.Hostname, options.Value.Port); - public ValueTask Send(DisplayPixelBuffer buffer) + public Task TickAsync() { - return _udpClient.SendAsync(buffer.Data); + return _udpClient.SendAsync(drawer.LastFrame.Data).AsTask(); + } + + public void Dispose() + { + _udpClient.Dispose(); } } diff --git a/TanksServer/Services/SpawnQueue.cs b/TanksServer/Services/SpawnQueue.cs new file mode 100644 index 0000000..40ae9dd --- /dev/null +++ b/TanksServer/Services/SpawnQueue.cs @@ -0,0 +1,51 @@ +using System.Collections.Concurrent; +using TanksServer.Models; + +namespace TanksServer.Services; + +internal sealed class SpawnQueue(TankManager tanks, MapService map) : ITickStep +{ + private readonly ConcurrentQueue _playersToSpawn = new(); + + public Task TickAsync() + { + while (_playersToSpawn.TryDequeue(out var player)) + { + var tank = new Tank(player, ChooseSpawnPosition()) + { + Rotation = Random.Shared.Next(0, 16) + }; + tanks.Add(tank); + } + + return Task.CompletedTask; + } + + private PixelPosition ChooseSpawnPosition() + { + List candidates = new(); + + for (var x = 0; x < MapService.TilesPerRow; x++) + for (var y = 0; y < MapService.TilesPerColumn; y++) + { + var tile = new TilePosition(x, y); + + if (map.IsCurrentlyWall(tile)) + continue; + + // TODO: check tanks + candidates.Add(tile); + } + + var chosenTile = candidates[Random.Shared.Next(candidates.Count)]; + return new PixelPosition( + chosenTile.X * MapService.TileSize, + chosenTile.Y * MapService.TileSize + ); + } + + public void SpawnTankForPlayer(Player player) + { + _playersToSpawn.Enqueue(player); + } +} diff --git a/TanksServer/Services/TankManager.cs b/TanksServer/Services/TankManager.cs new file mode 100644 index 0000000..3022403 --- /dev/null +++ b/TanksServer/Services/TankManager.cs @@ -0,0 +1,20 @@ +using System.Collections; +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using TanksServer.Models; + +namespace TanksServer.Services; + +internal sealed class TankManager(ILogger logger) : IEnumerable +{ + private readonly ConcurrentBag _tanks = new(); + + public void Add(Tank tank) + { + logger.LogInformation("Tank added"); + _tanks.Add(tank); + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public IEnumerator GetEnumerator() => _tanks.GetEnumerator(); +} diff --git a/TanksServer/TanksServer.csproj b/TanksServer/TanksServer.csproj index 4f56b75..6a5b5f4 100644 --- a/TanksServer/TanksServer.csproj +++ b/TanksServer/TanksServer.csproj @@ -14,6 +14,10 @@ .dockerignore + + + + Recommended diff --git a/TanksServer/assets/tank.png b/TanksServer/assets/tank.png new file mode 100644 index 0000000000000000000000000000000000000000..9210ac136f39c4cdc12f4dc662d9d9bb97e9a335 GIT binary patch literal 355 zcmV-p0i6DcP)N-I}PV^kM65X)YyEfd~@khkQR{&A#J#P~hiZ*t7jv^WZq=qL?AL zLd4#Sx}Q*}0dCRzq=O@FeA5Ygy^xnKMDmfO0T;H__0u!!f(en)HH3tUNeAnJg9NSu zh5JeGiv+a-3rs4j0e=B+8(3ScLH2XhNt+hC?d7O(Peg z85;mQX5HLcgVjrDVVQ-=TBg*&Mh_QVsMi*=n#{j9#WgQ_Z;YOGD_-89q#6}c`57l` zk4m1Ur|k>kh1-|fwXES5XC&XmeBXHKVrTpaFaWRY10{hZ_TT^j002ovPDHLkV1lKg Bn{xmF literal 0 HcmV?d00001