optimization

master
Denis Ranneft 1 month ago
parent 0d6a82e0e1
commit 5d5a7a4685

@ -1,5 +1,5 @@
{ {
"version": 13, "version": 20,
"assetsRoot": "frontend/assets", "assetsRoot": "frontend/assets",
"note": "file paths relative to frontend/assets. Rest camp: prop.camp_tent/fire/bag.v0 (wild rest). Other props + heroes + NPC.", "note": "file paths relative to frontend/assets. Rest camp: prop.camp_tent/fire/bag.v0 (wild rest). Other props + heroes + NPC.",
"textures": { "textures": {
@ -659,72 +659,86 @@
"enemy.boar_l5_5_canyon.south": { "enemy.boar_l5_5_canyon.south": {
"file": "enemies/enemy.boar_l5_5_canyon.south.png", "file": "enemies/enemy.boar_l5_5_canyon.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "e0a21c3e-f78a-4e13-b09b-14d9a4dea9b8"
}, },
"enemy.boar_l5_5_swamp.south": { "enemy.boar_l5_5_swamp.south": {
"file": "enemies/enemy.boar_l5_5_swamp.south.png", "file": "enemies/enemy.boar_l5_5_swamp.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "0f3bdb7e-a9ce-48eb-b177-b0f753a527ed"
}, },
"enemy.boar_l6_6_volcanic.south": { "enemy.boar_l6_6_volcanic.south": {
"file": "enemies/enemy.boar_l6_6_volcanic.south.png", "file": "enemies/enemy.boar_l6_6_volcanic.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "344f7710-9aa0-4ba7-ae6c-4f4b1a66f053"
}, },
"enemy.boar_l6_6_astral.south": { "enemy.boar_l6_6_astral.south": {
"file": "enemies/enemy.boar_l6_6_astral.south.png", "file": "enemies/enemy.boar_l6_6_astral.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "ab9e6ea1-b63d-4c93-9853-8814bae2d2b3"
}, },
"enemy.zombie_l3_4_meadow.south": { "enemy.zombie_l3_4_meadow.south": {
"file": "enemies/enemy.zombie_l3_4_meadow.south.png", "file": "enemies/enemy.zombie_l3_4_meadow.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "5e55c635-8947-407d-a86c-b508dbcc72b6"
}, },
"enemy.zombie_l3_4_forest.south": { "enemy.zombie_l3_4_forest.south": {
"file": "enemies/enemy.zombie_l3_4_forest.south.png", "file": "enemies/enemy.zombie_l3_4_forest.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "e33f6775-7399-4087-8baa-520cb17101f9"
}, },
"enemy.zombie_l5_5_forest.south": { "enemy.zombie_l5_5_forest.south": {
"file": "enemies/enemy.zombie_l5_5_forest.south.png", "file": "enemies/enemy.zombie_l5_5_forest.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "79169a55-37f0-46b3-b988-d358498163a6"
}, },
"enemy.zombie_l5_5_ruins.south": { "enemy.zombie_l5_5_ruins.south": {
"file": "enemies/enemy.zombie_l5_5_ruins.south.png", "file": "enemies/enemy.zombie_l5_5_ruins.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "ca489425-7ff8-41f1-aebc-4dce32e74fd5"
}, },
"enemy.zombie_l6_6_ruins.south": { "enemy.zombie_l6_6_ruins.south": {
"file": "enemies/enemy.zombie_l6_6_ruins.south.png", "file": "enemies/enemy.zombie_l6_6_ruins.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "96b5dacc-f089-43d3-92f2-45f826c64cad"
}, },
"enemy.zombie_l6_6_canyon.south": { "enemy.zombie_l6_6_canyon.south": {
"file": "enemies/enemy.zombie_l6_6_canyon.south.png", "file": "enemies/enemy.zombie_l6_6_canyon.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "9890b8a8-fed4-4fc1-b79f-7c9d2ebb0c60"
}, },
"enemy.zombie_l7_7_canyon.south": { "enemy.zombie_l7_7_canyon.south": {
"file": "enemies/enemy.zombie_l7_7_canyon.south.png", "file": "enemies/enemy.zombie_l7_7_canyon.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "fbc52c9e-f2ba-4737-9eda-358a4cc19531"
}, },
"enemy.zombie_l7_7_swamp.south": { "enemy.zombie_l7_7_swamp.south": {
"file": "enemies/enemy.zombie_l7_7_swamp.south.png", "file": "enemies/enemy.zombie_l7_7_swamp.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "2cb494a5-46ed-4390-b8d6-0c71ba4c02b0"
}, },
"enemy.zombie_l8_8_volcanic.south": { "enemy.zombie_l8_8_volcanic.south": {
"file": "enemies/enemy.zombie_l8_8_volcanic.south.png", "file": "enemies/enemy.zombie_l8_8_volcanic.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "2981d74e-06e5-45c4-8401-88bd121b0543"
}, },
"enemy.zombie_l8_8_astral.south": { "enemy.zombie_l8_8_astral.south": {
"file": "enemies/enemy.zombie_l8_8_astral.south.png", "file": "enemies/enemy.zombie_l8_8_astral.south.png",
"kind": "map_object", "kind": "map_object",
"rotation": "south" "rotation": "south",
"pixellabObjectId": "a1700116-d83f-4c04-9fe6-79d7e0f8d2c4"
}, },
"enemy.spider_l4_5_meadow.south": { "enemy.spider_l4_5_meadow.south": {
"file": "enemies/enemy.spider_l4_5_meadow.south.png", "file": "enemies/enemy.spider_l4_5_meadow.south.png",

@ -106,10 +106,11 @@ export class GameRenderer {
private _usedTileSprites = new Set<string>(); private _usedTileSprites = new Set<string>();
private _usedObjectSprites = new Set<string>(); private _usedObjectSprites = new Set<string>();
private _emptySpriteSet = new Set<string>(); private _emptySpriteSet = new Set<string>();
private _groundTerrainCache = new Map<string, string>();
private _buildingSpritePool = new Map<string, SpritePoolEntry>(); private _buildingSpritePool = new Map<string, SpritePoolEntry>();
private _characterSpritePool = new Map<string, SpritePoolEntry>(); private _characterSpritePool = new Map<string, SpritePoolEntry>();
private _npcSpritePool = new Map<string, SpritePoolEntry>(); private _npcSpritePool = new Map<string, SpritePoolEntry>();
private _usedBuildingSprites = new Set<string>();
private _usedNpcSprites = new Set<string>();
private _groundDirty = true; private _groundDirty = true;
private _lastGroundBounds: private _lastGroundBounds:
| { | {
@ -157,6 +158,13 @@ export class GameRenderer {
private _lastEntitySortMs = 0; private _lastEntitySortMs = 0;
private _entitySortIntervalMs = 120; private _entitySortIntervalMs = 120;
private _townDrawDirty = true;
private _lastTownDrawMs = 0;
private _townDrawIntervalMs = 120;
private _lastTownCameraX = Number.NaN;
private _lastTownCameraY = Number.NaN;
private _lastTownsRef: TownData[] | null = null;
private _lastTownCount = -1;
private _drawBush(gfx: Graphics, x: number, y: number, variant: number): void { private _drawBush(gfx: Graphics, x: number, y: number, variant: number): void {
const s = (0.9 + variant * 0.25) * 3.5; const s = (0.9 + variant * 0.25) * 3.5;
@ -571,6 +579,7 @@ export class GameRenderer {
this.worldContainer.x = viewport.width / 2; this.worldContainer.x = viewport.width / 2;
this.worldContainer.y = viewport.height / 2; this.worldContainer.y = viewport.height / 2;
this.worldContainer.scale.set(MAP_ZOOM); this.worldContainer.scale.set(MAP_ZOOM);
this._townDrawDirty = true;
} }
/** /**
@ -641,25 +650,40 @@ export class GameRenderer {
const usedObjectSprites = this._usedObjectSprites; const usedObjectSprites = this._usedObjectSprites;
usedTileSprites.clear(); usedTileSprites.clear();
usedObjectSprites.clear(); usedObjectSprites.clear();
const terrainCache = this._groundTerrainCache; const tileMinX = cx - (halfW + hw);
terrainCache.clear(); const tileMaxX = cx + (halfW + hw);
const tileMinY = cy - (halfH + hh);
const tileMaxY = cy + (halfH + hh);
// Pass 2 needs slightly expanded bounds for oversized props.
const objectPaddingTiles = 4;
const objectStartX = startX - objectPaddingTiles;
const objectEndX = endX + objectPaddingTiles;
const objectStartY = startY - objectPaddingTiles;
const objectEndY = endY + objectPaddingTiles;
const objectGridH = objectEndY - objectStartY + 1;
const terrainGrid = new Array<string | undefined>(
(objectEndX - objectStartX + 1) * objectGridH,
);
const terrainIndex = (wx: number, wy: number): number =>
(wx - objectStartX) * objectGridH + (wy - objectStartY);
const terrainAt = (wx: number, wy: number): string => { const terrainAt = (wx: number, wy: number): string => {
const key = `${wx},${wy}`; const idx = terrainIndex(wx, wy);
const cached = terrainCache.get(key); const cached = terrainGrid[idx];
if (cached) return cached; if (cached !== undefined) return cached;
const terrain = proceduralTerrain(wx, wy, terrainCtx); const terrain = proceduralTerrain(wx, wy, terrainCtx);
terrainCache.set(key, terrain); terrainGrid[idx] = terrain;
return terrain; return terrain;
}; };
// Pass 1: tiles // Pass 1: tiles
for (let wx = startX; wx <= endX; wx++) { for (let wx = startX; wx <= endX; wx++) {
let isoX = (wx - startY) * hw;
let isoY = (wx + startY) * hh;
for (let wy = startY; wy <= endY; wy++) { for (let wy = startY; wy <= endY; wy++) {
const iso = worldToScreen(wx, wy); if (isoX < tileMinX || isoX > tileMaxX || isoY < tileMinY || isoY > tileMaxY) {
if ( isoX -= hw;
Math.abs(iso.x - cx) > halfW + hw || isoY += hh;
Math.abs(iso.y - cy) > halfH + hh
) {
continue; continue;
} }
const terrain = terrainAt(wx, wy); const terrain = terrainAt(wx, wy);
@ -680,9 +704,9 @@ export class GameRenderer {
wy, wy,
this._tileSpriteFreeList, this._tileSpriteFreeList,
); );
entry.sprite.x = iso.x; entry.sprite.x = isoX;
entry.sprite.y = iso.y + hh; entry.sprite.y = isoY + hh;
entry.sprite.zIndex = iso.y + hh; entry.sprite.zIndex = isoY + hh;
const texW = entry.sprite.texture.orig.width || entry.sprite.texture.width || TILE_WIDTH; const texW = entry.sprite.texture.orig.width || entry.sprite.texture.width || TILE_WIDTH;
const scale = (TILE_WIDTH / texW) * TERRAIN_SEAM_BLEED_SCALE; const scale = (TILE_WIDTH / texW) * TERRAIN_SEAM_BLEED_SCALE;
entry.sprite.scale.set(scale); entry.sprite.scale.set(scale);
@ -691,18 +715,18 @@ export class GameRenderer {
const color = this._terrainColors(terrain, dark); const color = this._terrainColors(terrain, dark);
gfx.poly([ gfx.poly([
iso.x, iso.y - hh, isoX, isoY - hh,
iso.x + hw, iso.y, isoX + hw, isoY,
iso.x, iso.y + hh, isoX, isoY + hh,
iso.x - hw, iso.y, isoX - hw, isoY,
]); ]);
gfx.fill({ color, alpha: 1 }); gfx.fill({ color, alpha: 1 });
gfx.poly([ gfx.poly([
iso.x, iso.y - hh, isoX, isoY - hh,
iso.x + hw, iso.y, isoX + hw, isoY,
iso.x, iso.y + hh, isoX, isoY + hh,
iso.x - hw, iso.y, isoX - hw, isoY,
]); ]);
gfx.stroke({ gfx.stroke({
color: this._terrainStrokeColor(terrain), color: this._terrainStrokeColor(terrain),
@ -710,28 +734,33 @@ export class GameRenderer {
alpha: 0.25, alpha: 0.25,
}); });
} }
isoX -= hw;
isoY += hh;
} }
} }
// Pass 2: objects (drawn after tiles so they layer on top) // Pass 2: objects (drawn after tiles so they layer on top)
// Slightly expanded object bounds prevent enlarged props from edge clipping. // Slightly expanded object bounds prevent enlarged props from edge clipping.
const objectPaddingTiles = 4; const objectMinX = cx - (halfW + TILE_WIDTH * 1.5);
const objectStartX = startX - objectPaddingTiles; const objectMaxX = cx + (halfW + TILE_WIDTH * 1.5);
const objectEndX = endX + objectPaddingTiles; const objectMinY = cy - (halfH + TILE_HEIGHT * 2);
const objectStartY = startY - objectPaddingTiles; const objectMaxY = cy + (halfH + TILE_HEIGHT * 2);
const objectEndY = endY + objectPaddingTiles;
for (let wx = objectStartX; wx <= objectEndX; wx++) { for (let wx = objectStartX; wx <= objectEndX; wx++) {
let isoX = (wx - objectStartY) * hw;
let isoY = (wx + objectStartY) * hh;
for (let wy = objectStartY; wy <= objectEndY; wy++) { for (let wy = objectStartY; wy <= objectEndY; wy++) {
const iso = worldToScreen(wx, wy); if (isoX < objectMinX || isoX > objectMaxX || isoY < objectMinY || isoY > objectMaxY) {
if ( isoX -= hw;
Math.abs(iso.x - cx) > halfW + TILE_WIDTH * 1.5 || isoY += hh;
Math.abs(iso.y - cy) > halfH + TILE_HEIGHT * 2
) {
continue; continue;
} }
const terrainHere = terrainAt(wx, wy); const terrainHere = terrainAt(wx, wy);
const obj = proceduralObject(wx, wy, terrainHere, terrainCtx); const obj = proceduralObject(wx, wy, terrainHere, terrainCtx);
if (!obj) continue; if (!obj) {
isoX -= hw;
isoY += hh;
continue;
}
const variant = tileHash(wx, wy, 999); const variant = tileHash(wx, wy, 999);
const objTextureKey = spritesReady ? objectToTextureKey(obj, variant) : null; const objTextureKey = spritesReady ? objectToTextureKey(obj, variant) : null;
const objTexture = objTextureKey ? this._spriteRegistry.getTexture(objTextureKey) : null; const objTexture = objTextureKey ? this._spriteRegistry.getTexture(objTextureKey) : null;
@ -748,25 +777,27 @@ export class GameRenderer {
wy, wy,
this._objectSpriteFreeList, this._objectSpriteFreeList,
); );
entry.sprite.x = iso.x; entry.sprite.x = isoX;
entry.sprite.y = iso.y; entry.sprite.y = isoY;
entry.sprite.zIndex = iso.y; entry.sprite.zIndex = isoY;
entry.sprite.visible = true; entry.sprite.visible = true;
} else { } else {
if (obj === 'tree') this._drawTree(gfx, iso.x, iso.y, variant); if (obj === 'tree') this._drawTree(gfx, isoX, isoY, variant);
else if (obj === 'bush') this._drawBush(gfx, iso.x, iso.y, variant); else if (obj === 'bush') this._drawBush(gfx, isoX, isoY, variant);
else if (obj === 'rock') this._drawRock(gfx, iso.x, iso.y, variant); else if (obj === 'rock') this._drawRock(gfx, isoX, isoY, variant);
else if (obj === 'stump') this._drawStump(gfx, iso.x, iso.y, variant); else if (obj === 'stump') this._drawStump(gfx, isoX, isoY, variant);
else if (obj === 'cart') this._drawBrokenCart(gfx, iso.x, iso.y, variant); else if (obj === 'cart') this._drawBrokenCart(gfx, isoX, isoY, variant);
else if (obj === 'bones') this._drawBones(gfx, iso.x, iso.y, variant); else if (obj === 'bones') this._drawBones(gfx, isoX, isoY, variant);
else if (obj === 'mushroom') this._drawMushroom(gfx, iso.x, iso.y, variant); else if (obj === 'mushroom') this._drawMushroom(gfx, isoX, isoY, variant);
else if (obj === 'ruin') this._drawRuin(gfx, iso.x, iso.y, variant); else if (obj === 'ruin') this._drawRuin(gfx, isoX, isoY, variant);
else if (obj === 'stall') this._drawMarketStall(gfx, iso.x, iso.y, variant); else if (obj === 'stall') this._drawMarketStall(gfx, isoX, isoY, variant);
else if (obj === 'well') this._drawWell(gfx, iso.x, iso.y, variant); else if (obj === 'well') this._drawWell(gfx, isoX, isoY, variant);
else if (obj === 'banner') this._drawBanner(gfx, iso.x, iso.y, variant); else if (obj === 'banner') this._drawBanner(gfx, isoX, isoY, variant);
else if (obj === 'barrel') this._drawBarrel(gfx, iso.x, iso.y, variant); else if (obj === 'barrel') this._drawBarrel(gfx, isoX, isoY, variant);
else if (obj === 'leaves') this._drawLeafPile(gfx, iso.x, iso.y, variant); else if (obj === 'leaves') this._drawLeafPile(gfx, isoX, isoY, variant);
} }
isoX -= hw;
isoY += hh;
} }
} }
@ -1578,6 +1609,28 @@ export class GameRenderer {
const gfx = this._townGfx; const gfx = this._townGfx;
const iconGfx = this._townIconGfx; const iconGfx = this._townIconGfx;
if (!gfx || !iconGfx) return; if (!gfx || !iconGfx) return;
const cx = camera.finalX;
const cy = camera.finalY;
if (this._lastTownsRef !== towns || this._lastTownCount !== towns.length) {
this._townDrawDirty = true;
this._lastTownsRef = towns;
this._lastTownCount = towns.length;
}
const now = performance.now();
const dx = Math.abs(cx - this._lastTownCameraX);
const dy = Math.abs(cy - this._lastTownCameraY);
if (
!this._townDrawDirty &&
now - this._lastTownDrawMs < this._townDrawIntervalMs &&
dx < TILE_WIDTH &&
dy < TILE_HEIGHT
) {
return;
}
this._townDrawDirty = false;
this._lastTownDrawMs = now;
this._lastTownCameraX = cx;
this._lastTownCameraY = cy;
gfx.clear(); gfx.clear();
iconGfx.clear(); iconGfx.clear();
@ -1586,14 +1639,13 @@ export class GameRenderer {
lbl.visible = false; lbl.visible = false;
} }
const cx = camera.finalX;
const cy = camera.finalY;
const halfW = screenWidth / (2 * MAP_ZOOM) + TILE_WIDTH * 10; const halfW = screenWidth / (2 * MAP_ZOOM) + TILE_WIDTH * 10;
const halfH = screenHeight / (2 * MAP_ZOOM) + TILE_HEIGHT * 10; const halfH = screenHeight / (2 * MAP_ZOOM) + TILE_HEIGHT * 10;
let labelIdx = 0; let labelIdx = 0;
const usedBuildingSprites = new Set<string>(); const usedBuildingSprites = this._usedBuildingSprites;
usedBuildingSprites.clear();
for (const town of towns) { for (const town of towns) {
// Convert town world position to screen space // Convert town world position to screen space
const townScreen = worldToScreen(town.centerX, town.centerY); const townScreen = worldToScreen(town.centerX, town.centerY);
@ -1720,7 +1772,8 @@ export class GameRenderer {
const halfH = screenHeight / (2 * MAP_ZOOM) + TILE_HEIGHT * 3; const halfH = screenHeight / (2 * MAP_ZOOM) + TILE_HEIGHT * 3;
let labelIdx = 0; let labelIdx = 0;
const usedNpcSprites = new Set<string>(); const usedNpcSprites = this._usedNpcSprites;
usedNpcSprites.clear();
for (const npc of npcs) { for (const npc of npcs) {
const iso = worldToScreen(npc.worldX, npc.worldY); const iso = worldToScreen(npc.worldX, npc.worldY);

Loading…
Cancel
Save