Blind and likely wrong untested game logic
Added gameloop worker
This commit is contained in:
parent
a4e22a4c52
commit
f31d18bd0b
@ -4,4 +4,5 @@ public enum GameStatus {
|
|||||||
WaitingForPlayers,
|
WaitingForPlayers,
|
||||||
InProgress,
|
InProgress,
|
||||||
Finished,
|
Finished,
|
||||||
|
Paused,
|
||||||
}
|
}
|
@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
public interface IPongClient {
|
public interface IPongClient {
|
||||||
Task GameStateChanged(GameStatus state);
|
Task GameStateChanged(GameStatus state);
|
||||||
|
Task ReceiveGameState(PongGameState state);
|
||||||
Task UsernameChanged(string value);
|
Task UsernameChanged(string value);
|
||||||
}
|
}
|
@ -1,35 +1,94 @@
|
|||||||
namespace PongGame.Hubs;
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace PongGame.Hubs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pong game state saving the positions of the paddles and the ball.
|
/// Pong game state saving the positions of the paddles and the ball.
|
||||||
/// The Pong board has aspect ratio 1:2 with height 500 and width 1000.
|
/// The Pong board has aspect ratio 1:2 with height 500 and width 1000.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public struct PongGameState {
|
public struct PongGameState {
|
||||||
private const double HEIGHT = 500.0d;
|
public const float HEIGHT = 500.0f;
|
||||||
private const double WIDTH = 2 * HEIGHT;
|
public const float WIDTH = 2 * HEIGHT;
|
||||||
private const double PADDLE_LENGTH = HEIGHT / 10;
|
public const float PADDLE1_OFFSET = 50.0f;
|
||||||
private const double BALL_SPEED = 8;
|
public const float PADDLE2_OFFSET = 1000 - PADDLE1_OFFSET;
|
||||||
|
|
||||||
public static readonly PongGameState Initial = new() {
|
public static readonly PongGameState Initial = new() {
|
||||||
BallPosition = PongBallPosition.Initial,
|
BallState = PongBallState.Initial,
|
||||||
P1Height = HEIGHT / 2,
|
Paddle1 = PongPaddleState.Initial,
|
||||||
P2Height = HEIGHT / 2,
|
Paddle2 = PongPaddleState.Initial,
|
||||||
P1Direction = PongPaddleDirection.Stop,
|
|
||||||
P2Direction = PongPaddleDirection.Stop,
|
|
||||||
Status = GameStatus.WaitingForPlayers
|
Status = GameStatus.WaitingForPlayers
|
||||||
};
|
};
|
||||||
public double P1Height { get; set; }
|
|
||||||
public double P2Height { get; set; }
|
|
||||||
public PongBallPosition BallPosition { get; set; }
|
|
||||||
public GameStatus Status { get; set; }
|
|
||||||
public PongPaddleDirection P1Direction { get; set; }
|
|
||||||
public PongPaddleDirection P2Direction { get; set; }
|
|
||||||
|
|
||||||
public struct PongBallPosition {
|
public PongPaddleState Paddle1;
|
||||||
public static readonly PongBallPosition Initial = new() {
|
public PongPaddleState Paddle2;
|
||||||
|
public PongBallState BallState;
|
||||||
|
public GameStatus Status;
|
||||||
|
|
||||||
|
public static void Update(ref PongGameState state) {
|
||||||
|
if (state.Status is not GameStatus.InProgress) return;
|
||||||
|
|
||||||
|
PongPaddleState.Update(ref state.Paddle1);
|
||||||
|
PongPaddleState.Update(ref state.Paddle2);
|
||||||
|
PongBallState.Update(ref state.BallState);
|
||||||
|
|
||||||
|
Collide(in state.Paddle1, ref state.BallState, true);
|
||||||
|
Collide(in state.Paddle2, ref state.BallState, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Collide(in PongPaddleState paddle, ref PongBallState ballState, bool left) {
|
||||||
|
var paddleX = left ? PADDLE1_OFFSET : PADDLE2_OFFSET;
|
||||||
|
var intersection = RectangleF.Intersect(paddle.GetCollider(paddleX), ballState.GetCollider());
|
||||||
|
if (intersection.IsEmpty) return;
|
||||||
|
|
||||||
|
// TODO: continuous collision
|
||||||
|
var ratio = (ballState.Y - paddle.Height + PongPaddleState.PADDLE_HALF_LENGTH) / PongPaddleState.PADDLE_LENGTH;
|
||||||
|
var upAngle = left ? MathF.PI * 3 / 8 : MathF.PI * 5 / 8;
|
||||||
|
var downAngle = -upAngle;
|
||||||
|
|
||||||
|
ballState.BallAngle = ratio * downAngle + (1 - ratio) * upAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct PongBallState {
|
||||||
|
public const float BALL_SPEED = 8;
|
||||||
|
public const float BALL_RADIUS = 2;
|
||||||
|
|
||||||
|
public static readonly PongBallState Initial = new() {
|
||||||
|
BallAngle = 0.0f,
|
||||||
X = WIDTH / 2,
|
X = WIDTH / 2,
|
||||||
Y = HEIGHT / 2
|
Y = HEIGHT / 2
|
||||||
};
|
};
|
||||||
public double X { get; set; }
|
|
||||||
public double Y { get; set; }
|
public float X;
|
||||||
|
public float Y;
|
||||||
|
public float BallAngle;
|
||||||
|
|
||||||
|
public static void Update(ref PongBallState state) {
|
||||||
|
var (dy, dx) = MathF.SinCos(state.BallAngle);
|
||||||
|
state.X += BALL_SPEED * dx;
|
||||||
|
state.Y -= BALL_SPEED * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RectangleF GetCollider() => new(X - BALL_RADIUS, Y - BALL_RADIUS, 2 * BALL_RADIUS, 2 * BALL_RADIUS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct PongPaddleState {
|
||||||
|
public const float PADDLE_LENGTH = HEIGHT / 10;
|
||||||
|
public const float PADDLE_HALF_LENGTH = PADDLE_LENGTH / 2;
|
||||||
|
public const float PADDLE_WIDTH = PADDLE_LENGTH / 6;
|
||||||
|
public const float PADDLE_SPEED = 8;
|
||||||
|
|
||||||
|
public static readonly PongPaddleState Initial = new() {
|
||||||
|
Direction = PongPaddleDirection.Stop,
|
||||||
|
Height = HEIGHT / 2
|
||||||
|
};
|
||||||
|
|
||||||
|
public float Height;
|
||||||
|
public PongPaddleDirection Direction;
|
||||||
|
|
||||||
|
public static void Update(ref PongPaddleState state) {
|
||||||
|
state.Height = Math.Clamp(state.Height + ((int)state.Direction) * PADDLE_SPEED, PADDLE_HALF_LENGTH, HEIGHT - PADDLE_HALF_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RectangleF GetCollider(float x) => new(x - PADDLE_HALF_LENGTH, Height - PADDLE_HALF_LENGTH, PADDLE_WIDTH, PADDLE_LENGTH);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.SignalR;
|
using System.ComponentModel;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
namespace PongGame.Hubs;
|
namespace PongGame.Hubs;
|
||||||
|
|
||||||
@ -12,6 +13,7 @@ public class PongRoom {
|
|||||||
public PongRoom(string id, ILogger logger) {
|
public PongRoom(string id, ILogger logger) {
|
||||||
ID = id;
|
ID = id;
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
|
gameWorker.DoWork += GameLoop;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PongPlayer? Player1 { get; private set; }
|
public PongPlayer? Player1 { get; private set; }
|
||||||
@ -44,9 +46,27 @@ public class PongRoom {
|
|||||||
_ = Task.Run(PlayersChanged);
|
_ = Task.Run(PlayersChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BackgroundWorker gameWorker = new() {
|
||||||
|
WorkerSupportsCancellation = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private void GameLoop(object? sender, DoWorkEventArgs e) {
|
||||||
|
while (!gameWorker.CancellationPending
|
||||||
|
&& Player1 is PongPlayer p1
|
||||||
|
&& Player2 is PongPlayer p2
|
||||||
|
&& State.Status is GameStatus.InProgress) {
|
||||||
|
PongGameState.Update(ref State);
|
||||||
|
_ = Task.Run(() => Task.WhenAll(
|
||||||
|
p1.Client.ReceiveGameState(State),
|
||||||
|
p2.Client.ReceiveGameState(State)));
|
||||||
|
Thread.Sleep(1000 / 60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Task PlayersChanged() {
|
private Task PlayersChanged() {
|
||||||
if (Player1 is PongPlayer player1 && Player2 is PongPlayer player2) {
|
if (Player1 is PongPlayer player1 && Player2 is PongPlayer player2) {
|
||||||
ResumeGame(player1, player2);
|
Logger.LogInformation("[{ID}] Pong game started: {player1} vs. {player2}", ID, player1, player2);
|
||||||
|
ResumeGame();
|
||||||
} else if (Player1 is null && Player2 is null) {
|
} else if (Player1 is null && Player2 is null) {
|
||||||
CloseRoom();
|
CloseRoom();
|
||||||
} else
|
} else
|
||||||
@ -54,25 +74,23 @@ public class PongRoom {
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResumeGame(PongPlayer player1, PongPlayer player2) {
|
private void ResumeGame() {
|
||||||
Logger.LogInformation("[{ID}] Pong game started: {player1} vs. {player2}", ID, player1, player2);
|
|
||||||
State.Status = GameStatus.InProgress;
|
State.Status = GameStatus.InProgress;
|
||||||
player1.Client.GameStateChanged(State.Status);
|
if (!gameWorker.IsBusy) gameWorker.RunWorkerAsync();
|
||||||
player2.Client.GameStateChanged(State.Status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PauseGame() => State.Status = GameStatus.WaitingForPlayers;
|
private void PauseGame() => State.Status = GameStatus.Paused;
|
||||||
private void CloseRoom() => State.Status = GameStatus.Finished;
|
private void CloseRoom() => State.Status = GameStatus.Finished;
|
||||||
|
|
||||||
public override string ToString() => $"[{ID}]";
|
public override string ToString() => $"[{ID}]";
|
||||||
|
|
||||||
public void MovePaddle(PongPlayer player, PongPaddleDirection direction) {
|
public void MovePaddle(PongPlayer player, PongPaddleDirection direction) {
|
||||||
if (Player1 == player) {
|
if (Player1 == player) {
|
||||||
State.P1Direction = direction;
|
State.Paddle1.Direction = direction;
|
||||||
Logger.LogInformation(DIRECTION_LOG_TEMPLATE, ID, 1, player, direction);
|
Logger.LogInformation(DIRECTION_LOG_TEMPLATE, ID, 1, player, direction);
|
||||||
return;
|
return;
|
||||||
} else if (Player2 == player) {
|
} else if (Player2 == player) {
|
||||||
State.P2Direction = direction;
|
State.Paddle2.Direction = direction;
|
||||||
Logger.LogInformation(DIRECTION_LOG_TEMPLATE, ID, 2, player, direction);
|
Logger.LogInformation(DIRECTION_LOG_TEMPLATE, ID, 2, player, direction);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user