Merge pull request #200 from Phlosioneer/ejector-cache-opt
Optimize ejector cache
This commit is contained in:
commit
b575bc4f41
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateCache() {
|
/**
|
||||||
|
*
|
||||||
|
* @param {Entity} entity
|
||||||
|
*/
|
||||||
|
invalidateCache(entity) {
|
||||||
|
if (!entity.components.StaticMapEntity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
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,59 +51,102 @@ 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) {
|
||||||
// Try to find acceptors for every ejector
|
// Only recompute caches of entities inside the rectangles.
|
||||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
for (let i = 0; i < this.smallCacheAreas.length; i++) {
|
||||||
const entity = this.allEntities[i];
|
entryCount += this.recomputeAreaCaches(this.smallCacheAreas[i]);
|
||||||
const ejectorComp = entity.components.ItemEjector;
|
}
|
||||||
const staticComp = entity.components.StaticMapEntity;
|
} else {
|
||||||
|
// Try to find acceptors for every ejector
|
||||||
// For every ejector slot, try to find an acceptor
|
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||||
for (let ejectorSlotIndex = 0; ejectorSlotIndex < ejectorComp.slots.length; ++ejectorSlotIndex) {
|
const entity = this.allEntities[i];
|
||||||
const ejectorSlot = ejectorComp.slots[ejectorSlotIndex];
|
entryCount += this.recomputeSingleEntityCache(entity);
|
||||||
|
|
||||||
// Figure out where and into which direction we eject items
|
|
||||||
const ejectSlotWsTile = staticComp.localTileToWorld(ejectorSlot.pos);
|
|
||||||
const ejectSlotWsDirection = staticComp.localDirectionToWorld(ejectorSlot.direction);
|
|
||||||
const ejectSlotWsDirectionVector = enumDirectionToVector[ejectSlotWsDirection];
|
|
||||||
const ejectSlotTargetWsTile = ejectSlotWsTile.add(ejectSlotWsDirectionVector);
|
|
||||||
|
|
||||||
// Try to find the given acceptor component to take the item
|
|
||||||
const targetEntity = this.root.map.getTileContent(ejectSlotTargetWsTile);
|
|
||||||
if (!targetEntity) {
|
|
||||||
// No consumer for item
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
|
|
||||||
const targetStaticComp = targetEntity.components.StaticMapEntity;
|
|
||||||
if (!targetAcceptorComp) {
|
|
||||||
// Entity doesn't accept items
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchingSlot = targetAcceptorComp.findMatchingSlot(
|
|
||||||
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
|
|
||||||
targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!matchingSlot) {
|
|
||||||
// No matching slot found
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ok we found a connection
|
|
||||||
cache.push({
|
|
||||||
targetEntity,
|
|
||||||
sourceSlot: ejectorSlot,
|
|
||||||
destSlot: matchingSlot,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger.log("Found", entryCount, "entries to update");
|
||||||
|
|
||||||
this.cache = cache;
|
this.smallCacheAreas = [];
|
||||||
logger.log("Found", cache.length, "entries to update");
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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 staticComp = entity.components.StaticMapEntity;
|
||||||
|
|
||||||
|
// Clear the old cache.
|
||||||
|
ejectorComp.cachedConnectedSlots = null;
|
||||||
|
|
||||||
|
// For every ejector slot, try to find an acceptor
|
||||||
|
let entryCount = 0;
|
||||||
|
for (let ejectorSlotIndex = 0; ejectorSlotIndex < ejectorComp.slots.length; ++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
|
||||||
|
const ejectSlotWsTile = staticComp.localTileToWorld(ejectorSlot.pos);
|
||||||
|
const ejectSlotWsDirection = staticComp.localDirectionToWorld(ejectorSlot.direction);
|
||||||
|
const ejectSlotWsDirectionVector = enumDirectionToVector[ejectSlotWsDirection];
|
||||||
|
const ejectSlotTargetWsTile = ejectSlotWsTile.add(ejectSlotWsDirectionVector);
|
||||||
|
|
||||||
|
// Try to find the given acceptor component to take the item
|
||||||
|
const targetEntity = this.root.map.getTileContent(ejectSlotTargetWsTile);
|
||||||
|
if (!targetEntity) {
|
||||||
|
// No consumer for item
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
|
||||||
|
const targetStaticComp = targetEntity.components.StaticMapEntity;
|
||||||
|
if (!targetAcceptorComp) {
|
||||||
|
// Entity doesn't accept items
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingSlot = targetAcceptorComp.findMatchingSlot(
|
||||||
|
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
|
||||||
|
targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingSlot) {
|
||||||
|
// No matching slot found
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok we found a connection
|
||||||
|
if (ejectorComp.cachedConnectedSlots) {
|
||||||
|
ejectorComp.cachedConnectedSlots.push(ejectorSlot);
|
||||||
|
} else {
|
||||||
|
ejectorComp.cachedConnectedSlots = [ejectorSlot];
|
||||||
|
}
|
||||||
|
ejectorSlot.cachedTargetEntity = targetEntity;
|
||||||
|
ejectorSlot.cachedDestSlot = matchingSlot;
|
||||||
|
entryCount += 1;
|
||||||
|
}
|
||||||
|
return entryCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
|
@ -109,35 +164,45 @@ 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 item = sourceSlot.item;
|
const sourceEjectorComp = sourceEntity.components.ItemEjector;
|
||||||
|
if (!sourceEjectorComp.cachedConnectedSlots) {
|
||||||
if (!item) {
|
|
||||||
// No item available to be ejected
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
for (let j = 0; j < sourceEjectorComp.cachedConnectedSlots.length; j++) {
|
||||||
|
const sourceSlot = sourceEjectorComp.cachedConnectedSlots[j];
|
||||||
|
const destSlot = sourceSlot.cachedDestSlot;
|
||||||
|
const targetEntity = sourceSlot.cachedTargetEntity;
|
||||||
|
|
||||||
// Advance items on the slot
|
const item = sourceSlot.item;
|
||||||
sourceSlot.progress = Math_min(1, sourceSlot.progress + progressGrowth);
|
|
||||||
|
|
||||||
// Check if we are still in the process of ejecting, can't proceed then
|
if (!item) {
|
||||||
if (sourceSlot.progress < 1.0) {
|
// No item available to be ejected
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the target acceptor can actually accept this item
|
// Advance items on the slot
|
||||||
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
|
sourceSlot.progress = Math_min(1, sourceSlot.progress + progressGrowth);
|
||||||
if (!targetAcceptorComp.canAcceptItem(destSlot.index, item)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to hand over the item
|
// Check if we are still in the process of ejecting, can't proceed then
|
||||||
if (this.tryPassOverItem(item, targetEntity, destSlot.index)) {
|
if (sourceSlot.progress < 1.0) {
|
||||||
// Handover successful, clear slot
|
continue;
|
||||||
targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.acceptedDirection, item);
|
}
|
||||||
sourceSlot.item = null;
|
|
||||||
continue;
|
// Check if the target acceptor can actually accept this item
|
||||||
|
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
|
||||||
|
if (!targetAcceptorComp.canAcceptItem(destSlot.index, item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to hand over the item
|
||||||
|
if (this.tryPassOverItem(item, targetEntity, destSlot.index)) {
|
||||||
|
// Handover successful, clear slot
|
||||||
|
targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.acceptedDirection, item);
|
||||||
|
sourceSlot.item = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue