mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-09 16:21:51 +00:00
Use Map and Set for entity storage
This is not a big optimization but an optimization nonetheless. Mostly based on awesome work by @Xiving. Further work should be done to get most out of these changes.
This commit is contained in:
parent
cd7c132411
commit
9cbb797ef6
@ -432,7 +432,7 @@ export class GameCore {
|
||||
this.overlayAlpha = lerp(this.overlayAlpha, desiredOverlayAlpha, 0.25);
|
||||
|
||||
// On low performance, skip the fade
|
||||
if (this.root.entityMgr.entities.length > 5000 || this.root.dynamicTickrate.averageFps < 50) {
|
||||
if (this.root.entityMgr.entities.size > 5000 || this.root.dynamicTickrate.averageFps < 50) {
|
||||
this.overlayAlpha = desiredOverlayAlpha;
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { arrayDeleteValue, newEmptyMap, fastArrayDeleteValue } from "../core/utils";
|
||||
import { Component } from "./component";
|
||||
import { GameRoot } from "./root";
|
||||
import { Entity } from "./entity";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { globalConfig } from "../core/config";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { newEmptyMap } from "../core/utils";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { Component } from "./component";
|
||||
import { Entity } from "./entity";
|
||||
import { GameRoot } from "./root";
|
||||
|
||||
const logger = createLogger("entity_manager");
|
||||
|
||||
@ -14,27 +14,23 @@ const logger = createLogger("entity_manager");
|
||||
// This is slower but we need it for the street path generation
|
||||
|
||||
export class EntityManager extends BasicSerializableObject {
|
||||
constructor(root) {
|
||||
readonly root: GameRoot;
|
||||
readonly entities = new Map<number, Entity>();
|
||||
|
||||
// We store a separate list with entities to destroy, since we don't destroy
|
||||
// them instantly
|
||||
private destroyList: Entity[] = [];
|
||||
|
||||
// Store a map from componentid to entities - This is used by the game system
|
||||
// for faster processing
|
||||
private readonly componentToEntity: Record<string, Set<Entity>> = newEmptyMap();
|
||||
|
||||
// Store the next uid to use
|
||||
private nextUid = 10000;
|
||||
|
||||
constructor(root: GameRoot) {
|
||||
super();
|
||||
|
||||
/** @type {GameRoot} */
|
||||
this.root = root;
|
||||
|
||||
/** @type {Array<Entity>} */
|
||||
this.entities = [];
|
||||
|
||||
// We store a separate list with entities to destroy, since we don't destroy
|
||||
// them instantly
|
||||
/** @type {Array<Entity>} */
|
||||
this.destroyList = [];
|
||||
|
||||
// Store a map from componentid to entities - This is used by the game system
|
||||
// for faster processing
|
||||
/** @type {Object.<string, Array<Entity>>} */
|
||||
this.componentToEntity = newEmptyMap();
|
||||
|
||||
// Store the next uid to use
|
||||
this.nextUid = 10000;
|
||||
}
|
||||
|
||||
static getId() {
|
||||
@ -48,7 +44,7 @@ export class EntityManager extends BasicSerializableObject {
|
||||
}
|
||||
|
||||
getStatsText() {
|
||||
return this.entities.length + " entities [" + this.destroyList.length + " to kill]";
|
||||
return this.entities.size + " entities [" + this.destroyList.length + " to kill]";
|
||||
}
|
||||
|
||||
// Main update
|
||||
@ -58,12 +54,14 @@ export class EntityManager extends BasicSerializableObject {
|
||||
|
||||
/**
|
||||
* Registers a new entity
|
||||
* @param {Entity} entity
|
||||
* @param {number=} uid Optional predefined uid
|
||||
* @param uid Optional predefined uid
|
||||
*/
|
||||
registerEntity(entity, uid = null) {
|
||||
registerEntity(entity: Entity, uid: number | null = null) {
|
||||
if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) {
|
||||
assert(this.entities.indexOf(entity) < 0, `RegisterEntity() called twice for entity ${entity}`);
|
||||
assert(
|
||||
this.entities.get(entity.uid) !== entity,
|
||||
`RegisterEntity() called twice for entity ${entity}`
|
||||
);
|
||||
}
|
||||
assert(!entity.destroyed, `Attempting to register destroyed entity ${entity}`);
|
||||
|
||||
@ -72,102 +70,78 @@ export class EntityManager extends BasicSerializableObject {
|
||||
assert(uid >= 0 && uid < Number.MAX_SAFE_INTEGER, "Invalid uid passed: " + uid);
|
||||
}
|
||||
|
||||
this.entities.push(entity);
|
||||
// Give each entity a unique id
|
||||
entity.uid = uid ? uid : this.generateUid();
|
||||
entity.registered = true;
|
||||
|
||||
this.entities.set(entity.uid, entity);
|
||||
|
||||
// Register into the componentToEntity map
|
||||
for (const componentId in entity.components) {
|
||||
if (entity.components[componentId]) {
|
||||
if (this.componentToEntity[componentId]) {
|
||||
this.componentToEntity[componentId].push(entity);
|
||||
} else {
|
||||
this.componentToEntity[componentId] = [entity];
|
||||
}
|
||||
const set = (this.componentToEntity[componentId] ??= new Set());
|
||||
set.add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// Give each entity a unique id
|
||||
entity.uid = uid ? uid : this.generateUid();
|
||||
entity.registered = true;
|
||||
|
||||
this.root.signals.entityAdded.dispatch(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new uid
|
||||
* @returns {number}
|
||||
*/
|
||||
generateUid() {
|
||||
generateUid(): number {
|
||||
return this.nextUid++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to attach a new component after the creation of the entity
|
||||
* @param {Entity} entity
|
||||
* @param {Component} component
|
||||
*/
|
||||
attachDynamicComponent(entity, component) {
|
||||
attachDynamicComponent(entity: Entity, component: Component) {
|
||||
entity.addComponent(component, true);
|
||||
const componentId = /** @type {typeof Component} */ (component.constructor).getId();
|
||||
if (this.componentToEntity[componentId]) {
|
||||
this.componentToEntity[componentId].push(entity);
|
||||
} else {
|
||||
this.componentToEntity[componentId] = [entity];
|
||||
}
|
||||
const componentId = /** @type {typeof Component} */ component.constructor.getId();
|
||||
const set = (this.componentToEntity[componentId] ??= new Set());
|
||||
set.add(entity);
|
||||
this.root.signals.entityGotNewComponent.dispatch(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call to remove a component after the creation of the entity
|
||||
* @param {Entity} entity
|
||||
* @param {typeof Component} component
|
||||
*/
|
||||
removeDynamicComponent(entity, component) {
|
||||
removeDynamicComponent(entity: Entity, component: typeof Component) {
|
||||
entity.removeComponent(component, true);
|
||||
const componentId = /** @type {typeof Component} */ (component.constructor).getId();
|
||||
const componentId = /** @type {typeof Component} */ component.constructor.getId();
|
||||
|
||||
fastArrayDeleteValue(this.componentToEntity[componentId], entity);
|
||||
this.componentToEntity[componentId].delete(entity);
|
||||
this.root.signals.entityComponentRemoved.dispatch(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an entity buy its uid, kinda slow since it loops over all entities
|
||||
* @param {number} uid
|
||||
* @param {boolean=} errorWhenNotFound
|
||||
* @returns {Entity}
|
||||
* Finds an entity by its uid
|
||||
*/
|
||||
findByUid(uid, errorWhenNotFound = true) {
|
||||
const arr = this.entities;
|
||||
for (let i = 0, len = arr.length; i < len; ++i) {
|
||||
const entity = arr[i];
|
||||
if (entity.uid === uid) {
|
||||
if (entity.queuedForDestroy || entity.destroyed) {
|
||||
if (errorWhenNotFound) {
|
||||
logger.warn("Entity with UID", uid, "not found (destroyed)");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return entity;
|
||||
findByUid(uid: number, errorWhenNotFound = true): Entity {
|
||||
const entity = this.entities.get(uid);
|
||||
|
||||
if (entity === undefined || entity.queuedForDestroy || entity.destroyed) {
|
||||
if (errorWhenNotFound) {
|
||||
logger.warn("Entity with UID", uid, "not found (destroyed)");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
if (errorWhenNotFound) {
|
||||
logger.warn("Entity with UID", uid, "not found");
|
||||
}
|
||||
return null;
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a map which gives a mapping from UID to Entity.
|
||||
* This map is not updated.
|
||||
*
|
||||
* @returns {Map<number, Entity>}
|
||||
*/
|
||||
getFrozenUidSearchMap() {
|
||||
getFrozenUidSearchMap(): Map<number, Entity> {
|
||||
const result = new Map();
|
||||
const array = this.entities;
|
||||
for (let i = 0, len = array.length; i < len; ++i) {
|
||||
const entity = array[i];
|
||||
for (const [uid, entity] of this.entities) {
|
||||
if (!entity.queuedForDestroy && !entity.destroyed) {
|
||||
result.set(entity.uid, entity);
|
||||
result.set(uid, entity);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@ -175,21 +149,19 @@ export class EntityManager extends BasicSerializableObject {
|
||||
|
||||
/**
|
||||
* Returns all entities having the given component
|
||||
* @param {typeof Component} componentHandle
|
||||
* @returns {Array<Entity>} entities
|
||||
*/
|
||||
getAllWithComponent(componentHandle) {
|
||||
return this.componentToEntity[componentHandle.getId()] || [];
|
||||
getAllWithComponent(componentHandle: typeof Component): Entity[] {
|
||||
// TODO: Convert usages to set as well
|
||||
return [...(this.componentToEntity[componentHandle.getId()] ?? new Set())];
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all components of an entity from the component to entity mapping
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
unregisterEntityComponents(entity) {
|
||||
unregisterEntityComponents(entity: Entity) {
|
||||
for (const componentId in entity.components) {
|
||||
if (entity.components[componentId]) {
|
||||
arrayDeleteValue(this.componentToEntity[componentId], entity);
|
||||
this.componentToEntity[componentId].delete(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -200,7 +172,7 @@ export class EntityManager extends BasicSerializableObject {
|
||||
const entity = this.destroyList[i];
|
||||
|
||||
// Remove from entities list
|
||||
arrayDeleteValue(this.entities, entity);
|
||||
this.entities.delete(entity.uid);
|
||||
|
||||
// Remove from componentToEntity list
|
||||
this.unregisterEntityComponents(entity);
|
||||
@ -216,9 +188,8 @@ export class EntityManager extends BasicSerializableObject {
|
||||
|
||||
/**
|
||||
* Queues an entity for destruction
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
destroyEntity(entity) {
|
||||
destroyEntity(entity: Entity) {
|
||||
if (entity.destroyed) {
|
||||
logger.error("Tried to destroy already destroyed entity:", entity.uid);
|
||||
return;
|
||||
|
||||
@ -93,7 +93,7 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
||||
|
||||
trim() {
|
||||
// Now, find the center
|
||||
const buildings = this.root.entityMgr.entities.slice();
|
||||
const buildings = [...this.root.entityMgr.entities.values()];
|
||||
|
||||
if (buildings.length === 0) {
|
||||
// nothing to do
|
||||
|
||||
@ -64,7 +64,7 @@ export class HUDWiresOverlay extends BaseHUDPart {
|
||||
const desiredAlpha = this.root.currentLayer === "wires" ? 1.0 : 0.0;
|
||||
|
||||
// On low performance, skip the fade
|
||||
if (this.root.entityMgr.entities.length > 5000 || this.root.dynamicTickrate.averageFps < 50) {
|
||||
if (this.root.entityMgr.entities.size > 5000 || this.root.dynamicTickrate.averageFps < 50) {
|
||||
this.currentAlpha = desiredAlpha;
|
||||
} else {
|
||||
this.currentAlpha = lerp(this.currentAlpha, desiredAlpha, 0.12);
|
||||
|
||||
@ -4,7 +4,6 @@ import { STOP_PROPAGATION } from "../core/signal";
|
||||
import { round2Digits } from "../core/utils";
|
||||
import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector";
|
||||
import { getBuildingDataFromCode } from "./building_codes";
|
||||
import { Component } from "./component";
|
||||
import { enumWireVariant } from "./components/wire";
|
||||
import { Entity } from "./entity";
|
||||
import { CHUNK_OVERLAY_RES } from "./map_chunk_view";
|
||||
@ -473,9 +472,9 @@ export class GameLogic {
|
||||
* Clears all belts and items
|
||||
*/
|
||||
clearAllBeltsAndItems() {
|
||||
for (const entity of this.root.entityMgr.entities) {
|
||||
for (const entity of this.root.entityMgr.entities.values()) {
|
||||
for (const component of Object.values(entity.components)) {
|
||||
/** @type {Component} */ (component).clear();
|
||||
/** @type {import("./component").Component} */ (component).clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +37,7 @@ export class SavegameSerializer {
|
||||
gameMode: root.gameMode.serialize(),
|
||||
entityMgr: root.entityMgr.serialize(),
|
||||
hubGoals: root.hubGoals.serialize(),
|
||||
entities: this.internal.serializeEntityArray(root.entityMgr.entities),
|
||||
entities: this.internal.serializeEntityMap(root.entityMgr.entities),
|
||||
beltPaths: root.systemMgr.systems.belt.serializePaths(),
|
||||
pinnedShapes: root.hud.parts.pinnedShapes ? root.hud.parts.pinnedShapes.serialize() : null,
|
||||
waypoints: root.hud.parts.waypoints ? root.hud.parts.waypoints.serialize() : null,
|
||||
|
||||
@ -11,12 +11,11 @@ const logger = createLogger("serializer_internal");
|
||||
export class SerializerInternal {
|
||||
/**
|
||||
* Serializes an array of entities
|
||||
* @param {Array<Entity>} array
|
||||
* @param {Map<number, Entity>} map
|
||||
*/
|
||||
serializeEntityArray(array) {
|
||||
serializeEntityMap(map) {
|
||||
const serialized = [];
|
||||
for (let i = 0; i < array.length; ++i) {
|
||||
const entity = array[i];
|
||||
for (const entity of map.values()) {
|
||||
if (!entity.queuedForDestroy && !entity.destroyed) {
|
||||
serialized.push(entity.serialize());
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user