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 SQUARE_SIZE = 30; // px
@ -6,21 +6,43 @@ const SQUARE_SIZE = 30; // px
const BOARD_SQUARES = BOARD_SIZE / SQUARE_SIZE;
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 });
let dead = false;
const snake = [vec2(1, 1), vec2(2, 1)];
let apple = vec2(8, 8);
// game logic --------------------------------------------------------------
function clearCanvas() {
engine.rect(0, 0, canvas.width, canvas.height);
}
function drawSquare(pos: Vec2) {
engine.rect(pos.x * SQUARE_SIZE, pos.y * SQUARE_SIZE, SQUARE_SIZE, SQUARE_SIZE);
}
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;
const snake = [vec2(1, 1), vec2(2, 1)];
let apple = getRandApplePos();
engine.setUpdateDelay(calcUpdateDelay(snake.length))
return { dead, snake, apple };
}
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]!);
const keyDirMap = {
@ -30,7 +52,7 @@ export default function runCanvas(canvas: HTMLCanvasElement) {
d: vec2(+1, 0),
};
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;
break;
}
@ -40,45 +62,58 @@ export default function runCanvas(canvas: HTMLCanvasElement) {
// check for snake intersection
for (const square of snake.slice(1)) {
if (nextHead.eq(square)) {
dead = true;
state.dead = true;
}
}
// check for snake out of bounds
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
if (nextHead.eq(apple)) {
apple = vec2(randint(0, BOARD_SQUARES), randint(0, BOARD_SQUARES));
state.apple = getRandApplePos();
engine.setUpdateDelay(calcUpdateDelay(snake.length))
} else {
snake.shift();
}
// move the snake
snake.push(nextHead);
keys.update();
}
async function render() {
engine.setFillStyle('#333333');
// rendering ---------------------------------------------------------------
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();
engine.setFillStyle('#277edb');
ui.setFillStyle('#277edb');
for (const square of snake.slice(0, snake.length - 1)) {
drawSquare(square);
}
engine.setFillStyle('#29d17a');
ui.setFillStyle('#29d17a');
drawSquare(snake[snake.length - 1]!);
engine.setFillStyle('#e01851');
ui.setFillStyle('#e01851');
drawSquare(apple);
if (dead) {
engine.setFillStyle('#ff0000');
engine.text('You Died', 60, 60);
ui.setFillStyle('#ff0000');
ui.text('You Died', 60, 60);
}
}
keys.bindKeys()
engine.run(update, render);
}

View File

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