Move Left:
AMove Right:
DShoot Ball:
SpacebarPause:
Escimport { BreakoutActions } from "~/helpers/breakout/actions";
import { Ball } from "~/helpers/breakout/entities/Ball";
import { BlockSpawner } from "~/helpers/breakout/entities/BlockSpawner";
import { Paddle } from "~/helpers/breakout/entities/Paddle";
import { PauseScreen } from "~/helpers/breakout/entities/PauseScreen";
import { ScoreText } from "~/helpers/breakout/entities/ScoreText";
import { ScreenFPS } from "~/helpers/breakout/entities/ScreenFPS";
import { GameEngine, SVGD3Selection } from "~/helpers/svgame/Game";
export const makeBreakoutGame = (svg: SVGD3Selection) => {
const game = new GameEngine({ svg: svg });
game.input.registerAction(BreakoutActions.ShootBall, " ");
game.input.registerAction(BreakoutActions.MoveRight, "d");
game.input.registerAction(BreakoutActions.MoveLeft, "a");
game.input.registerAction(BreakoutActions.Reset, "r");
game.addEntities([
new PauseScreen(game),
new ScreenFPS(game),
new Paddle(game, "paddle"),
new BlockSpawner(game, "block-spawner"),
new Ball(game, "ball"),
new ScoreText(game),
]);
return game;
};
export enum BreakoutEvents {
BallShot = "BALL_SHOT",
BlockBroken = "BLOCK_BROKEN",
BlockHit = "BLOCK_HIT",
PaddleHit = "PADDLE_HIT",
Death = "DEATH",
Lose = "LOSE",
Win = "WIN",
}
export enum BreakoutActions {
MoveLeft = "MOVE_LEFT",
MoveRight = "MOVE_RIGHT",
Pause = "PAUSE",
ShootBall = "SHOOT_BALL",
Reset = "RESET",
}
import { Vec2 } from "gl-matrix/dist/esm";
import { BreakoutActions } from "~/helpers/breakout/actions";
import { Block } from "~/helpers/breakout/entities/Block";
import { Paddle } from "~/helpers/breakout/entities/Paddle";
import { BreakoutEvents } from "~/helpers/breakout/events";
import { Entity } from "~/helpers/svgame";
import { drawCircles } from "~/helpers/svgame/lib/drawing";
export class Ball extends Entity {
position = new Vec2(250, 250);
radius = 15;
velocity = new Vec2();
attached = true;
speed = 10;
setup() {
// Ball Shooting Event
this.game.eventBus.on(BreakoutEvents.BallShot, this.shoot);
this.game.eventBus.on(BreakoutEvents.BlockHit, this.blockHit);
this.game.eventBus.on(BreakoutEvents.PaddleHit, this.paddleHit);
}
begin(timestamp: number, delta: number) {
// Handle Reset Action
if (this.game.input.isActionJustPressed(BreakoutActions.Reset)) {
this.reset();
}
}
update = (delta: number) => {
// Bounce Back if hit top
if (this.position.y <= this.radius) {
this.wallHit();
}
// Bounce back if hit walls
if (
this.position.x <= this.radius ||
this.position.x >= this.game.resolution.x
) {
this.wallHit();
}
// Reset if hits bottom
if (this.position.y >= this.game.resolution.y) this.reset();
// If we're attached update the ball positions
if (this.attached) {
this.updateBallPosition();
}
this.velocity.normalize().scale(this.speed);
// Add velocity always each frame
this.position.add(this.velocity);
};
draw(interpolationPercentage: number) {
drawCircles({
className: "ball",
fill: "#fff",
radius: this.radius,
svg: this.game.svg,
positions: [this.position],
});
}
// reset it to its default state
reset = () => {
this.game.eventBus.emit(BreakoutEvents.Death);
this.attached = true;
this.velocity.set([0, 0]);
};
// Shoot the ball up transferring the velocity of the paddle
private shoot = (paddleVelocity: Vec2) => {
if (this.attached) {
this.velocity.add([0, -1 * this.speed]).add(paddleVelocity);
}
this.attached = false;
};
// Update the position of the ball, so it's on the paddle
private updateBallPosition = () => {
this.position = new Vec2(
this.game.getEntity<Paddle>("paddle")!.position,
).sub([0, 30]);
};
private blockHit = (block: Block) => {
this.velocity.negate();
};
private paddleHit = (paddle: Paddle) => {
// This makes sure we don't trigger collision too much!
this.position.add([0, -1]);
this.velocity.negate().add(paddle.velocity);
};
private wallHit = () => {
// If the ball hits the left or right wall
if (
this.position.x <= this.radius ||
this.position.x >= this.game.resolution.x - this.radius
) {
this.velocity.x = -this.velocity.x; // Reflect the horizontal component
}
// If the ball hits the roof
if (this.position.y <= this.radius) {
this.velocity.y = -this.velocity.y; // Reflect the vertical component
}
};
}
import { Vec2, Vec3 } from "gl-matrix/dist/esm";
import { Ball } from "~/helpers/breakout/entities/Ball";
import { BreakoutEvents } from "~/helpers/breakout/events";
import { Entity } from "~/helpers/svgame";
import { BoxCollision } from "~/helpers/svgame/lib/BoxCollision";
import { createSquare, drawPolygons } from "~/helpers/svgame/lib/drawing";
export class Block extends Entity {
ball?: Ball;
vertices: Vec2[] = [];
position = new Vec2(250, 60);
size = new Vec2(50, 20);
color = new Vec3(1, 1, 1);
collision?: BoxCollision;
health = 1;
setup = () => {
this.vertices = createSquare(this.position, this.size);
this.ball = this.game.getEntity<Ball>("ball");
this.collision = new BoxCollision(
this.position,
this.size,
this.handleCollision,
);
};
update = (delta: number) => {
if (this.ball) {
this.collision?.checkCollision(this.ball?.position);
}
};
draw(interpolationPercentage: number) {
drawPolygons({
svg: this.game.svg,
vertices: [this.vertices],
fill: `rgb(${this.color.x * 255}, ${this.color.y * 255}, ${
this.color.z * 255
})`,
className: this.name,
});
}
private handleCollision = () => {
this.health -= 1;
this.game.eventBus.emit(BreakoutEvents.BlockHit, this);
if (this.health <= 0) {
this.game.eventBus.emit(BreakoutEvents.BlockBroken);
this.delete();
}
};
}
import { Vec2, Vec3 } from "gl-matrix/dist/esm";
import { Block } from "~/helpers/breakout/entities/Block";
import { Entity } from "~/helpers/svgame";
export class BlockSpawner extends Entity {
blocks: Block[] = [];
// Define the number of rows and columns for the blocks
private readonly rows: number = 5;
private readonly columns: number = 10;
// Block size
private readonly blockSize: Vec2 = Vec2.fromValues(60, 20); // Example size, adjust as needed
setup() {
this.reset();
}
reset = () => {
for (const block of this.blocks) {
block.delete();
}
this.blocks = [];
// Calculate the starting position and gap between blocks
const gap = 10; // Gap between blocks
const startX =
(this.game.resolution.x -
(this.columns * this.blockSize[0] + (this.columns - 1) * gap)) /
2;
const startY = this.game.resolution.y / 4; // Start from 1/4th of the screen height
// Create blocks in a grid pattern
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.columns; col++) {
// Calculate position for each block
const x = startX + col * (this.blockSize[0] + gap);
const y = startY + row * (this.blockSize[1] + gap);
const position = Vec2.fromValues(x, y);
// Create new block at the calculated position
const block = new Block(this.game, `block-${row * this.columns + col}`);
block.position = position;
block.color = new Vec3(Math.random(), Math.random(), Math.random());
this.blocks.push(block);
}
}
// Add all blocks to the game
this.game.addEntities(this.blocks);
};
}
import { Vec2, Vec3 } from "gl-matrix/dist/esm";
import { BreakoutActions } from "~/helpers/breakout/actions";
import { Ball } from "~/helpers/breakout/entities/Ball";
import { BreakoutEvents } from "~/helpers/breakout/events";
import { Entity } from "~/helpers/svgame";
import { BoxCollision } from "~/helpers/svgame/lib/BoxCollision";
import { createSquare, drawPolygons } from "~/helpers/svgame/lib/drawing";
export class Paddle extends Entity {
ball?: Ball;
name = "paddle";
vertices: Vec2[] = [];
position = new Vec2(this.game.resolution.x / 2, this.game.resolution.y - 30);
size = new Vec2(300, 20);
velocity = new Vec2();
speed = 2;
color = new Vec3(255, 255, 255);
collision?: BoxCollision;
setup() {
this.game.eventBus.on(BreakoutEvents.PaddleHit, this.paddleHit);
this.ball = this.game.getEntity<Ball>("ball");
this.collision = new BoxCollision(
this.position,
this.size,
this.handleCollision,
);
}
begin(timestamp: number, delta: number) {
// Handle input
// Move Left
if (this.game.input.isActionPressed(BreakoutActions.MoveLeft)) {
this.velocity.add(new Vec2([-this.speed, 0]));
}
// Move Right
if (this.game.input.isActionPressed(BreakoutActions.MoveRight)) {
this.velocity.add(new Vec2([this.speed, 0]));
}
// Shoot the ball transferring velocity
if (this.game.input.isActionPressed(BreakoutActions.ShootBall)) {
this.game.eventBus.emit(BreakoutEvents.BallShot, this.velocity);
}
}
update(delta: number) {
if (this.ball) {
this.collision?.checkCollision(this.ball?.position);
}
this.velocity.scale(0.9);
this.position.add(this.velocity);
// Bound to play space
this.position.x = Math.max(
this.size.x / 2,
Math.min(this.position.x, this.game.resolution.x - this.size.x / 2),
);
this.vertices = createSquare(this.position, this.size);
this.color.x = (Math.sin(this.game.time * 0.001) + 2) * 0.5;
this.color.y = (Math.cos(this.game.time * 0.001) + 2) * 0.5;
this.color.z = (Math.sin(-this.game.time * 0.001) + 2) * 0.5;
}
draw(interpolationPercentage: number) {
drawPolygons({
svg: this.game.svg,
vertices: [this.vertices],
fill: `rgb(${this.color.x * 255}, ${this.color.y * 255}, ${
this.color.z * 255
})`,
className: this.name,
});
}
private paddleHit = () => {};
private handleCollision = () => {
this.game.eventBus.emit(BreakoutEvents.PaddleHit, this);
};
}
import { Vec2 } from "gl-matrix/dist/esm";
import { BreakoutActions } from "~/helpers/breakout/actions";
import { Entity } from "~/helpers/svgame";
import { drawText } from "~/helpers/svgame/lib/drawing";
export class PauseScreen extends Entity {
setup() {
this.game.input.registerAction(BreakoutActions.Pause, "Escape");
}
begin(timestamp: number, delta: number) {
// Kinda wierd to put logic in draw but draw always runs
if (this.game.input.isActionJustPressed(BreakoutActions.Pause)) {
this.game.paused = !this.game.paused;
}
}
draw(interpolationPercentage: number) {
if (this.game.paused) {
drawText({
positions: [
new Vec2(
Number(this.game.svg.attr("width")),
Number(this.game.svg.attr("height")),
).scale(0.5),
],
className: "pause-text",
fill: "#fff",
fontSize: "38px",
svg: this.game.svg,
texts: [`PAUSED`],
}).attr("text-anchor", "middle");
drawText({
positions: [
new Vec2(
Number(this.game.svg.attr("width")),
Number(this.game.svg.attr("height")),
)
.scale(0.5)
.sub([0, -50]),
],
className: "pause-instructions-text",
fill: "#fff",
fontSize: "18px",
svg: this.game.svg,
texts: [`Press Esc to unpause`],
}).attr("text-anchor", "middle");
}
}
}
import { Vec2 } from "gl-matrix/dist/esm";
import { BlockSpawner } from "~/helpers/breakout/entities/BlockSpawner";
import { BreakoutEvents } from "~/helpers/breakout/events";
import { Entity } from "~/helpers/svgame";
import { drawText } from "~/helpers/svgame/lib/drawing";
export class ScoreText extends Entity {
score = 0;
lives = 3;
setup() {
const blockSpawner = this.game.getEntity<BlockSpawner>("block-spawner")!;
this.game.eventBus.on(BreakoutEvents.BlockBroken, () => {
this.score++;
if (this.score >= blockSpawner.blocks.length) {
this.game.eventBus.emit(BreakoutEvents.Win);
this.game.paused = true;
}
});
this.game.eventBus.on(BreakoutEvents.Death, () => {
this.lives--;
if (this.lives <= 0) {
this.game.eventBus.emit(BreakoutEvents.Lose, this.score);
this.score = 0;
this.lives = 3;
blockSpawner.reset();
}
});
}
draw(interpolationPercentage: number) {
drawText({
positions: [new Vec2(100, 40)],
className: "lives-text",
fill: "#fff",
fontSize: "16px",
svg: this.game.svg,
texts: [`Lives: ${this.lives}`],
}).attr("text-anchor", "middle");
drawText({
positions: [new Vec2(100, 20)],
className: "score-text",
fill: "#fff",
fontSize: "16px",
svg: this.game.svg,
texts: [`Score: ${this.score}`],
}).attr("text-anchor", "middle");
}
}
import { Vec2 } from "gl-matrix/dist/esm";
import { Entity } from "~/helpers/svgame";
import { drawText } from "~/helpers/svgame/lib/drawing";
export class ScreenFPS extends Entity {
draw(interpolationPercentage: number) {
drawText({
positions: [new Vec2(1100, 20)],
className: "debug-text",
fill: "#fff",
fontSize: "16px",
svg: this.game.svg,
texts: [`FPS: ${this.game.loop.getFPS().toFixed(1)}`],
}).attr("text-anchor", "middle");
}
}