diff --git a/TanksServer/ClientScreenServer.cs b/TanksServer/ClientScreenServer.cs index c9afccf..62513c4 100644 --- a/TanksServer/ClientScreenServer.cs +++ b/TanksServer/ClientScreenServer.cs @@ -74,21 +74,20 @@ internal sealed class ClientScreenServer( public async Task SendAsync(DisplayPixelBuffer buf) { - if (await _wantedFrames.WaitAsync(TimeSpan.Zero)) - { - _logger.LogTrace("sending"); - try - { - await _channel.Writer.WriteAsync(buf.Data); - } - catch (ChannelClosedException) - { - _logger.LogWarning("send failed, channel is closed"); - } - } - else + if (!await _wantedFrames.WaitAsync(TimeSpan.Zero)) { _logger.LogTrace("client does not want a frame yet"); + return; + } + + _logger.LogTrace("sending"); + try + { + await _channel.Writer.WriteAsync(buf.Data); + } + catch (ChannelClosedException) + { + _logger.LogWarning("send failed, channel is closed"); } } diff --git a/TanksServer/DrawSteps/BulletDrawer.cs b/TanksServer/DrawSteps/BulletDrawer.cs new file mode 100644 index 0000000..9773e80 --- /dev/null +++ b/TanksServer/DrawSteps/BulletDrawer.cs @@ -0,0 +1,13 @@ +using TanksServer.Helpers; +using TanksServer.Services; + +namespace TanksServer.DrawSteps; + +internal sealed class BulletDrawer(BulletManager bullets): IDrawStep +{ + public void Draw(DisplayPixelBuffer buffer) + { + foreach (var bullet in bullets.GetAll()) + buffer.Pixels[bullet.Position.ToPixelPosition().GetPixelIndex()] = true; + } +} diff --git a/TanksServer/DrawSteps/DrawHelpers.cs b/TanksServer/DrawSteps/DrawHelpers.cs new file mode 100644 index 0000000..cd84e00 --- /dev/null +++ b/TanksServer/DrawSteps/DrawHelpers.cs @@ -0,0 +1,23 @@ +using System.Diagnostics; +using TanksServer.Models; +using TanksServer.Services; + +namespace TanksServer.DrawSteps; + +internal static class DrawHelpers +{ + public static int GetPixelIndex(this PixelPosition position) + { + return position.Y * MapService.PixelsPerRow + position.X; + } + + public static PixelPosition GetPixel(this TilePosition position, byte subX, byte subY) + { + Debug.Assert(subX < 8); + Debug.Assert(subY < 8); + return new PixelPosition( + X: position.X * MapService.TileSize + subX, + Y: position.Y * MapService.TileSize + subY + ); + } +} diff --git a/TanksServer/DrawSteps/IDrawStep.cs b/TanksServer/DrawSteps/IDrawStep.cs new file mode 100644 index 0000000..799b4b9 --- /dev/null +++ b/TanksServer/DrawSteps/IDrawStep.cs @@ -0,0 +1,8 @@ +using TanksServer.Helpers; + +namespace TanksServer.DrawSteps; + +internal interface IDrawStep +{ + void Draw(DisplayPixelBuffer buffer); +} diff --git a/TanksServer/DrawSteps/MapDrawer.cs b/TanksServer/DrawSteps/MapDrawer.cs new file mode 100644 index 0000000..c46e731 --- /dev/null +++ b/TanksServer/DrawSteps/MapDrawer.cs @@ -0,0 +1,26 @@ +using TanksServer.Helpers; +using TanksServer.Models; +using TanksServer.Services; + +namespace TanksServer.DrawSteps; + +internal sealed class MapDrawer(MapService map) : IDrawStep +{ + public void Draw(DisplayPixelBuffer buffer) + { + 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; + + for (byte pixelInTileY = 0; pixelInTileY < MapService.TileSize; pixelInTileY++) + for (byte pixelInTileX = 0; pixelInTileX < MapService.TileSize; pixelInTileX++) + { + var index = tile.GetPixel(pixelInTileX, pixelInTileY).GetPixelIndex(); + buffer.Pixels[index] = pixelInTileX % 2 == pixelInTileY % 2; + } + } + } +} diff --git a/TanksServer/DrawSteps/TankDrawer.cs b/TanksServer/DrawSteps/TankDrawer.cs new file mode 100644 index 0000000..b4ee9b2 --- /dev/null +++ b/TanksServer/DrawSteps/TankDrawer.cs @@ -0,0 +1,62 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using TanksServer.Helpers; +using TanksServer.Services; + +namespace TanksServer.DrawSteps; + +internal sealed class TankDrawer : IDrawStep +{ + private readonly TankManager _tanks; + private readonly bool[] _tankSprite; + private readonly int _tankSpriteWidth; + + public TankDrawer(TankManager tanks) + { + _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 void Draw(DisplayPixelBuffer buffer) + { + foreach (var tank in _tanks) + { + var pos = tank.Position.ToPixelPosition(); + var rotationVariant = (int)Math.Floor(tank.Rotation); + for (var dy = 0; dy < MapService.TileSize; dy++) + { + var rowStartIndex = (pos.Y + dy) * MapService.PixelsPerRow; + + for (var dx = 0; dx < MapService.TileSize; dx++) + { + if (!TankSpriteAt(dx, dy, rotationVariant)) + continue; + + var i = rowStartIndex + pos.X + dx; + buffer.Pixels[i] = true; + } + } + } + } + + private bool TankSpriteAt(int dx, int dy, int tankRotation) + { + var x = tankRotation % 4 * (MapService.TileSize + 1); + var y = (int)Math.Floor(tankRotation / 4d) * (MapService.TileSize + 1); + var index = (y + dy) * _tankSpriteWidth + x + dx; + + return _tankSprite[index]; + } +} diff --git a/TanksServer/Models/Bullet.cs b/TanksServer/Models/Bullet.cs new file mode 100644 index 0000000..ebb7ce8 --- /dev/null +++ b/TanksServer/Models/Bullet.cs @@ -0,0 +1,10 @@ +namespace TanksServer.Models; + +internal sealed class Bullet(Player tankOwner, FloatPosition position, double rotation) +{ + public Player TankOwner { get; } = tankOwner; + + public FloatPosition Position { get; set; } = position; + + public double Rotation { get; set; } = rotation; +} diff --git a/TanksServer/Models/Tank.cs b/TanksServer/Models/Tank.cs index 8fdb1dc..f3e59b0 100644 --- a/TanksServer/Models/Tank.cs +++ b/TanksServer/Models/Tank.cs @@ -16,4 +16,6 @@ internal sealed class Tank(Player player, FloatPosition spawnPosition) } public FloatPosition Position { get; set; } = spawnPosition; + + public DateTime NextShotAfter { get; set; } } diff --git a/TanksServer/Program.cs b/TanksServer/Program.cs index 67b730a..3495f00 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.DrawSteps; using TanksServer.Helpers; using TanksServer.Services; @@ -76,10 +77,13 @@ internal static class Program 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()); @@ -95,6 +99,10 @@ internal static class Program builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + return builder.Build(); } } diff --git a/TanksServer/Services/BulletManager.cs b/TanksServer/Services/BulletManager.cs new file mode 100644 index 0000000..b840144 --- /dev/null +++ b/TanksServer/Services/BulletManager.cs @@ -0,0 +1,32 @@ +using System.Collections; +using TanksServer.Models; + +namespace TanksServer.Services; + +internal sealed class BulletManager : ITickStep +{ + private readonly HashSet _bullets = new(); + + public void Spawn(Bullet bullet) => _bullets.Add(bullet); + + public Task TickAsync() + { + foreach (var bullet in _bullets) + { + MoveBullet(bullet); + } + + return Task.CompletedTask; + } + + private static void MoveBullet(Bullet bullet) + { + var angle = bullet.Rotation / 16 * 2 * Math.PI; + bullet.Position = new FloatPosition( + X: bullet.Position.X + Math.Sin(angle) * 3, + Y: bullet.Position.Y - Math.Cos(angle) * 3 + ); + } + + public IEnumerable GetAll() => _bullets; +} diff --git a/TanksServer/Services/PixelDrawer.cs b/TanksServer/Services/PixelDrawer.cs index bb6a6d1..865d695 100644 --- a/TanksServer/Services/PixelDrawer.cs +++ b/TanksServer/Services/PixelDrawer.cs @@ -3,38 +3,17 @@ using System.Net.Mime; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using TanksServer.DrawSteps; using TanksServer.Helpers; using TanksServer.Models; namespace TanksServer.Services; -internal sealed class PixelDrawer : ITickStep +internal sealed class PixelDrawer(IEnumerable drawSteps) : 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; - } + private readonly List _drawSteps = drawSteps.ToList(); public DisplayPixelBuffer LastFrame { @@ -45,66 +24,12 @@ internal sealed class PixelDrawer : ITickStep public Task TickAsync() { var buffer = CreateGameFieldPixelBuffer(); - DrawMap(buffer); - DrawTanks(buffer); + foreach (var step in _drawSteps) + step.Draw(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) - { - var pos = tank.Position.ToPixelPosition(); - var rotationVariant = (int)Math.Floor(tank.Rotation); - for (var dy = 0; dy < MapService.TileSize; dy++) - { - var rowStartIndex = (pos.Y + dy) * MapService.PixelsPerRow; - - for (var dx = 0; dx < MapService.TileSize; dx++) - { - if (!TankSpriteAt(dx, dy, rotationVariant)) - continue; - - var i = rowStartIndex + pos.X + dx; - buf.Pixels[i] = true; - } - } - } - } - - private bool TankSpriteAt(int dx, int dy, int tankRotation) - { - var x = tankRotation % 4 * (MapService.TileSize + 1); - var y = (int)Math.Floor(tankRotation / 4d) * (MapService.TileSize + 1); - var index = (y + dy) * _tankSpriteWidth + x + dx; - - if (index < 0 || index > _tankSprite.Length) - Debugger.Break(); - - return _tankSprite[index]; - } - + private static DisplayPixelBuffer CreateGameFieldPixelBuffer() { var data = new byte[10 + GameFieldPixelCount / 8]; diff --git a/TanksServer/Services/TankManager.cs b/TanksServer/Services/TankManager.cs index 96e6429..78e0379 100644 --- a/TanksServer/Services/TankManager.cs +++ b/TanksServer/Services/TankManager.cs @@ -6,8 +6,12 @@ using TanksServer.Models; namespace TanksServer.Services; -internal sealed class TankManager(ILogger logger, IOptions options, MapService map) - : ITickStep, IEnumerable +internal sealed class TankManager( + ILogger logger, + IOptions options, + MapService map, + BulletManager bullets +) : ITickStep, IEnumerable { private readonly ConcurrentBag _tanks = new(); private readonly TanksConfiguration _config = options.Value; @@ -22,7 +26,9 @@ internal sealed class TankManager(ILogger logger, IOptions logger, IOptions logger, IOptions= DateTime.Now) + return; + + tank.NextShotAfter = DateTime.Now.AddMilliseconds(_config.ShootDelayMs); + + var angle = tank.Rotation / 16 * 2 * Math.PI; + var position = new FloatPosition( + X: tank.Position.X + MapService.TileSize / 2d + Math.Sin(angle) * _config.BulletSpeed, + Y: tank.Position.Y + MapService.TileSize / 2d - Math.Cos(angle) * _config.BulletSpeed + ); + + bullets.Spawn(new Bullet(tank.Owner, position, tank.Rotation)); + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public IEnumerator GetEnumerator() => _tanks.GetEnumerator(); }