A lightweight JavaScript game engine
git clone git://soophie.de/game-engine
1
const DEVICE_PIXEL_RATIO = window.devicePixelRatio || 1;3
export function scaleCanvas(canvas, width, height) {4
const g = canvas.getContext('2d');5
const backingStoreRatio = g.webkitBackingStorePixelRatio || 1;6
const ratio = DEVICE_PIXEL_RATIO / backingStoreRatio;7
if(DEVICE_PIXEL_RATIO != backingStoreRatio) {8
canvas.width = width * ratio;9
canvas.height = height * ratio;10
canvas.style.width = width + 'px';11
canvas.style.height = height + 'px';12
}13
else {14
canvas.width = width;15
canvas.height = height;16
canvas.style.width = '';17
canvas.style.height = '';18
}19
g.scale(ratio, ratio);20
}
1
import { Shape } from './game.js';3
export function collideRect(a, b) {4
return a.x < b.x + b.shape.width && a.x + a.shape.width > b.x5
&& a.y < b.y + b.shape.height && a.y + a.shape.height > b.y;6
}8
export function collideCircle(a, b) {9
const dx = Math.abs(a.x - b.x);10
const dy = Math.abs(a.y - b.y);11
return Math.sqrt(dx ** 2 + dy ** 2) < a.shape.radius + b.shape.radius;12
}14
export function collideRectAndCircle(a, b) {15
const [rect, circle] = a instanceof Shape.Rect ? [a, b] : [b, a];16
let x = circle.x;17
let y = circle.y;18
if(circle.x < rect.x) {19
x = rect.x;20
}21
else if(circle.x > rect.x + rect.shape.width) {22
x = rect.x + rect.shape.width;23
}24
if(circle.y < rect.y) {25
y = rect.y;26
}27
else if(circle.y > rect.y + rect.shape.height) {28
y = rect.y + rect.shape.height;29
}30
const distance = Math.sqrt((circle.x - x) ** 2 + (circle.y - y) ** 2);31
return distance <= circle.shape.radius;32
}34
export function collidePoint(object, point) {35
if(object.shape instanceof Shape.Rect) {36
const {x, y, shape: {width, height}} = object;37
return x < point.x && x + width > point.x && y < point.y && y + height > point.y;38
}39
if(object.shape instanceof Shape.Circle) {40
const {x, y, shape: {radius}} = object;41
return Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2) < radius;42
}43
}
1
import { scaleCanvas } from './canvas.js';2
import * as Collision from './collision.js';4
export class Game {5
constructor(width, height, { fps } = {}) {6
this.width = width || 900;7
this.height = height || 600;8
this.fps = 1000 / (fps || 60);9
this.then = Date.now();10
this.canvas = document.createElement('canvas');11
scaleCanvas(this.canvas, this.width, this.height);12
this.g = this.canvas.getContext('2d');13
this.events = [];14
}15
start() {16
requestAnimationFrame(this.loop.bind(this));17
this.update();18
this.render(this.g);19
}20
loop() {21
requestAnimationFrame(this.loop.bind(this));22
this.now = Date.now();23
this.elapsed = this.now - this.then;24
if(this.elapsed > this.fps) {25
this.then = this.now - (this.elapsed % this.fps);26
for(const event of this.events) {27
if(this['on' + event.type]) {28
this['on' + event.type](event);29
}30
}31
this.update();32
this.render(this.g);33
this.events.length = 0;34
}35
}36
update() {37
// nothing38
}39
render() {40
this.g.clearRect(0, 0, this.width, this.height);41
}42
resize(width, height) {43
this.width = width;44
this.height = height;45
scaleCanvas(this.canvas, this.width, this.height);46
}47
bindEvent(element, type) {48
element['on' + type] = event => this.events.push(event);49
}50
}52
export class Group {53
constructor() {54
this.items = [];55
}56
update() {57
for(let item of this.items) {58
item.update();59
}60
}61
render(g) {62
for(let item of this.items) {63
item.render(g);64
}65
}66
causeEvent(type, events) {67
for(let item of this.items) {68
item.causeEvent(type, events);69
}70
}71
collide() {72
const duplicate = [];73
for(const a of this.items) {74
for(const b of this.items) {75
if(!duplicate.includes(b)) {76
if(a !== b) {77
let collided = false;78
if(a.shape instanceof Shape.Rect && b.shape instanceof Shape.Rect) {79
collided = Collision.collideRect(a, b);80
}81
else if(a.shape instanceof Shape.Circle && b.shape instanceof Shape.Circle) {82
collided = Collision.collideCircle(a, b);83
}84
else {85
collided = Collision.collideRectAndCircle(a, b);86
}87
if(collided) {88
if(a.oncollide) {89
a.oncollide(b);90
}91
if(b.oncollide) {92
b.oncollide(a);93
}94
}95
}96
}97
}98
duplicate.push(a);99
}100
}101
add(item) {102
this.items.push(item);103
}104
remove(item) {105
const i = this.items.findIndex(n => n === item);106
if(i !== -1) {107
this.items.splice(i, 1);108
}109
}110
[Symbol.iterator]() {111
return this.items.values();112
}113
}115
export class Surface {116
constructor(x, y, shape) {117
this.x = x;118
this.y = y;119
this.shape = shape;120
}121
update() {122
// nothing123
}124
render(g) {125
if(this.shape instanceof Shape.Rect) {126
g.beginPath();127
g.rect(this.x, this.y, this.shape.width, this.shape.height);128
g.stroke();129
}130
if(this.shape instanceof Shape.Circle) {131
g.beginPath();132
g.arc(this.x, this.y, this.shape.radius, 2 * Math.PI, 0);133
g.stroke();134
}135
}136
causeEvent(type, events) {137
if(this['on' + type]) {138
for(const event of events) {139
if(event.type === type) {140
this['on' + type](event);141
}142
}143
}144
}145
}147
class Rect {148
constructor(width, height) {149
this.width = width;150
this.height = height;151
}152
}154
class Circle {155
constructor(radius) {156
this.radius = radius;157
}158
}160
export const Shape = { Rect, Circle };160
\ No newline at end of file
1
export function html(strings, ...props) {2
try {3
let code = '';4
let events = [];5
let children = [];6
for(let i = 0; i < strings.length; i++) {7
code += strings[i];8
if(i < props.length) {9
if(typeof props[i] === 'function') {10
const exp = new RegExp(/on(\w+)=/, 'g');11
const match = exp.exec(strings[i]);12
if(match) {13
const [, type] = match;14
code = code.slice(0, -(type.length + 3));15
code += `data-event-ref-${events.length}`;16
events.push({ type, handler: props[i] });17
}18
continue;19
}20
if(props[i] instanceof Array) {21
for(const item of props[i]) {22
if(item instanceof HTMLElement) {23
const tag = item.tagName.toLowerCase();24
code += `<${tag} data-child-ref-${children.length}></${tag}>`;25
children.push(item);26
continue;27
}28
code += item;29
}30
continue;31
}32
if(props[i] instanceof HTMLElement) {33
const tag = props[i].tagName.toLowerCase();34
code += `<${tag} data-child-ref-${children.length}></${tag}>`;35
children.push(props[i]);36
continue;37
}38
code += props[i];39
}40
}41
const template = document.createElement('template');42
template.innerHTML = code;43
const fragment = template.content.cloneNode(true);44
const element = fragment.firstElementChild;45
for(const [id, {type, handler}] of Object.entries(events)) {46
let target;47
if(element.hasAttribute(`data-event-ref-${id}`)) {48
target = element;49
}50
else {51
target = element.querySelector(`[data-event-ref-${id}]`);52
}53
if(target) {54
target.addEventListener(type, handler);55
target.removeAttribute(`data-event-ref-${id}`);56
}57
else {58
console.warn(`Unable to bind ${type} event to element with id: ${id}`);59
}60
}61
for(const [id, child] of Object.entries(children)) {62
const target = element.querySelector(`[data-child-ref-${id}]`);63
if(target) {64
target.replaceWith(child);65
}66
else {67
console.warn(`Unable to reference <${child.tagName.toLowerCase()}> tag with id: ${id}`);68
}69
}70
return element;71
}72
catch(error) {73
console.error(error);74
}75
}77
export function escape(string) {78
return string.replace(/&/g, '&')79
.replace(/</g, '<')80
.replace(/>/g, '>')81
.replace(/"/g, '"')82
.replace(/'/g, ''');83
}83
\ No newline at end of file