feat: tile layer chunking

This commit is contained in:
cha0s 2024-07-11 01:16:10 -05:00
parent 41baef2571
commit 3d4d29625d
4 changed files with 206 additions and 40 deletions

View File

@ -1,3 +1,5 @@
export const CHUNK_SIZE = 32;
export const CLIENT_LATENCY = 0; export const CLIENT_LATENCY = 0;
export const CLIENT_PREDICTION = true; export const CLIENT_PREDICTION = true;

View File

@ -1,24 +1,35 @@
import {CHUNK_SIZE} from '@/constants.js';
import Component from '@/ecs/component.js'; import Component from '@/ecs/component.js';
import {floodwalk2D, ortho, removeCollinear} from '@/util/math.js'; import {floodwalk2D, ortho, removeCollinear} from '@/util/math.js';
import vector2d from './helpers/vector-2d'; import vector2d from './helpers/vector-2d';
class LayerProxy { class LayerProxy {
$$sourceJson; $$chunks = [];
$$sourceJson = {};
constructor(instance, Component, index) { constructor(instance, Component, index) {
this.instance = instance; this.instance = instance;
this.Component = Component; this.Component = Component;
this.index = index; 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() { get area() {
return this.layer.area; return this.layer.area;
} }
clone() { 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); const proxy = new LayerProxy(this.instance, this.Component, this.index);
proxy.$$chunks = [...$$chunks];
proxy.$$sourceJson = $$sourceJson; proxy.$$sourceJson = $$sourceJson;
return proxy; 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() { get data() {
return this.layer.data; return this.layer.data;
@ -126,22 +137,66 @@ class LayerProxy {
} }
export default class TileLayers extends Component { export default class TileLayers extends Component {
async insertMany(entities) { async createMany(entities) {
for (const [id, {layerChange}] of 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) { if (layerChange) {
const component = this.get(id);
const {layers} = component;
for (const layerIndex in layerChange) { for (const layerIndex in layerChange) {
for (const calculated in layerChange[layerIndex]) { for (const calculated in layerChange[layerIndex]) {
const tile = layerChange[layerIndex][calculated]; const tile = layerChange[layerIndex][calculated];
layers[layerIndex].data[calculated] = tile; 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() { instanceFromSchema() {
return class TileLayersInstance extends super.instanceFromSchema() { return class TileLayersInstance extends super.instanceFromSchema() {

View File

@ -1,4 +1,5 @@
import { import {
CHUNK_SIZE,
RESOLUTION, RESOLUTION,
TPS, TPS,
} from '@/constants.js'; } from '@/constants.js';
@ -92,7 +93,10 @@ export default class Engine {
// remove entity link to connection to start queueing actions and pause updates // remove entity link to connection to start queueing actions and pause updates
delete connectedPlayer.entity; delete connectedPlayer.entity;
// forget previous state // forget previous state
connectedPlayer.memory.clear(); connectedPlayer.memory = {
chunks: new Map(),
nearby: new Set(),
};
// inform client of the upcoming change // inform client of the upcoming change
server.send( server.send(
connection, connection,
@ -214,7 +218,10 @@ export default class Engine {
{ {
entity: ecs.get(entity), entity: ecs.get(entity),
id, id,
memory: new Set(), memory: {
chunks: new Map(),
nearby: new Set(),
},
}, },
); );
} }
@ -359,12 +366,14 @@ export default class Engine {
y1: y0 + (RESOLUTION.y * 2), y1: y0 + (RESOLUTION.y * 2),
}); });
// Master entity. // Master entity.
nearby.add(ecs.get(1)); const master = ecs.get(1);
const lastMemory = new Set(memory.values()); nearby.add(master);
const lastNearby = new Set(memory.nearby.values());
const firstUpdate = 0 === lastNearby.size;
for (const entity of nearby) { for (const entity of nearby) {
const {id} = entity; const {id} = entity;
lastMemory.delete(id); lastNearby.delete(id);
if (!memory.has(id)) { if (!memory.nearby.has(id)) {
update[id] = entity.toJSON(); update[id] = entity.toJSON();
if (mainEntityId === id) { if (mainEntityId === id) {
update[id].MainEntity = {}; update[id].MainEntity = {};
@ -373,12 +382,91 @@ export default class Engine {
else if (ecs.diff[id]) { else if (ecs.diff[id]) {
update[id] = ecs.diff[id]; update[id] = ecs.diff[id];
} }
memory.add(id); memory.nearby.add(id);
} }
for (const id of lastMemory) { for (const id of lastNearby) {
memory.delete(id); memory.nearby.delete(id);
update[id] = false; 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; return update;
} }

View File

@ -3,42 +3,64 @@ import {PixiComponent} from '@pixi/react';
import '@pixi/spritesheet'; // NECESSARY! import '@pixi/spritesheet'; // NECESSARY!
import {CompositeTilemap} from '@pixi/tilemap'; import {CompositeTilemap} from '@pixi/tilemap';
import {CHUNK_SIZE} from '@/constants.js';
import {useAsset} from '@/context/assets.js'; import {useAsset} from '@/context/assets.js';
const TileLayerInternal = PixiComponent('TileLayer', { const TileLayerInternal = PixiComponent('TileLayer', {
create: () => { create: ({tileLayer}) => {
const container = new Container(); 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; return container;
}, },
applyProps: (container, {mask: oldMask, tileLayer: oldTileLayer}, props) => { applyProps: (container, {tileLayer: oldTileLayer}, props) => {
const {asset, mask, tileLayer} = props; const {asset, tileLayer} = props;
const extless = tileLayer.source.slice('/assets/'.length, -'.json'.length); const extless = tileLayer.source.slice('/assets/'.length, -'.json'.length);
const {textures} = asset; const {textures} = asset;
if (tileLayer === oldTileLayer) { if (tileLayer === oldTileLayer) {
return; return;
} }
if (oldMask) { for (const i in tileLayer.$$chunks) {
container.removeChildAt(1); if (!oldTileLayer || oldTileLayer.$$chunks[i] !== tileLayer.$$chunks[i]) {
container.mask = undefined; const tilemap = container.children[i];
} tilemap.clear();
if (mask) { const ax = Math.ceil(tileLayer.area.x / CHUNK_SIZE);
container.addChild(mask); const cy = Math.floor(i / ax);
container.mask = mask; const cx = i % ax;
} for (let y = 0; y < CHUNK_SIZE; ++y) {
const tilemap = container.children[0]; for (let x = 0; x < CHUNK_SIZE; ++x) {
tilemap.clear(); const ty = (cy * CHUNK_SIZE) + y;
let i = 0; const tx = (cx * CHUNK_SIZE) + x;
for (let y = 0; y < tileLayer.area.y; ++y) { if (
for (let x = 0; x < tileLayer.area.x; ++x) { tx < 0
tilemap.tile(textures[`${extless}/${tileLayer.data[i++]}`], tileLayer.tileSize.x * x, tileLayer.tileSize.y * y); || 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) { export default function TileLayer(props) {
const {mask, tileLayer} = props; const {tileLayer} = props;
const asset = useAsset(tileLayer.source); const asset = useAsset(tileLayer.source);
if (!asset) { if (!asset) {
return false; return false;
@ -48,7 +70,6 @@ export default function TileLayer(props) {
<TileLayerInternal <TileLayerInternal
{...props} {...props}
asset={asset} asset={asset}
mask={mask}
/> />
</> </>
); );