From cda24ceb04e34261c0fbb84865038440c050acac Mon Sep 17 00:00:00 2001 From: tobspr Date: Sun, 14 Jun 2020 14:37:13 +0200 Subject: [PATCH] Refactor item acceptor system for huge performance improvement --- src/js/changelog.js | 5 ++ src/js/game/components/item_acceptor.js | 14 +-- src/js/game/game_system_manager.js | 3 + src/js/game/systems/item_ejector.js | 113 +++++++++++++++++------- 4 files changed, 100 insertions(+), 35 deletions(-) diff --git a/src/js/changelog.js b/src/js/changelog.js index 292be78a..55f0b76c 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -1,4 +1,9 @@ export const CHANGELOG = [ + { + version: "1.1.12", + date: "unreleased", + entries: ["Huge performance improvements! The game should now run up to 80% faster"], + }, { version: "1.1.11", date: "13.06.2020", diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index d370ae61..6f3895aa 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -20,6 +20,14 @@ export const enumItemAcceptorItemFilter = { * filter?: enumItemAcceptorItemFilter * }} ItemAcceptorSlot */ +/** + * Contains information about a slot plus its location + * @typedef {{ + * slot: ItemAcceptorSlot, + * index: number, + * acceptedDirection: enumDirection + * }} ItemAcceptorLocatedSlot */ + export class ItemAcceptorComponent extends Component { static getId() { return "ItemAcceptor"; @@ -164,11 +172,7 @@ export class ItemAcceptorComponent extends Component { * Tries to find a slot which accepts the current item * @param {Vector} targetLocalTile * @param {enumDirection} fromLocalDirection - * @returns {{ - * slot: ItemAcceptorSlot, - * index: number, - * acceptedDirection: enumDirection - * }|null} + * @returns {ItemAcceptorLocatedSlot|null} */ findMatchingSlot(targetLocalTile, fromLocalDirection) { // We need to invert our direction since the acceptor specifies *from* which direction diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index a8e9cf0c..20d74dda 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -92,6 +92,9 @@ export class GameSystemManager { add("staticMapEntities", StaticMapEntitySystem); + // IMPORTANT: Must be after belt system since belt system can change the + // orientation of an entity after it is placed -> the item acceptor cache + // then would be invalid add("itemAcceptor", ItemAcceptorSystem); logger.log("📦 There are", this.systemUpdateOrder.length, "game systems"); diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index e714cce5..2f327725 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -6,19 +6,40 @@ import { ItemEjectorComponent } from "../components/item_ejector"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { Math_min } from "../../core/builtins"; +import { createLogger } from "../../core/logging"; + +const logger = createLogger("systems/ejector"); export class ItemEjectorSystem extends GameSystemWithFilter { constructor(root) { 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.root.signals.entityAdded.add(this.invalidateCache, this); + this.root.signals.entityDestroyed.add(this.invalidateCache, this); } - update() { - const effectiveBeltSpeed = this.root.hubGoals.getBeltBaseSpeed() * globalConfig.itemSpacingOnBelts; - let progressGrowth = (effectiveBeltSpeed / 0.5) * this.root.dynamicTickrate.deltaSeconds; + invalidateCache() { + this.cacheNeedsUpdate = true; + } - if (G_IS_DEV && globalConfig.debug.instantBelts) { - progressGrowth = 1; - } + /** + * Precomputes the cache, which makes up for a huge performance improvement + */ + recomputeCache() { + logger.log("Recomputing cache"); + + const cache = []; // Try to find acceptors for every ejector for (let i = 0; i < this.allEntities.length; ++i) { @@ -29,17 +50,6 @@ export class ItemEjectorSystem extends GameSystemWithFilter { // For every ejector slot, try to find an acceptor for (let ejectorSlotIndex = 0; ejectorSlotIndex < ejectorComp.slots.length; ++ejectorSlotIndex) { const ejectorSlot = ejectorComp.slots[ejectorSlotIndex]; - const ejectingItem = ejectorSlot.item; - if (!ejectingItem) { - // No item ejected - continue; - } - - ejectorSlot.progress = Math_min(1, ejectorSlot.progress + progressGrowth); - if (ejectorSlot.progress < 1.0) { - // Still ejecting - continue; - } // Figure out where and into which direction we eject items const ejectSlotWsTile = staticComp.localTileToWorld(ejectorSlot.pos); @@ -71,20 +81,63 @@ export class ItemEjectorSystem extends GameSystemWithFilter { continue; } - if (!targetAcceptorComp.canAcceptItem(matchingSlot.index, ejectingItem)) { - // Can not accept item - continue; - } + // Ok we found a connection + cache.push({ + targetEntity, + sourceSlot: ejectorSlot, + destSlot: matchingSlot, + }); + } + } - if (this.tryPassOverItem(ejectingItem, targetEntity, matchingSlot.index)) { - targetAcceptorComp.onItemAccepted( - matchingSlot.index, - matchingSlot.acceptedDirection, - ejectingItem - ); - ejectorSlot.item = null; - continue; - } + this.cache = cache; + logger.log("Found", cache.length, "entries to update"); + } + + update() { + if (this.cacheNeedsUpdate) { + this.cacheNeedsUpdate = false; + this.recomputeCache(); + } + + // Precompute effective belt speed + const effectiveBeltSpeed = this.root.hubGoals.getBeltBaseSpeed() * globalConfig.itemSpacingOnBelts; + let progressGrowth = (effectiveBeltSpeed / 0.5) * this.root.dynamicTickrate.deltaSeconds; + + if (G_IS_DEV && globalConfig.debug.instantBelts) { + progressGrowth = 1; + } + + // Go over all cache entries + for (let i = 0; i < this.cache.length; ++i) { + const { sourceSlot, destSlot, targetEntity } = this.cache[i]; + const item = sourceSlot.item; + + if (!item) { + // No item available to be ejected + continue; + } + + // Advance items on the slot + sourceSlot.progress = Math_min(1, sourceSlot.progress + progressGrowth); + + // Check if we are still in the process of ejecting, can't proceed then + if (sourceSlot.progress < 1.0) { + 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; } } }