Added crude client rendering with pixi.js
Added keyboard controlling Added fixed ball reflections on right paddle added reflections on screen borders Added goal (miss the paddle) detection
This commit is contained in:
parent
16d001c084
commit
f728240db9
@ -9,20 +9,22 @@ namespace PongGame.Hubs;
|
|||||||
public struct PongGameState {
|
public struct PongGameState {
|
||||||
public const float HEIGHT = 500.0f;
|
public const float HEIGHT = 500.0f;
|
||||||
public const float WIDTH = 2 * HEIGHT;
|
public const float WIDTH = 2 * HEIGHT;
|
||||||
public const float PADDLE1_OFFSET = 50.0f;
|
public const float PADDLE1_OFFSET = WIDTH / 20;
|
||||||
public const float PADDLE2_OFFSET = 1000 - PADDLE1_OFFSET;
|
public const float PADDLE2_OFFSET = WIDTH - PADDLE1_OFFSET;
|
||||||
|
|
||||||
public static readonly PongGameState Initial = new() {
|
public static readonly PongGameState Initial = new() {
|
||||||
BallState = PongBallState.Initial,
|
BallState = PongBallState.Initial,
|
||||||
Paddle1 = PongPaddleState.Initial,
|
Paddle1 = PongPaddleState.Initial,
|
||||||
Paddle2 = PongPaddleState.Initial,
|
Paddle2 = PongPaddleState.Initial,
|
||||||
Status = GameStatus.WaitingForPlayers
|
Status = GameStatus.WaitingForPlayers,
|
||||||
|
WinnerLeft = null
|
||||||
};
|
};
|
||||||
|
|
||||||
public PongPaddleState Paddle1;
|
public PongPaddleState Paddle1;
|
||||||
public PongPaddleState Paddle2;
|
public PongPaddleState Paddle2;
|
||||||
public PongBallState BallState;
|
public PongBallState BallState;
|
||||||
public GameStatus Status;
|
public GameStatus Status;
|
||||||
|
public bool? WinnerLeft;
|
||||||
|
|
||||||
public static void Update(ref PongGameState state) {
|
public static void Update(ref PongGameState state) {
|
||||||
if (state.Status is not GameStatus.InProgress) return;
|
if (state.Status is not GameStatus.InProgress) return;
|
||||||
@ -30,6 +32,12 @@ public struct PongGameState {
|
|||||||
PongPaddleState.Update(ref state.Paddle1);
|
PongPaddleState.Update(ref state.Paddle1);
|
||||||
PongPaddleState.Update(ref state.Paddle2);
|
PongPaddleState.Update(ref state.Paddle2);
|
||||||
PongBallState.Update(ref state.BallState);
|
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.Paddle1, ref state.BallState, true);
|
||||||
Collide(in state.Paddle2, ref state.BallState, false);
|
Collide(in state.Paddle2, ref state.BallState, false);
|
||||||
@ -41,40 +49,48 @@ public struct PongGameState {
|
|||||||
if (intersection.IsEmpty) return;
|
if (intersection.IsEmpty) return;
|
||||||
|
|
||||||
// TODO: continuous collision
|
// TODO: continuous collision
|
||||||
var ratio = (ballState.Y - paddle.Height + PongPaddleState.PADDLE_HALF_LENGTH) / PongPaddleState.PADDLE_LENGTH;
|
var ratio = (ballState.Pos.Y - paddle.Height + PongPaddleState.PADDLE_HALF_LENGTH) / PongPaddleState.PADDLE_LENGTH;
|
||||||
var upAngle = left ? MathF.PI * 3 / 8 : MathF.PI * 5 / 8;
|
|
||||||
var downAngle = -upAngle;
|
|
||||||
|
|
||||||
|
// 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;
|
ballState.BallAngle = ratio * downAngle + (1 - ratio) * upAngle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PongBallState {
|
public struct PongBallState {
|
||||||
public const float BALL_SPEED = 8;
|
public const float BALL_SPEED = 2 * BALL_RADIUS;
|
||||||
public const float BALL_RADIUS = 2;
|
public const float BALL_RADIUS = HEIGHT / 125;
|
||||||
|
|
||||||
public static readonly PongBallState Initial = new() {
|
public static readonly PongBallState Initial = new() {
|
||||||
BallAngle = 0.0f,
|
BallAngle = 0.0f,
|
||||||
X = WIDTH / 2,
|
Pos = new() {
|
||||||
Y = HEIGHT / 2
|
X = WIDTH / 2,
|
||||||
|
Y = HEIGHT / 2
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public float X;
|
public PointF Pos;
|
||||||
public float Y;
|
|
||||||
public float BallAngle;
|
public float BallAngle;
|
||||||
|
|
||||||
public static void Update(ref PongBallState state) {
|
public static void Update(ref PongBallState state) {
|
||||||
var (dy, dx) = MathF.SinCos(state.BallAngle);
|
var (dy, dx) = MathF.SinCos(state.BallAngle);
|
||||||
state.X += BALL_SPEED * dx;
|
state.Pos.X += BALL_SPEED * dx;
|
||||||
state.Y -= BALL_SPEED * dy;
|
state.Pos.Y -= BALL_SPEED * dy;
|
||||||
|
|
||||||
|
if (state.Pos.Y < BALL_RADIUS
|
||||||
|
|| state.Pos.Y > HEIGHT - BALL_RADIUS)
|
||||||
|
state.BallAngle = 2 * MathF.PI - state.BallAngle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RectangleF GetCollider() => new(X - BALL_RADIUS, Y - BALL_RADIUS, 2 * BALL_RADIUS, 2 * BALL_RADIUS);
|
public RectangleF GetCollider() => new(Pos.X - BALL_RADIUS, Pos.Y - BALL_RADIUS, 2 * BALL_RADIUS, 2 * BALL_RADIUS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct PongPaddleState {
|
public struct PongPaddleState {
|
||||||
public const float PADDLE_LENGTH = HEIGHT / 10;
|
public const float PADDLE_LENGTH = HEIGHT / 10;
|
||||||
public const float PADDLE_HALF_LENGTH = PADDLE_LENGTH / 2;
|
public const float PADDLE_HALF_LENGTH = PADDLE_LENGTH / 2;
|
||||||
public const float PADDLE_WIDTH = PADDLE_LENGTH / 6;
|
public const float PADDLE_WIDTH = PADDLE_LENGTH / 5;
|
||||||
public const float PADDLE_SPEED = 8;
|
public const float PADDLE_SPEED = 8;
|
||||||
|
|
||||||
public static readonly PongPaddleState Initial = new() {
|
public static readonly PongPaddleState Initial = new() {
|
||||||
@ -86,9 +102,9 @@ public struct PongGameState {
|
|||||||
public PongPaddleDirection Direction;
|
public PongPaddleDirection Direction;
|
||||||
|
|
||||||
public static void Update(ref PongPaddleState state) {
|
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);
|
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);
|
public RectangleF GetCollider(float x) => new(x - PADDLE_WIDTH / 2, Height - PADDLE_HALF_LENGTH, PADDLE_WIDTH, PADDLE_LENGTH);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,8 +20,11 @@
|
|||||||
<input id="username" type="text" class="form-control" placeholder="Username" aria-label="Username" aria-describedby="setusername">
|
<input id="username" type="text" class="form-control" placeholder="Username" aria-label="Username" aria-describedby="setusername">
|
||||||
<button id="setusername" class="btn btn-outline-secondary" type="button">Set Username</button>
|
<button id="setusername" class="btn btn-outline-secondary" type="button">Set Username</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="canvas-container" class="mb-3"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script>
|
<script src="~/lib/signalr/dist/browser/signalr.min.js"></script>
|
||||||
<script src="~/lib/signalr/dist/browser/signalr-protocol-msgpack.min.js"></script>
|
<script src="~/lib/signalr/dist/browser/signalr-protocol-msgpack.min.js"></script>
|
||||||
|
<script src="~/lib/pixi/dist/pixi.min.js"></script>
|
||||||
<script src="~/js/pong.js"></script>
|
<script src="~/js/pong.js"></script>
|
@ -21,6 +21,17 @@
|
|||||||
"dist/browser/signalr.min.js",
|
"dist/browser/signalr.min.js",
|
||||||
"dist/browser/signalr.min.js.map"
|
"dist/browser/signalr.min.js.map"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"provider": "jsdelivr",
|
||||||
|
"library": "pixi.js@7.0.2",
|
||||||
|
"destination": "wwwroot/lib/pixi/",
|
||||||
|
"files": [
|
||||||
|
"dist/pixi.js",
|
||||||
|
"dist/pixi.js.map",
|
||||||
|
"dist/pixi.min.js.map",
|
||||||
|
"dist/pixi.min.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -1,7 +1,5 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
console.log("Pong script was run!");
|
|
||||||
|
|
||||||
const connection = new signalR.HubConnectionBuilder()
|
const connection = new signalR.HubConnectionBuilder()
|
||||||
.withUrl("/pong/hub")
|
.withUrl("/pong/hub")
|
||||||
.withAutomaticReconnect()
|
.withAutomaticReconnect()
|
||||||
@ -14,7 +12,7 @@ function getElement(id) {
|
|||||||
|
|
||||||
const connectionStatus = getElement("connection");
|
const connectionStatus = getElement("connection");
|
||||||
const createlobby = getElement("createlobby");
|
const createlobby = getElement("createlobby");
|
||||||
const roomid = getElement("roomid");
|
const roomidinput = getElement("roomid");
|
||||||
const joinroom = getElement("joinroom");
|
const joinroom = getElement("joinroom");
|
||||||
const usernameinput = getElement("username");
|
const usernameinput = getElement("username");
|
||||||
const setusername = getElement("setusername");
|
const setusername = getElement("setusername");
|
||||||
@ -44,7 +42,10 @@ connection.onreconnected(function (connectionId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
createlobby.addEventListener("click", function (event) {
|
createlobby.addEventListener("click", function (event) {
|
||||||
connection.invoke("CreateRoom").catch(function (err) {
|
connection.invoke("CreateRoom").then(function (roomId) {
|
||||||
|
roomidinput.value = roomId;
|
||||||
|
console.info(`Joined room [${roomId}]`);
|
||||||
|
}).catch(function (err) {
|
||||||
return console.error(err.toString());
|
return console.error(err.toString());
|
||||||
});
|
});
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -59,7 +60,7 @@ connection.on("UsernameChanged", function (username) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
joinroom.addEventListener("click", function (event) {
|
joinroom.addEventListener("click", function (event) {
|
||||||
connection.invoke("JoinRoom", roomid.value).catch(function (err) {
|
connection.invoke("JoinRoom", roomidinput.value).catch(function (err) {
|
||||||
return console.error(err.toString());
|
return console.error(err.toString());
|
||||||
});
|
});
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -89,6 +90,76 @@ function moveUp() { return movePaddle(-1); }
|
|||||||
function stopPaddle() { return movePaddle(0); }
|
function stopPaddle() { return movePaddle(0); }
|
||||||
function moveDown() { return movePaddle(1); }
|
function moveDown() { return movePaddle(1); }
|
||||||
|
|
||||||
|
// Create the application helper and add its render target to the page
|
||||||
|
let app = new PIXI.Application({ width: 1000, height: 500 });
|
||||||
|
getElement('canvas-container').appendChild(app.view);
|
||||||
|
|
||||||
|
let graphics = new PIXI.Graphics();
|
||||||
|
app.stage.addChild(graphics);
|
||||||
|
|
||||||
|
function renderPaddle(graphics, state, xMid) {
|
||||||
|
var xLeft = xMid - 5;
|
||||||
|
graphics.beginFill(0x00FFFF);
|
||||||
|
graphics.drawRect(xLeft, state.Height - 25, 10, 50);
|
||||||
|
graphics.endFill();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBall(graphics, state) {
|
||||||
|
graphics.beginFill(0xFF00FF);
|
||||||
|
graphics.drawCircle(state.Pos.X, state.Pos.Y, 4);
|
||||||
|
graphics.endFill();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGameState(graphics, state) {
|
||||||
|
graphics.clear();
|
||||||
|
renderPaddle(graphics, state.Paddle1, 50);
|
||||||
|
renderPaddle(graphics, state.Paddle2, 1000 - 50);
|
||||||
|
renderBall(graphics, state.BallState);
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.on("ReceiveGameState", function (state) {
|
||||||
|
renderGameState(graphics, state);
|
||||||
|
});
|
||||||
|
|
||||||
|
const keyEvent = (function () {
|
||||||
|
var upPressed = false;
|
||||||
|
var downPressed = false;
|
||||||
|
|
||||||
|
function moveUpdated() {
|
||||||
|
if (upPressed == downPressed) stopPaddle();
|
||||||
|
else if (upPressed) moveUp();
|
||||||
|
else if (downPressed) moveDown();
|
||||||
|
else console.error("unknown move!");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handler(event) {
|
||||||
|
if (event.repeat) return;
|
||||||
|
if (event.path.indexOf(document.body) != 0) return; // only use key if it was pressed on empty space
|
||||||
|
var pressed = event.type == "keyup";
|
||||||
|
// W Key is 87, Up arrow is 87
|
||||||
|
// S Key is 83, Down arrow is 40
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 87:
|
||||||
|
case 38:
|
||||||
|
upPressed = pressed;
|
||||||
|
break;
|
||||||
|
case 83:
|
||||||
|
case 40:
|
||||||
|
downPressed = pressed;
|
||||||
|
break;
|
||||||
|
default: return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
moveUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler;
|
||||||
|
})();
|
||||||
|
|
||||||
|
document.addEventListener('keydown', keyEvent);
|
||||||
|
document.addEventListener('keyup', keyEvent);
|
||||||
|
|
||||||
connection.start().then(function () {
|
connection.start().then(function () {
|
||||||
console.info(`Connected!`);
|
console.info(`Connected!`);
|
||||||
connectionStatus.textContent = "Connected!";
|
connectionStatus.textContent = "Connected!";
|
||||||
|
24183
PongGame/wwwroot/lib/pixi/dist/pixi.js
vendored
Normal file
24183
PongGame/wwwroot/lib/pixi/dist/pixi.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
PongGame/wwwroot/lib/pixi/dist/pixi.js.map
vendored
Normal file
1
PongGame/wwwroot/lib/pixi/dist/pixi.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1116
PongGame/wwwroot/lib/pixi/dist/pixi.min.js
vendored
Normal file
1116
PongGame/wwwroot/lib/pixi/dist/pixi.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
PongGame/wwwroot/lib/pixi/dist/pixi.min.js.map
vendored
Normal file
1
PongGame/wwwroot/lib/pixi/dist/pixi.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user