Improve mass deletion performance

pull/670/head
tobspr 4 years ago
parent b7c773a70e
commit 5bdf6386a1

@ -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;
}
}

@ -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<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
* @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);
}

@ -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);
}
/**

@ -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<number, Entity>}
*/
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() {

Loading…
Cancel
Save