mirror of
https://github.com/tobspr/shapez.io.git
synced 2024-10-27 20:34:29 +00:00
Improve performance by caching area of changed ejectors
This commit is contained in:
parent
b575bc4f41
commit
9789468c2d
@ -46,6 +46,7 @@ export class Rectangle {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if a intersects b
|
||||
* @param {Rectangle} a
|
||||
* @param {Rectangle} b
|
||||
*/
|
||||
@ -74,7 +75,21 @@ export class Rectangle {
|
||||
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
|
||||
}
|
||||
|
||||
// Ensures the rectangle contains the given square
|
||||
/**
|
||||
* Copies this instance
|
||||
* @returns {Rectangle}
|
||||
*/
|
||||
clone() {
|
||||
return new Rectangle(this.x, this.y, this.w, this.h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the rectangle contains the given square
|
||||
* @param {number} centerX
|
||||
* @param {number} centerY
|
||||
* @param {number} halfWidth
|
||||
* @param {number} halfHeight
|
||||
*/
|
||||
extendBySquare(centerX, centerY, halfWidth, halfHeight) {
|
||||
if (this.isEmpty()) {
|
||||
// Just assign values since this rectangle is empty
|
||||
@ -90,10 +105,19 @@ export class Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this rectangle is empty
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEmpty() {
|
||||
return epsilonCompare(this.w * this.h, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this rectangle is equal to the other while taking an epsilon into account
|
||||
* @param {Rectangle} other
|
||||
* @param {number} epsilon
|
||||
*/
|
||||
equalsEpsilon(other, epsilon) {
|
||||
return (
|
||||
epsilonCompare(this.x, other.x, epsilon) &&
|
||||
@ -103,71 +127,126 @@ export class Rectangle {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
left() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
right() {
|
||||
return this.x + this.w;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
top() {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
bottom() {
|
||||
return this.y + this.h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Top, Right, Bottom, Left
|
||||
* @returns {[number, number, number, number]}
|
||||
*/
|
||||
trbl() {
|
||||
return [this.y, this.right(), this.bottom(), this.x];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the center of the rect
|
||||
* @returns {Vector}
|
||||
*/
|
||||
getCenter() {
|
||||
return new Vector(this.x + this.w / 2, this.y + this.h / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the right side of the rect without moving it
|
||||
* @param {number} right
|
||||
*/
|
||||
setRight(right) {
|
||||
this.w = right - this.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bottom side of the rect without moving it
|
||||
* @param {number} bottom
|
||||
*/
|
||||
setBottom(bottom) {
|
||||
this.h = bottom - this.y;
|
||||
}
|
||||
|
||||
// Sets top while keeping bottom
|
||||
/**
|
||||
* Sets the top side of the rect without scaling it
|
||||
* @param {number} top
|
||||
*/
|
||||
setTop(top) {
|
||||
const bottom = this.bottom();
|
||||
this.y = top;
|
||||
this.setBottom(bottom);
|
||||
}
|
||||
|
||||
// Sets left while keeping right
|
||||
/**
|
||||
* Sets the left side of the rect without scaling it
|
||||
* @param {number} left
|
||||
*/
|
||||
setLeft(left) {
|
||||
const right = this.right();
|
||||
this.x = left;
|
||||
this.setRight(right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top left point
|
||||
* @returns {Vector}
|
||||
*/
|
||||
topLeft() {
|
||||
return new Vector(this.x, this.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bottom left point
|
||||
* @returns {Vector}
|
||||
*/
|
||||
bottomRight() {
|
||||
return new Vector(this.right(), this.bottom());
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the rectangle by the given parameters
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
moveBy(x, y) {
|
||||
this.x += x;
|
||||
this.y += y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the rectangle by the given vector
|
||||
* @param {Vector} vec
|
||||
*/
|
||||
moveByVector(vec) {
|
||||
this.x += vec.x;
|
||||
this.y += vec.y;
|
||||
}
|
||||
|
||||
// Returns a scaled version which also scales the position of the rectangle
|
||||
/**
|
||||
* Scales every parameter (w, h, x, y) by the given factor. Useful to transform from world to
|
||||
* tile space and vice versa
|
||||
* @param {number} factor
|
||||
*/
|
||||
allScaled(factor) {
|
||||
return new Rectangle(this.x * factor, this.y * factor, this.w * factor, this.h * factor);
|
||||
}
|
||||
@ -177,12 +256,14 @@ export class Rectangle {
|
||||
* @param {number} amount
|
||||
* @returns {Rectangle} new rectangle
|
||||
*/
|
||||
|
||||
expandedInAllDirections(amount) {
|
||||
return new Rectangle(this.x - amount, this.y - amount, this.w + 2 * amount, this.h + 2 * amount);
|
||||
}
|
||||
|
||||
// Culling helpers
|
||||
/**
|
||||
* Helper for computing a culling area. Returns the top left tile
|
||||
* @returns {Vector}
|
||||
*/
|
||||
getMinStartTile() {
|
||||
return new Vector(this.x, this.y).snapWorldToTile();
|
||||
}
|
||||
@ -201,6 +282,14 @@ export class Rectangle {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this rectangle contains the other rectangle specified by the parameters
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} w
|
||||
* @param {number} h
|
||||
* @returns {boolean}
|
||||
*/
|
||||
containsRect4Params(x, y, w, h) {
|
||||
return this.x <= x + w && x <= this.right() && this.y <= y + h && y <= this.bottom();
|
||||
}
|
||||
@ -210,6 +299,7 @@ export class Rectangle {
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} radius
|
||||
* @returns {boolean}
|
||||
*/
|
||||
containsCircle(x, y, radius) {
|
||||
return (
|
||||
@ -224,6 +314,7 @@ export class Rectangle {
|
||||
* Returns if hte rectangle contains the given point
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @returns {boolean}
|
||||
*/
|
||||
containsPoint(x, y) {
|
||||
return x >= this.x && x < this.right() && y >= this.y && y < this.bottom();
|
||||
@ -234,7 +325,7 @@ export class Rectangle {
|
||||
* @param {Rectangle} rect
|
||||
* @returns {Rectangle|null}
|
||||
*/
|
||||
getUnion(rect) {
|
||||
getIntersection(rect) {
|
||||
const left = Math_max(this.x, rect.x);
|
||||
const top = Math_max(this.y, rect.y);
|
||||
|
||||
@ -244,6 +335,30 @@ export class Rectangle {
|
||||
if (right <= left || bottom <= top) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Rectangle.fromTRBL(top, right, bottom, left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the union of this rectangle with another
|
||||
* @param {Rectangle} rect
|
||||
*/
|
||||
getUnion(rect) {
|
||||
if (this.isEmpty()) {
|
||||
// If this is rect is empty, return the other one
|
||||
return rect.clone();
|
||||
}
|
||||
if (rect.isEmpty()) {
|
||||
// If the other is empty, return this one
|
||||
return this.clone();
|
||||
}
|
||||
|
||||
// Find contained area
|
||||
const left = Math_min(this.x, rect.x);
|
||||
const top = Math_min(this.y, rect.y);
|
||||
const right = Math_max(this.right(), rect.right());
|
||||
const bottom = Math_max(this.bottom(), rect.bottom());
|
||||
|
||||
return Rectangle.fromTRBL(top, right, bottom, left);
|
||||
}
|
||||
|
||||
|
@ -170,7 +170,7 @@ export class AtlasSprite extends BaseSprite {
|
||||
|
||||
if (clipping) {
|
||||
const rect = new Rectangle(destX, destY, destW, destH);
|
||||
intersection = rect.getUnion(visibleRect);
|
||||
intersection = rect.getIntersection(visibleRect);
|
||||
if (!intersection) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { Math_min } from "../../core/builtins";
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { DrawParameters } from "../../core/draw_parameters";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { Rectangle } from "../../core/rectangle";
|
||||
import { enumDirectionToVector, Vector } from "../../core/vector";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { Entity } from "../entity";
|
||||
import { GameSystemWithFilter } from "../game_system_with_filter";
|
||||
import { Math_min } from "../../core/builtins";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { Rectangle } from "../../core/rectangle";
|
||||
|
||||
const logger = createLogger("systems/ejector");
|
||||
|
||||
@ -15,71 +15,81 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
constructor(root) {
|
||||
super(root, [ItemEjectorComponent]);
|
||||
|
||||
this.cacheNeedsUpdate = true;
|
||||
|
||||
this.root.signals.entityAdded.add(this.invalidateCache, this);
|
||||
this.root.signals.entityDestroyed.add(this.invalidateCache, this);
|
||||
this.root.signals.entityAdded.add(this.checkForCacheInvalidation, this);
|
||||
this.root.signals.entityDestroyed.add(this.checkForCacheInvalidation, this);
|
||||
this.root.signals.postLoadHook.add(this.recomputeCache, this);
|
||||
|
||||
/**
|
||||
* @type {Rectangle[]}
|
||||
* @type {Rectangle}
|
||||
*/
|
||||
this.smallCacheAreas = [];
|
||||
this.areaToRecompute = null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
invalidateCache(entity) {
|
||||
checkForCacheInvalidation(entity) {
|
||||
if (!this.root.gameInitialized) {
|
||||
return;
|
||||
}
|
||||
if (!entity.components.StaticMapEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cacheNeedsUpdate = true;
|
||||
|
||||
// Optimize for the common case: adding or removing one building at a time. Clicking
|
||||
// and dragging can cause up to 4 add/remove signals.
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const bounds = staticComp.getTileSpaceBounds();
|
||||
const expandedBounds = bounds.expandedInAllDirections(2);
|
||||
this.smallCacheAreas.push(expandedBounds);
|
||||
|
||||
if (this.areaToRecompute) {
|
||||
this.areaToRecompute = this.areaToRecompute.getUnion(expandedBounds);
|
||||
} else {
|
||||
this.areaToRecompute = expandedBounds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Precomputes the cache, which makes up for a huge performance improvement
|
||||
*/
|
||||
recomputeCache() {
|
||||
logger.log("Recomputing cache");
|
||||
|
||||
let entryCount = 0;
|
||||
if (this.smallCacheAreas.length <= 4) {
|
||||
// Only recompute caches of entities inside the rectangles.
|
||||
for (let i = 0; i < this.smallCacheAreas.length; i++) {
|
||||
entryCount += this.recomputeAreaCaches(this.smallCacheAreas[i]);
|
||||
}
|
||||
if (this.areaToRecompute) {
|
||||
logger.log("Recomputing cache using rectangle");
|
||||
this.recomputeAreaCache(this.areaToRecompute);
|
||||
this.areaToRecompute = null;
|
||||
} else {
|
||||
logger.log("Full cache recompute");
|
||||
// Try to find acceptors for every ejector
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
entryCount += this.recomputeSingleEntityCache(entity);
|
||||
this.recomputeSingleEntityCache(entity);
|
||||
}
|
||||
}
|
||||
logger.log("Found", entryCount, "entries to update");
|
||||
|
||||
this.smallCacheAreas = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Rectangle} area
|
||||
*/
|
||||
recomputeAreaCaches(area) {
|
||||
recomputeAreaCache(area) {
|
||||
let entryCount = 0;
|
||||
|
||||
logger.log("Recomputing area:", area.x, area.y, "/", area.w, area.h);
|
||||
|
||||
// Store the entities we already recomputed, so we don't do work twice
|
||||
const recomputedEntities = new Set();
|
||||
|
||||
for (let x = area.x; x < area.right(); ++x) {
|
||||
for (let y = area.y; y < area.bottom(); ++y) {
|
||||
const entity = this.root.map.getTileContentXY(x, y);
|
||||
if (entity && entity.components.ItemEjector) {
|
||||
entryCount += this.recomputeSingleEntityCache(entity);
|
||||
if (entity) {
|
||||
// Recompute the entity in case its relevant for this system and it
|
||||
// hasn't already been computed
|
||||
if (!recomputedEntities.has(entity.uid) && entity.components.ItemEjector) {
|
||||
recomputedEntities.add(entity.uid);
|
||||
this.recomputeSingleEntityCache(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -87,7 +97,6 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
recomputeSingleEntityCache(entity) {
|
||||
@ -97,8 +106,6 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
// Clear the old cache.
|
||||
ejectorComp.cachedConnectedSlots = null;
|
||||
|
||||
// For every ejector slot, try to find an acceptor
|
||||
let entryCount = 0;
|
||||
for (let ejectorSlotIndex = 0; ejectorSlotIndex < ejectorComp.slots.length; ++ejectorSlotIndex) {
|
||||
const ejectorSlot = ejectorComp.slots[ejectorSlotIndex];
|
||||
|
||||
@ -144,14 +151,11 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
}
|
||||
ejectorSlot.cachedTargetEntity = targetEntity;
|
||||
ejectorSlot.cachedDestSlot = matchingSlot;
|
||||
entryCount += 1;
|
||||
}
|
||||
return entryCount;
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.cacheNeedsUpdate) {
|
||||
this.cacheNeedsUpdate = false;
|
||||
if (this.areaToRecompute) {
|
||||
this.recomputeCache();
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user