Improve mass deletion performance

This commit is contained in:
tobspr 2020-09-19 08:51:28 +02:00
parent b7c773a70e
commit 5bdf6386a1
4 changed files with 269 additions and 239 deletions

View File

@ -1,229 +1,221 @@
/* typehints:start */ /* typehints:start */
import { DrawParameters } from "../core/draw_parameters"; import { DrawParameters } from "../core/draw_parameters";
import { Component } from "./component"; import { Component } from "./component";
/* typehints:end */ /* typehints:end */
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { globalConfig } from "../core/config"; import { globalConfig } from "../core/config";
import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector"; import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector";
import { BasicSerializableObject, types } from "../savegame/serialization"; import { BasicSerializableObject, types } from "../savegame/serialization";
import { EntityComponentStorage } from "./entity_components"; import { EntityComponentStorage } from "./entity_components";
import { Loader } from "../core/loader"; import { Loader } from "../core/loader";
import { drawRotatedSprite } from "../core/draw_utils"; import { drawRotatedSprite } from "../core/draw_utils";
import { gComponentRegistry } from "../core/global_registries"; import { gComponentRegistry } from "../core/global_registries";
export class Entity extends BasicSerializableObject { export class Entity extends BasicSerializableObject {
/** /**
* @param {GameRoot} root * @param {GameRoot} root
*/ */
constructor(root) { constructor(root) {
super(); super();
/** /**
* Handle to the global game root * Handle to the global game root
*/ */
this.root = root; this.root = root;
/** /**
* The components of the entity * The components of the entity
*/ */
this.components = new EntityComponentStorage(); this.components = new EntityComponentStorage();
/** /**
* Whether this entity was registered on the @see EntityManager so far * Whether this entity was registered on the @see EntityManager so far
*/ */
this.registered = false; this.registered = false;
/** /**
* On which layer this entity is * On which layer this entity is
* @type {Layer} * @type {Layer}
*/ */
this.layer = "regular"; this.layer = "regular";
/** /**
* Internal entity unique id, set by the @see EntityManager * Internal entity unique id, set by the @see EntityManager
*/ */
this.uid = 0; this.uid = 0;
/* typehints:start */ /* typehints:start */
/** /**
* Stores if this entity is destroyed, set by the @see EntityManager * Stores if this entity is destroyed, set by the @see EntityManager
* @type {boolean} */ * @type {boolean} */
this.destroyed; this.destroyed;
/** /**
* Stores if this entity is queued to get destroyed in the next tick * Stores if this entity is queued to get destroyed in the next tick
* of the @see EntityManager * of the @see EntityManager
* @type {boolean} */ * @type {boolean} */
this.queuedForDestroy; this.queuedForDestroy;
/** /**
* Stores the reason why this entity was destroyed * Stores the reason why this entity was destroyed
* @type {string} */ * @type {string} */
this.destroyReason; this.destroyReason;
/* typehints:end */ /* typehints:end */
} }
static getId() { static getId() {
return "Entity"; return "Entity";
} }
/** /**
* @see BasicSerializableObject.getSchema * @see BasicSerializableObject.getSchema
* @returns {import("../savegame/serialization").Schema} * @returns {import("../savegame/serialization").Schema}
*/ */
static getSchema() { static getSchema() {
return { return {
uid: types.uint, uid: types.uint,
components: types.keyValueMap(types.objData(gComponentRegistry), false), components: types.keyValueMap(types.objData(gComponentRegistry), false),
}; };
} }
/** /**
* Returns a clone of this entity without contents * Returns a clone of this entity without contents
*/ */
duplicateWithoutContents() { duplicateWithoutContents() {
const clone = new Entity(this.root); const clone = new Entity(this.root);
for (const key in this.components) { for (const key in this.components) {
clone.components[key] = this.components[key].duplicateWithoutContents(); clone.components[key] = this.components[key].duplicateWithoutContents();
} }
clone.layer = this.layer; clone.layer = this.layer;
return clone; return clone;
} }
/** /**
* Internal destroy callback * Adds a new component, only possible until the entity is registered on the entity manager,
*/ * after that use @see EntityManager.addDynamicComponent
internalDestroyCallback() { * @param {Component} componentInstance
assert(!this.destroyed, "Can not destroy entity twice"); * @param {boolean} force Used by the entity manager. Internal parameter, do not change
this.destroyed = true; */
} addComponent(componentInstance, force = false) {
if (!force && this.registered) {
/** this.root.entityMgr.attachDynamicComponent(this, componentInstance);
* Adds a new component, only possible until the entity is registered on the entity manager, return;
* after that use @see EntityManager.addDynamicComponent }
* @param {Component} componentInstance assert(force || !this.registered, "Entity already registered, use EntityManager.addDynamicComponent");
* @param {boolean} force Used by the entity manager. Internal parameter, do not change const id = /** @type {typeof Component} */ (componentInstance.constructor).getId();
*/ assert(!this.components[id], "Component already present");
addComponent(componentInstance, force = false) { this.components[id] = componentInstance;
if (!force && this.registered) { }
this.root.entityMgr.attachDynamicComponent(this, componentInstance);
return; /**
} * Removes a given component, only possible until the entity is registered on the entity manager,
assert(force || !this.registered, "Entity already registered, use EntityManager.addDynamicComponent"); * after that use @see EntityManager.removeDynamicComponent
const id = /** @type {typeof Component} */ (componentInstance.constructor).getId(); * @param {typeof Component} componentClass
assert(!this.components[id], "Component already present"); * @param {boolean} force
this.components[id] = componentInstance; */
} removeComponent(componentClass, force = false) {
if (!force && this.registered) {
/** this.root.entityMgr.removeDynamicComponent(this, componentClass);
* Removes a given component, only possible until the entity is registered on the entity manager, return;
* after that use @see EntityManager.removeDynamicComponent }
* @param {typeof Component} componentClass assert(
* @param {boolean} force force || !this.registered,
*/ "Entity already registered, use EntityManager.removeDynamicComponent"
removeComponent(componentClass, force = false) { );
if (!force && this.registered) { const id = componentClass.getId();
this.root.entityMgr.removeDynamicComponent(this, componentClass); assert(this.components[id], "Component does not exist on entity");
return; delete this.components[id];
} }
assert(
force || !this.registered, /**
"Entity already registered, use EntityManager.removeDynamicComponent" * Draws the entity, to override use @see Entity.drawImpl
); * @param {DrawParameters} parameters
const id = componentClass.getId(); */
assert(this.components[id], "Component does not exist on entity"); drawDebugOverlays(parameters) {
delete this.components[id]; const context = parameters.context;
} const staticComp = this.components.StaticMapEntity;
/** if (G_IS_DEV && staticComp && globalConfig.debug.showEntityBounds) {
* Draws the entity, to override use @see Entity.drawImpl if (staticComp) {
* @param {DrawParameters} parameters const transformed = staticComp.getTileSpaceBounds();
*/ context.strokeStyle = "rgba(255, 0, 0, 0.5)";
drawDebugOverlays(parameters) { context.lineWidth = 2;
const context = parameters.context; // const boundsSize = 20;
const staticComp = this.components.StaticMapEntity; context.beginPath();
context.rect(
if (G_IS_DEV && staticComp && globalConfig.debug.showEntityBounds) { transformed.x * globalConfig.tileSize,
if (staticComp) { transformed.y * globalConfig.tileSize,
const transformed = staticComp.getTileSpaceBounds(); transformed.w * globalConfig.tileSize,
context.strokeStyle = "rgba(255, 0, 0, 0.5)"; transformed.h * globalConfig.tileSize
context.lineWidth = 2; );
// const boundsSize = 20; context.stroke();
context.beginPath(); }
context.rect( }
transformed.x * globalConfig.tileSize,
transformed.y * globalConfig.tileSize, if (G_IS_DEV && staticComp && globalConfig.debug.showAcceptorEjectors) {
transformed.w * globalConfig.tileSize, const ejectorComp = this.components.ItemEjector;
transformed.h * globalConfig.tileSize
); if (ejectorComp) {
context.stroke(); 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);
if (G_IS_DEV && staticComp && globalConfig.debug.showAcceptorEjectors) { const direction = staticComp.localDirectionToWorld(slot.direction);
const ejectorComp = this.components.ItemEjector; const directionVector = enumDirectionToVector[direction];
const angle = Math.radians(enumDirectionToAngle[direction]);
if (ejectorComp) {
const ejectorSprite = Loader.getSprite("sprites/debug/ejector_slot.png"); context.globalAlpha = slot.item ? 1 : 0.2;
for (let i = 0; i < ejectorComp.slots.length; ++i) { drawRotatedSprite({
const slot = ejectorComp.slots[i]; parameters,
const slotTile = staticComp.localTileToWorld(slot.pos); sprite: ejectorSprite,
const direction = staticComp.localDirectionToWorld(slot.direction); x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize,
const directionVector = enumDirectionToVector[direction]; y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize,
const angle = Math.radians(enumDirectionToAngle[direction]); angle,
size: globalConfig.tileSize * 0.25,
context.globalAlpha = slot.item ? 1 : 0.2; });
drawRotatedSprite({ }
parameters, }
sprite: ejectorSprite, const acceptorComp = this.components.ItemAcceptor;
x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize,
y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize, if (acceptorComp) {
angle, const acceptorSprite = Loader.getSprite("sprites/misc/acceptor_slot.png");
size: globalConfig.tileSize * 0.25, 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 acceptorComp = this.components.ItemAcceptor; const direction = staticComp.localDirectionToWorld(slot.directions[k]);
const directionVector = enumDirectionToVector[direction];
if (acceptorComp) { const angle = Math.radians(enumDirectionToAngle[direction] + 180);
const acceptorSprite = Loader.getSprite("sprites/misc/acceptor_slot.png"); context.globalAlpha = 0.4;
for (let i = 0; i < acceptorComp.slots.length; ++i) { drawRotatedSprite({
const slot = acceptorComp.slots[i]; parameters,
const slotTile = staticComp.localTileToWorld(slot.pos); sprite: acceptorSprite,
for (let k = 0; k < slot.directions.length; ++k) { x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize,
const direction = staticComp.localDirectionToWorld(slot.directions[k]); y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize,
const directionVector = enumDirectionToVector[direction]; angle,
const angle = Math.radians(enumDirectionToAngle[direction] + 180); size: globalConfig.tileSize * 0.25,
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, context.globalAlpha = 1;
angle, }
size: globalConfig.tileSize * 0.25, // this.drawImpl(parameters);
}); }
}
} ///// Helper interfaces
}
///// Interface to override by subclasses
context.globalAlpha = 1;
} /**
// this.drawImpl(parameters); * override, should draw the entity
} * @param {DrawParameters} parameters
*/
///// Helper interfaces drawImpl(parameters) {
abstract;
///// Interface to override by subclasses }
}
/**
* override, should draw the entity
* @param {DrawParameters} parameters
*/
drawImpl(parameters) {
abstract;
}
}

View File

@ -155,6 +155,24 @@ export class EntityManager extends BasicSerializableObject {
return null; return null;
} }
/**
* Returns a map which gives a mapping from UID to Entity.
* This map is not updated.
*
* @returns {Map<number, Entity>}
*/
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 * Returns all entities having the given component
* @param {typeof Component} componentHandle * @param {typeof Component} componentHandle
@ -206,7 +224,7 @@ export class EntityManager extends BasicSerializableObject {
this.unregisterEntityComponents(entity); this.unregisterEntityComponents(entity);
entity.registered = false; entity.registered = false;
entity.internalDestroyCallback(); entity.destroyed = true;
this.root.signals.entityDestroyed.dispatch(entity); this.root.signals.entityDestroyed.dispatch(entity);
} }

View File

@ -88,15 +88,16 @@ export class GameSystemWithFilter extends GameSystem {
} }
refreshCaches() { refreshCaches() {
this.allEntities.sort((a, b) => a.uid - b.uid);
// Remove all entities which are queued for destroy // Remove all entities which are queued for destroy
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];
if (entity.queuedForDestroy || entity.destroyed) { if (entity.queuedForDestroy || entity.destroyed) {
this.allEntities.splice(i, 1); this.allEntities.splice(i, 1);
i -= 1;
} }
} }
this.allEntities.sort((a, b) => a.uid - b.uid);
} }
/** /**

View File

@ -48,6 +48,9 @@ export class HUDMassSelector extends BaseHUDPart {
* @param {Entity} entity * @param {Entity} entity
*/ */
onEntityDestroyed(entity) { onEntityDestroyed(entity) {
if (this.root.bulkOperationRunning) {
return;
}
this.selectedUids.delete(entity.uid); this.selectedUids.delete(entity.uid);
} }
@ -90,14 +93,30 @@ export class HUDMassSelector extends BaseHUDPart {
doDelete() { doDelete() {
const entityUids = Array.from(this.selectedUids); const entityUids = Array.from(this.selectedUids);
for (let i = 0; i < entityUids.length; ++i) {
const uid = entityUids[i]; // Build mapping from uid to entity
const entity = this.root.entityMgr.findByUid(uid); /**
if (!this.root.logic.tryDeleteBuilding(entity)) { * @type {Map<number, Entity>}
logger.error("Error in mass delete, could not remove building"); */
this.selectedUids.delete(uid); 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() { startCopy() {