generated from michael/webpack-base
split out engine components
This commit is contained in:
parent
566336fd33
commit
8ece068331
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user