Oops! I made a game engine

How this all started

I can make a thing!

Keeping the scope "small"

  • Be sensibly close to Pico-8
  • Lean into JS + other web tech
  • No build step

Buffering + Text

gameprogrammingpatterns.com
/double-buffer.html
textPixels(string, x, y, color = 7) { var pixels = []; var { textContext } = this._memory.entities; var { width, height, fontSize, namespace } = this._config; textContext.font = `${fontSize}px ${namespace}-font`; textContext.fillStyle = "#ffffff"; textContext.fillText(string, x, y + fontSize); var data = textContext.getImageData(0, 0, width, height, { colorSpace: "srgb" }).data; for (let i = 0; i < data.length; i += 4) { var r = data[i]; var g = data[i + 1]; var b = data[i + 2]; var a = data[i + 3]; var dx = (i / 4) % width; var dy = Math.floor(i / 4 / width); if (r == 255 && g == 255 && b == 255 && a > 200) { pixels.push([Math.round(dx), Math.round(dy), color]); } } textContext.clearRect(0, 0, width, height); return pixels; },
_config = { namespace: "pico", width: 128, height: 128, woff: "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", woff2: "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", fontSize: 8, circTolerance: 2, tileWidth: 8, tileHeight: 8, frequencySteps: 64, minFrequency: 0, maxFrequency: 1500, }

Drawing pixels

import { Engine } from 'https://floaty.dev/engine-v1.js' var engine = new Engine() engine.expose() var sprites = { // ... } var sounds = { // ... } function init() { // ... } function update() { // things to do each game tick: // updating the game state // moving the player // handling input } function draw() { // things to do each time the screen updates: // - clearing the canvas // - drawing on the canvas } engine.start({ sprites, sounds, init, update, draw, })
_address(x, y, width) { if (!width) { width = this._config.width } return (y * width) + x }, pget(x, y) { return this._memory.pixels[this._address(x, y)] }, pset(x, y, color = 0) { if (x < 0 || x > this._config.width - 1 || y < 0 || y > this._config.width - 1) { return } this._memory.pixels[this._address(x, y)] = color },
_animate({ time, frame }) { var { drawContext, offscreenContext, offscreenCanvas } = this._memory.entities var { width, height } = this._config if (this._env.update) { this._env.update(time, frame) } this._env.draw(time, frame) var imageData = drawContext.createImageData(width, height) var pixels = this._memory.pixels var i = 0 for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var color = pixels[this._address(x, y)] if (this._env.shader) { color = this._env.shader(pixels, x, y, color) } var { r, g, b } = this._hexToRgb(this._memory.colors[color]) imageData.data[i + 0] = r imageData.data[i + 1] = g imageData.data[i + 2] = b imageData.data[i + 3] = 255 i += 4 } } offscreenContext.putImageData(imageData, 0, 0) drawContext.drawImage(offscreenCanvas, 0, 0) }
  • Simple to understand
  • Fast to render
  • Easy to support shaders
clauswilke.com/art/post/shaders
engine.start({ // ... shader: function(pixels, x, y, color) { if (color == 15) { return 0 } return color + 1 }, })

Drawing lines

line(x1, y1, x2, y2, color = 7) { var pixels = this.linePixels( Math.round(x1), Math.round(y1), Math.round(x2), Math.round(y2), ) for (var [x, y] of pixels) { this.pset(x, y, color) } }, linePixels(x1, y1, x2, y2) { var dx = x2 - x1 var dy = y2 - y1 var max = Math.max(Math.abs(dx), Math.abs(dy)) var pixels = [] for (var i = 0; i < max; i++) { pixels.push([ Math.round(x1 + i * (dx / max)), Math.round(y1 + i * (dy / max)), ]) } pixels = this.removeDuplicatePixels(pixels) pixels = this.removeCornerPixels(pixels) return pixels }

Drawing circles

circ(x, y, radius, color = 7, thickness = 1) { var pixels = this.circPixels( Math.round(x), Math.round(y), Math.round(radius), thickness, ) for (var [x, y] of pixels) { if (x < 0 || x > this._config.width) { continue } if (y < 0 || y > this._config.height) { continue } this.pset(x, y, color) } }, circPixels(x, y, radius, thickness = 1) { var { width, height } = this._config var minX = bx - radius - 1 var maxX = bx + radius + 1 var minY = by - radius - 1 var maxY = by + radius + 1 var innerMem = {} // figure out which inner pixels we can omit from the outer filled circle for (var dy = minY; dy < maxY; dy += 1) { for (var dx = minX; dx < maxX; dx += 1) { // inner pixels are everying in the circle minus the circumference thickness if (this._isInsideCircle(dx, dy, bx, by, radius - thickness)) { innerMem[[dx, dy].join(":")] = true } } } var pixels = [] for (var dy = minY; dy < maxY; dy += 1) { for (var dx = minX; dx < maxX; dx += 1) { var key = [dx, dy].join(":") if (innerMem[key]) { continue } if (this._isInsideCircle(dx, dy, bx, by, radius)) { pixels.push([dx, dy]) } } } pixels = this.removeDuplicatePixels(pixels) if (thickness == 1) { pixels = this.removeCornerPixels(pixels) } return pixels }, _isInsideCircle(x, y, circleX, circleY, radius, tolerance) { if (typeof tolerance == "undefined") { tolerance = this._config.circTolerance } var pixels = Math.pow(x - circleX, 2) + Math.pow(y - circleY, 2) radius *= radius // 1 + 2 radius circles look gross without these overrides if (radius < 3) { tolerance = radius } if (pixels < radius + tolerance) { return true } return false },

Removing bad pixels

removeCornerPixels(pixels) { var { width } = this._config; var joiner = (pixel) => pixel.join(":"); var splitter = (pixel) => pixel.split(":").map((value) => parseInt(value, 10)); var mapped = pixels.map((pixel) => pixel.join(":")); var removed = []; var remover = (pixel) => { var [x, y] = splitter(pixel); var pixels = { left: [x - 1, y].join(":"), right: [x + 1, y].join(":"), top: [x, y - 1].join(":"), bottom: [x, y + 1].join(":"), } var has = { left: mapped.includes(pixels.left) && !removed.includes(pixels.left), right: mapped.includes(pixels.right) && !removed.includes(pixels.right), top: mapped.includes(pixels.top) && !removed.includes(pixels.top), bottom: mapped.includes(pixels.bottom) && !removed.includes(pixels.bottom), } if ( (has.left && has.top && !has.right && !has.bottom) || (has.left && has.bottom && !has.right && !has.top) || (has.right && has.top && !has.left && !has.bottom) || (has.right && has.bottom && !has.left && !has.top) ) { removed.push(pixel); return false; } return true; }; return [ ...pixels // only take the left side pixels .filter(pixel => pixel[0] <= (width / 2)) // ...and sort them ascending .sort((a, b) => a[1] - b[1] || a[0] - b[0]) .map(joiner) .filter(remover) .map(splitter), ...pixels // only take the right side pixels .filter(pixel => pixel[0] >= (width / 2)) // ...and sort them descending .sort((a, b) => a[1] - b[1] || b[0] - a[0]) .map(joiner) .filter(remover) .map(splitter), ]; },

Playing sounds

async enableAudio() { if (!this._memory.tone) { this._memory.tone = await import('/tone-v14.js'); await this._memory.tone.start(); } }, async sfx(sound) { await this.enableAudio(); var [ length, ...notes ] = this._env.sounds[sound] // length is a centisecond (100th of a second) // for oscillator timing: we take length / 100 (because it wants seconds) // for timeout timing: we take length * 10 (because it wants milliseconds) var oscillator = new this._memory.tone.OmniOscillator().toDestination().start(); for (var i = 0; i < notes.length; i++) { var [frequency, volume, waveType] = notes[i]; if (volume === 0 || frequency === 0) { oscillator.volume.exponentialRampToValueAtTime(-100, oscillator.now() + length / 100); } else { oscillator.volume.exponentialRampToValueAtTime(this._sfxVolume(volume), oscillator.now() + length / 100); // TODO: replace magic numbers with enum, and expand allowed instruments oscillator.type = this._sfxWaveType(waveType); oscillator.frequency.value = this._sfxFrequency(frequency); } await new Promise(resolve => setTimeout(resolve, length * 10)); } if (oscillator.volume.value > -100) { oscillator.volume.exponentialRampToValueAtTime(-100, oscillator.now() + length / 100); oscillator.stop(oscillator.now() + length / 100); } else { oscillator.stop(); } }, _sfxFrequency(frequency) { var percent = frequency / this._config.frequencySteps; var difference = this._config.maxFrequency - this._config.minFrequency; return Math.round(difference * percent); }, _sfxVolume(volume) { // -15db → 15db return (volume * 30) - 15; }, _sfxWaveType(waveType) { return this._memory.waveTypes[waveType]; },

Resources

Dive in!

Back to assertchris.dev →