tank movement (without collision)
This commit is contained in:
		
							parent
							
								
									a3bd582b2e
								
							
						
					
					
						commit
						54b840da3e
					
				
					 18 changed files with 321 additions and 254 deletions
				
			
		|  | @ -1,3 +1,5 @@ | |||
| using System.Collections.Concurrent; | ||||
| using System.Diagnostics; | ||||
| using System.Net.WebSockets; | ||||
| using Microsoft.Extensions.Hosting; | ||||
| using Microsoft.Extensions.Logging; | ||||
|  | @ -12,27 +14,36 @@ internal sealed class ClientScreenServer( | |||
|     PixelDrawer drawer | ||||
| ) : IHostedLifecycleService, ITickStep | ||||
| { | ||||
|     private readonly List<ClientScreenServerConnection> _connections = new(); | ||||
|     private readonly ConcurrentDictionary<ClientScreenServerConnection, byte> _connections = new(); | ||||
|     private bool _closing; | ||||
| 
 | ||||
|     public Task HandleClient(WebSocket socket) | ||||
|     { | ||||
|         if (_closing) | ||||
|         { | ||||
|             logger.LogWarning("ignoring request because connections are closing"); | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
| 
 | ||||
|         logger.LogDebug("HandleClient"); | ||||
|         var connection = | ||||
|             new ClientScreenServerConnection(socket, loggerFactory.CreateLogger<ClientScreenServerConnection>(), this); | ||||
|         _connections.Add(connection); | ||||
|         var added = _connections.TryAdd(connection, 0); | ||||
|         Debug.Assert(added); | ||||
|         return connection.Done; | ||||
|     } | ||||
| 
 | ||||
|     public Task StoppingAsync(CancellationToken cancellationToken) | ||||
|     { | ||||
|         logger.LogInformation("closing connections"); | ||||
|         return Task.WhenAll(_connections.Select(c => c.CloseAsync())); | ||||
|         _closing = true; | ||||
|         return Task.WhenAll(_connections.Keys.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))); | ||||
|         return Task.WhenAll(_connections.Keys.Select(c => c.SendAsync(drawer.LastFrame))); | ||||
|     } | ||||
| 
 | ||||
|     public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; | ||||
|  | @ -40,35 +51,62 @@ internal sealed class ClientScreenServer( | |||
|     public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; | ||||
|     public Task StartingAsync(CancellationToken cancellationToken) => Task.CompletedTask; | ||||
|     public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; | ||||
|      | ||||
|     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) | ||||
| 
 | ||||
|     private void Remove(ClientScreenServerConnection connection) => _connections.TryRemove(connection, out _); | ||||
| 
 | ||||
|     private sealed class ClientScreenServerConnection: IDisposable | ||||
|     { | ||||
|         private bool _wantsNewFrame = true; | ||||
|         private readonly ByteChannelWebSocket _channel; | ||||
|         private readonly SemaphoreSlim _wantedFrames = new(1); | ||||
|         private readonly ClientScreenServer _server; | ||||
|         private readonly ILogger<ClientScreenServerConnection> _logger; | ||||
| 
 | ||||
|         public Task SendAsync(DisplayPixelBuffer buf) | ||||
|         public ClientScreenServerConnection(WebSocket webSocket, | ||||
|             ILogger<ClientScreenServerConnection> logger, | ||||
|             ClientScreenServer server) | ||||
|         { | ||||
|             if (!_wantsNewFrame) | ||||
|                 return Task.CompletedTask; | ||||
|             _wantsNewFrame = false; | ||||
|             return TrySendAsync(buf.Data); | ||||
|             _server = server; | ||||
|             _logger = logger; | ||||
|             _channel = new(webSocket, logger, 0); | ||||
|             Done = ReceiveAsync(); | ||||
|         } | ||||
| 
 | ||||
|         protected override Task ReceiveAsync(ArraySegment<byte> buffer) | ||||
|         public async Task SendAsync(DisplayPixelBuffer buf) | ||||
|         { | ||||
|             _wantsNewFrame = true; | ||||
|             return Task.CompletedTask; | ||||
|             if (await _wantedFrames.WaitAsync(TimeSpan.Zero)) | ||||
|             { | ||||
|                 _logger.LogTrace("sending"); | ||||
|                 await _channel.Writer.WriteAsync(buf.Data); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 _logger.LogTrace("client does not want a frame yet"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         protected override Task ClosingAsync() | ||||
|         private async Task ReceiveAsync() | ||||
|         { | ||||
|             server.Remove(this); | ||||
|             return Task.CompletedTask; | ||||
|             await foreach (var _ in _channel.Reader.ReadAllAsync()) | ||||
|             { | ||||
|                 _wantedFrames.Release(); | ||||
|             } | ||||
| 
 | ||||
|             _logger.LogTrace("done receiving"); | ||||
|             _server.Remove(this); | ||||
|         } | ||||
| 
 | ||||
|         public Task CloseAsync() | ||||
|         { | ||||
|             _logger.LogDebug("closing connection"); | ||||
|             return _channel.CloseAsync(); | ||||
|         } | ||||
| 
 | ||||
|         public Task Done { get; } | ||||
| 
 | ||||
|         public void Dispose() | ||||
|         { | ||||
|             _wantedFrames.Dispose(); | ||||
|             Done.Dispose(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -36,11 +36,25 @@ internal sealed class ControlsServer(ILogger<ControlsServer> logger, ILoggerFact | |||
|         _connections.Remove(connection); | ||||
|     } | ||||
| 
 | ||||
|     private sealed class ControlsServerConnection( | ||||
|             WebSocket socket, ILogger<ControlsServerConnection> logger, | ||||
|             ControlsServer server, Player player) | ||||
|         : EasyWebSocket(socket, logger, new byte[2]) | ||||
|     private sealed class ControlsServerConnection | ||||
|     { | ||||
|         private readonly ByteChannelWebSocket _binaryWebSocket; | ||||
|         private readonly ILogger<ControlsServerConnection> _logger; | ||||
|         private readonly ControlsServer _server; | ||||
|         private readonly Player _player; | ||||
| 
 | ||||
|         public ControlsServerConnection(WebSocket socket, ILogger<ControlsServerConnection> logger, | ||||
|             ControlsServer server, Player player) | ||||
|         { | ||||
|             _logger = logger; | ||||
|             _server = server; | ||||
|             _player = player; | ||||
|             _binaryWebSocket = new(socket, logger, 2); | ||||
|             Done = ReceiveAsync(); | ||||
|         } | ||||
| 
 | ||||
|         public Task Done { get; } | ||||
| 
 | ||||
|         private enum MessageType : byte | ||||
|         { | ||||
|             Enable = 0x01, | ||||
|  | @ -56,48 +70,47 @@ internal sealed class ControlsServer(ILogger<ControlsServer> logger, ILoggerFact | |||
|             Shoot = 0x05 | ||||
|         } | ||||
| 
 | ||||
|         protected override Task ReceiveAsync(ArraySegment<byte> buffer) | ||||
|         private async Task ReceiveAsync() | ||||
|         { | ||||
|             var type = (MessageType)buffer[0]; | ||||
|             var control = (InputType)buffer[1]; | ||||
| 
 | ||||
|             Logger.LogTrace("player input {} {} {}", player.Id, type, control); | ||||
| 
 | ||||
|             var isEnable = type switch | ||||
|             await foreach (var buffer in _binaryWebSocket.Reader.ReadAllAsync()) | ||||
|             { | ||||
|                 MessageType.Enable => true, | ||||
|                 MessageType.Disable => false, | ||||
|                 _ => throw new ArgumentException("invalid message type") | ||||
|             }; | ||||
|                 var type = (MessageType)buffer[0]; | ||||
|                 var control = (InputType)buffer[1]; | ||||
| 
 | ||||
|             switch (control) | ||||
|             { | ||||
|                 case InputType.Forward: | ||||
|                     player.Controls.Forward = isEnable; | ||||
|                     break; | ||||
|                 case InputType.Backward: | ||||
|                     player.Controls.Backward = isEnable; | ||||
|                     break; | ||||
|                 case InputType.Left: | ||||
|                     player.Controls.TurnLeft = isEnable; | ||||
|                     break; | ||||
|                 case InputType.Right: | ||||
|                     player.Controls.TurnRight = isEnable; | ||||
|                     break; | ||||
|                 case InputType.Shoot: | ||||
|                     player.Controls.Shoot = isEnable; | ||||
|                     break; | ||||
|                 default: | ||||
|                     throw new ArgumentException("invalid control type"); | ||||
|                 _logger.LogTrace("player input {} {} {}", _player.Id, type, control); | ||||
| 
 | ||||
|                 var isEnable = type switch | ||||
|                 { | ||||
|                     MessageType.Enable => true, | ||||
|                     MessageType.Disable => false, | ||||
|                     _ => throw new ArgumentException("invalid message type") | ||||
|                 }; | ||||
| 
 | ||||
|                 switch (control) | ||||
|                 { | ||||
|                     case InputType.Forward: | ||||
|                         _player.Controls.Forward = isEnable; | ||||
|                         break; | ||||
|                     case InputType.Backward: | ||||
|                         _player.Controls.Backward = isEnable; | ||||
|                         break; | ||||
|                     case InputType.Left: | ||||
|                         _player.Controls.TurnLeft = isEnable; | ||||
|                         break; | ||||
|                     case InputType.Right: | ||||
|                         _player.Controls.TurnRight = isEnable; | ||||
|                         break; | ||||
|                     case InputType.Shoot: | ||||
|                         _player.Controls.Shoot = isEnable; | ||||
|                         break; | ||||
|                     default: | ||||
|                         throw new ArgumentException("invalid control type"); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return Task.CompletedTask; | ||||
|             _server.Remove(this); | ||||
|         } | ||||
| 
 | ||||
|         protected override Task ClosingAsync() | ||||
|         { | ||||
|             server.Remove(this); | ||||
|             return Task.CompletedTask; | ||||
|         } | ||||
|         public Task CloseAsync() => _binaryWebSocket.CloseAsync(); | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										90
									
								
								TanksServer/Helpers/ByteChannelWebSocket.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								TanksServer/Helpers/ByteChannelWebSocket.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | |||
| using System.Diagnostics; | ||||
| using System.Net.WebSockets; | ||||
| using System.Threading.Channels; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace TanksServer.Helpers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Hacky class for easier semantics | ||||
| /// </summary> | ||||
| internal sealed class ByteChannelWebSocket : Channel<byte[]> | ||||
| { | ||||
|     private readonly ILogger _logger; | ||||
|     private readonly WebSocket _socket; | ||||
|     private readonly Task _backgroundDone; | ||||
|     private readonly byte[] _buffer; | ||||
| 
 | ||||
|     private readonly Channel<byte[]> _outgoing = Channel.CreateUnbounded<byte[]>(); | ||||
|     private readonly Channel<byte[]> _incoming = Channel.CreateUnbounded<byte[]>(); | ||||
| 
 | ||||
|     public ByteChannelWebSocket(WebSocket socket, ILogger logger, int messageSize) | ||||
|     { | ||||
|         _socket = socket; | ||||
|         _logger = logger; | ||||
|         _buffer = new byte[messageSize]; | ||||
|         _backgroundDone = Task.WhenAll(ReadLoopAsync(), WriteLoopAsync()); | ||||
| 
 | ||||
|         Reader = _incoming.Reader; | ||||
|         Writer = _outgoing.Writer; | ||||
|     } | ||||
| 
 | ||||
|     private async Task ReadLoopAsync() | ||||
|     { | ||||
|         while (true) | ||||
|         { | ||||
|             if (_socket.State is not (WebSocketState.Open or WebSocketState.CloseSent)) | ||||
|                 break; | ||||
| 
 | ||||
|             var response = await _socket.ReceiveAsync(_buffer, CancellationToken.None); | ||||
|             if (response.MessageType == WebSocketMessageType.Close) | ||||
|             { | ||||
|                 if (_socket.State == WebSocketState.CloseReceived) | ||||
|                     await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, | ||||
|                         CancellationToken.None); | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             if (response.Count != _buffer.Length) | ||||
|             { | ||||
|                 await _socket.CloseAsync( | ||||
|                     WebSocketCloseStatus.InvalidPayloadData, | ||||
|                     "response has unexpected size", | ||||
|                     CancellationToken.None); | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             await _incoming.Writer.WriteAsync(_buffer.ToArray()); | ||||
|         } | ||||
| 
 | ||||
|         if (_socket.State != WebSocketState.Closed) | ||||
|             Debugger.Break(); | ||||
| 
 | ||||
|         _incoming.Writer.Complete(); | ||||
|     } | ||||
| 
 | ||||
|     private async Task WriteLoopAsync() | ||||
|     { | ||||
|         await foreach (var data in _outgoing.Reader.ReadAllAsync()) | ||||
|         { | ||||
|             _logger.LogTrace("sending {} bytes of data", data.Length); | ||||
|             try | ||||
|             { | ||||
|                 await _socket.SendAsync(data, WebSocketMessageType.Binary, true, CancellationToken.None); | ||||
|             } | ||||
|             catch (WebSocketException wsEx) | ||||
|             { | ||||
|                 _logger.LogDebug(wsEx, "send failed"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); | ||||
|     } | ||||
| 
 | ||||
|     public async Task CloseAsync() | ||||
|     { | ||||
|         _logger.LogDebug("closing socket"); | ||||
|         _outgoing.Writer.Complete(); | ||||
|         await _backgroundDone; | ||||
|     } | ||||
| } | ||||
|  | @ -1,74 +0,0 @@ | |||
| using System.Net.WebSockets; | ||||
| using Microsoft.Extensions.Logging; | ||||
| 
 | ||||
| namespace TanksServer.Helpers; | ||||
| 
 | ||||
| /// <summary> | ||||
| /// Hacky class for easier semantics | ||||
| /// </summary> | ||||
| internal abstract class EasyWebSocket | ||||
| { | ||||
|     private readonly TaskCompletionSource _completionSource = new(); | ||||
| 
 | ||||
|     protected readonly ILogger Logger; | ||||
|     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) | ||||
|     { | ||||
|         _socket = socket; | ||||
|         Logger = logger; | ||||
|         _buffer = buffer; | ||||
|         _readLoop = ReadLoopAsync(); | ||||
|     } | ||||
| 
 | ||||
|     public Task Done => _completionSource.Task; | ||||
| 
 | ||||
|     private async Task ReadLoopAsync() | ||||
|     { | ||||
|         do | ||||
|         { | ||||
|             var response = await _socket.ReceiveAsync(_buffer, CancellationToken.None); | ||||
|             if (response.CloseStatus.HasValue) | ||||
|                 break; | ||||
| 
 | ||||
|             await ReceiveAsync(_buffer[..response.Count]); | ||||
|         } while (_socket.State == WebSocketState.Open); | ||||
|     } | ||||
| 
 | ||||
|     protected abstract Task ReceiveAsync(ArraySegment<byte> buffer); | ||||
|     protected abstract Task ClosingAsync(); | ||||
| 
 | ||||
|     protected async Task TrySendAsync(byte[] data) | ||||
|     { | ||||
|         if (_socket.State != WebSocketState.Open) | ||||
|             await CloseAsync(); | ||||
| 
 | ||||
|         Logger.LogTrace("sending {} bytes of data", _buffer.Count); | ||||
| 
 | ||||
|         try | ||||
|         { | ||||
|             await _socket.SendAsync(data, WebSocketMessageType.Binary, true, CancellationToken.None); | ||||
|         } | ||||
|         catch (WebSocketException wsEx) | ||||
|         { | ||||
|             Logger.LogDebug(wsEx, "send failed"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public async Task CloseAsync( | ||||
|         WebSocketCloseStatus status = WebSocketCloseStatus.NormalClosure, | ||||
|         string? description = null | ||||
|     ) | ||||
|     { | ||||
|         if (Interlocked.Exchange(ref _closed, 1) == 1) | ||||
|             return; | ||||
|         Logger.LogDebug("closing socket"); | ||||
|         await _socket.CloseAsync(status, description, CancellationToken.None); | ||||
|         await _readLoop; | ||||
|         await ClosingAsync(); | ||||
|         _completionSource.SetResult(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										3
									
								
								TanksServer/Models/FloatPosition.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								TanksServer/Models/FloatPosition.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| namespace TanksServer.Models; | ||||
| 
 | ||||
| internal record struct FloatPosition(double X, double Y); | ||||
|  | @ -1,8 +1,16 @@ | |||
| namespace TanksServer.Models; | ||||
| 
 | ||||
| internal sealed class Tank(Player player, PixelPosition spawnPosition) | ||||
| internal sealed class Tank(Player player, FloatPosition spawnPosition) | ||||
| { | ||||
|     private double _rotation; | ||||
|      | ||||
|     public Player Owner { get; } = player; | ||||
|     public int Rotation { get; set; } | ||||
|     public PixelPosition Position { get; set; } = spawnPosition; | ||||
| 
 | ||||
|     public double Rotation | ||||
|     { | ||||
|         get => _rotation; | ||||
|         set => _rotation = value % 16d; | ||||
|     } | ||||
| 
 | ||||
|     public FloatPosition Position { get; set; } = spawnPosition; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										9
									
								
								TanksServer/Models/TanksConfiguration.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								TanksServer/Models/TanksConfiguration.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| namespace TanksServer.Models; | ||||
| 
 | ||||
| public class TanksConfiguration | ||||
| { | ||||
|     public double MoveSpeed { get; set; } = 1.4; | ||||
|     public double TurnSpeed { get; set; } = 0.4; | ||||
|     public double ShootDelayMs { get; set; } = 0.4 * 1000; | ||||
|     public double BulletSpeed { get; set; } = 8; | ||||
| } | ||||
|  | @ -74,23 +74,25 @@ internal static class Program | |||
| 
 | ||||
|         builder.Services.AddSingleton<ServicePointDisplay>(); | ||||
|         builder.Services.AddSingleton<MapService>(); | ||||
|         builder.Services.AddSingleton<TankManager>(); | ||||
| 
 | ||||
|         builder.Services.AddHostedService<GameTickService>(); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<SpawnQueue>(); | ||||
|         builder.Services.AddSingleton<ITickStep>(sp => sp.GetRequiredService<SpawnQueue>()); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<PixelDrawer>(); | ||||
|         builder.Services.AddSingleton<ITickStep, PixelDrawer>(sp => sp.GetRequiredService<PixelDrawer>()); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<ClientScreenServer>(); | ||||
|         builder.Services.AddHostedService(sp => sp.GetRequiredService<ClientScreenServer>()); | ||||
|         builder.Services.AddSingleton<ITickStep, ClientScreenServer>(sp => sp.GetRequiredService<ClientScreenServer>()); | ||||
|          | ||||
|         builder.Services.AddSingleton<TankManager>(); | ||||
|         builder.Services.AddSingleton<ITickStep>(sp => sp.GetRequiredService<TankManager>()); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<ControlsServer>(); | ||||
|         builder.Services.AddHostedService(sp => sp.GetRequiredService<ControlsServer>()); | ||||
|          | ||||
|         builder.Services.AddSingleton<SpawnQueue>(); | ||||
|         builder.Services.AddSingleton<ITickStep>(sp => sp.GetRequiredService<SpawnQueue>()); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<PixelDrawer>(); | ||||
|         builder.Services.AddSingleton<ITickStep>(sp => sp.GetRequiredService<PixelDrawer>()); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<ClientScreenServer>(); | ||||
|         builder.Services.AddHostedService(sp => sp.GetRequiredService<ClientScreenServer>()); | ||||
|         builder.Services.AddSingleton<ITickStep>(sp => sp.GetRequiredService<ClientScreenServer>()); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<PlayerServer>(); | ||||
| 
 | ||||
|         return builder.Build(); | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ internal sealed class GameTickService(IEnumerable<ITickStep> steps) : IHostedSer | |||
|         { | ||||
|             foreach (var step in _steps) | ||||
|                 await step.TickAsync(); | ||||
|             await Task.Delay(1000); | ||||
|             await Task.Delay(1000/25); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -75,14 +75,17 @@ internal sealed class PixelDrawer : ITickStep | |||
|     { | ||||
|         foreach (var tank in _tanks) | ||||
|         { | ||||
|             var pos = new PixelPosition((int)tank.Position.X, (int)tank.Position.Y); | ||||
|             var rotationVariant = (int)Math.Floor(tank.Rotation); | ||||
|             for (var dy = 0; dy < MapService.TileSize; dy++) | ||||
|             { | ||||
|                 var rowStartIndex = (tank.Position.Y + dy) * MapService.PixelsPerRow; | ||||
|                 var rowStartIndex = (pos.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); | ||||
|                     var i = rowStartIndex + pos.X + dx; | ||||
|                     if (TankSpriteAt(dx, dy, rotationVariant)) | ||||
|                         buf.Pixels[i] = true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | @ -91,8 +94,13 @@ internal sealed class PixelDrawer : ITickStep | |||
|     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]; | ||||
|         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() | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ internal sealed class SpawnQueue(TankManager tanks, MapService map) : ITickStep | |||
|         return Task.CompletedTask; | ||||
|     } | ||||
| 
 | ||||
|     private PixelPosition ChooseSpawnPosition() | ||||
|     private FloatPosition ChooseSpawnPosition() | ||||
|     { | ||||
|         List<TilePosition> candidates = new(); | ||||
|          | ||||
|  | @ -33,12 +33,12 @@ internal sealed class SpawnQueue(TankManager tanks, MapService map) : ITickStep | |||
|             if (map.IsCurrentlyWall(tile)) | ||||
|                 continue; | ||||
|              | ||||
|             // TODO: check tanks | ||||
|             // TODO: check tanks and bullets | ||||
|             candidates.Add(tile); | ||||
|         } | ||||
| 
 | ||||
|         var chosenTile = candidates[Random.Shared.Next(candidates.Count)]; | ||||
|         return new PixelPosition( | ||||
|         return new FloatPosition( | ||||
|             chosenTile.X * MapService.TileSize, | ||||
|             chosenTile.Y * MapService.TileSize | ||||
|         ); | ||||
|  |  | |||
|  | @ -1,20 +1,70 @@ | |||
| using System.Collections; | ||||
| using System.Collections.Concurrent; | ||||
| using Microsoft.Extensions.Logging; | ||||
| using Microsoft.Extensions.Options; | ||||
| using TanksServer.Models; | ||||
| 
 | ||||
| namespace TanksServer.Services; | ||||
| 
 | ||||
| internal sealed class TankManager(ILogger<TankManager> logger) : IEnumerable<Tank> | ||||
| internal sealed class TankManager(ILogger<TankManager> logger, IOptions<TanksConfiguration> options) | ||||
|     : ITickStep, IEnumerable<Tank> | ||||
| { | ||||
|     private readonly ConcurrentBag<Tank> _tanks = new(); | ||||
|     private readonly TanksConfiguration _config = options.Value; | ||||
| 
 | ||||
|     public void Add(Tank tank) | ||||
|     { | ||||
|         logger.LogInformation("Tank added"); | ||||
|         logger.LogInformation("Tank added for player {}", tank.Owner.Id); | ||||
|         _tanks.Add(tank); | ||||
|     } | ||||
| 
 | ||||
|     public Task TickAsync() | ||||
|     { | ||||
|         foreach (var tank in _tanks) | ||||
|         { | ||||
|             TryMoveTank(tank); | ||||
|         } | ||||
| 
 | ||||
|         return Task.CompletedTask; | ||||
|     } | ||||
| 
 | ||||
|     private bool TryMoveTank(Tank tank) | ||||
|     { | ||||
|         logger.LogTrace("moving tank for player {}", tank.Owner.Id); | ||||
|         var player = tank.Owner; | ||||
| 
 | ||||
|         // move turret | ||||
|         if (player.Controls.TurnLeft) Rotate(tank, -_config.TurnSpeed); | ||||
|         if (player.Controls.TurnRight) Rotate(tank, +_config.TurnSpeed); | ||||
| 
 | ||||
|         if (player.Controls is { Forward: false, Backward: false }) | ||||
|             return false; | ||||
| 
 | ||||
|         var direction = player.Controls.Forward ? 1 : -1; | ||||
|         var angle = tank.Rotation / 16d * 2d * Math.PI; | ||||
|         var newX = tank.Position.X + Math.Sin(angle) * direction * _config.MoveSpeed; | ||||
|         var newY = tank.Position.Y - Math.Cos(angle) * direction * _config.MoveSpeed; | ||||
| 
 | ||||
|         return TryMove(tank, newX, newY) | ||||
|                || TryMove(tank, newX, tank.Position.Y) | ||||
|                || TryMove(tank, tank.Position.X, newY); | ||||
|     } | ||||
| 
 | ||||
|     private static bool TryMove(Tank tank, double newX, double newY) | ||||
|     { | ||||
|         // TODO implement | ||||
| 
 | ||||
|         tank.Position = new FloatPosition(newX, newY); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     private void Rotate(Tank t, double speed) | ||||
|     { | ||||
|         var newRotation = (t.Rotation + speed + 16) % 16; | ||||
|         logger.LogTrace("rotating tank for {} from {} to {}", t.Owner.Id, t.Rotation, newRotation); | ||||
|         t.Rotation = newRotation; | ||||
|     } | ||||
| 
 | ||||
|     IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | ||||
|     public IEnumerator<Tank> GetEnumerator() => _tanks.GetEnumerator(); | ||||
| } | ||||
|  |  | |||
|  | @ -1,8 +0,0 @@ | |||
| { | ||||
|     "Logging": { | ||||
|         "LogLevel": { | ||||
|             "Default": "Debug", | ||||
|             "Microsoft.AspNetCore": "Information" | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -2,7 +2,8 @@ | |||
|     "Logging": { | ||||
|         "LogLevel": { | ||||
|             "Default": "Information", | ||||
|             "Microsoft.AspNetCore": "Warning" | ||||
|             "Microsoft.AspNetCore": "Warning", | ||||
|             "TanksServer": "Trace" | ||||
|         } | ||||
|     }, | ||||
|     "AllowedHosts": "*", | ||||
|  |  | |||
|  | @ -20,8 +20,6 @@ function drawPixelsToCanvas(pixels: Uint8Array, canvas: HTMLCanvasElement) { | |||
|     const imageData = drawContext.getImageData(0, 0, canvas.width, canvas.height, {colorSpace: 'srgb'}); | ||||
|     const data = imageData.data; | ||||
| 
 | ||||
|     console.log('draw', {width: canvas.width, height: canvas.height, dataLength: data.byteLength}); | ||||
| 
 | ||||
|     for (let y = 0; y < canvas.height; y++) { | ||||
|         const rowStartPixelIndex = y * pixelsPerRow; | ||||
|         for (let x = 0; x < canvas.width; x++) { | ||||
|  |  | |||
|  | @ -42,7 +42,6 @@ export default function Controls({playerId}: { | |||
|             return; | ||||
| 
 | ||||
|         const message = new Uint8Array([typeCode, value]); | ||||
|         console.log('input', message); | ||||
|         sendMessage(message); | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										75
									
								
								tank-frontend/src/controls.js
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										75
									
								
								tank-frontend/src/controls.js
									
										
									
									
										vendored
									
									
								
							|  | @ -1,75 +0,0 @@ | |||
| import './controls.css'; | ||||
| 
 | ||||
| const body = document.querySelector('body'); | ||||
| const splash = document.querySelector('.splash'); | ||||
| 
 | ||||
| if (!splash || !body) | ||||
|     throw new Error('required element not found'); | ||||
| 
 | ||||
| splash.addEventListener('transitionend', function () { | ||||
|     body.classList.remove('was-killed'); | ||||
| }); | ||||
| 
 | ||||
| const connection = new WebSocket(`ws://${window.location.hostname}:3000`); | ||||
| connection.binaryType = 'blob'; | ||||
| 
 | ||||
| connection.onmessage = function (message) { | ||||
|     message = JSON.parse(message.data); | ||||
|     console.log('got message', {message}); | ||||
|     if (message.type === 'shot') | ||||
|         body.classList.add('was-killed'); | ||||
| }; | ||||
| 
 | ||||
| connection.onerror = event => { | ||||
|     console.log('error', event); | ||||
|     alert('connection error'); | ||||
| }; | ||||
| 
 | ||||
| connection.onclose = event => { | ||||
|     console.log('closed', event); | ||||
|     alert('connection closed - maybe a player with this name is already connected'); | ||||
| }; | ||||
| 
 | ||||
| const keyEventListener = (type) => (event) => { | ||||
|     if (event.defaultPrevented) | ||||
|         return; | ||||
| 
 | ||||
|     const controls = { | ||||
|         'ArrowLeft': 'left', | ||||
|         'ArrowUp': 'up', | ||||
|         'ArrowRight': 'right', | ||||
|         'ArrowDown': 'down', | ||||
|         'Space': 'shoot', | ||||
|         'KeyW': 'up', | ||||
|         'KeyA': 'left', | ||||
|         'KeyS': 'down', | ||||
|         'KeyD': 'right', | ||||
|     }; | ||||
| 
 | ||||
|     const value = controls[event.code]; | ||||
|     if (!value) | ||||
|         return; | ||||
| 
 | ||||
|     send({type, value}); | ||||
| }; | ||||
| 
 | ||||
| connection.onopen = () => { | ||||
|     let name = getPlayerName(); | ||||
|     send({type: 'name', value: name}); | ||||
| 
 | ||||
|     window.onkeyup = keyEventListener('input-off'); | ||||
|     window.onkeydown = keyEventListener('input-on'); | ||||
| 
 | ||||
|     console.log('connection opened, game ready'); | ||||
| }; | ||||
| 
 | ||||
| function getPlayerName() { | ||||
|     let name; | ||||
|     while (!name) | ||||
|         name = prompt('Player Name'); | ||||
|     return name; | ||||
| } | ||||
| 
 | ||||
| function send(obj) { | ||||
|     connection.send(JSON.stringify(obj)); | ||||
| } | ||||
|  | @ -1,7 +1,12 @@ | |||
| import { defineConfig } from 'vite' | ||||
| import react from '@vitejs/plugin-react' | ||||
| import {defineConfig} from 'vite'; | ||||
| import react from '@vitejs/plugin-react'; | ||||
| 
 | ||||
| // https://vitejs.dev/config/
 | ||||
| export default defineConfig({ | ||||
|   plugins: [react()], | ||||
| }) | ||||
|     plugins: [react()], | ||||
| 
 | ||||
|     build: { | ||||
|         outDir: '../TanksServer/client', | ||||
|         emptyOutDir: true | ||||
|     } | ||||
| }); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Vinzenz Schroeter
						Vinzenz Schroeter