using System.Drawing; using System.Reflection; using MessagePack; 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 { public const float HEIGHT = 500.0f; public const float WIDTH = 2 * HEIGHT; public const float PADDLE1_OFFSET = WIDTH / 20; public const float PADDLE2_OFFSET = WIDTH - PADDLE1_OFFSET; public static readonly PongGameState Initial = new() { BallState = PongBallState.Initial, Paddle1 = PongPaddleState.Initial, Paddle2 = PongPaddleState.Initial, Status = GameStatus.WaitingForPlayers }; 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); switch (state.BallState.Pos.X) { case > WIDTH: AnnounceScore(ref state, ref state.Paddle1); return; case < 0: AnnounceScore(ref state, ref state.Paddle2); return; } } private static void AnnounceScore(ref PongGameState state, ref PongPaddleState winner) { winner.Score++; state.Paddle1.ResetPosition(); state.Paddle2.ResetPosition(); state.BallState.ResetPosition(); } 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.Pos.Y - paddle.Height + PongPaddleState.PADDLE_HALF_LENGTH) / PongPaddleState.PADDLE_LENGTH; // TODO: lesser angles var upAngle = left ? MathF.PI * 3 / 8 : MathF.PI * 5 / 8; var downAngle = left ? -MathF.PI * 3 / 8 : MathF.PI * 11 / 8; // TODO: reflect ball on surface instead of launching ballState.BallAngle = ratio * downAngle + (1 - ratio) * upAngle; } public struct PongBallState { public const float BALL_SPEED = 2 * BALL_RADIUS; public const float BALL_RADIUS = HEIGHT / 125; public static readonly PongBallState Initial = new() { BallAngle = 0.0f, Pos = new() { X = WIDTH / 2, Y = HEIGHT / 2 } }; public PointF Pos; public float BallAngle; public static void Update(ref PongBallState state) { var (dy, dx) = MathF.SinCos(state.BallAngle); state.Pos.X += BALL_SPEED * dx; state.Pos.Y -= BALL_SPEED * dy; // If reflected from lower or upper border calculate continuous reflection // This prevents the ball from temporarily glitching into paddles if (state.Pos.Y < BALL_RADIUS) { state.Pos.Y = 2 * BALL_RADIUS - state.Pos.Y; state.BallAngle = 2 * MathF.PI - state.BallAngle; } else if (state.Pos.Y > HEIGHT - BALL_RADIUS) { state.Pos.Y = 2 * (HEIGHT - BALL_RADIUS) - state.Pos.Y; state.BallAngle = 2 * MathF.PI - state.BallAngle; } } public RectangleF GetCollider() => new(Pos.X - BALL_RADIUS, Pos.Y - BALL_RADIUS, 2 * BALL_RADIUS, 2 * BALL_RADIUS); public void ResetPosition() { BallAngle = 0.0f; Pos.X = WIDTH / 2; Pos.Y = HEIGHT / 2; } } public struct PongPaddleState { public const float PADDLE_LENGTH = HEIGHT / 6; public const float PADDLE_HALF_LENGTH = PADDLE_LENGTH / 2; public const float PADDLE_WIDTH = PADDLE_LENGTH / 5; public const float PADDLE_SPEED = 8; private const float PADDLE_INITIAL_HEIGHT = HEIGHT / 2; public static readonly PongPaddleState Initial = new() { Score = 0, Direction = PongPaddleDirection.Stop, Height = PADDLE_INITIAL_HEIGHT }; public float Height; public PongPaddleDirection Direction; [IgnoreMember] private int _score; public int Score { get => _score; set { if (_score != value) { _score = value; _ = Task.Run(() => ScoreChanged.Invoke(this, value)); } } } public event EventHandler ScoreChanged; 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_WIDTH / 2, Height - PADDLE_HALF_LENGTH, PADDLE_WIDTH, PADDLE_LENGTH); public void ResetPosition() { Direction = PongPaddleDirection.Stop; Height = PADDLE_INITIAL_HEIGHT; } } }