From f31d18bd0bd5355d20d30fb7f34ddd74969b3402 Mon Sep 17 00:00:00 2001 From: Michael Chen Date: Fri, 4 Nov 2022 01:46:48 +0100 Subject: [PATCH] Blind and likely wrong untested game logic Added gameloop worker --- PongGame/Hubs/GameState.cs | 1 + PongGame/Hubs/IPongClient.cs | 1 + PongGame/Hubs/PongGameState.cs | 99 +++++++++++++++++++++++++++------- PongGame/Hubs/PongRoom.cs | 36 +++++++++---- 4 files changed, 108 insertions(+), 29 deletions(-) diff --git a/PongGame/Hubs/GameState.cs b/PongGame/Hubs/GameState.cs index 786a769..dfa17c4 100644 --- a/PongGame/Hubs/GameState.cs +++ b/PongGame/Hubs/GameState.cs @@ -4,4 +4,5 @@ public enum GameStatus { WaitingForPlayers, InProgress, Finished, + Paused, } \ No newline at end of file diff --git a/PongGame/Hubs/IPongClient.cs b/PongGame/Hubs/IPongClient.cs index 77117d5..873271f 100644 --- a/PongGame/Hubs/IPongClient.cs +++ b/PongGame/Hubs/IPongClient.cs @@ -2,5 +2,6 @@ public interface IPongClient { Task GameStateChanged(GameStatus state); + Task ReceiveGameState(PongGameState state); Task UsernameChanged(string value); } \ No newline at end of file diff --git a/PongGame/Hubs/PongGameState.cs b/PongGame/Hubs/PongGameState.cs index 2981cb9..39c4e83 100644 --- a/PongGame/Hubs/PongGameState.cs +++ b/PongGame/Hubs/PongGameState.cs @@ -1,35 +1,94 @@ -namespace PongGame.Hubs; +using System.Drawing; + +namespace PongGame.Hubs; /// /// 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. /// public struct PongGameState { - private const double HEIGHT = 500.0d; - private const double WIDTH = 2 * HEIGHT; - private const double PADDLE_LENGTH = HEIGHT / 10; - private const double BALL_SPEED = 8; + public const float HEIGHT = 500.0f; + public const float WIDTH = 2 * HEIGHT; + public const float PADDLE1_OFFSET = 50.0f; + public const float PADDLE2_OFFSET = 1000 - PADDLE1_OFFSET; + public static readonly PongGameState Initial = new() { - BallPosition = PongBallPosition.Initial, - P1Height = HEIGHT / 2, - P2Height = HEIGHT / 2, - P1Direction = PongPaddleDirection.Stop, - P2Direction = PongPaddleDirection.Stop, + BallState = PongBallState.Initial, + Paddle1 = PongPaddleState.Initial, + Paddle2 = PongPaddleState.Initial, 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 static readonly PongBallPosition Initial = new() { + public PongPaddleState Paddle1; + 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, 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); } } \ No newline at end of file diff --git a/PongGame/Hubs/PongRoom.cs b/PongGame/Hubs/PongRoom.cs index 26ee9c8..da85f33 100644 --- a/PongGame/Hubs/PongRoom.cs +++ b/PongGame/Hubs/PongRoom.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.SignalR; +using System.ComponentModel; +using Microsoft.AspNetCore.SignalR; namespace PongGame.Hubs; @@ -12,6 +13,7 @@ public class PongRoom { public PongRoom(string id, ILogger logger) { ID = id; Logger = logger; + gameWorker.DoWork += GameLoop; } public PongPlayer? Player1 { get; private set; } @@ -44,9 +46,27 @@ public class PongRoom { _ = 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() { 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) { CloseRoom(); } else @@ -54,25 +74,23 @@ public class PongRoom { return Task.CompletedTask; } - private void ResumeGame(PongPlayer player1, PongPlayer player2) { - Logger.LogInformation("[{ID}] Pong game started: {player1} vs. {player2}", ID, player1, player2); + private void ResumeGame() { State.Status = GameStatus.InProgress; - player1.Client.GameStateChanged(State.Status); - player2.Client.GameStateChanged(State.Status); + if (!gameWorker.IsBusy) gameWorker.RunWorkerAsync(); } - private void PauseGame() => State.Status = GameStatus.WaitingForPlayers; + private void PauseGame() => State.Status = GameStatus.Paused; private void CloseRoom() => State.Status = GameStatus.Finished; public override string ToString() => $"[{ID}]"; public void MovePaddle(PongPlayer player, PongPaddleDirection direction) { if (Player1 == player) { - State.P1Direction = direction; + State.Paddle1.Direction = direction; Logger.LogInformation(DIRECTION_LOG_TEMPLATE, ID, 1, player, direction); return; } else if (Player2 == player) { - State.P2Direction = direction; + State.Paddle2.Direction = direction; Logger.LogInformation(DIRECTION_LOG_TEMPLATE, ID, 2, player, direction); return; }