Merge pull request #200 from Phlosioneer/ejector-cache-opt

Optimize ejector cache
This commit is contained in:
tobspr 2020-06-24 19:41:49 +02:00 committed by GitHub
commit b575bc4f41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 159 additions and 84 deletions

View File

@ -3,13 +3,16 @@ import { BaseItem } from "../base_item";
import { Component } from "../component"; import { Component } from "../component";
import { types } from "../../savegame/serialization"; import { types } from "../../savegame/serialization";
import { gItemRegistry } from "../../core/global_registries"; import { gItemRegistry } from "../../core/global_registries";
import { Entity } from "../entity";
/** /**
* @typedef {{ * @typedef {{
* pos: Vector, * pos: Vector,
* direction: enumDirection, * direction: enumDirection,
* item: BaseItem, * item: BaseItem,
* progress: number? * progress: number?,
* cachedDestSlot?: import("./item_acceptor").ItemAcceptorLocatedSlot,
* cachedTargetEntity?: Entity
* }} ItemEjectorSlot * }} ItemEjectorSlot
*/ */
@ -19,6 +22,8 @@ export class ItemEjectorComponent extends Component {
} }
static getSchema() { static getSchema() {
// The cachedDestSlot, cachedTargetEntity, and cachedConnectedSlots fields
// are not serialized.
return { return {
instantEject: types.bool, instantEject: types.bool,
slots: types.array( slots: types.array(
@ -61,6 +66,9 @@ export class ItemEjectorComponent extends Component {
this.instantEject = instantEject; this.instantEject = instantEject;
this.setSlots(slots); this.setSlots(slots);
/** @type {ItemEjectorSlot[]} */
this.cachedConnectedSlots = null;
} }
/** /**
@ -76,6 +84,8 @@ export class ItemEjectorComponent extends Component {
direction: slot.direction, direction: slot.direction,
item: null, item: null,
progress: 0, progress: 0,
cachedDestSlot: null,
cachedTargetEntity: null,
}); });
} }
} }

View File

@ -7,6 +7,7 @@ import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
import { Math_min } from "../../core/builtins"; import { Math_min } from "../../core/builtins";
import { createLogger } from "../../core/logging"; import { createLogger } from "../../core/logging";
import { Rectangle } from "../../core/rectangle";
const logger = createLogger("systems/ejector"); const logger = createLogger("systems/ejector");
@ -14,23 +15,34 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
constructor(root) { constructor(root) {
super(root, [ItemEjectorComponent]); super(root, [ItemEjectorComponent]);
/**
* @type {Array<{
* targetEntity: Entity,
* sourceSlot: import("../components/item_ejector").ItemEjectorSlot,
* destSlot: import("../components/item_acceptor").ItemAcceptorLocatedSlot
* }>}
*/
this.cache = [];
this.cacheNeedsUpdate = true; this.cacheNeedsUpdate = true;
this.root.signals.entityAdded.add(this.invalidateCache, this); this.root.signals.entityAdded.add(this.invalidateCache, this);
this.root.signals.entityDestroyed.add(this.invalidateCache, this); this.root.signals.entityDestroyed.add(this.invalidateCache, this);
/**
* @type {Rectangle[]}
*/
this.smallCacheAreas = [];
}
/**
*
* @param {Entity} entity
*/
invalidateCache(entity) {
if (!entity.components.StaticMapEntity) {
return;
} }
invalidateCache() {
this.cacheNeedsUpdate = true; this.cacheNeedsUpdate = true;
// Optimize for the common case: adding or removing one building at a time. Clicking
// and dragging can cause up to 4 add/remove signals.
const staticComp = entity.components.StaticMapEntity;
const bounds = staticComp.getTileSpaceBounds();
const expandedBounds = bounds.expandedInAllDirections(2);
this.smallCacheAreas.push(expandedBounds);
} }
/** /**
@ -39,18 +51,61 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
recomputeCache() { recomputeCache() {
logger.log("Recomputing cache"); logger.log("Recomputing cache");
const cache = []; let entryCount = 0;
if (this.smallCacheAreas.length <= 4) {
// Only recompute caches of entities inside the rectangles.
for (let i = 0; i < this.smallCacheAreas.length; i++) {
entryCount += this.recomputeAreaCaches(this.smallCacheAreas[i]);
}
} else {
// Try to find acceptors for every ejector // Try to find acceptors for every ejector
for (let i = 0; i < this.allEntities.length; ++i) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];
entryCount += this.recomputeSingleEntityCache(entity);
}
}
logger.log("Found", entryCount, "entries to update");
this.smallCacheAreas = [];
}
/**
*
* @param {Rectangle} area
*/
recomputeAreaCaches(area) {
let entryCount = 0;
for (let x = area.x; x < area.right(); ++x) {
for (let y = area.y; y < area.bottom(); ++y) {
const entity = this.root.map.getTileContentXY(x, y);
if (entity && entity.components.ItemEjector) {
entryCount += this.recomputeSingleEntityCache(entity);
}
}
}
return entryCount;
}
/**
*
* @param {Entity} entity
*/
recomputeSingleEntityCache(entity) {
const ejectorComp = entity.components.ItemEjector; const ejectorComp = entity.components.ItemEjector;
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
// Clear the old cache.
ejectorComp.cachedConnectedSlots = null;
// For every ejector slot, try to find an acceptor // For every ejector slot, try to find an acceptor
let entryCount = 0;
for (let ejectorSlotIndex = 0; ejectorSlotIndex < ejectorComp.slots.length; ++ejectorSlotIndex) { for (let ejectorSlotIndex = 0; ejectorSlotIndex < ejectorComp.slots.length; ++ejectorSlotIndex) {
const ejectorSlot = ejectorComp.slots[ejectorSlotIndex]; const ejectorSlot = ejectorComp.slots[ejectorSlotIndex];
// Clear the old cache.
ejectorSlot.cachedDestSlot = null;
ejectorSlot.cachedTargetEntity = null;
// Figure out where and into which direction we eject items // Figure out where and into which direction we eject items
const ejectSlotWsTile = staticComp.localTileToWorld(ejectorSlot.pos); const ejectSlotWsTile = staticComp.localTileToWorld(ejectorSlot.pos);
const ejectSlotWsDirection = staticComp.localDirectionToWorld(ejectorSlot.direction); const ejectSlotWsDirection = staticComp.localDirectionToWorld(ejectorSlot.direction);
@ -82,16 +137,16 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
} }
// Ok we found a connection // Ok we found a connection
cache.push({ if (ejectorComp.cachedConnectedSlots) {
targetEntity, ejectorComp.cachedConnectedSlots.push(ejectorSlot);
sourceSlot: ejectorSlot, } else {
destSlot: matchingSlot, ejectorComp.cachedConnectedSlots = [ejectorSlot];
});
} }
ejectorSlot.cachedTargetEntity = targetEntity;
ejectorSlot.cachedDestSlot = matchingSlot;
entryCount += 1;
} }
return entryCount;
this.cache = cache;
logger.log("Found", cache.length, "entries to update");
} }
update() { update() {
@ -109,8 +164,17 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
} }
// Go over all cache entries // Go over all cache entries
for (let i = 0; i < this.cache.length; ++i) { for (let i = 0; i < this.allEntities.length; ++i) {
const { sourceSlot, destSlot, targetEntity } = this.cache[i]; const sourceEntity = this.allEntities[i];
const sourceEjectorComp = sourceEntity.components.ItemEjector;
if (!sourceEjectorComp.cachedConnectedSlots) {
continue;
}
for (let j = 0; j < sourceEjectorComp.cachedConnectedSlots.length; j++) {
const sourceSlot = sourceEjectorComp.cachedConnectedSlots[j];
const destSlot = sourceSlot.cachedDestSlot;
const targetEntity = sourceSlot.cachedTargetEntity;
const item = sourceSlot.item; const item = sourceSlot.item;
if (!item) { if (!item) {
@ -141,6 +205,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
} }
} }
} }
}
/** /**
* *