116 lines
4.3 KiB
C#
116 lines
4.3 KiB
C#
using System.Drawing;
|
|
|
|
namespace PongGame.Hubs;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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,
|
|
WinnerLeft = null
|
|
};
|
|
|
|
public PongPaddleState Paddle1;
|
|
public PongPaddleState Paddle2;
|
|
public PongBallState BallState;
|
|
public GameStatus Status;
|
|
public bool? WinnerLeft;
|
|
|
|
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);
|
|
var ballX = state.BallState.Pos.X;
|
|
if (ballX is < 0 or > WIDTH) {
|
|
state.WinnerLeft = ballX < 0;
|
|
state.Status = GameStatus.Finished;
|
|
return;
|
|
}
|
|
|
|
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.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 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 / 5;
|
|
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_WIDTH / 2, Height - PADDLE_HALF_LENGTH, PADDLE_WIDTH, PADDLE_LENGTH);
|
|
}
|
|
} |