load maps from png files

This commit is contained in:
Vinzenz Schroeter 2024-04-14 21:10:21 +02:00
parent 51334af8c3
commit 6bed7d918f
13 changed files with 86 additions and 61 deletions

View file

@ -9,5 +9,5 @@ internal sealed class CollideBulletsWithMap(BulletManager bullets, MapService ma
} }
private bool BulletHitsWall(Bullet bullet) => private bool BulletHitsWall(Bullet bullet) =>
map.IsCurrentlyWall(bullet.Position.ToPixelPosition().ToTilePosition()); map.Current.IsCurrentlyWall(bullet.Position.ToPixelPosition());
} }

View file

@ -1,4 +1,6 @@
using System.IO; using System.IO;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace TanksServer.GameLogic; namespace TanksServer.GameLogic;
@ -9,35 +11,65 @@ internal sealed class MapService
public const ushort TileSize = 8; public const ushort TileSize = 8;
public const ushort PixelsPerRow = TilesPerRow * TileSize; public const ushort PixelsPerRow = TilesPerRow * TileSize;
public const ushort PixelsPerColumn = TilesPerColumn * TileSize; public const ushort PixelsPerColumn = TilesPerColumn * TileSize;
private readonly string _map;
private readonly ILogger<MapService> _logger;
private string[] LoadMaps() => Directory.EnumerateFiles("./assets/maps/", "*.txt") public Map Current { get; }
.Select(LoadMap)
.Where(s => s != null)
.Select(s => s!)
.ToArray();
private string? LoadMap(string file) public MapService()
{ {
var text = File.ReadAllText(file).ReplaceLineEndings(string.Empty).Trim(); var textMaps = Directory.EnumerateFiles("./assets/maps/", "*.txt").Select(LoadMapString);
if (text.Length != TilesPerColumn * TilesPerRow) var pngMaps = Directory.EnumerateFiles("./assets/maps/", "*.png").Select(LoadMapPng);
{
_logger.LogWarning("cannot load map {}: invalid length", file); var maps = textMaps.Concat(pngMaps).ToList();
return null; Current = maps[Random.Shared.Next(maps.Count)];
} }
return text; private static Map LoadMapPng(string file)
}
public MapService(ILogger<MapService> logger)
{ {
_logger = logger; var dict = new Dictionary<PixelPosition, bool>();
var maps = LoadMaps(); using var image = Image.Load<Rgba32>(file);
_map = maps[Random.Shared.Next(0, maps.Length)];
if (image.Width != PixelsPerRow || image.Height != PixelsPerColumn)
throw new FileLoadException($"invalid image size in file {file}");
var whitePixel = new Rgba32(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
for (var y = 0; y < image.Height; y++)
for (var x = 0; x < image.Width; x++)
{
if (image[x, y] == whitePixel)
dict[new PixelPosition(x, y)] = true;
} }
private char this[int tileX, int tileY] => _map[tileX + tileY * TilesPerRow]; return new Map(dict);
}
public bool IsCurrentlyWall(TilePosition position) => this[position.X, position.Y] == '#'; private static Map LoadMapString(string file)
{
var map = File.ReadAllText(file).ReplaceLineEndings(string.Empty).Trim();
if (map.Length != TilesPerColumn * TilesPerRow)
throw new FileLoadException($"cannot load map {file}: invalid length");
var dict = new Dictionary<PixelPosition, bool>();
for (ushort tileY = 0; tileY < MapService.TilesPerColumn; tileY++)
for (ushort tileX = 0; tileX < MapService.TilesPerRow; tileX++)
{
var tile = new TilePosition(tileX, tileY);
if (map[tileX + tileY * TilesPerRow] != '#')
continue;
for (byte pixelInTileY = 0; pixelInTileY < MapService.TileSize; pixelInTileY++)
for (byte pixelInTileX = 0; pixelInTileX < MapService.TileSize; pixelInTileX++)
{
var pixel = tile.ToPixelPosition().GetPixelRelative(pixelInTileX, pixelInTileY);
dict[pixel] = true;
}
}
return new Map(dict);
}
}
internal sealed class Map(IReadOnlyDictionary<PixelPosition, bool> walls)
{
public bool IsCurrentlyWall(PixelPosition position) => walls.TryGetValue(position, out var value) && value;
} }

View file

@ -63,14 +63,16 @@ internal sealed class MoveTanks(
private bool HitsWall(FloatPosition newPosition) private bool HitsWall(FloatPosition newPosition)
{ {
var (topLeft, bottomRight) = Tank.GetBoundsForCenter(newPosition); var (topLeft, _) = Tank.GetBoundsForCenter(newPosition);
TilePosition[] positions =
[ for (short y = 0; y < MapService.TileSize; y++)
topLeft.ToTilePosition(), for (short x = 0; x < MapService.TileSize; x++)
new PixelPosition(bottomRight.X, topLeft.Y).ToTilePosition(), {
new PixelPosition(topLeft.X, bottomRight.Y).ToTilePosition(), var pixelToCheck = topLeft.GetPixelRelative(x, y);
bottomRight.ToTilePosition() if (map.Current.IsCurrentlyWall(pixelToCheck))
]; return true;
return positions.Any(map.IsCurrentlyWall); }
return false;
} }
} }

View file

@ -29,7 +29,8 @@ internal sealed class SpawnNewTanks(
{ {
var tile = new TilePosition(x, y); var tile = new TilePosition(x, y);
if (map.IsCurrentlyWall(tile)) // TODO: implement lookup for non tile aligned walls
if (map.Current.IsCurrentlyWall(tile.ToPixelPosition()))
continue; continue;
var tilePixelCenter = tile.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition(); var tilePixelCenter = tile.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();

View file

@ -7,19 +7,13 @@ internal sealed class DrawMapStep(MapService map) : IDrawStep
{ {
public void Draw(PixelGrid buffer) public void Draw(PixelGrid buffer)
{ {
for (ushort tileY = 0; tileY < MapService.TilesPerColumn; tileY++) for (ushort y = 0; y < MapService.PixelsPerColumn; y++)
for (ushort tileX = 0; tileX < MapService.TilesPerRow; tileX++) for (ushort x = 0; x < MapService.PixelsPerRow; x++)
{ {
var tile = new TilePosition(tileX, tileY); var pixel = new PixelPosition(x, y);
if (!map.IsCurrentlyWall(tile)) if (!map.Current.IsCurrentlyWall(pixel))
continue; continue;
buffer[x, y] = true;
for (byte pixelInTileY = 0; pixelInTileY < MapService.TileSize; pixelInTileY++)
for (byte pixelInTileX = 0; pixelInTileX < MapService.TileSize; pixelInTileX++)
{
var (x, y) = tile.ToPixelPosition().GetPixelRelative(pixelInTileX, pixelInTileY);
buffer[(ushort)x, (ushort)y] = pixelInTileX % 2 == pixelInTileY % 2;
}
} }
} }
} }

View file

@ -9,13 +9,14 @@ internal sealed class GeneratePixelsTickStep(
) : ITickStep ) : ITickStep
{ {
private readonly List<IDrawStep> _drawSteps = drawSteps.ToList(); private readonly List<IDrawStep> _drawSteps = drawSteps.ToList();
private readonly PixelGrid _drawGrid = new(MapService.PixelsPerRow, MapService.PixelsPerColumn);
public Task TickAsync() public Task TickAsync()
{ {
var drawGrid = new PixelGrid(MapService.PixelsPerRow, MapService.PixelsPerColumn); _drawGrid.Clear();
foreach (var step in _drawSteps) foreach (var step in _drawSteps)
step.Draw(drawGrid); step.Draw(_drawGrid);
lastFrameProvider.LastFrame = drawGrid; lastFrameProvider.LastFrame = _drawGrid;
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View file

@ -56,7 +56,6 @@ internal sealed class ClientScreenServer(
private readonly ILogger<ClientScreenServerConnection> _logger; private readonly ILogger<ClientScreenServerConnection> _logger;
private readonly ClientScreenServer _server; private readonly ClientScreenServer _server;
private readonly SemaphoreSlim _wantedFrames = new(1); private readonly SemaphoreSlim _wantedFrames = new(1);
private PixelGrid? _lastSentPixels;
public ClientScreenServerConnection(WebSocket webSocket, public ClientScreenServerConnection(WebSocket webSocket,
ILogger<ClientScreenServerConnection> logger, ILogger<ClientScreenServerConnection> logger,
@ -78,9 +77,6 @@ internal sealed class ClientScreenServer(
public async Task SendAsync(PixelGrid pixels) public async Task SendAsync(PixelGrid pixels)
{ {
if (_lastSentPixels == pixels)
return;
if (!await _wantedFrames.WaitAsync(TimeSpan.Zero)) if (!await _wantedFrames.WaitAsync(TimeSpan.Zero))
{ {
_logger.LogTrace("client does not want a frame yet"); _logger.LogTrace("client does not want a frame yet");
@ -91,7 +87,6 @@ internal sealed class ClientScreenServer(
try try
{ {
await _channel.SendAsync(pixels.Data); await _channel.SendAsync(pixels.Data);
_lastSentPixels = pixels;
} }
catch (WebSocketException ex) catch (WebSocketException ex)
{ {

View file

@ -4,8 +4,7 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning", "Microsoft.AspNetCore": "Warning",
"TanksServer": "Debug", "TanksServer": "Debug",
"Microsoft.AspNetCore.HttpLogging": "Information", "Microsoft.AspNetCore.HttpLogging": "Information"
"TanksServer.GameLogic.RotateTanks": "Trace"
} }
}, },
"AllowedHosts": "*", "AllowedHosts": "*",

View file

@ -10,7 +10,7 @@
................#..##...#...#..............# ................#..##...#...#..............#
................#...#...#..#...............# ................#...#...#..#...............#
........................###................# ........................###................#
........................#..#......###......# ........................#..#.....####......#
........................#...#...#..........# ........................#...#...#..........#
.................................###.......# .................................###.......#
....................................#...#..# ....................................#...#..#

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -28,7 +28,7 @@ export default function App() {
if (isLoggedIn) if (isLoggedIn)
return; return;
const result = await postPlayer(nameId); const result = await postPlayer(nameId);
setLoggedIn(result !== null); setLoggedIn(result.ok);
}, [nameId, isLoggedIn])(); }, [nameId, isLoggedIn])();
return <Column className='flex-grow'> return <Column className='flex-grow'>
@ -43,11 +43,10 @@ export default function App() {
</Row> </Row>
<ClientScreen logout={logout} theme={theme}/> <ClientScreen logout={logout} theme={theme}/>
{nameId.name === '' && <JoinForm setNameId={setNameId} clientId={nameId.id}/>} {nameId.name === '' && <JoinForm setNameId={setNameId} clientId={nameId.id}/>}
{isLoggedIn && <Row className='GadgetRows'> <Row className='GadgetRows'>
<Controls playerId={nameId.id} logout={logout}/> {isLoggedIn && <Controls playerId={nameId.id} logout={logout}/>}
<PlayerInfo playerId={nameId.id} logout={logout}/> {isLoggedIn && <PlayerInfo playerId={nameId.id} logout={logout}/>}
<Scoreboard/> <Scoreboard/>
</Row> </Row>
}
</Column>; </Column>;
} }

View file

@ -55,7 +55,8 @@ export default function ClientScreen({logout, theme}: { logout: () => void, them
sendMessage, sendMessage,
getWebSocket getWebSocket
} = useWebSocket(import.meta.env.VITE_TANK_SCREEN_URL, { } = useWebSocket(import.meta.env.VITE_TANK_SCREEN_URL, {
onError: logout onError: logout,
shouldReconnect: () => true,
}); });
const socket = getWebSocket(); const socket = getWebSocket();

View file

@ -13,7 +13,8 @@ export default function Controls({playerId, logout}: {
getWebSocket, getWebSocket,
readyState readyState
} = useWebSocket(url.toString(), { } = useWebSocket(url.toString(), {
onError: logout onError: logout,
shouldReconnect: () => true,
}); });
const socket = getWebSocket(); const socket = getWebSocket();