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 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);
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
|
||||||
return this.keys.has(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isKeyUp(key: string) {
|
export class Engine {
|
||||||
// by default, keys are considered up
|
updateDelay: number
|
||||||
return !this.keys.has(key);
|
|
||||||
|
constructor({ updateDelay }: { updateDelay: number }) {
|
||||||
|
this.updateDelay = updateDelay;
|
||||||
}
|
}
|
||||||
|
|
||||||
isKeyPressed(key: string) {
|
setUpdateDelay(updateDelay: number) {
|
||||||
return this.keysPressed.has(key);
|
this.updateDelay = updateDelay;
|
||||||
}
|
}
|
||||||
|
|
||||||
isKeyReleased(key: string) {
|
run(update: () => void, render: () => void) {
|
||||||
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user