From 5bdf6386a1696d6970b0b5bb99584ae32ea17560 Mon Sep 17 00:00:00 2001 From: tobspr Date: Sat, 19 Sep 2020 08:51:28 +0200 Subject: [PATCH] Improve mass deletion performance --- src/js/game/entity.js | 450 ++++++++++++------------- src/js/game/entity_manager.js | 20 +- src/js/game/game_system_with_filter.js | 5 +- src/js/game/hud/parts/mass_selector.js | 33 +- 4 files changed, 269 insertions(+), 239 deletions(-) diff --git a/src/js/game/entity.js b/src/js/game/entity.js index ca21a16d..095fda8f 100644 --- a/src/js/game/entity.js +++ b/src/js/game/entity.js @@ -1,229 +1,221 @@ -/* typehints:start */ -import { DrawParameters } from "../core/draw_parameters"; -import { Component } from "./component"; -/* typehints:end */ - -import { GameRoot } from "./root"; -import { globalConfig } from "../core/config"; -import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector"; -import { BasicSerializableObject, types } from "../savegame/serialization"; -import { EntityComponentStorage } from "./entity_components"; -import { Loader } from "../core/loader"; -import { drawRotatedSprite } from "../core/draw_utils"; -import { gComponentRegistry } from "../core/global_registries"; - -export class Entity extends BasicSerializableObject { - /** - * @param {GameRoot} root - */ - constructor(root) { - super(); - - /** - * Handle to the global game root - */ - this.root = root; - - /** - * The components of the entity - */ - this.components = new EntityComponentStorage(); - - /** - * Whether this entity was registered on the @see EntityManager so far - */ - this.registered = false; - - /** - * On which layer this entity is - * @type {Layer} - */ - this.layer = "regular"; - - /** - * Internal entity unique id, set by the @see EntityManager - */ - this.uid = 0; - - /* typehints:start */ - - /** - * Stores if this entity is destroyed, set by the @see EntityManager - * @type {boolean} */ - this.destroyed; - - /** - * Stores if this entity is queued to get destroyed in the next tick - * of the @see EntityManager - * @type {boolean} */ - this.queuedForDestroy; - - /** - * Stores the reason why this entity was destroyed - * @type {string} */ - this.destroyReason; - - /* typehints:end */ - } - - static getId() { - return "Entity"; - } - - /** - * @see BasicSerializableObject.getSchema - * @returns {import("../savegame/serialization").Schema} - */ - static getSchema() { - return { - uid: types.uint, - components: types.keyValueMap(types.objData(gComponentRegistry), false), - }; - } - - /** - * Returns a clone of this entity without contents - */ - duplicateWithoutContents() { - const clone = new Entity(this.root); - for (const key in this.components) { - clone.components[key] = this.components[key].duplicateWithoutContents(); - } - clone.layer = this.layer; - return clone; - } - - /** - * Internal destroy callback - */ - internalDestroyCallback() { - assert(!this.destroyed, "Can not destroy entity twice"); - this.destroyed = true; - } - - /** - * Adds a new component, only possible until the entity is registered on the entity manager, - * after that use @see EntityManager.addDynamicComponent - * @param {Component} componentInstance - * @param {boolean} force Used by the entity manager. Internal parameter, do not change - */ - addComponent(componentInstance, force = false) { - if (!force && this.registered) { - this.root.entityMgr.attachDynamicComponent(this, componentInstance); - return; - } - assert(force || !this.registered, "Entity already registered, use EntityManager.addDynamicComponent"); - const id = /** @type {typeof Component} */ (componentInstance.constructor).getId(); - assert(!this.components[id], "Component already present"); - this.components[id] = componentInstance; - } - - /** - * Removes a given component, only possible until the entity is registered on the entity manager, - * after that use @see EntityManager.removeDynamicComponent - * @param {typeof Component} componentClass - * @param {boolean} force - */ - removeComponent(componentClass, force = false) { - if (!force && this.registered) { - this.root.entityMgr.removeDynamicComponent(this, componentClass); - return; - } - assert( - force || !this.registered, - "Entity already registered, use EntityManager.removeDynamicComponent" - ); - const id = componentClass.getId(); - assert(this.components[id], "Component does not exist on entity"); - delete this.components[id]; - } - - /** - * Draws the entity, to override use @see Entity.drawImpl - * @param {DrawParameters} parameters - */ - drawDebugOverlays(parameters) { - const context = parameters.context; - const staticComp = this.components.StaticMapEntity; - - if (G_IS_DEV && staticComp && globalConfig.debug.showEntityBounds) { - if (staticComp) { - const transformed = staticComp.getTileSpaceBounds(); - context.strokeStyle = "rgba(255, 0, 0, 0.5)"; - context.lineWidth = 2; - // const boundsSize = 20; - context.beginPath(); - context.rect( - transformed.x * globalConfig.tileSize, - transformed.y * globalConfig.tileSize, - transformed.w * globalConfig.tileSize, - transformed.h * globalConfig.tileSize - ); - context.stroke(); - } - } - - if (G_IS_DEV && staticComp && globalConfig.debug.showAcceptorEjectors) { - const ejectorComp = this.components.ItemEjector; - - if (ejectorComp) { - const ejectorSprite = Loader.getSprite("sprites/debug/ejector_slot.png"); - for (let i = 0; i < ejectorComp.slots.length; ++i) { - const slot = ejectorComp.slots[i]; - const slotTile = staticComp.localTileToWorld(slot.pos); - const direction = staticComp.localDirectionToWorld(slot.direction); - const directionVector = enumDirectionToVector[direction]; - const angle = Math.radians(enumDirectionToAngle[direction]); - - context.globalAlpha = slot.item ? 1 : 0.2; - drawRotatedSprite({ - parameters, - sprite: ejectorSprite, - x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize, - y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize, - angle, - size: globalConfig.tileSize * 0.25, - }); - } - } - const acceptorComp = this.components.ItemAcceptor; - - if (acceptorComp) { - const acceptorSprite = Loader.getSprite("sprites/misc/acceptor_slot.png"); - for (let i = 0; i < acceptorComp.slots.length; ++i) { - const slot = acceptorComp.slots[i]; - const slotTile = staticComp.localTileToWorld(slot.pos); - for (let k = 0; k < slot.directions.length; ++k) { - const direction = staticComp.localDirectionToWorld(slot.directions[k]); - const directionVector = enumDirectionToVector[direction]; - const angle = Math.radians(enumDirectionToAngle[direction] + 180); - context.globalAlpha = 0.4; - drawRotatedSprite({ - parameters, - sprite: acceptorSprite, - x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize, - y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize, - angle, - size: globalConfig.tileSize * 0.25, - }); - } - } - } - - context.globalAlpha = 1; - } - // this.drawImpl(parameters); - } - - ///// Helper interfaces - - ///// Interface to override by subclasses - - /** - * override, should draw the entity - * @param {DrawParameters} parameters - */ - drawImpl(parameters) { - abstract; - } -} +/* typehints:start */ +import { DrawParameters } from "../core/draw_parameters"; +import { Component } from "./component"; +/* typehints:end */ + +import { GameRoot } from "./root"; +import { globalConfig } from "../core/config"; +import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { EntityComponentStorage } from "./entity_components"; +import { Loader } from "../core/loader"; +import { drawRotatedSprite } from "../core/draw_utils"; +import { gComponentRegistry } from "../core/global_registries"; + +export class Entity extends BasicSerializableObject { + /** + * @param {GameRoot} root + */ + constructor(root) { + super(); + + /** + * Handle to the global game root + */ + this.root = root; + + /** + * The components of the entity + */ + this.components = new EntityComponentStorage(); + + /** + * Whether this entity was registered on the @see EntityManager so far + */ + this.registered = false; + + /** + * On which layer this entity is + * @type {Layer} + */ + this.layer = "regular"; + + /** + * Internal entity unique id, set by the @see EntityManager + */ + this.uid = 0; + + /* typehints:start */ + + /** + * Stores if this entity is destroyed, set by the @see EntityManager + * @type {boolean} */ + this.destroyed; + + /** + * Stores if this entity is queued to get destroyed in the next tick + * of the @see EntityManager + * @type {boolean} */ + this.queuedForDestroy; + + /** + * Stores the reason why this entity was destroyed + * @type {string} */ + this.destroyReason; + + /* typehints:end */ + } + + static getId() { + return "Entity"; + } + + /** + * @see BasicSerializableObject.getSchema + * @returns {import("../savegame/serialization").Schema} + */ + static getSchema() { + return { + uid: types.uint, + components: types.keyValueMap(types.objData(gComponentRegistry), false), + }; + } + + /** + * Returns a clone of this entity without contents + */ + duplicateWithoutContents() { + const clone = new Entity(this.root); + for (const key in this.components) { + clone.components[key] = this.components[key].duplicateWithoutContents(); + } + clone.layer = this.layer; + return clone; + } + + /** + * Adds a new component, only possible until the entity is registered on the entity manager, + * after that use @see EntityManager.addDynamicComponent + * @param {Component} componentInstance + * @param {boolean} force Used by the entity manager. Internal parameter, do not change + */ + addComponent(componentInstance, force = false) { + if (!force && this.registered) { + this.root.entityMgr.attachDynamicComponent(this, componentInstance); + return; + } + assert(force || !this.registered, "Entity already registered, use EntityManager.addDynamicComponent"); + const id = /** @type {typeof Component} */ (componentInstance.constructor).getId(); + assert(!this.components[id], "Component already present"); + this.components[id] = componentInstance; + } + + /** + * Removes a given component, only possible until the entity is registered on the entity manager, + * after that use @see EntityManager.removeDynamicComponent + * @param {typeof Component} componentClass + * @param {boolean} force + */ + removeComponent(componentClass, force = false) { + if (!force && this.registered) { + this.root.entityMgr.removeDynamicComponent(this, componentClass); + return; + } + assert( + force || !this.registered, + "Entity already registered, use EntityManager.removeDynamicComponent" + ); + const id = componentClass.getId(); + assert(this.components[id], "Component does not exist on entity"); + delete this.components[id]; + } + + /** + * Draws the entity, to override use @see Entity.drawImpl + * @param {DrawParameters} parameters + */ + drawDebugOverlays(parameters) { + const context = parameters.context; + const staticComp = this.components.StaticMapEntity; + + if (G_IS_DEV && staticComp && globalConfig.debug.showEntityBounds) { + if (staticComp) { + const transformed = staticComp.getTileSpaceBounds(); + context.strokeStyle = "rgba(255, 0, 0, 0.5)"; + context.lineWidth = 2; + // const boundsSize = 20; + context.beginPath(); + context.rect( + transformed.x * globalConfig.tileSize, + transformed.y * globalConfig.tileSize, + transformed.w * globalConfig.tileSize, + transformed.h * globalConfig.tileSize + ); + context.stroke(); + } + } + + if (G_IS_DEV && staticComp && globalConfig.debug.showAcceptorEjectors) { + const ejectorComp = this.components.ItemEjector; + + if (ejectorComp) { + const ejectorSprite = Loader.getSprite("sprites/debug/ejector_slot.png"); + for (let i = 0; i < ejectorComp.slots.length; ++i) { + const slot = ejectorComp.slots[i]; + const slotTile = staticComp.localTileToWorld(slot.pos); + const direction = staticComp.localDirectionToWorld(slot.direction); + const directionVector = enumDirectionToVector[direction]; + const angle = Math.radians(enumDirectionToAngle[direction]); + + context.globalAlpha = slot.item ? 1 : 0.2; + drawRotatedSprite({ + parameters, + sprite: ejectorSprite, + x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize, + y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize, + angle, + size: globalConfig.tileSize * 0.25, + }); + } + } + const acceptorComp = this.components.ItemAcceptor; + + if (acceptorComp) { + const acceptorSprite = Loader.getSprite("sprites/misc/acceptor_slot.png"); + for (let i = 0; i < acceptorComp.slots.length; ++i) { + const slot = acceptorComp.slots[i]; + const slotTile = staticComp.localTileToWorld(slot.pos); + for (let k = 0; k < slot.directions.length; ++k) { + const direction = staticComp.localDirectionToWorld(slot.directions[k]); + const directionVector = enumDirectionToVector[direction]; + const angle = Math.radians(enumDirectionToAngle[direction] + 180); + context.globalAlpha = 0.4; + drawRotatedSprite({ + parameters, + sprite: acceptorSprite, + x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize, + y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize, + angle, + size: globalConfig.tileSize * 0.25, + }); + } + } + } + + context.globalAlpha = 1; + } + // this.drawImpl(parameters); + } + + ///// Helper interfaces + + ///// Interface to override by subclasses + + /** + * override, should draw the entity + * @param {DrawParameters} parameters + */ + drawImpl(parameters) { + abstract; + } +} diff --git a/src/js/game/entity_manager.js b/src/js/game/entity_manager.js index 334f4e28..613ed12d 100644 --- a/src/js/game/entity_manager.js +++ b/src/js/game/entity_manager.js @@ -155,6 +155,24 @@ export class EntityManager extends BasicSerializableObject { return null; } + /** + * Returns a map which gives a mapping from UID to Entity. + * This map is not updated. + * + * @returns {Map} + */ + getFrozenUidSearchMap() { + const result = new Map(); + const array = this.entities; + for (let i = 0, len = array.length; i < len; ++i) { + const entity = array[i]; + if (!entity.queuedForDestroy && !entity.destroyed) { + result.set(entity.uid, entity); + } + } + return result; + } + /** * Returns all entities having the given component * @param {typeof Component} componentHandle @@ -206,7 +224,7 @@ export class EntityManager extends BasicSerializableObject { this.unregisterEntityComponents(entity); entity.registered = false; - entity.internalDestroyCallback(); + entity.destroyed = true; this.root.signals.entityDestroyed.dispatch(entity); } diff --git a/src/js/game/game_system_with_filter.js b/src/js/game/game_system_with_filter.js index 988f09c4..a6efeffd 100644 --- a/src/js/game/game_system_with_filter.js +++ b/src/js/game/game_system_with_filter.js @@ -88,15 +88,16 @@ export class GameSystemWithFilter extends GameSystem { } refreshCaches() { - this.allEntities.sort((a, b) => a.uid - b.uid); - // Remove all entities which are queued for destroy for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; if (entity.queuedForDestroy || entity.destroyed) { this.allEntities.splice(i, 1); + i -= 1; } } + + this.allEntities.sort((a, b) => a.uid - b.uid); } /** diff --git a/src/js/game/hud/parts/mass_selector.js b/src/js/game/hud/parts/mass_selector.js index a8972434..08a11769 100644 --- a/src/js/game/hud/parts/mass_selector.js +++ b/src/js/game/hud/parts/mass_selector.js @@ -48,6 +48,9 @@ export class HUDMassSelector extends BaseHUDPart { * @param {Entity} entity */ onEntityDestroyed(entity) { + if (this.root.bulkOperationRunning) { + return; + } this.selectedUids.delete(entity.uid); } @@ -90,14 +93,30 @@ export class HUDMassSelector extends BaseHUDPart { doDelete() { const entityUids = Array.from(this.selectedUids); - for (let i = 0; i < entityUids.length; ++i) { - const uid = entityUids[i]; - const entity = this.root.entityMgr.findByUid(uid); - if (!this.root.logic.tryDeleteBuilding(entity)) { - logger.error("Error in mass delete, could not remove building"); - this.selectedUids.delete(uid); + + // Build mapping from uid to entity + /** + * @type {Map} + */ + const mapUidToEntity = this.root.entityMgr.getFrozenUidSearchMap(); + + this.root.logic.performBulkOperation(() => { + for (let i = 0; i < entityUids.length; ++i) { + const uid = entityUids[i]; + const entity = mapUidToEntity.get(uid); + if (!entity) { + logger.error("Entity not found by uid:", uid); + continue; + } + + if (!this.root.logic.tryDeleteBuilding(entity)) { + logger.error("Error in mass delete, could not remove building"); + } } - } + }); + + // Clear uids later + this.selectedUids = new Set(); } startCopy() {