Browse Source

* Added map customization

master
vvandenb 2 years ago
parent
commit
4d75fec2d1
  1. 25
      back/volume/src/pong/dtos/GameCreationDto.ts
  2. 8
      back/volume/src/pong/dtos/PlayerNamesDto.ts
  3. 21
      back/volume/src/pong/game/Ball.ts
  4. 46
      back/volume/src/pong/game/Game.ts
  5. 32
      back/volume/src/pong/game/Games.ts
  6. 15
      back/volume/src/pong/game/Map.ts
  7. 3
      back/volume/src/pong/game/constants.ts
  8. 21
      back/volume/src/pong/pong.gateway.ts
  9. 2
      back/volume/src/pong/pong.spec.ts
  10. 12
      front/volume/src/components/Pong/Game.ts
  11. 11
      front/volume/src/components/Pong/Map.ts
  12. 82
      front/volume/src/components/Pong/MapCustomization.svelte
  13. 78
      front/volume/src/components/Pong/Pong.svelte
  14. 3
      front/volume/src/components/Pong/constants.ts
  15. 6
      front/volume/src/components/Pong/dtos/GameCreationDto.ts

25
back/volume/src/pong/dtos/GameCreationDto.ts

@ -0,0 +1,25 @@
import { Type } from 'class-transformer'
import {
ArrayMaxSize,
ArrayMinSize,
IsDefined,
IsNotEmptyObject,
IsObject,
IsString,
ValidateNested
} from 'class-validator'
import { Map } from '../game/Map'
export class GameCreationDto {
@IsString({ each: true })
@ArrayMaxSize(2)
@ArrayMinSize(2)
playerNames!: string[]
@IsDefined()
@IsObject()
@IsNotEmptyObject()
@ValidateNested()
@Type(() => Map)
map!: Map
}

8
back/volume/src/pong/dtos/PlayerNamesDto.ts

@ -1,8 +0,0 @@
import { ArrayMaxSize, ArrayMinSize, IsString } from 'class-validator'
export class PlayerNamesDto {
@IsString({ each: true })
@ArrayMaxSize(2)
@ArrayMinSize(2)
playerNames!: string[]
}

21
back/volume/src/pong/game/Ball.ts

@ -1,6 +1,7 @@
import { gameInfoConstants } from './constants'
import { type Paddle } from './Paddle'
import { Point, Rect } from './utils'
import { type Map } from './Map'
export class Ball {
rect: Rect
@ -23,16 +24,16 @@ export class Ball {
return this.indexPlayerScored
}
update (canvasRect: Rect, paddles: Paddle[]): void {
update (canvasRect: Rect, paddles: Paddle[], map: Map): void {
if (!canvasRect.contains_x(this.rect)) {
this.indexPlayerScored = this.score()
this.indexPlayerScored = this.playerScored()
} else {
this.indexPlayerScored = -1
this.move(canvasRect, paddles)
this.move(canvasRect, paddles, map)
}
}
move (canvasRect: Rect, paddles: Paddle[]): void {
move (canvasRect: Rect, paddles: Paddle[], map: Map): void {
for (const paddle of paddles) {
if (paddle.rect.collides(this.rect)) {
if (this.speed.x < 0) {
@ -45,12 +46,20 @@ export class Ball {
break
}
}
for (const wall of map.walls) {
if (wall.collides(this.rect)) {
this.speed.x = this.speed.x * -1
this.speed.y = this.speed.y * -1
break
}
}
if (!canvasRect.contains_y(this.rect)) this.speed.y = this.speed.y * -1
this.rect.center.add_inplace(this.speed)
}
// A player scored: return his index and reposition the ball
score (): number {
playerScored (): number {
let indexPlayerScored: number
if (this.rect.center.x <= this.spawn.x) {
indexPlayerScored = 1

46
back/volume/src/pong/game/Game.ts

@ -10,17 +10,20 @@ import {
} from './constants'
import { randomUUID } from 'crypto'
import { Spectator } from './Spectator'
import { type Map } from './Map'
const GAME_TICKS = 30
function gameLoop (game: Game): void {
const canvasRect = new Rect(
new Point(gameInfoConstants.mapSize.x / 2, gameInfoConstants.mapSize.y / 2),
new Point(gameInfoConstants.mapSize.x, gameInfoConstants.mapSize.y)
const canvasRect: Rect = new Rect(
new Point(game.map.size.x / 2, game.map.size.y / 2),
new Point(game.map.size.x, game.map.size.y)
)
game.ball.update(
canvasRect,
game.players.map((p) => p.paddle)
game.players.map((p) => p.paddle),
game.map
)
const indexPlayerScored: number = game.ball.getIndexPlayerScored()
if (indexPlayerScored !== -1) {
@ -46,21 +49,23 @@ function gameLoop (game: Game): void {
export class Game {
id: string
timer: NodeJS.Timer | null
map: Map
ball: Ball
players: Player[] = []
spectators: Spectator[] = []
playing: boolean
constructor (sockets: WebSocket[], uuids: string[], names: string[]) {
constructor (
sockets: WebSocket[],
uuids: string[],
names: string[],
map: Map
) {
this.id = randomUUID()
this.timer = null
this.playing = false
this.ball = new Ball(
new Point(
gameInfoConstants.mapSize.x / 2,
gameInfoConstants.mapSize.y / 2
)
)
this.map = map
this.ball = new Ball(new Point(this.map.size.x / 2, this.map.size.y / 2))
for (let i = 0; i < uuids.length; i++) {
this.addPlayer(sockets[i], uuids[i], names[i])
}
@ -70,8 +75,10 @@ export class Game {
const yourPaddleIndex = this.players.findIndex((p) => p.name === name)
return {
...gameInfoConstants,
mapSize: this.map.size,
yourPaddleIndex,
gameId: this.id
gameId: this.id,
walls: this.map.walls
}
}
@ -83,16 +90,16 @@ export class Game {
private addPlayer (socket: WebSocket, uuid: string, name: string): void {
let paddleCoords = new Point(
gameInfoConstants.playerXOffset,
gameInfoConstants.mapSize.y / 2
this.map.size.y / 2
)
if (this.players.length === 1) {
paddleCoords = new Point(
gameInfoConstants.mapSize.x - gameInfoConstants.playerXOffset,
gameInfoConstants.mapSize.y / 2
this.map.size.x - gameInfoConstants.playerXOffset,
this.map.size.y / 2
)
}
this.players.push(
new Player(socket, uuid, name, paddleCoords, gameInfoConstants.mapSize)
new Player(socket, uuid, name, paddleCoords, this.map.size)
)
}
@ -119,12 +126,7 @@ export class Game {
private start (): boolean {
if (this.timer === null && this.players.length === 2) {
this.ball = new Ball(
new Point(
gameInfoConstants.mapSize.x / 2,
gameInfoConstants.mapSize.y / 2
)
)
this.ball = new Ball(new Point(this.map.size.x / 2, this.map.size.y / 2))
this.players.forEach((p) => {
p.newGame()
})

32
back/volume/src/pong/pong.ts → back/volume/src/pong/game/Games.ts

@ -1,18 +1,28 @@
import { type WebSocket } from 'ws'
import { type GameInfo } from './game/constants'
import { Game } from './game/Game'
import { type Point } from './game/utils'
import { gameInfoConstants } from './game/constants'
import { type GameInfo } from './constants'
import { Game } from './Game'
import { Point } from './utils'
import { gameInfoConstants } from './constants'
import { type Map as GameMap } from './Map'
import { type GameCreationDto } from '../dtos/GameCreationDto'
export class Games {
private readonly playerNameToGameIndex = new Map<string, number>()
private readonly games = new Array<Game>()
newGame (sockets: WebSocket[], uuids: string[], names: string[]): void {
this.games.push(new Game(sockets, uuids, names))
this.playerNameToGameIndex.set(names[0], this.games.length - 1)
this.playerNameToGameIndex.set(names[1], this.games.length - 1)
console.log(`Created game ${names[0]} vs ${names[1]}`)
newGame (
sockets: WebSocket[],
uuids: string[],
gameCreationDto: GameCreationDto
): void {
const names: string[] = gameCreationDto.playerNames
const map: GameMap = gameCreationDto.map
if (!this.isInAGame(names[0]) && !this.isInAGame(names[1])) {
this.games.push(new Game(sockets, uuids, names, map))
this.playerNameToGameIndex.set(names[0], this.games.length - 1)
this.playerNameToGameIndex.set(names[1], this.games.length - 1)
console.log(`Created game ${names[0]} vs ${names[1]}`)
}
}
removePlayer (name: string): void {
@ -45,7 +55,9 @@ export class Games {
return {
...gameInfoConstants,
yourPaddleIndex: 0,
gameId: ''
gameId: '',
mapSize: new Point(0, 0),
walls: []
}
}

15
back/volume/src/pong/game/Map.ts

@ -0,0 +1,15 @@
import { Type } from 'class-transformer'
import { ArrayMaxSize, IsArray, IsDefined, IsObject } from 'class-validator'
import { Point, Rect } from './utils'
export class Map {
@IsObject()
@IsDefined()
@Type(() => Point)
size!: Point
@IsArray()
@ArrayMaxSize(5)
@Type(() => Rect)
walls!: Rect[]
}

3
back/volume/src/pong/game/constants.ts

@ -1,4 +1,4 @@
import { Point } from './utils'
import { Point, type Rect } from './utils'
export const GAME_EVENTS = {
START_GAME: 'START_GAME',
@ -14,6 +14,7 @@ export const GAME_EVENTS = {
export interface GameInfo extends GameInfoConstants {
yourPaddleIndex: number
gameId: string
walls: Rect[]
}
export interface GameInfoConstants {
mapSize: Point

21
back/volume/src/pong/pong.gateway.ts

@ -8,10 +8,10 @@ import {
WebSocketGateway
} from '@nestjs/websockets'
import { randomUUID } from 'crypto'
import { Games } from './pong'
import { formatWebsocketData, Point } from './game/utils'
import { Games } from './game/Games'
import { formatWebsocketData, Point, Rect } from './game/utils'
import { GAME_EVENTS } from './game/constants'
import { PlayerNamesDto } from './dtos/PlayerNamesDto'
import { GameCreationDto } from './dtos/GameCreationDto'
import { UsePipes, ValidationPipe } from '@nestjs/common'
import { type Game } from './game/Game'
@ -82,20 +82,27 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
createGame (
@ConnectedSocket()
client: WebSocketWithId,
@MessageBody() playerNamesDto: PlayerNamesDto
@MessageBody() gameCreationDto: GameCreationDto
): void {
gameCreationDto.map.walls = gameCreationDto.map.walls.map((wall) => {
return new Rect(
new Point(wall.center.x, wall.center.y),
new Point(wall.size.x, wall.size.y)
)
})
if (this.socketToPlayerName.size >= 2) {
const player1Socket: WebSocketWithId | undefined = Array.from(
this.socketToPlayerName.keys()
).find(
(key) =>
this.socketToPlayerName.get(key) === playerNamesDto.playerNames[0]
this.socketToPlayerName.get(key) === gameCreationDto.playerNames[0]
)
const player2Socket: WebSocketWithId | undefined = Array.from(
this.socketToPlayerName.keys()
).find(
(key) =>
this.socketToPlayerName.get(key) === playerNamesDto.playerNames[1]
this.socketToPlayerName.get(key) === gameCreationDto.playerNames[1]
)
if (
@ -107,7 +114,7 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
this.games.newGame(
[player1Socket, player2Socket],
[player1Socket.id, player2Socket.id],
playerNamesDto.playerNames
gameCreationDto
)
}
}

2
back/volume/src/pong/pong.spec.ts

@ -1,5 +1,5 @@
import { Test, type TestingModule } from '@nestjs/testing'
import { Games } from './pong'
import { Games } from './game/Games'
describe('Pong', () => {
let provider: Games

12
front/volume/src/components/Pong/Game.ts

@ -3,7 +3,7 @@ import { GAME_EVENTS } from "./constants";
import type { GameInfo, GameUpdate } from "./constants";
import { Paddle } from "./Paddle";
import { Player } from "./Player";
import { formatWebsocketData, Point } from "./utils";
import { formatWebsocketData, Point, Rect } from "./utils";
const BG_COLOR = "black";
@ -14,12 +14,14 @@ export class Game {
players: Player[];
my_paddle: Paddle;
id: string;
walls: Rect[];
constructor(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D) {
this.canvas = canvas;
this.context = context;
this.players = [];
this.my_paddle = null;
this.walls = [];
}
setInfo(data: GameInfo) {
@ -43,6 +45,13 @@ export class Game {
if (data.yourPaddleIndex != -1)
this.my_paddle = this.players[data.yourPaddleIndex].paddle;
this.id = data.gameId;
this.walls = data.walls.map(
(w) =>
new Rect(
new Point(w.center.x, w.center.y),
new Point(w.size.x, w.size.y)
)
);
}
start(socket: WebSocket) {
@ -76,6 +85,7 @@ export class Game {
this.context.fillStyle = BG_COLOR;
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.walls.forEach((w) => w.draw(this.context, "white"));
this.players.forEach((p) => p.draw(this.context));
this.ball.draw(this.context);

11
front/volume/src/components/Pong/Map.ts

@ -0,0 +1,11 @@
import type { Point, Rect } from "./utils";
export class Map {
size: Point;
walls: Rect[];
constructor(size: Point, walls: Rect[]) {
this.size = size;
this.walls = walls;
}
}

82
front/volume/src/components/Pong/MapCustomization.svelte

@ -0,0 +1,82 @@
<script lang="ts">
import { onMount } from "svelte";
import { Point, Rect } from "./utils";
import type { Map } from "./Map";
import { gameInfoConstants } from "./constants";
export let map: Map;
let canvas: HTMLCanvasElement;
let context: CanvasRenderingContext2D;
let wallWidth = 20;
let wallHeight = 80;
onMount(() => {
if (canvas) {
canvas.width = map.size.x;
canvas.height = map.size.y;
context = canvas.getContext("2d");
drawMap();
}
});
$: {
if (canvas) {
canvas.width = map.size.x;
canvas.height = map.size.y;
}
drawMap();
}
function drawMap() {
if (canvas && context) {
context.fillStyle = "black";
context.fillRect(0, 0, map.size.x, map.size.y);
for (const wall of map.walls) {
wall.draw(context, "white");
}
}
}
function click(e: MouseEvent, rightClick: boolean) {
if (rightClick) removeWall(e);
else addWall(e);
drawMap();
}
function addWall(e: MouseEvent) {
const wall = new Rect(
new Point(e.offsetX, e.offsetY),
new Point(wallWidth, wallHeight)
);
const ballSpawn = new Rect(
new Point(map.size.x / 2, map.size.y / 2),
new Point(
gameInfoConstants.ballSize.x * 5,
gameInfoConstants.ballSize.y * 5
)
);
if (map.walls.length < 5 && !wall.collides(ballSpawn)) map.walls.push(wall);
}
function removeWall(e: MouseEvent) {
e.preventDefault();
const click = new Rect(new Point(e.offsetX, e.offsetY), new Point(1, 1));
const index = map.walls.findIndex((w) => w.collides(click));
if (index != -1) map.walls.splice(index, 1);
}
</script>
<div>
<h2>Map Customization:</h2>
<div>
Width:
<input type="range" min="400" max="1000" bind:value={map.size.x} />
Height:
<input type="range" min="300" max="800" bind:value={map.size.y} />
</div>
<canvas
bind:this={canvas}
on:click={(e) => click(e, false)}
on:contextmenu={(e) => click(e, true)}
/>
</div>

78
front/volume/src/components/Pong/Pong.svelte

@ -1,7 +1,10 @@
<script lang="ts">
import { GAME_EVENTS } from "./constants";
import { gameInfoConstants, GAME_EVENTS } from "./constants";
import type { GameCreationDto } from "./dtos/GameCreationDto";
import { Game } from "./Game";
import { formatWebsocketData } from "./utils";
import MapCustomization from "./MapCustomization.svelte";
import { formatWebsocketData, Point } from "./utils";
import { Map } from "./Map";
const FPS = 144;
const SERVER_URL = "ws://localhost:3001";
@ -12,6 +15,10 @@
let username: string = "John";
let otherUsername: string = "Garfield";
let spectateUsername: string = "Garfield";
let map: Map = new Map(
new Point(gameInfoConstants.mapSize.x, gameInfoConstants.mapSize.y),
[]
);
//Get canvas and its context
window.onload = () => {
@ -87,40 +94,43 @@
}
function createGame() {
socket.send(
formatWebsocketData(GAME_EVENTS.CREATE_GAME, {
playerNames: [username, otherUsername],
})
);
const gameCreationDto: GameCreationDto = {
playerNames: [username, otherUsername],
map,
};
socket.send(formatWebsocketData(GAME_EVENTS.CREATE_GAME, gameCreationDto));
}
</script>
<div>
{#if connected}
Your name:
<input bind:value={username} />
<br />
<button on:click={logIn}> Log in </button>
<br />
Other player name:
<input bind:value={otherUsername} disabled={!loggedIn} />
<br />
<button on:click={createGame} disabled={!loggedIn}>
Create game vs {otherUsername}
</button>
<br />
<button
on:click={() => socket.send(formatWebsocketData(GAME_EVENTS.READY))}
disabled={!loggedIn}>Ready</button
>
<br />
<input bind:value={spectateUsername} disabled={!loggedIn} />
<button on:click={spectate} disabled={!loggedIn}
>Spectate {spectateUsername}</button
>
<br />
{:else}
Connecting to game server...
{/if}
<canvas id="pong_canvas" />
<div>
{#if connected}
Your name:
<input bind:value={username} />
<br />
<button on:click={logIn}> Log in </button>
<br />
Other player name:
<input bind:value={otherUsername} disabled={!loggedIn} />
<br />
<button on:click={createGame} disabled={!loggedIn}>
Create game vs {otherUsername}
</button>
<br />
<button
on:click={() => socket.send(formatWebsocketData(GAME_EVENTS.READY))}
disabled={!loggedIn}>Ready</button
>
<br />
<input bind:value={spectateUsername} disabled={!loggedIn} />
<button on:click={spectate} disabled={!loggedIn}
>Spectate {spectateUsername}</button
>
<br />
{:else}
Connecting to game server...
{/if}
<canvas id="pong_canvas" />
</div>
<MapCustomization {map} />
</div>

3
front/volume/src/components/Pong/constants.ts

@ -1,4 +1,4 @@
import { Point } from "./utils";
import { Point, Rect } from "./utils";
export const GAME_EVENTS = {
START_GAME: "START_GAME",
@ -14,6 +14,7 @@ export const GAME_EVENTS = {
export interface GameInfo extends GameInfoConstants {
yourPaddleIndex: number;
gameId: string;
walls: Rect[];
}
export interface GameInfoConstants {
mapSize: Point;

6
front/volume/src/components/Pong/dtos/GameCreationDto.ts

@ -0,0 +1,6 @@
import type { Map } from "../Map";
export class GameCreationDto {
playerNames: string[];
map: Map;
}
Loading…
Cancel
Save