Skip to content

Commit

Permalink
Add WebSocket support and refactoring (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
devfacet authored Mar 17, 2024
1 parent 2870ae7 commit 5f8309f
Show file tree
Hide file tree
Showing 18 changed files with 873 additions and 106 deletions.
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@
]
},
"cSpell.words": [
"bufferutil",
"dotp",
"gamepadconnected",
"gamepaddisconnected"
"gamepaddisconnected",
"pino"
]
}
6 changes: 3 additions & 3 deletions app/components/game.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
text-align: center;
box-sizing: border-box;
min-width: 120px;
min-height: 44px;
min-height: 54px;
}

.gameWrapper .playerControls {
Expand All @@ -80,7 +80,7 @@
border-radius: 5px;
text-align: center;
min-height: 120px;
min-width: 44px;
min-width: 54px;
}

.gameWrapper .playerControls .item .shortcut {
Expand All @@ -107,7 +107,7 @@
text-align: center;
box-sizing: border-box;
min-width: 120px;
min-height: 44px;
min-height: 54px;
}

.gameWrapper .gameControls .item .shortcut {
Expand Down
35 changes: 22 additions & 13 deletions app/components/game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ import { Game } from '@/lib/game'
import { MdArrowUpward, MdArrowDownward } from 'react-icons/md'
import { useTheme } from 'next-themes'
import { useStopwatch } from 'react-timer-hook'
// import toast from 'react-hot-toast'
import '@/components/game.css'

// Init vars
const isGamepadSupported = typeof window !== 'undefined' && window.navigator.getGamepads !== undefined

// Props represents the component props.
type Props = {
wsAddress?: string
}

// GameComponent represents a game component.
export default function GameComponent() {
export default function GameComponent(props: Props) {
const { wsAddress } = props

const { setTheme } = useTheme()
const canvasRef = useRef<HTMLCanvasElement>(null)
const gameServiceRef = useRef<GameService>()
Expand Down Expand Up @@ -59,7 +65,7 @@ export default function GameComponent() {

// onGameOver handles the game over event.
const onGameOver = useCallback((game: Game) => {
console.debug('GameComponent.onTogglePause', game.getId(), game.getGameState(), game.getWinner())
console.debug('GameComponent.onGameOver', game.getId(), game.getGameState(), game.getWinner())

if (game.getWinner() === 'dark') {
setScore((prevScore) => ({ ...prevScore, dark: prevScore.dark + 1 }))
Expand All @@ -76,7 +82,9 @@ export default function GameComponent() {
if (gameServiceRef.current) {
if (document.visibilityState !== 'visible') {
stopwatchControlsRef.current.pause()
gameServiceRef.current.pause()
if (gameServiceRef.current.getGameState() === 'running') {
gameServiceRef.current.pause()
}
}
}
}, [])
Expand All @@ -92,14 +100,15 @@ export default function GameComponent() {
gameServiceRef.current = new GameService({
canvasElement: canvasRef.current,
gamepadEnabled: isGamepadSupported,
wsAddress: wsAddress,
onNewGame: onNewGame,
onTogglePause: onTogglePause,
onGameOver: onGameOver,
})
gameServiceRef.current.handleMouseEvent('controlW', 'w', 'dark')
gameServiceRef.current.handleMouseEvent('controlS', 's', 'dark')
gameServiceRef.current.handleMouseEvent('controlO', 'o', 'light')
gameServiceRef.current.handleMouseEvent('controlL', 'l', 'light')
gameServiceRef.current.handleMouseEventByElementId('controlDarkUp', 'dark', 'up')
gameServiceRef.current.handleMouseEventByElementId('controlDarkDown', 'dark', 'down')
gameServiceRef.current.handleMouseEventByElementId('controlLightUp', 'light', 'up')
gameServiceRef.current.handleMouseEventByElementId('controlLightDown', 'light', 'down')

// Splash screen
if (showSplash) {
Expand All @@ -114,7 +123,7 @@ export default function GameComponent() {
gameServiceRef.current?.destroy()
document.removeEventListener('visibilitychange', onVisibilityChange)
}
}, [showSplash, onNewGame, onTogglePause, onGameOver, onVisibilityChange, setTheme])
}, [wsAddress, showSplash, onNewGame, onTogglePause, onGameOver, onVisibilityChange, setTheme])

return (
<div className="gameWrapper">
Expand All @@ -126,8 +135,8 @@ export default function GameComponent() {
</div>
<div className="row">
<div className="playerControls">
<div id="controlW" className="item hover:cursor-pointer bg-light dark:bg-dark"><MdArrowUpward /><span className="shortcut">W</span></div>
<div id="controlS" className="item hover:cursor-pointer bg-light dark:bg-dark"><span className="shortcut">S</span><MdArrowDownward /></div>
<div id="controlDarkUp" className="item hover:cursor-pointer bg-light dark:bg-dark"><MdArrowUpward /><span className="shortcut">W</span></div>
<div id="controlDarkDown" className="item hover:cursor-pointer bg-light dark:bg-dark"><span className="shortcut">S</span><MdArrowDownward /></div>
</div>
<div className="canvasContainer">
<canvas
Expand All @@ -145,8 +154,8 @@ export default function GameComponent() {
/>
</div>
<div className="playerControls">
<div id="controlO" className="item hover:cursor-pointer bg-light dark:bg-dark"><MdArrowUpward /><span className="shortcut">O</span></div>
<div id="controlL" className="item hover:cursor-pointer bg-light dark:bg-dark"><span className="shortcut">L</span><MdArrowDownward /></div>
<div id="controlLightUp" className="item hover:cursor-pointer bg-light dark:bg-dark"><MdArrowUpward /><span className="shortcut">O</span></div>
<div id="controlLightDown" className="item hover:cursor-pointer bg-light dark:bg-dark"><span className="shortcut">L</span><MdArrowDownward /></div>
</div>
</div>
<div className="row">
Expand Down
22 changes: 16 additions & 6 deletions app/lib/ball.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// For the full copyright and license information, please view the LICENSE.txt file.

import { Game, PlayerSide } from '@/lib/game'
import { Game, Side, PlayerSide } from '@/lib/game'

// BallOptions represents the options to create a new ball.
export interface BallOptions {
game: Game
side: PlayerSide
playerSide: PlayerSide
x: number
y: number
speedX: number
Expand All @@ -21,7 +21,8 @@ export class Ball {

private options: BallOptions
private game: Game
private side: PlayerSide
private side: Side
private playerSide: PlayerSide
private x: number
private y: number
private speedX: number
Expand All @@ -31,11 +32,15 @@ export class Ball {

// constructor creates a new instance.
constructor(options: BallOptions) {
const { game, side, x, y, speedX, speedY, radius, color } = options
const { game, playerSide, x, y, speedX, speedY, radius, color } = options

this.options = options
this.game = game
this.side = side
this.playerSide = playerSide
// Light ball collides with the light grid cells and dark ball collides with the dark grid cells.
// Hence we launch light ball at the dark side and dark ball at the light side,
// so that balls can collide with the opposite side grid cells.
this.side = playerSide === 'dark' ? 'light' : 'dark'
this.x = x
this.y = y
this.speedX = speedX
Expand Down Expand Up @@ -79,9 +84,14 @@ export class Ball {
return this.speedY
}

// getSide returns the side of the ball.
public getSide(): Side {
return this.side
}

// getPlayerSide returns the player side of the paddle.
public getPlayerSide(): PlayerSide {
return this.side
return this.playerSide
}

// setX sets the x position of the ball.
Expand Down
64 changes: 48 additions & 16 deletions app/lib/collision.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
// For the full copyright and license information, please view the LICENSE.txt file.

import { Game } from '@/lib/game'
import { Game, PlayerSide } from '@/lib/game'
import { Ball } from '@/lib/ball'
import { Paddle } from '@/lib/paddle'

// CollisionManagerOptions represents the options to create a new collision manager.
export interface CollisionManagerOptions {
game: Game
onBallToBoundaryCollision?: (collision: BallToBoundaryCollision) => void
onBallToPaddleCollision?: (collision: BallToPaddleCollision) => void
onBallToGridCollision?: (collision: BallToGridCollision) => void
}

// CollisionManager manages collisions between game components.
export class CollisionManager {
private options: CollisionManagerOptions
private game: Game
private onBallToBoundaryCollision?: (collision: BallToBoundaryCollision) => void
private onBallToPaddleCollision?: (collision: BallToPaddleCollision) => void
private onBallToGridCollision?: (collision: BallToGridCollision) => void

// constructor creates a new instance.
constructor(options: CollisionManagerOptions) {
const { game } = options

this.options = options
this.game = game
this.onBallToBoundaryCollision = options.onBallToBoundaryCollision
this.onBallToPaddleCollision = options.onBallToPaddleCollision
this.onBallToGridCollision = options.onBallToGridCollision
}

// reset resets the collision manager.
Expand All @@ -38,11 +47,10 @@ export class CollisionManager {
// Check for collision between the ball and boundaries
const b2b = this.checkBallToBoundaryCollision(ball, ballFutureX, ballFutureY)
if (b2b.collided) {
if (this.onBallToBoundaryCollision) this.onBallToBoundaryCollision(b2b)

if (b2b.oppositeSide) {
// Note that we launch light ball at the dark side and dark ball at the light side,
// so that balls can collide with the opposite side boundary.
// Hence we reverse the player side here.
this.game.gameOver(ball.getPlayerSide() === 'dark' ? 'light' : 'dark')
this.game.gameOver(ball.getPlayerSide())
return
}
if (b2b.speedX) ball.setSpeedX(b2b.speedX)
Expand All @@ -55,6 +63,8 @@ export class CollisionManager {
for (let j = 0, m = paddles.length; j < m; j++) {
const b2p = this.checkBallToPaddleCollision(ball, paddles[j], ballFutureX, ballFutureY)
if (b2p.collided) {
if (this.onBallToPaddleCollision) this.onBallToPaddleCollision(b2p)

if (b2p.speedX) ball.setSpeedX(b2p.speedX)
if (b2p.speedY) ball.setSpeedY(b2p.speedY)
if (b2p.futureX) ball.setX(b2p.futureX)
Expand All @@ -80,14 +90,16 @@ export class CollisionManager {
// Check for collisions between the balls and the grid
const b2g = this.checkBallToGridCollision(ball, ballFutureX, ballFutureY)
if (b2g.collided) {
if (this.onBallToGridCollision) this.onBallToGridCollision(b2g)

if (b2g.speedX) ball.setSpeedX(b2g.speedX)
if (b2g.speedY) ball.setSpeedY(b2g.speedY)
if (b2g.futureX) ball.setX(b2g.futureX)
if (b2g.futureY) ball.setY(b2g.futureY)
if (b2g.cells) {
for (let k = 0, n = b2g.cells.length; k < n; k++) {
const cell = b2g.cells[k]
this.game.getGrid().setCell(cell[0], cell[1], ball.getPlayerSide() === 'dark' ? 'light' : 'dark')
this.game.getGrid().setCell(cell[0], cell[1], ball.getPlayerSide())
}
}
}
Expand All @@ -109,18 +121,24 @@ export class CollisionManager {
collision.speedX = -ball.getSpeedX()
collision.futureX = radius // for avoiding sticking

// Check if the ball is on the opposite side of the boundary
if (ball.getPlayerSide() === 'dark') {
// ballX - radius <= 0 means the ball is on the left side of the boundary (dark side)
if (ball.getPlayerSide() === 'light') {
collision.oppositeSide = true
} else {
collision.ownSide = true
}
} else if (ballX + radius >= canvasWidth) {
collision.collided = true
collision.speedX = -ball.getSpeedX()
collision.futureX = canvasWidth - radius // for avoiding sticking

// Check if the ball is on the opposite side of the boundary
if (ball.getPlayerSide() === 'light') {
// ballX + radius >= canvasWidth means the ball is on the right side of the boundary (light side)
// If the ball side is light (own by the dark side) then
// it's on the opposite side of the boundary, since we launch light ball at the dark side.
if (ball.getPlayerSide() === 'dark') {
collision.oppositeSide = true
} else {
collision.ownSide = true
}
}

Expand All @@ -135,6 +153,10 @@ export class CollisionManager {
collision.futureY = canvasHeight - radius // for avoiding sticking
}

if (collision.collided) {
collision.playerSide = ball.getPlayerSide()
}

return collision
}

Expand Down Expand Up @@ -174,6 +196,10 @@ export class CollisionManager {
return collision
}

// Set the collision properties
collision.playerSide = ball.getPlayerSide()
collision.paddlePlayerSide = paddle.getPlayerSide()

// Calculate the angle of the collision
let collidePoint = ball.getY() - (paddle.getY() + paddle.getHeight() / 2)
collidePoint = collidePoint / (paddle.getHeight() / 2)
Expand Down Expand Up @@ -235,11 +261,12 @@ export class CollisionManager {

// Check if the ball is within the grid bounds
if (gridY >= 0 && gridY < this.game.getGrid().getRowLength() && gridX >= 0 && gridX < this.game.getGrid().getColLength()) {
const cellPlayerSide = this.game.getGrid().getCell(gridX, gridY)
const cellSide = this.game.getGrid().getCell(gridX, gridY)

// Check for collision with a cell of the same player side
if (cellPlayerSide === ball.getPlayerSide()) {
// If the ball side is the same as the cell side then it's a collision
if (cellSide === ball.getSide()) {
collision.collided = true
collision.playerSide = ball.getPlayerSide()
collision.speedX = -ball.getSpeedX()
collision.speedY = -ball.getSpeedY()
collision.futureX = ballX - ball.getSpeedX() * this.game.getSinceLastFrame()
Expand All @@ -255,22 +282,26 @@ export class CollisionManager {
}

// BallToBoundaryCollision represents a collision between a ball and a boundary.
type BallToBoundaryCollision = {
export type BallToBoundaryCollision = {
collided: boolean
speedX?: number
speedY?: number
futureX?: number
futureY?: number
playerSide?: PlayerSide
oppositeSide?: boolean
ownSide?: boolean
}

// BallToPaddleCollision represents a collision between a ball and a paddle.
type BallToPaddleCollision = {
export type BallToPaddleCollision = {
collided: boolean
speedX?: number
speedY?: number
futureX?: number
futureY?: number
playerSide?: PlayerSide
paddlePlayerSide?: PlayerSide
}

// BallToBallCollision represents a collision between two balls.
Expand All @@ -283,11 +314,12 @@ type BallToBallCollision = {
}

// BallToGridCollision represents a collision between a ball and a grid cell.
type BallToGridCollision = {
export type BallToGridCollision = {
collided: boolean
speedX?: number
speedY?: number
futureX?: number
futureY?: number
cells?: number[][]
playerSide?: PlayerSide
}
Loading

0 comments on commit 5f8309f

Please sign in to comment.