mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-11 09:11:50 +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);
|
this.overlayAlpha = lerp(this.overlayAlpha, desiredOverlayAlpha, 0.25);
|
||||||
|
|
||||||
// On low performance, skip the fade
|
// 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;
|
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 { 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");
|
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
|
// This is slower but we need it for the street path generation
|
||||||
|
|
||||||
export class EntityManager extends BasicSerializableObject {
|
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();
|
super();
|
||||||
|
|
||||||
/** @type {GameRoot} */
|
|
||||||
this.root = root;
|
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() {
|
static getId() {
|
||||||
@ -48,7 +44,7 @@ export class EntityManager extends BasicSerializableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStatsText() {
|
getStatsText() {
|
||||||
return this.entities.length + " entities [" + this.destroyList.length + " to kill]";
|
return this.entities.size + " entities [" + this.destroyList.length + " to kill]";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main update
|
// Main update
|
||||||
@ -58,12 +54,14 @@ export class EntityManager extends BasicSerializableObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a new entity
|
* Registers a new entity
|
||||||
* @param {Entity} entity
|
* @param uid Optional predefined uid
|
||||||
* @param {number=} uid Optional predefined uid
|
|
||||||
*/
|
*/
|
||||||
registerEntity(entity, uid = null) {
|
registerEntity(entity: Entity, uid: number | null = null) {
|
||||||
if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) {
|
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}`);
|
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);
|
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
|
// Register into the componentToEntity map
|
||||||
for (const componentId in entity.components) {
|
for (const componentId in entity.components) {
|
||||||
if (entity.components[componentId]) {
|
if (entity.components[componentId]) {
|
||||||
if (this.componentToEntity[componentId]) {
|
const set = (this.componentToEntity[componentId] ??= new Set());
|
||||||
this.componentToEntity[componentId].push(entity);
|
set.add(entity);
|
||||||
} else {
|
|
||||||
this.componentToEntity[componentId] = [entity];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give each entity a unique id
|
|
||||||
entity.uid = uid ? uid : this.generateUid();
|
|
||||||
entity.registered = true;
|
|
||||||
|
|
||||||
this.root.signals.entityAdded.dispatch(entity);
|
this.root.signals.entityAdded.dispatch(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new uid
|
* Generates a new uid
|
||||||
* @returns {number}
|
|
||||||
*/
|
*/
|
||||||
generateUid() {
|
generateUid(): number {
|
||||||
return this.nextUid++;
|
return this.nextUid++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call to attach a new component after the creation of the entity
|
* 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);
|
entity.addComponent(component, true);
|
||||||
const componentId = /** @type {typeof Component} */ (component.constructor).getId();
|
const componentId = /** @type {typeof Component} */ component.constructor.getId();
|
||||||
if (this.componentToEntity[componentId]) {
|
const set = (this.componentToEntity[componentId] ??= new Set());
|
||||||
this.componentToEntity[componentId].push(entity);
|
set.add(entity);
|
||||||
} else {
|
|
||||||
this.componentToEntity[componentId] = [entity];
|
|
||||||
}
|
|
||||||
this.root.signals.entityGotNewComponent.dispatch(entity);
|
this.root.signals.entityGotNewComponent.dispatch(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call to remove a component after the creation of the 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);
|
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);
|
this.root.signals.entityComponentRemoved.dispatch(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds an entity buy its uid, kinda slow since it loops over all entities
|
* Finds an entity by its uid
|
||||||
* @param {number} uid
|
|
||||||
* @param {boolean=} errorWhenNotFound
|
|
||||||
* @returns {Entity}
|
|
||||||
*/
|
*/
|
||||||
findByUid(uid, errorWhenNotFound = true) {
|
findByUid(uid: number, errorWhenNotFound = true): Entity {
|
||||||
const arr = this.entities;
|
const entity = this.entities.get(uid);
|
||||||
for (let i = 0, len = arr.length; i < len; ++i) {
|
|
||||||
const entity = arr[i];
|
if (entity === undefined || entity.queuedForDestroy || entity.destroyed) {
|
||||||
if (entity.uid === uid) {
|
if (errorWhenNotFound) {
|
||||||
if (entity.queuedForDestroy || entity.destroyed) {
|
logger.warn("Entity with UID", uid, "not found (destroyed)");
|
||||||
if (errorWhenNotFound) {
|
|
||||||
logger.warn("Entity with UID", uid, "not found (destroyed)");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return entity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (errorWhenNotFound) {
|
|
||||||
logger.warn("Entity with UID", uid, "not found");
|
return entity;
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a map which gives a mapping from UID to Entity.
|
* Returns a map which gives a mapping from UID to Entity.
|
||||||
* This map is not updated.
|
* This map is not updated.
|
||||||
*
|
|
||||||
* @returns {Map<number, Entity>}
|
|
||||||
*/
|
*/
|
||||||
getFrozenUidSearchMap() {
|
getFrozenUidSearchMap(): Map<number, Entity> {
|
||||||
const result = new Map();
|
const result = new Map();
|
||||||
const array = this.entities;
|
for (const [uid, entity] of this.entities) {
|
||||||
for (let i = 0, len = array.length; i < len; ++i) {
|
|
||||||
const entity = array[i];
|
|
||||||
if (!entity.queuedForDestroy && !entity.destroyed) {
|
if (!entity.queuedForDestroy && !entity.destroyed) {
|
||||||
result.set(entity.uid, entity);
|
result.set(uid, entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@ -175,21 +149,19 @@ export class EntityManager extends BasicSerializableObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all entities having the given component
|
* Returns all entities having the given component
|
||||||
* @param {typeof Component} componentHandle
|
|
||||||
* @returns {Array<Entity>} entities
|
|
||||||
*/
|
*/
|
||||||
getAllWithComponent(componentHandle) {
|
getAllWithComponent(componentHandle: typeof Component): Entity[] {
|
||||||
return this.componentToEntity[componentHandle.getId()] || [];
|
// 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
|
* 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) {
|
for (const componentId in entity.components) {
|
||||||
if (entity.components[componentId]) {
|
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];
|
const entity = this.destroyList[i];
|
||||||
|
|
||||||
// Remove from entities list
|
// Remove from entities list
|
||||||
arrayDeleteValue(this.entities, entity);
|
this.entities.delete(entity.uid);
|
||||||
|
|
||||||
// Remove from componentToEntity list
|
// Remove from componentToEntity list
|
||||||
this.unregisterEntityComponents(entity);
|
this.unregisterEntityComponents(entity);
|
||||||
@ -216,9 +188,8 @@ export class EntityManager extends BasicSerializableObject {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Queues an entity for destruction
|
* Queues an entity for destruction
|
||||||
* @param {Entity} entity
|
|
||||||
*/
|
*/
|
||||||
destroyEntity(entity) {
|
destroyEntity(entity: Entity) {
|
||||||
if (entity.destroyed) {
|
if (entity.destroyed) {
|
||||||
logger.error("Tried to destroy already destroyed entity:", entity.uid);
|
logger.error("Tried to destroy already destroyed entity:", entity.uid);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
|||||||
|
|
||||||
trim() {
|
trim() {
|
||||||
// Now, find the center
|
// Now, find the center
|
||||||
const buildings = this.root.entityMgr.entities.slice();
|
const buildings = [...this.root.entityMgr.entities.values()];
|
||||||
|
|
||||||
if (buildings.length === 0) {
|
if (buildings.length === 0) {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
|
|||||||
@ -64,7 +64,7 @@ export class HUDWiresOverlay extends BaseHUDPart {
|
|||||||
const desiredAlpha = this.root.currentLayer === "wires" ? 1.0 : 0.0;
|
const desiredAlpha = this.root.currentLayer === "wires" ? 1.0 : 0.0;
|
||||||
|
|
||||||
// On low performance, skip the fade
|
// 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;
|
this.currentAlpha = desiredAlpha;
|
||||||
} else {
|
} else {
|
||||||
this.currentAlpha = lerp(this.currentAlpha, desiredAlpha, 0.12);
|
this.currentAlpha = lerp(this.currentAlpha, desiredAlpha, 0.12);
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { STOP_PROPAGATION } from "../core/signal";
|
|||||||
import { round2Digits } from "../core/utils";
|
import { round2Digits } from "../core/utils";
|
||||||
import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector";
|
import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector";
|
||||||
import { getBuildingDataFromCode } from "./building_codes";
|
import { getBuildingDataFromCode } from "./building_codes";
|
||||||
import { Component } from "./component";
|
|
||||||
import { enumWireVariant } from "./components/wire";
|
import { enumWireVariant } from "./components/wire";
|
||||||
import { Entity } from "./entity";
|
import { Entity } from "./entity";
|
||||||
import { CHUNK_OVERLAY_RES } from "./map_chunk_view";
|
import { CHUNK_OVERLAY_RES } from "./map_chunk_view";
|
||||||
@ -473,9 +472,9 @@ export class GameLogic {
|
|||||||
* Clears all belts and items
|
* Clears all belts and items
|
||||||
*/
|
*/
|
||||||
clearAllBeltsAndItems() {
|
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)) {
|
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(),
|
gameMode: root.gameMode.serialize(),
|
||||||
entityMgr: root.entityMgr.serialize(),
|
entityMgr: root.entityMgr.serialize(),
|
||||||
hubGoals: root.hubGoals.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(),
|
beltPaths: root.systemMgr.systems.belt.serializePaths(),
|
||||||
pinnedShapes: root.hud.parts.pinnedShapes ? root.hud.parts.pinnedShapes.serialize() : null,
|
pinnedShapes: root.hud.parts.pinnedShapes ? root.hud.parts.pinnedShapes.serialize() : null,
|
||||||
waypoints: root.hud.parts.waypoints ? root.hud.parts.waypoints.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 {
|
export class SerializerInternal {
|
||||||
/**
|
/**
|
||||||
* Serializes an array of entities
|
* Serializes an array of entities
|
||||||
* @param {Array<Entity>} array
|
* @param {Map<number, Entity>} map
|
||||||
*/
|
*/
|
||||||
serializeEntityArray(array) {
|
serializeEntityMap(map) {
|
||||||
const serialized = [];
|
const serialized = [];
|
||||||
for (let i = 0; i < array.length; ++i) {
|
for (const entity of map.values()) {
|
||||||
const entity = array[i];
|
|
||||||
if (!entity.queuedForDestroy && !entity.destroyed) {
|
if (!entity.queuedForDestroy && !entity.destroyed) {
|
||||||
serialized.push(entity.serialize());
|
serialized.push(entity.serialize());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user