From 3d4d29625d9c2686b07a04195eeed0e84c9b14cb Mon Sep 17 00:00:00 2001 From: cha0s Date: Thu, 11 Jul 2024 01:16:10 -0500 Subject: [PATCH] feat: tile layer chunking --- app/constants.js | 2 + app/ecs-components/tile-layers.js | 75 +++++++++++++++++--- app/engine.js | 106 +++++++++++++++++++++++++--- app/react-components/tile-layer.jsx | 63 +++++++++++------ 4 files changed, 206 insertions(+), 40 deletions(-) diff --git a/app/constants.js b/app/constants.js index f9fa32f..2ed20fb 100644 --- a/app/constants.js +++ b/app/constants.js @@ -1,3 +1,5 @@ +export const CHUNK_SIZE = 32; + export const CLIENT_LATENCY = 0; export const CLIENT_PREDICTION = true; diff --git a/app/ecs-components/tile-layers.js b/app/ecs-components/tile-layers.js index 23cc1a8..40bdb8b 100644 --- a/app/ecs-components/tile-layers.js +++ b/app/ecs-components/tile-layers.js @@ -1,24 +1,35 @@ +import {CHUNK_SIZE} from '@/constants.js'; import Component from '@/ecs/component.js'; import {floodwalk2D, ortho, removeCollinear} from '@/util/math.js'; import vector2d from './helpers/vector-2d'; class LayerProxy { - $$sourceJson; + $$chunks = []; + $$sourceJson = {}; constructor(instance, Component, index) { this.instance = instance; this.Component = Component; this.index = index; + this.$$chunks = Array( + Math.ceil(this.area.x / CHUNK_SIZE) * Math.ceil(this.area.y / CHUNK_SIZE) + ).fill(0).map(() => ({})); } get area() { return this.layer.area; } clone() { - const {$$sourceJson} = this.instance.$$layersProxies[this.index]; + const {$$chunks, $$sourceJson} = this.instance.$$layersProxies[this.index]; const proxy = new LayerProxy(this.instance, this.Component, this.index); + proxy.$$chunks = [...$$chunks]; proxy.$$sourceJson = $$sourceJson; return proxy; - + } + compute(index) { + const cx = Math.floor((index % this.area.x) / CHUNK_SIZE); + const cy = Math.floor(Math.floor(index / this.area.x) / CHUNK_SIZE); + const ax = Math.ceil(this.area.x / CHUNK_SIZE); + return cy * ax + cx; } get data() { return this.layer.data; @@ -126,22 +137,66 @@ class LayerProxy { } export default class TileLayers extends Component { - async insertMany(entities) { - for (const [id, {layerChange}] of entities) { + async createMany(entities) { + for (const [, {layerChange, layers}] of entities) { + if (layers) { + for (const layer of layers) { + const area = layer.area.x * layer.area.y; + if (layer.data.length < area) { + for (let i = 0; i < area; ++i) { + if (!layer.data[i]) { + layer.data[i] = 0; + } + } + } + } + } if (layerChange) { - const component = this.get(id); - const {layers} = component; for (const layerIndex in layerChange) { for (const calculated in layerChange[layerIndex]) { const tile = layerChange[layerIndex][calculated]; layers[layerIndex].data[calculated] = tile; } - layers[layerIndex] = {...layers[layerIndex]}; - component.$$layersProxies[layerIndex] = component.$$layersProxies[layerIndex].clone(); } } } - return super.insertMany(entities); + return super.createMany(entities); + } + async insertMany(entities) { + for (const [, {layers}] of entities) { + if (layers) { + for (const layer of layers) { + const area = layer.area.x * layer.area.y; + if (layer.data.length < area) { + for (let i = 0; i < area; ++i) { + if (!layer.data[i]) { + layer.data[i] = 0; + } + } + } + } + } + } + await super.insertMany(entities); + for (const [id, {layerChange}] of entities) { + if (layerChange) { + const component = this.get(id); + const {layers} = component; + for (const layerIndex in layerChange) { + const proxy = component.$$layersProxies[layerIndex].clone(); + const chunksChanged = new Set(); + for (const tileIndex in layerChange[layerIndex]) { + chunksChanged.add(proxy.compute(tileIndex)); + const tile = layerChange[layerIndex][tileIndex]; + layers[layerIndex].data[tileIndex] = tile; + } + for (const chunkChanged of chunksChanged) { + proxy.$$chunks[chunkChanged] = {}; + } + component.$$layersProxies[layerIndex] = proxy; + } + } + } } instanceFromSchema() { return class TileLayersInstance extends super.instanceFromSchema() { diff --git a/app/engine.js b/app/engine.js index d4e0075..7d9dd5c 100644 --- a/app/engine.js +++ b/app/engine.js @@ -1,4 +1,5 @@ import { + CHUNK_SIZE, RESOLUTION, TPS, } from '@/constants.js'; @@ -92,7 +93,10 @@ export default class Engine { // remove entity link to connection to start queueing actions and pause updates delete connectedPlayer.entity; // forget previous state - connectedPlayer.memory.clear(); + connectedPlayer.memory = { + chunks: new Map(), + nearby: new Set(), + }; // inform client of the upcoming change server.send( connection, @@ -214,7 +218,10 @@ export default class Engine { { entity: ecs.get(entity), id, - memory: new Set(), + memory: { + chunks: new Map(), + nearby: new Set(), + }, }, ); } @@ -359,12 +366,14 @@ export default class Engine { y1: y0 + (RESOLUTION.y * 2), }); // Master entity. - nearby.add(ecs.get(1)); - const lastMemory = new Set(memory.values()); + const master = ecs.get(1); + nearby.add(master); + const lastNearby = new Set(memory.nearby.values()); + const firstUpdate = 0 === lastNearby.size; for (const entity of nearby) { const {id} = entity; - lastMemory.delete(id); - if (!memory.has(id)) { + lastNearby.delete(id); + if (!memory.nearby.has(id)) { update[id] = entity.toJSON(); if (mainEntityId === id) { update[id].MainEntity = {}; @@ -373,12 +382,91 @@ export default class Engine { else if (ecs.diff[id]) { update[id] = ecs.diff[id]; } - memory.add(id); + memory.nearby.add(id); } - for (const id of lastMemory) { - memory.delete(id); + for (const id of lastNearby) { + memory.nearby.delete(id); update[id] = false; } + // Tile layer chunking + const {TileLayers} = master; + const {layers} = TileLayers; + let layerChange; + for (const i in layers) { + const layer = TileLayers.layer(i); + const cx = CHUNK_SIZE * layer.tileSize.x; + const cy = CHUNK_SIZE * layer.tileSize.y; + const rx = 1 + Math.ceil((RESOLUTION.x * 2) / cx); + const ry = 1 + Math.ceil((RESOLUTION.y * 2) / cy); + const lx = Math.floor((entity.Position.x - RESOLUTION.x) / cx); + const ly = Math.floor((entity.Position.y - RESOLUTION.y) / cy); + const ax = Math.ceil(layer.area.x / CHUNK_SIZE); + for (let wy = 0; wy < ry; ++wy) { + for (let wx = 0; wx < rx; ++wx) { + const iy = wy + ly; + const ix = wx + lx; + if ( + ix >= 0 + && iy >= 0 + && ix < Math.ceil(layer.area.x / CHUNK_SIZE) + && iy < Math.ceil(layer.area.y / CHUNK_SIZE) + ) { + const chunk = iy * ax + ix; + if (!memory.chunks.has(i)) { + memory.chunks.set(i, new Set()); + } + if (!memory.chunks.get(i).has(chunk)) { + memory.chunks.get(i).add(chunk); + if (!layerChange) { + layerChange = {}; + } + if (!layerChange[i]) { + layerChange[i] = {}; + } + for (let y = 0; y < CHUNK_SIZE; ++y) { + for (let x = 0; x < CHUNK_SIZE; ++x) { + const ty = (iy * CHUNK_SIZE) + y; + const tx = (ix * CHUNK_SIZE) + x; + if ( + tx < 0 + || ty < 0 + || tx >= layers[i].area.x + || ty >= layers[i].area.y + ) { + continue; + } + const computed = ((iy * CHUNK_SIZE) + y) * layers[i].area.x + ((ix * CHUNK_SIZE) + x); + layerChange[i][computed] = layers[i].data[computed]; + } + } + } + } + } + } + } + if (firstUpdate && update['1']) { + const {TileLayers} = update['1']; + if (TileLayers) { + const layersUpdate = []; + const {layers} = TileLayers; + for (const l in layers) { + layersUpdate[l] = { + ...layers[l], + data: [], + }; + } + update['1'].TileLayers = {layers: layersUpdate}; + } + } + if (layerChange) { + if (!update['1']) { + update['1'] = {}; + } + if (!update['1'].TileLayers) { + update['1'].TileLayers = {}; + } + update['1'].TileLayers.layerChange = layerChange; + } return update; } diff --git a/app/react-components/tile-layer.jsx b/app/react-components/tile-layer.jsx index ff53939..3d6d39e 100644 --- a/app/react-components/tile-layer.jsx +++ b/app/react-components/tile-layer.jsx @@ -3,42 +3,64 @@ import {PixiComponent} from '@pixi/react'; import '@pixi/spritesheet'; // NECESSARY! import {CompositeTilemap} from '@pixi/tilemap'; +import {CHUNK_SIZE} from '@/constants.js'; import {useAsset} from '@/context/assets.js'; const TileLayerInternal = PixiComponent('TileLayer', { - create: () => { + create: ({tileLayer}) => { const container = new Container(); - container.addChild(new CompositeTilemap()); + const cy = Math.ceil(tileLayer.area.y / CHUNK_SIZE); + const cx = Math.ceil(tileLayer.area.x / CHUNK_SIZE); + for (let iy = 0; iy < cy; ++iy) { + for (let ix = 0; ix < cx; ++ix) { + const tilemap = new CompositeTilemap(); + tilemap.x = tileLayer.tileSize.x * CHUNK_SIZE * ix; + tilemap.y = tileLayer.tileSize.y * CHUNK_SIZE * iy; + container.addChild(tilemap); + } + } return container; }, - applyProps: (container, {mask: oldMask, tileLayer: oldTileLayer}, props) => { - const {asset, mask, tileLayer} = props; + applyProps: (container, {tileLayer: oldTileLayer}, props) => { + const {asset, tileLayer} = props; const extless = tileLayer.source.slice('/assets/'.length, -'.json'.length); const {textures} = asset; if (tileLayer === oldTileLayer) { return; } - if (oldMask) { - container.removeChildAt(1); - container.mask = undefined; - } - if (mask) { - container.addChild(mask); - container.mask = mask; - } - const tilemap = container.children[0]; - tilemap.clear(); - let i = 0; - for (let y = 0; y < tileLayer.area.y; ++y) { - for (let x = 0; x < tileLayer.area.x; ++x) { - tilemap.tile(textures[`${extless}/${tileLayer.data[i++]}`], tileLayer.tileSize.x * x, tileLayer.tileSize.y * y); + for (const i in tileLayer.$$chunks) { + if (!oldTileLayer || oldTileLayer.$$chunks[i] !== tileLayer.$$chunks[i]) { + const tilemap = container.children[i]; + tilemap.clear(); + const ax = Math.ceil(tileLayer.area.x / CHUNK_SIZE); + const cy = Math.floor(i / ax); + const cx = i % ax; + for (let y = 0; y < CHUNK_SIZE; ++y) { + for (let x = 0; x < CHUNK_SIZE; ++x) { + const ty = (cy * CHUNK_SIZE) + y; + const tx = (cx * CHUNK_SIZE) + x; + if ( + tx < 0 + || ty < 0 + || tx >= tileLayer.area.x + || ty >= tileLayer.area.y + ) { + continue; + } + tilemap.tile( + textures[`${extless}/${tileLayer.data[ty * tileLayer.area.x + tx]}`], + tileLayer.tileSize.x * x, + tileLayer.tileSize.y * y, + ); + } + } } } }, -}) +}); export default function TileLayer(props) { - const {mask, tileLayer} = props; + const {tileLayer} = props; const asset = useAsset(tileLayer.source); if (!asset) { return false; @@ -48,7 +70,6 @@ export default function TileLayer(props) { );