diff --git a/src/debug.js b/src/debug.js index 01a0bdc..43c4581 100644 --- a/src/debug.js +++ b/src/debug.js @@ -1,20 +1,25 @@ import { ctx } from './canvas' class Debugging { + constructor () { + this.overlay = document.createElement('div') + this.overlay.style = 'color:#fff;position:absolute;top:15px;right:15px;background-color:hsla(0,0%,47%,0.5);padding:10px;display:flex;flex-direction:column;' + document.body.appendChild(this.overlay) + this.drawGrid = false + } + draw (vp, world, fps) { let p = vp.chunkIn(world.chunkSize * world.tileSize) ctx.fillStyle = '#fff' ctx.fillText(fps + ' fps', 4, 16) - ctx.fillText('cam-in-chunk (x: ' + p.x + '; y: ' + p.y + ')', 4, 32) - ctx.fillText('loaded ' + world.chunks.length, 4, 48) - ctx.fillText('drawn ' + world._lastDrawCount, 4, 64) - ctx.fillText('updates ' + world._lastUpdateCount, 4, 80) + ctx.fillText('cam (x: ' + vp.x + '; y: ' + vp.y + ')', 4, 16 * 2) + ctx.fillText('cam-in-chunk (x: ' + p.x + '; y: ' + p.y + ')', 4, 16 * 3) + ctx.fillText('loaded ' + world.chunks.length, 4, 16 * 4) + ctx.fillText('drawn ' + world._lastDrawCount, 4, 16 * 5) + ctx.fillText('updates ' + world._lastUpdateCount, 4, 16 * 6) } createSliders (obj, args, fn) { - let overlay = document.createElement('div') - overlay.style = 'color:#fff;position:absolute;top:15px;right:15px;background-color:hsla(0,0%,47%,0.5);padding:10px;display:flex;flex-direction:column;' - for (let a in args) { let min = args[a][0] let max = args[a][1] @@ -40,9 +45,62 @@ class Debugging { div.appendChild(name) div.appendChild(slider) div.appendChild(value) - overlay.appendChild(div) + this.overlay.appendChild(div) } - document.body.appendChild(overlay) + } + + addCheckbox (obj, arg, fn) { + let div = document.createElement('div') + div.style = 'display:flex;flex-direction:row;' + let name = document.createElement('span') + let checkbox = document.createElement('input') + name.style = 'flex-grow:1;' + name.innerHTML = arg + checkbox.type = 'checkbox' + checkbox.checked = obj[arg] === true + checkbox.addEventListener('change', function (e) { + obj[arg] = checkbox.checked + fn(arg, obj[arg]) + }) + div.appendChild(checkbox) + div.appendChild(name) + this.overlay.appendChild(div) + } + + chunkGrid (ctx, chunk, view) { + if (!this.drawGrid) return + // Inner grid + ctx.lineWidth = 0.15 + ctx.strokeStyle = '#041fff' + for (let x = 0; x <= chunk.fullSize; x += chunk.tile) { + ctx.beginPath() + // Vertical lines + ctx.moveTo(0 + x, 0) + ctx.lineTo(0 + x, chunk.fullSize) + + // Horizontal lines + ctx.moveTo(0, 0 + x) + ctx.lineTo(chunk.fullSize, 0 + x) + + // Close + ctx.closePath() + ctx.stroke() + } + + // Chunk border + ctx.lineWidth = 1 + ctx.strokeStyle = '#040404' + ctx.beginPath() + ctx.moveTo(0.5, 0) + ctx.lineTo(0.5, chunk.fullSize) + ctx.moveTo(0.5, chunk.fullSize) + ctx.lineTo(chunk.fullSize, chunk.fullSize) + ctx.closePath() + ctx.stroke() + + // Chunk index + ctx.fillStyle = '#fff' + ctx.fillText(chunk.x + ';' + chunk.y, 5, 16) } } diff --git a/src/heightmap.js b/src/heightmap.js index 997b9f6..460d7d3 100644 --- a/src/heightmap.js +++ b/src/heightmap.js @@ -83,7 +83,7 @@ class HeightMap { } getHeight (x) { - return this.getNoise(x) * this.amplitude + return this.getNoise(x) * this.amplitude + this.iy } } diff --git a/src/index.js b/src/index.js index 48ec239..96d9883 100644 --- a/src/index.js +++ b/src/index.js @@ -14,7 +14,7 @@ let fps = 0 let vp = new Viewport(0, 0) -let height = new HeightMap(0, 0, 16, 0) +let height = new HeightMap(0, 32, 16, 0) let map = new TileMap('assets/ground.png', 32) const chunkSize = 32 @@ -59,26 +59,39 @@ map.define({ 'STONE': 10 }) -let test = new World(height, { GROUND: map }, chunkSize, tileSize) +let world = new World(height, { GROUND: map }, chunkSize, tileSize, 32, 64) function update (dt) { - test.update(dt, vp) + world.update(dt, vp) if (Input.isDown('w')) { - vp.y -= 5 + vp.y -= 15 } else if (Input.isDown('s')) { - vp.y += 5 + vp.y += 15 } if (Input.isDown('a')) { - vp.x -= 5 + vp.x -= 15 } else if (Input.isDown('d')) { - vp.x += 5 + vp.x += 15 + } + + let full = world.chunkSize * world.tileSize + if (vp.x < 0) { + vp.x = 0 + } else if (vp.x + vp.width > world.width * full) { + vp.x = (full * world.width) - vp.width + } + + if (vp.y < 0) { + vp.y = 0 + } else if (vp.y + vp.height > world.height * full) { + vp.y = (full * world.height) - vp.height } } function draw () { - test.draw(vp) - Debug.draw(vp, test, fps) + world.draw(vp) + Debug.draw(vp, world, fps) } function step () { @@ -115,7 +128,10 @@ function start () { period: [1, 100, 1], lacunarity: [1, 5, 1] }, function (key, val) { - test.chunks = [] + world.chunks = [] + }) + Debug.addCheckbox(Debug, 'drawGrid', function (argument) { + world.chunks = [] }) playing = true gameLoop() diff --git a/src/tiles.js b/src/tiles.js index 51890dc..aa40c55 100644 --- a/src/tiles.js +++ b/src/tiles.js @@ -1,7 +1,10 @@ import { ctx, ResourceCacheFactory } from './canvas' +import { distanceTo } from './utils' import Resource from './resource' +import Debug from './debug' const cacheFactory = new ResourceCacheFactory() +const UPDATE_RADIUS = 6 class TileMap { constructor (image, rows) { @@ -47,37 +50,13 @@ class TileMap { } } -class TileChunk { - constructor (ix, iy, size = 16, tileSize = 16) { - this.x = ix - this.y = iy +class TileLayer { + constructor (name, collider = false, size = 16, tileSize = 16) { + this.name = name + this.collide = collider this.size = size this.tile = tileSize this.tiles = [] - this.dirty = true - this.img = null - this._updated = false - } - - generateMap (tileMap, heightMap) { - for (let i = 0; i < this.size * this.size; i++) { - let tileCoords = this.toXY(i) - let tileAbs = this.toAbs(tileCoords) - let y = Math.ceil(heightMap.getHeight(tileAbs.x) * 5 / 2) - 4 - if (tileAbs.y < y) { - this.tiles.push(-1) - continue - } - if (tileAbs.y === y) { - this.tiles.push(tileMap.indexOf('GRASS_TOP')) - continue - } - if (tileAbs.y < y + 10) { - this.tiles.push(tileMap.indexOf('DIRT')) - continue - } - this.tiles.push(tileMap.indexOf('STONE')) - } } tileAt (i) { @@ -92,6 +71,63 @@ class TileChunk { return { x: i % this.size, y: Math.floor(i / this.size) } } + draw (ctx, view, map) { + for (let i in this.tiles) { + let tilei = this.tiles[i] + if (tilei === -1) continue + let coords = this.toXY(parseInt(i)) + let tileCoords = map.tileAt(tilei) + ctx.drawImage(map.image, tileCoords.x, tileCoords.y, map.tile, map.tile, + coords.x * this.tile, coords.y * this.tile, this.tile, this.tile) + } + } + + update (dt) { + + } +} + +class Chunk { + constructor (ix, iy, size = 16, tileSize = 16) { + this.x = ix + this.y = iy + this.size = size + this.tile = tileSize + this.layers = [] + this.dirty = true + this.img = null + this._updated = false + } + + generateMap (tileMap, heightMap) { + this.layers = [] + let fgLayer = new TileLayer('fg', true, this.size, this.tile) + for (let i = 0; i < this.size * this.size; i++) { + let tileCoords = fgLayer.toXY(i) + let tileAbs = this.toAbs(tileCoords) + let y = Math.ceil(heightMap.getHeight(tileAbs.x) * 5 / 2) - 4 + if (tileAbs.y < y) { + fgLayer.tiles.push(-1) + continue + } + if (tileAbs.y === y) { + fgLayer.tiles.push(tileMap.indexOf('GRASS_TOP')) + continue + } + if (tileAbs.y < y + 10) { + fgLayer.tiles.push(tileMap.indexOf('DIRT')) + continue + } + if (tileAbs.y > heightMap.falloff - y + 64) { + fgLayer.tiles.push(-1) + continue + } + fgLayer.tiles.push(tileMap.indexOf('STONE')) + } + this.layers.push(fgLayer) + this.dirty = true + } + toAbs (x, y) { if (typeof x === 'object') { y = x.y @@ -101,22 +137,30 @@ class TileChunk { } get absPos () { - return { x: this.x * this.size * this.tile, y: this.y * this.size * this.tile } + return { x: this.x * this.fullSize, y: this.y * this.fullSize } + } + + get fullSize () { + return this.size * this.tile } draw (view, map) { // Create a cached image of the chunk if (this.dirty || !this.img) { cacheFactory.prepare(this.size * this.tile, this.size * this.tile) - for (let i in this.tiles) { - let tilei = this.tiles[i] - if (tilei === -1) continue - let coords = this.toXY(parseInt(i)) - let tileCoords = map.tileAt(tilei) - cacheFactory.ctx.drawImage(map.image, tileCoords.x, tileCoords.y, map.tile, map.tile, - coords.x * this.tile, coords.y * this.tile, this.tile, this.tile) + // Draw all layers + for (let i in this.layers) { + let layer = this.layers[i] + layer.draw(cacheFactory.ctx, view, map) } + + // Draw a debug grid when enabled + Debug.chunkGrid(cacheFactory.ctx, this, view) + + // Create cached image this.img = cacheFactory.capture() + + // Don't update again next tick this.dirty = false this._updated = true return @@ -127,19 +171,27 @@ class TileChunk { } update (dt) { - + for (let i in this.layers) { + this.layers[i].update(dt) + } } } class World { - constructor (heightMap, tileMaps, chunkSize = 16, tileSize = 16) { + constructor (heightMap, tileMaps, chunkSize = 16, tileSize = 16, height = 64, width = 128) { this.heightMap = heightMap this.chunkSize = chunkSize this.tileSize = tileSize this.tileMaps = tileMaps this.chunks = [] + this.height = height + this.width = width + + // Indicate to the height map where the base of the world is + this.heightMap.falloff = height * this.chunkSize // Debug info + this._unloadTick = 0 this._lastDrawCount = 0 this._lastUpdateCount = 0 } @@ -156,10 +208,10 @@ class World { let posPoint = vp.chunkIn(this.chunkSize * this.tileSize) for (let x = posPoint.x - 4; x < posPoint.x + 5; x++) { for (let y = posPoint.y - 4; y < posPoint.y + 5; y++) { - if (x < 0 || y < 0) continue + if (x < 0 || y < 0 || x >= this.width || y >= this.height) continue let exists = this.getChunk(x, y) if (!exists) { - let n = new TileChunk(x, y, this.chunkSize, this.tileSize) + let n = new Chunk(x, y, this.chunkSize, this.tileSize) n.generateMap(this.tileMaps.GROUND, this.heightMap) this.chunks.push(n) break @@ -170,17 +222,34 @@ class World { for (let i in this.chunks) { this.chunks[i].update(dt) } + + // Remove far away chunks from memory + this._unloadTick++ + if (this._unloadTick === 60) { + this._unloadTick = 0 + let keep = [] + for (let i in this.chunks) { + let chunk = this.chunks[i] + let pos = chunk.absPos + let distance = distanceTo(vp.adjustCentered, pos) + if (distance <= chunk.fullSize * UPDATE_RADIUS) { + // Keep chunk + keep.push(chunk) + } + } + this.chunks = keep + } } draw (vp) { this._lastDrawCount = 0 this._lastUpdateCount = 0 + const adj = this.tileSize for (let i in this.chunks) { let chunk = this.chunks[i] let absPos = chunk.absPos - let chunkSize = chunk.size * this.tileSize - if (absPos.x > vp.x + vp.width || absPos.x + chunkSize < vp.x || - absPos.y > vp.y + vp.height || absPos.y + chunkSize < vp.y) continue + if (absPos.x > vp.x + vp.width + adj || absPos.x + chunk.fullSize < vp.x - adj || + absPos.y > vp.y + vp.height + adj || absPos.y + chunk.fullSize < vp.y - adj) continue chunk._updated = false chunk.draw(vp, this.tileMaps.GROUND) if (chunk._updated) this._lastUpdateCount++ @@ -189,4 +258,4 @@ class World { } } -export { TileMap, TileChunk, World } +export { TileMap, Chunk, World } diff --git a/src/viewport.js b/src/viewport.js index 566a040..c64fa0f 100644 --- a/src/viewport.js +++ b/src/viewport.js @@ -15,8 +15,13 @@ class Viewport { return canvas.height } + get adjustCentered () { + return { x: Math.floor(this.x + this.width / 2), y: Math.floor(this.y + this.height / 2) } + } + chunkIn (size) { - return { x: Math.floor((this.x + this.width / 2) / size), y: Math.floor((this.y + this.height / 2) / size) } + let adj = this.adjustCentered + return { x: Math.floor(adj.x / size), y: Math.floor(adj.y / size) } } }