split out engine components

This commit is contained in:
Michael Peters 2024-08-02 11:19:05 -07:00
parent 566336fd33
commit 8ece068331
2 changed files with 112 additions and 61 deletions

View File

@ -1,4 +1,4 @@
import { Engine, randint, Vec2, vec2 } from './game-engine'; import { Engine, Keys, randint, UI, Vec2, vec2 } from './game-engine';
const BOARD_SIZE = 600; // px const BOARD_SIZE = 600; // px
const SQUARE_SIZE = 30; // px const SQUARE_SIZE = 30; // px
@ -6,21 +6,43 @@ const SQUARE_SIZE = 30; // px
const BOARD_SQUARES = BOARD_SIZE / SQUARE_SIZE; const BOARD_SQUARES = BOARD_SIZE / SQUARE_SIZE;
export default function runCanvas(canvas: HTMLCanvasElement) { export default function runCanvas(canvas: HTMLCanvasElement) {
const engine = new Engine(canvas); const ui = new UI(canvas);
const keys = new Keys();
const engine = new Engine({ updateDelay: 200 });
// game logic --------------------------------------------------------------
function getRandApplePos() {
return vec2(randint(0, BOARD_SQUARES), randint(0, BOARD_SQUARES))
}
function calcUpdateDelay(snakeLength: number) {
// 5 squares per second base (200ms) + 1 sps on every apple
return 1000 / (5 + snakeLength)
}
function resetState() {
let dead = false; let dead = false;
const snake = [vec2(1, 1), vec2(2, 1)]; const snake = [vec2(1, 1), vec2(2, 1)];
let apple = vec2(8, 8); let apple = getRandApplePos();
function clearCanvas() { engine.setUpdateDelay(calcUpdateDelay(snake.length))
engine.rect(0, 0, canvas.width, canvas.height);
} return { dead, snake, apple };
function drawSquare(pos: Vec2) { }
engine.rect(pos.x * SQUARE_SIZE, pos.y * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE);
let state = resetState();
function update() {
const { dead, snake, apple } = state;
if (dead) {
if (keys.isKeyPressed(' ')) {
state = resetState();
}
return;
} }
async function update() {
if (dead) return;
let dir = snake[snake.length - 1]!.sub(snake[snake.length - 2]!); let dir = snake[snake.length - 1]!.sub(snake[snake.length - 2]!);
const keyDirMap = { const keyDirMap = {
@ -30,7 +52,7 @@ export default function runCanvas(canvas: HTMLCanvasElement) {
d: vec2(+1, 0), d: vec2(+1, 0),
}; };
for (const [key, newDir] of Object.entries(keyDirMap)) { for (const [key, newDir] of Object.entries(keyDirMap)) {
if (engine.isKeyPressed(key) && Vec2.dot(dir, newDir) === 0) { if (keys.isKeyPressed(key) && Vec2.dot(dir, newDir) === 0) {
dir = newDir; dir = newDir;
break; break;
} }
@ -40,45 +62,58 @@ export default function runCanvas(canvas: HTMLCanvasElement) {
// check for snake intersection // check for snake intersection
for (const square of snake.slice(1)) { for (const square of snake.slice(1)) {
if (nextHead.eq(square)) { if (nextHead.eq(square)) {
dead = true; state.dead = true;
} }
} }
// check for snake out of bounds // check for snake out of bounds
if (nextHead.x < 0 || nextHead.x >= BOARD_SQUARES || nextHead.y < 0 || nextHead.y >= BOARD_SQUARES) { if (nextHead.x < 0 || nextHead.x >= BOARD_SQUARES || nextHead.y < 0 || nextHead.y >= BOARD_SQUARES) {
dead = true; state.dead = true;
} }
// check for snake hitting apple // check for snake hitting apple
if (nextHead.eq(apple)) { if (nextHead.eq(apple)) {
apple = vec2(randint(0, BOARD_SQUARES), randint(0, BOARD_SQUARES)); state.apple = getRandApplePos();
engine.setUpdateDelay(calcUpdateDelay(snake.length))
} else { } else {
snake.shift(); snake.shift();
} }
// move the snake // move the snake
snake.push(nextHead); snake.push(nextHead);
keys.update();
} }
async function render() { // rendering ---------------------------------------------------------------
engine.setFillStyle('#333333'); function clearCanvas() {
ui.rect(0, 0, canvas.width, canvas.height);
}
function drawSquare(pos: Vec2) {
ui.rect(pos.x * SQUARE_SIZE, pos.y * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE);
}
function render() {
const { dead, snake, apple } = state;
ui.setFillStyle('#333333');
clearCanvas(); clearCanvas();
engine.setFillStyle('#277edb'); ui.setFillStyle('#277edb');
for (const square of snake.slice(0, snake.length - 1)) { for (const square of snake.slice(0, snake.length - 1)) {
drawSquare(square); drawSquare(square);
} }
engine.setFillStyle('#29d17a'); ui.setFillStyle('#29d17a');
drawSquare(snake[snake.length - 1]!); drawSquare(snake[snake.length - 1]!);
engine.setFillStyle('#e01851'); ui.setFillStyle('#e01851');
drawSquare(apple); drawSquare(apple);
if (dead) { if (dead) {
engine.setFillStyle('#ff0000'); ui.setFillStyle('#ff0000');
engine.text('You Died', 60, 60); ui.text('You Died', 60, 60);
} }
} }
keys.bindKeys()
engine.run(update, render); engine.run(update, render);
} }

View File

@ -73,14 +73,50 @@ export function vec2(x: number, y: number) {
return new Vec2(x, y); return new Vec2(x, y);
} }
export class Engine { export class Keys {
ctx: CanvasRenderingContext2D;
camera = vec2(0, 0);
keys = new Set<string>(); keys = new Set<string>();
keysPressed = new Set<string>(); keysPressed = new Set<string>();
keysReleased = new Set<string>(); keysReleased = new Set<string>();
isKeyDown(key: string) {
return this.keys.has(key);
}
isKeyUp(key: string) {
// by default, keys are considered up
return !this.keys.has(key);
}
isKeyPressed(key: string) {
return this.keysPressed.has(key);
}
isKeyReleased(key: string) {
return this.keysReleased.has(key);
}
bindKeys() {
document.addEventListener('keydown', (event: KeyboardEvent) => {
this.keys.add(event.key);
this.keysPressed.add(event.key);
});
document.addEventListener('keyup', (event: KeyboardEvent) => {
this.keys.delete(event.key);
this.keysReleased.add(event.key);
});
}
update() {
this.keysPressed.clear();
this.keysReleased.clear();
}
}
export class UI {
ctx: CanvasRenderingContext2D;
camera = vec2(0, 0);
constructor(canvasElement: HTMLCanvasElement) { constructor(canvasElement: HTMLCanvasElement) {
const ctx = canvasElement.getContext('2d'); const ctx = canvasElement.getContext('2d');
if (ctx === null) throw Error('unable to get 2d canvas context'); if (ctx === null) throw Error('unable to get 2d canvas context');
@ -100,49 +136,29 @@ export class Engine {
const c = this.camera; const c = this.camera;
this.ctx.fillText(text, x - c.x, y - c.y); this.ctx.fillText(text, x - c.x, y - c.y);
} }
}
isKeyDown(key: string) { export class Engine {
return this.keys.has(key); updateDelay: number
constructor({ updateDelay }: { updateDelay: number }) {
this.updateDelay = updateDelay;
} }
isKeyUp(key: string) { setUpdateDelay(updateDelay: number) {
// by default, keys are considered up this.updateDelay = updateDelay;
return !this.keys.has(key);
} }
isKeyPressed(key: string) { run(update: () => void, render: () => void) {
return this.keysPressed.has(key);
}
isKeyReleased(key: string) {
return this.keysReleased.has(key);
}
private bindKeys() {
document.addEventListener('keydown', (event: KeyboardEvent) => {
this.keys.add(event.key);
this.keysPressed.add(event.key);
});
document.addEventListener('keyup', (event: KeyboardEvent) => {
this.keys.delete(event.key);
this.keysReleased.add(event.key);
});
}
run(update: () => Promise<void>, render: () => Promise<void>) {
this.bindKeys();
const updateLoop = async () => { const updateLoop = async () => {
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
await update(); update();
this.keysPressed.clear(); await new Promise(resolve => setTimeout(resolve, this.updateDelay));
await new Promise(resolve => setTimeout(resolve, 100));
await new Promise(resolve => setTimeout(resolve, 0));
} }
}; };
const renderLoop = async () => { const renderLoop = async () => {
await render(); render();
window.requestAnimationFrame(renderLoop); window.requestAnimationFrame(renderLoop);
}; };