show tank infos in client

This commit is contained in:
Vinzenz Schroeter 2024-04-22 19:44:28 +02:00
parent a50a9770c9
commit 0f4eec6343
9 changed files with 73 additions and 26 deletions

View file

@ -6,11 +6,19 @@ import {useEffect, useState} from 'react';
function ScoreRow({name, value}: { function ScoreRow({name, value}: {
name: string; name: string;
value?: any; value?: string | any;
}) { }) {
let valueStr;
if (value === undefined)
valueStr = '?';
else if (typeof value === 'string' || value instanceof String)
valueStr = value;
else
valueStr = JSON.stringify(value);
return <tr> return <tr>
<td>{name}</td> <td>{name}</td>
<td>{value ?? '?'}</td> <td>{valueStr}</td>
</tr>; </tr>;
} }
@ -22,35 +30,45 @@ type Controls = {
readonly shoot: boolean; readonly shoot: boolean;
} }
type TankInfo = {
readonly explosiveBullets: number;
readonly position: { x: number; y: number };
readonly orientation: number;
readonly moving: boolean;
}
type PlayerInfoMessage = { type PlayerInfoMessage = {
readonly name: string; readonly name: string;
readonly scores: Scores; readonly scores: Scores;
readonly controls: Controls; readonly controls: Controls;
readonly tank?: TankInfo;
} }
function controlsString(controls: Controls) { function controlsString(controls: Controls) {
let str = ""; let str = '';
if (controls.forward) if (controls.forward)
str += "▲"; str += '▲';
if (controls.backward) if (controls.backward)
str += "▼"; str += '▼';
if (controls.turnLeft) if (controls.turnLeft)
str += "◄"; str += '◄';
if (controls.turnRight) if (controls.turnRight)
str += "►"; str += '►';
if (controls.shoot) if (controls.shoot)
str += "•"; str += '•';
return str; return str;
} }
export default function PlayerInfo({playerId}: { playerId: Guid }) { export default function PlayerInfo({playerId}: { playerId: Guid }) {
const [shouldSendMessage, setShouldSendMessage] = useState(true); const [shouldSendMessage, setShouldSendMessage] = useState(false);
const url = makeApiUrl('/player'); const url = makeApiUrl('/player');
url.searchParams.set('id', playerId); url.searchParams.set('id', playerId);
const {lastJsonMessage, readyState, sendMessage} = useWebSocket<PlayerInfoMessage>(url.toString(), { const {lastJsonMessage, readyState, sendMessage} = useWebSocket<PlayerInfoMessage>(url.toString(), {
onMessage: () => setShouldSendMessage(true) onMessage: () => setShouldSendMessage(true),
shouldReconnect: () => true,
}); });
useEffect(() => { useEffect(() => {
@ -73,6 +91,10 @@ export default function PlayerInfo({playerId}: { playerId: Guid }) {
<ScoreRow name="kills" value={lastJsonMessage.scores.kills}/> <ScoreRow name="kills" value={lastJsonMessage.scores.kills}/>
<ScoreRow name="deaths" value={lastJsonMessage.scores.deaths}/> <ScoreRow name="deaths" value={lastJsonMessage.scores.deaths}/>
<ScoreRow name="walls" value={lastJsonMessage.scores.wallsDestroyed}/> <ScoreRow name="walls" value={lastJsonMessage.scores.wallsDestroyed}/>
<ScoreRow name="explosive bullets" value={lastJsonMessage.tank?.explosiveBullets}/>
<ScoreRow name="position" value={lastJsonMessage.tank?.position}/>
<ScoreRow name="orientation" value={lastJsonMessage.tank?.orientation}/>
<ScoreRow name="moving" value={lastJsonMessage.tank?.moving}/>
</tbody> </tbody>
</table> </table>
</Column>; </Column>;

View file

@ -11,13 +11,13 @@ internal sealed class CollideBullets(
public Task TickAsync(TimeSpan _) public Task TickAsync(TimeSpan _)
{ {
entityManager.RemoveBulletsWhere(BulletHitsTank); entityManager.RemoveWhere(BulletHitsTank);
entityManager.RemoveBulletsWhere(TryHitAndDestroyWall); entityManager.RemoveWhere(BulletHitsWall);
entityManager.RemoveBulletsWhere(TimeoutBullet); entityManager.RemoveWhere(BulletTimesOut);
return Task.CompletedTask; return Task.CompletedTask;
} }
private bool TimeoutBullet(Bullet bullet) private bool BulletTimesOut(Bullet bullet)
{ {
if (bullet.Timeout > DateTime.Now) if (bullet.Timeout > DateTime.Now)
return false; return false;
@ -27,7 +27,7 @@ internal sealed class CollideBullets(
return true; return true;
} }
private bool TryHitAndDestroyWall(Bullet bullet) private bool BulletHitsWall(Bullet bullet)
{ {
var pixel = bullet.Position.ToPixelPosition(); var pixel = bullet.Position.ToPixelPosition();
if (!map.Current.IsWall(pixel)) if (!map.Current.IsWall(pixel))

View file

@ -9,6 +9,7 @@ internal sealed class MapEntityManager(
private readonly HashSet<Bullet> _bullets = []; private readonly HashSet<Bullet> _bullets = [];
private readonly HashSet<Tank> _tanks = []; private readonly HashSet<Tank> _tanks = [];
private readonly HashSet<PowerUp> _powerUps = []; private readonly HashSet<PowerUp> _powerUps = [];
private readonly Dictionary<Player, Tank> _playerTanks = [];
private readonly TimeSpan _bulletTimeout = TimeSpan.FromMilliseconds(options.Value.BulletTimeoutMs); private readonly TimeSpan _bulletTimeout = TimeSpan.FromMilliseconds(options.Value.BulletTimeoutMs);
public IEnumerable<Bullet> Bullets => _bullets; public IEnumerable<Bullet> Bullets => _bullets;
@ -31,14 +32,16 @@ internal sealed class MapEntityManager(
OwnerCollisionAfter = DateTime.Now + TimeSpan.FromSeconds(1), OwnerCollisionAfter = DateTime.Now + TimeSpan.FromSeconds(1),
}); });
public void RemoveBulletsWhere(Predicate<Bullet> predicate) => _bullets.RemoveWhere(predicate); public void RemoveWhere(Predicate<Bullet> predicate) => _bullets.RemoveWhere(predicate);
public void SpawnTank(Player player) public void SpawnTank(Player player)
{ {
_tanks.Add(new Tank(player, ChooseSpawnPosition()) var tank = new Tank(player, ChooseSpawnPosition())
{ {
Rotation = Random.Shared.NextDouble() Rotation = Random.Shared.NextDouble()
}); };
_tanks.Add(tank);
_playerTanks[player] = tank;
logger.LogInformation("Tank added for player {}", player.Id); logger.LogInformation("Tank added for player {}", player.Id);
} }
@ -50,6 +53,7 @@ internal sealed class MapEntityManager(
{ {
logger.LogInformation("Tank removed for player {}", tank.Owner.Id); logger.LogInformation("Tank removed for player {}", tank.Owner.Id);
_tanks.Remove(tank); _tanks.Remove(tank);
_playerTanks.Remove(tank.Owner);
} }
public FloatPosition ChooseSpawnPosition() public FloatPosition ChooseSpawnPosition()
@ -75,4 +79,6 @@ internal sealed class MapEntityManager(
var min = candidates.MaxBy(pair => pair.Value).Key; var min = candidates.MaxBy(pair => pair.Value).Key;
return min.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition(); return min.ToPixelPosition().GetPixelRelative(4, 4).ToFloatPosition();
} }
public Tank? GetCurrentTankOfPlayer(Player player) => _playerTanks.GetValueOrDefault(player);
} }

View file

@ -11,7 +11,7 @@ internal sealed class MoveTanks(
public Task TickAsync(TimeSpan delta) public Task TickAsync(TimeSpan delta)
{ {
foreach (var tank in entityManager.Tanks) foreach (var tank in entityManager.Tanks)
tank.Moved = TryMoveTank(tank, delta); tank.Moving = TryMoveTank(tank, delta);
return Task.CompletedTask; return Task.CompletedTask;
} }

View file

@ -11,7 +11,7 @@ internal sealed class ShootFromTanks(
public Task TickAsync(TimeSpan _) public Task TickAsync(TimeSpan _)
{ {
foreach (var tank in entityManager.Tanks.Where(t => !t.Moved)) foreach (var tank in entityManager.Tanks.Where(t => !t.Moving))
Shoot(tank); Shoot(tank);
return Task.CompletedTask; return Task.CompletedTask;

View file

@ -1,12 +1,14 @@
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text.Json; using System.Text.Json;
using TanksServer.GameLogic;
namespace TanksServer.Interactivity; namespace TanksServer.Interactivity;
internal sealed class PlayerInfoConnection( internal sealed class PlayerInfoConnection(
Player player, Player player,
ILogger logger, ILogger logger,
WebSocket rawSocket WebSocket rawSocket,
MapEntityManager entityManager
) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0)), IDisposable ) : WebsocketServerConnection(logger, new ByteChannelWebSocket(rawSocket, logger, 0)), IDisposable
{ {
private readonly SemaphoreSlim _wantedFrames = new(1); private readonly SemaphoreSlim _wantedFrames = new(1);
@ -45,7 +47,11 @@ internal sealed class PlayerInfoConnection(
private byte[]? GetMessageToSend() private byte[]? GetMessageToSend()
{ {
var info = new PlayerInfo(player.Name, player.Scores, player.Controls); var tank = entityManager.GetCurrentTankOfPlayer(player);
var tankInfo = tank != null
? new TankInfo(tank.Orientation, tank.ExplosiveBullets, tank.Position.ToPixelPosition(), tank.Moving)
: null;
var info = new PlayerInfo(player.Name, player.Scores, player.Controls, tankInfo);
var response = JsonSerializer.SerializeToUtf8Bytes(info, _context.PlayerInfo); var response = JsonSerializer.SerializeToUtf8Bytes(info, _context.PlayerInfo);
if (response.SequenceEqual(_lastMessage)) if (response.SequenceEqual(_lastMessage))

View file

@ -7,7 +7,8 @@ namespace TanksServer.Interactivity;
internal sealed class PlayerServer( internal sealed class PlayerServer(
ILogger<PlayerServer> logger, ILogger<PlayerServer> logger,
ILogger<PlayerInfoConnection> connectionLogger, ILogger<PlayerInfoConnection> connectionLogger,
TankSpawnQueue tankSpawnQueue TankSpawnQueue tankSpawnQueue,
MapEntityManager entityManager
) : WebsocketServer<PlayerInfoConnection>(logger), ITickStep ) : WebsocketServer<PlayerInfoConnection>(logger), ITickStep
{ {
private readonly ConcurrentDictionary<string, Player> _players = new(); private readonly ConcurrentDictionary<string, Player> _players = new();
@ -46,7 +47,7 @@ internal sealed class PlayerServer(
public IEnumerable<Player> GetAll() => _players.Values; public IEnumerable<Player> GetAll() => _players.Values;
public Task HandleClientAsync(WebSocket webSocket, Player player) public Task HandleClientAsync(WebSocket webSocket, Player player)
=> HandleClientAsync(new PlayerInfoConnection(player, connectionLogger, webSocket)); => HandleClientAsync(new PlayerInfoConnection(player, connectionLogger, webSocket, entityManager));
public Task TickAsync(TimeSpan delta) public Task TickAsync(TimeSpan delta)
=> ParallelForEachConnectionAsync(connection => connection.OnGameTickAsync()); => ParallelForEachConnectionAsync(connection => connection.OnGameTickAsync());

View file

@ -1,3 +1,15 @@
namespace TanksServer.Models; namespace TanksServer.Models;
internal sealed record class PlayerInfo(string Name, Scores Scores, PlayerControls Controls); internal sealed record class TankInfo(
int Orientation,
byte ExplosiveBullets,
PixelPosition Position,
bool Moving
);
internal sealed record class PlayerInfo(
string Name,
Scores Scores,
PlayerControls Controls,
TankInfo? Tank
);

View file

@ -22,7 +22,7 @@ internal sealed class Tank(Player player, FloatPosition spawnPosition) : IMapEnt
public DateTime NextShotAfter { get; set; } public DateTime NextShotAfter { get; set; }
public bool Moved { get; set; } public bool Moving { get; set; }
public FloatPosition Position { get; set; } = spawnPosition; public FloatPosition Position { get; set; } = spawnPosition;