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_PREDICTION = true;

View File

@ -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() {

View File

@ -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;
}

View File

@ -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];
for (const i in tileLayer.$$chunks) {
if (!oldTileLayer || oldTileLayer.$$chunks[i] !== tileLayer.$$chunks[i]) {
const tilemap = container.children[i];
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);
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) {
<TileLayerInternal
{...props}
asset={asset}
mask={mask}
/>
</>
);