improve spawn position checks

This commit is contained in:
Vinzenz Schroeter 2024-04-11 20:48:21 +02:00
parent 91ab911f9c
commit ad211433fb
16 changed files with 147 additions and 80 deletions

View file

@ -1,7 +1,7 @@
namespace TanksServer.GameLogic; namespace TanksServer.GameLogic;
internal sealed class CollideBulletsWithTanks( internal sealed class CollideBulletsWithTanks(
BulletManager bullets, TankManager tanks, SpawnQueueProvider spawnQueueProvider BulletManager bullets, TankManager tanks, SpawnQueue spawnQueue
) : ITickStep ) : ITickStep
{ {
public Task TickAsync() public Task TickAsync()
@ -24,7 +24,7 @@ internal sealed class CollideBulletsWithTanks(
tank.Owner.Deaths++; tank.Owner.Deaths++;
tanks.Remove(tank); tanks.Remove(tank);
spawnQueueProvider.Queue.Enqueue(tank.Owner); spawnQueue.EnqueueForDelayedSpawn(tank.Owner);
return true; return true;
} }

View file

@ -0,0 +1,6 @@
namespace TanksServer.GameLogic;
public class PlayersConfiguration
{
public int SpawnDelayMs { get; set; }
}

View file

@ -1,24 +1,28 @@
namespace TanksServer.GameLogic; namespace TanksServer.GameLogic;
internal sealed class SpawnNewTanks(TankManager tanks, MapService map, SpawnQueueProvider queueProvider) : ITickStep internal sealed class SpawnNewTanks(
TankManager tanks,
MapService map,
SpawnQueue queue,
BulletManager bullets
) : ITickStep
{ {
public Task TickAsync() public Task TickAsync()
{ {
while (queueProvider.Queue.TryDequeue(out var player)) if (!queue.TryDequeueNext(out var player))
{ return Task.CompletedTask;
var tank = new Tank(player, ChooseSpawnPosition())
tanks.Add(new Tank(player, ChooseSpawnPosition())
{ {
Rotation = Random.Shared.Next(0, 16) Rotation = Random.Shared.Next(0, 16)
}; });
tanks.Add(tank);
}
return Task.CompletedTask; return Task.CompletedTask;
} }
private FloatPosition ChooseSpawnPosition() private FloatPosition ChooseSpawnPosition()
{ {
List<TilePosition> candidates = new(); Dictionary<TilePosition, double> candidates = [];
for (var x = 0; x < MapService.TilesPerRow; x++) for (var x = 0; x < MapService.TilesPerRow; x++)
for (var y = 0; y < MapService.TilesPerColumn; y++) for (var y = 0; y < MapService.TilesPerColumn; y++)
@ -28,14 +32,23 @@ internal sealed class SpawnNewTanks(TankManager tanks, MapService map, SpawnQueu
if (map.IsCurrentlyWall(tile)) if (map.IsCurrentlyWall(tile))
continue; continue;
// TODO: check tanks and bullets var tilePixelCenter = tile.GetPixelRelative(4, 4);
candidates.Add(tile);
var minDistance = bullets.GetAll()
.Cast<IMapEntity>()
.Concat(tanks)
.Select(entity => Math.Sqrt(
Math.Pow(entity.Position.X - tilePixelCenter.X, 2) +
Math.Pow(entity.Position.Y - tilePixelCenter.Y, 2)))
.Aggregate(double.MaxValue, Math.Min);
candidates.Add(tile, minDistance);
} }
var chosenTile = candidates[Random.Shared.Next(candidates.Count)]; var min = candidates.MaxBy(kvp => kvp.Value).Key;
return new FloatPosition( return new FloatPosition(
chosenTile.X * MapService.TileSize, min.X * MapService.TileSize,
chosenTile.Y * MapService.TileSize min.Y * MapService.TileSize
); );
} }
} }

View file

@ -0,0 +1,36 @@
using System.Diagnostics.CodeAnalysis;
namespace TanksServer.GameLogic;
internal sealed class SpawnQueue(
IOptions<PlayersConfiguration> options
)
{
private ConcurrentQueue<Player> _queue = new();
private ConcurrentDictionary<Player, DateTime> _spawnTimes = new();
private readonly TimeSpan _spawnDelay = TimeSpan.FromMilliseconds(options.Value.SpawnDelayMs);
public void EnqueueForImmediateSpawn(Player player)
{
_queue.Enqueue(player);
}
public void EnqueueForDelayedSpawn(Player player)
{
_queue.Enqueue(player);
_spawnTimes.AddOrUpdate(player, DateTime.MinValue, (_, _) => DateTime.Now + _spawnDelay);
}
public bool TryDequeueNext([MaybeNullWhen(false)] out Player player)
{
if (!_queue.TryDequeue(out player))
return false;
var now = DateTime.Now;
if (_spawnTimes.GetOrAdd(player, DateTime.MinValue) <= now)
return true;
_queue.Enqueue(player);
return false;
}
}

View file

@ -1,6 +0,0 @@
namespace TanksServer.GameLogic;
internal sealed class SpawnQueueProvider
{
public ConcurrentQueue<Player> Queue { get; } = new();
}

View file

@ -0,0 +1,9 @@
namespace TanksServer.GameLogic;
public class TanksConfiguration
{
public double MoveSpeed { get; set; }
public double TurnSpeed { get; set; }
public double ShootDelayMs { get; set; }
public double BulletSpeed { get; set; }
}

View file

@ -3,7 +3,7 @@ using TanksServer.GameLogic;
namespace TanksServer.Interactivity; namespace TanksServer.Interactivity;
internal sealed class PlayerServer(ILogger<PlayerServer> logger, SpawnQueueProvider spawnQueueProvider) internal sealed class PlayerServer(ILogger<PlayerServer> logger, SpawnQueue spawnQueue)
{ {
private readonly ConcurrentDictionary<string, Player> _players = new(); private readonly ConcurrentDictionary<string, Player> _players = new();
@ -33,7 +33,7 @@ internal sealed class PlayerServer(ILogger<PlayerServer> logger, SpawnQueueProvi
private Player AddAndSpawn(string name) private Player AddAndSpawn(string name)
{ {
var player = new Player(name); var player = new Player(name);
spawnQueueProvider.Queue.Enqueue(player); spawnQueue.EnqueueForImmediateSpawn(player);
return player; return player;
} }
} }

View file

@ -1,6 +1,6 @@
namespace TanksServer.Models; namespace TanksServer.Models;
internal sealed class Bullet(Player tankOwner, FloatPosition position, double rotation) internal sealed class Bullet(Player tankOwner, FloatPosition position, double rotation): IMapEntity
{ {
public Player Owner { get; } = tankOwner; public Player Owner { get; } = tankOwner;

View file

@ -1,5 +1,3 @@
namespace TanksServer.Models; namespace TanksServer.Models;
internal readonly record struct FloatPosition(double X, double Y) internal readonly record struct FloatPosition(double X, double Y);
{
}

View file

@ -0,0 +1,7 @@
namespace TanksServer.Models;
internal interface IMapEntity
{
FloatPosition Position { get; set; }
double Rotation { get; set; }
}

View file

@ -2,7 +2,7 @@ using TanksServer.GameLogic;
namespace TanksServer.Models; namespace TanksServer.Models;
internal sealed class Tank(Player player, FloatPosition spawnPosition) internal sealed class Tank(Player player, FloatPosition spawnPosition): IMapEntity
{ {
private double _rotation; private double _rotation;

View file

@ -1,9 +0,0 @@
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;
}

View file

@ -1,4 +1,5 @@
using System.IO; using System.IO;
using System.Xml;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -59,6 +60,13 @@ public static class Program
{ {
var builder = WebApplication.CreateSlimBuilder(args); var builder = WebApplication.CreateSlimBuilder(args);
builder.Logging.AddSimpleConsole(options =>
{
options.SingleLine = true;
options.IncludeScopes = true;
options.TimestampFormat = "HH:mm:ss ";
});
builder.Services.AddCors(options => options builder.Services.AddCors(options => options
.AddDefaultPolicy(policy => policy .AddDefaultPolicy(policy => policy
.AllowAnyHeader() .AllowAnyHeader()
@ -80,7 +88,7 @@ public static class Program
builder.Services.AddSingleton<PlayerServer>(); builder.Services.AddSingleton<PlayerServer>();
builder.Services.AddSingleton<ClientScreenServer>(); builder.Services.AddSingleton<ClientScreenServer>();
builder.Services.AddSingleton<LastFinishedFrameProvider>(); builder.Services.AddSingleton<LastFinishedFrameProvider>();
builder.Services.AddSingleton<SpawnQueueProvider>(); builder.Services.AddSingleton<SpawnQueue>();
builder.Services.AddHostedService<GameTickWorker>(); builder.Services.AddHostedService<GameTickWorker>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<ControlsServer>()); builder.Services.AddHostedService(sp => sp.GetRequiredService<ControlsServer>());
@ -103,6 +111,10 @@ public static class Program
builder.Services.Configure<ServicePointDisplayConfiguration>( builder.Services.Configure<ServicePointDisplayConfiguration>(
builder.Configuration.GetSection("ServicePointDisplay")); builder.Configuration.GetSection("ServicePointDisplay"));
builder.Services.Configure<TanksConfiguration>(
builder.Configuration.GetSection("Tanks"));
builder.Services.Configure<PlayersConfiguration>(
builder.Configuration.GetSection("Players"));
var app = builder.Build(); var app = builder.Build();

View file

@ -40,8 +40,8 @@ internal sealed class SendToServicePointDisplay : ITickStep, IDisposable
{ {
Rows = Rows =
{ {
[0] = "== TANKS! ==", [00] = "== TANKS! ==",
[1] = "-- scores --", [01] = "-- scores --",
[17] = "-- join --", [17] = "-- join --",
[18] = string.Join('.', localIp[..2]), [18] = string.Join('.', localIp[..2]),
[19] = string.Join('.', localIp[2..]) [19] = string.Join('.', localIp[2..])

View file

@ -4,7 +4,7 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning", "Microsoft.AspNetCore": "Warning",
"TanksServer": "Debug", "TanksServer": "Debug",
"Microsoft.AspNetCore.HttpLogging": "Debug" "Microsoft.AspNetCore.HttpLogging": "Information"
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
@ -16,9 +16,17 @@
} }
}, },
"ServicePointDisplay": { "ServicePointDisplay": {
"Enable": true, "Enable": false,
//"Hostname": "172.23.42.29", "Hostname": "172.23.42.29",
"Hostname": "localhost",
"Port": 2342 "Port": 2342
},
"Tanks": {
"MoveSpeed": 1.4,
"TurnSpeed": 0.4,
"ShootDelayMs": 400,
"BulletSpeed": 8
},
"Players": {
"SpawnDelayMs": 3000
} }
} }

View file

@ -6,6 +6,9 @@ import {statusTextForReadyState} from './statusTextForReadyState.tsx';
const pixelsPerRow = 352; const pixelsPerRow = 352;
const pixelsPerCol = 160; const pixelsPerCol = 160;
const onColor = [0, 180, 0, 255];
const offColor = [0, 0, 0, 255];
function getIndexes(bitIndex: number) { function getIndexes(bitIndex: number) {
return { return {
byteIndex: 10 + Math.floor(bitIndex / 8), byteIndex: 10 + Math.floor(bitIndex / 8),
@ -26,28 +29,18 @@ function drawPixelsToCanvas(pixels: Uint8Array, canvas: HTMLCanvasElement) {
for (let x = 0; x < canvas.width; x++) { for (let x = 0; x < canvas.width; x++) {
const pixelIndex = rowStartPixelIndex + x; const pixelIndex = rowStartPixelIndex + x;
const {byteIndex, bitInByteIndex} = getIndexes(pixelIndex); const {byteIndex, bitInByteIndex} = getIndexes(pixelIndex);
const byte = pixels[byteIndex];
const mask = (1 << bitInByteIndex); const mask = (1 << bitInByteIndex);
const bitCheck = byte & mask; const isOn = (pixels[byteIndex] & mask) !== 0;
const isOn = bitCheck !== 0; const color = isOn ? onColor : offColor;
const dataIndex = pixelIndex * 4; for (let colorChannel of [0, 1, 2, 3])
if (isOn) { data[pixelIndex * 4 + colorChannel] = color[colorChannel];
data[dataIndex] = 0; // r
data[dataIndex + 1] = 180; // g
data[dataIndex + 2] = 0; // b
data[dataIndex + 3] = 255; // a
} else {
data[dataIndex] = 0; // r
data[dataIndex + 1] = 0; // g
data[dataIndex + 2] = 0; // b
data[dataIndex + 3] = 255; // a
}
} }
} }
drawContext.putImageData(imageData, 0, 0); drawContext.putImageData(imageData, 0, 0);
} }
export default function ClientScreen({}: {}) { export default function ClientScreen({}: {}) {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);