Improve performance by caching area of changed ejectors

This commit is contained in:
tobspr 2020-06-24 20:25:43 +02:00
parent b575bc4f41
commit 9789468c2d
3 changed files with 162 additions and 43 deletions

View File

@ -46,6 +46,7 @@ export class Rectangle {
} }
/** /**
* Returns if a intersects b
* @param {Rectangle} a * @param {Rectangle} a
* @param {Rectangle} b * @param {Rectangle} b
*/ */
@ -74,7 +75,21 @@ export class Rectangle {
return new Rectangle(minX, minY, maxX - minX, maxY - minY); 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) { extendBySquare(centerX, centerY, halfWidth, halfHeight) {
if (this.isEmpty()) { if (this.isEmpty()) {
// Just assign values since this rectangle is empty // Just assign values since this rectangle is empty
@ -90,10 +105,19 @@ export class Rectangle {
} }
} }
/**
* Returns if this rectangle is empty
* @returns {boolean}
*/
isEmpty() { isEmpty() {
return epsilonCompare(this.w * this.h, 0); 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) { equalsEpsilon(other, epsilon) {
return ( return (
epsilonCompare(this.x, other.x, epsilon) && epsilonCompare(this.x, other.x, epsilon) &&
@ -103,71 +127,126 @@ export class Rectangle {
); );
} }
/**
* @returns {number}
*/
left() { left() {
return this.x; return this.x;
} }
/**
* @returns {number}
*/
right() { right() {
return this.x + this.w; return this.x + this.w;
} }
/**
* @returns {number}
*/
top() { top() {
return this.y; return this.y;
} }
/**
* @returns {number}
*/
bottom() { bottom() {
return this.y + this.h; return this.y + this.h;
} }
/**
* Returns Top, Right, Bottom, Left
* @returns {[number, number, number, number]}
*/
trbl() { trbl() {
return [this.y, this.right(), this.bottom(), this.x]; return [this.y, this.right(), this.bottom(), this.x];
} }
/**
* Returns the center of the rect
* @returns {Vector}
*/
getCenter() { getCenter() {
return new Vector(this.x + this.w / 2, this.y + this.h / 2); 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) { setRight(right) {
this.w = right - this.x; this.w = right - this.x;
} }
/**
* Sets the bottom side of the rect without moving it
* @param {number} bottom
*/
setBottom(bottom) { setBottom(bottom) {
this.h = bottom - this.y; 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) { setTop(top) {
const bottom = this.bottom(); const bottom = this.bottom();
this.y = top; this.y = top;
this.setBottom(bottom); this.setBottom(bottom);
} }
// Sets left while keeping right /**
* Sets the left side of the rect without scaling it
* @param {number} left
*/
setLeft(left) { setLeft(left) {
const right = this.right(); const right = this.right();
this.x = left; this.x = left;
this.setRight(right); this.setRight(right);
} }
/**
* Returns the top left point
* @returns {Vector}
*/
topLeft() { topLeft() {
return new Vector(this.x, this.y); return new Vector(this.x, this.y);
} }
/**
* Returns the bottom left point
* @returns {Vector}
*/
bottomRight() { bottomRight() {
return new Vector(this.right(), this.bottom()); return new Vector(this.right(), this.bottom());
} }
/**
* Moves the rectangle by the given parameters
* @param {number} x
* @param {number} y
*/
moveBy(x, y) { moveBy(x, y) {
this.x += x; this.x += x;
this.y += y; this.y += y;
} }
/**
* Moves the rectangle by the given vector
* @param {Vector} vec
*/
moveByVector(vec) { moveByVector(vec) {
this.x += vec.x; this.x += vec.x;
this.y += vec.y; 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) { allScaled(factor) {
return new Rectangle(this.x * factor, this.y * factor, this.w * factor, this.h * 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 * @param {number} amount
* @returns {Rectangle} new rectangle * @returns {Rectangle} new rectangle
*/ */
expandedInAllDirections(amount) { expandedInAllDirections(amount) {
return new Rectangle(this.x - amount, this.y - amount, this.w + 2 * amount, this.h + 2 * 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() { getMinStartTile() {
return new Vector(this.x, this.y).snapWorldToTile(); 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) { containsRect4Params(x, y, w, h) {
return this.x <= x + w && x <= this.right() && this.y <= y + h && y <= this.bottom(); 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} x
* @param {number} y * @param {number} y
* @param {number} radius * @param {number} radius
* @returns {boolean}
*/ */
containsCircle(x, y, radius) { containsCircle(x, y, radius) {
return ( return (
@ -224,6 +314,7 @@ export class Rectangle {
* Returns if hte rectangle contains the given point * Returns if hte rectangle contains the given point
* @param {number} x * @param {number} x
* @param {number} y * @param {number} y
* @returns {boolean}
*/ */
containsPoint(x, y) { containsPoint(x, y) {
return x >= this.x && x < this.right() && y >= this.y && y < this.bottom(); return x >= this.x && x < this.right() && y >= this.y && y < this.bottom();
@ -234,7 +325,7 @@ export class Rectangle {
* @param {Rectangle} rect * @param {Rectangle} rect
* @returns {Rectangle|null} * @returns {Rectangle|null}
*/ */
getUnion(rect) { getIntersection(rect) {
const left = Math_max(this.x, rect.x); const left = Math_max(this.x, rect.x);
const top = Math_max(this.y, rect.y); const top = Math_max(this.y, rect.y);
@ -244,6 +335,30 @@ export class Rectangle {
if (right <= left || bottom <= top) { if (right <= left || bottom <= top) {
return null; 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); return Rectangle.fromTRBL(top, right, bottom, left);
} }

View File

@ -170,7 +170,7 @@ export class AtlasSprite extends BaseSprite {
if (clipping) { if (clipping) {
const rect = new Rectangle(destX, destY, destW, destH); const rect = new Rectangle(destX, destY, destW, destH);
intersection = rect.getUnion(visibleRect); intersection = rect.getIntersection(visibleRect);
if (!intersection) { if (!intersection) {
return; return;
} }

View File

@ -1,13 +1,13 @@
import { Math_min } from "../../core/builtins";
import { globalConfig } from "../../core/config"; import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters"; import { DrawParameters } from "../../core/draw_parameters";
import { createLogger } from "../../core/logging";
import { Rectangle } from "../../core/rectangle";
import { enumDirectionToVector, Vector } from "../../core/vector"; import { enumDirectionToVector, Vector } from "../../core/vector";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { ItemEjectorComponent } from "../components/item_ejector"; import { ItemEjectorComponent } from "../components/item_ejector";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter"; 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"); const logger = createLogger("systems/ejector");
@ -15,71 +15,81 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
constructor(root) { constructor(root) {
super(root, [ItemEjectorComponent]); super(root, [ItemEjectorComponent]);
this.cacheNeedsUpdate = true; this.root.signals.entityAdded.add(this.checkForCacheInvalidation, this);
this.root.signals.entityDestroyed.add(this.checkForCacheInvalidation, this);
this.root.signals.entityAdded.add(this.invalidateCache, this); this.root.signals.postLoadHook.add(this.recomputeCache, this);
this.root.signals.entityDestroyed.add(this.invalidateCache, this);
/** /**
* @type {Rectangle[]} * @type {Rectangle}
*/ */
this.smallCacheAreas = []; this.areaToRecompute = null;
} }
/** /**
* *
* @param {Entity} entity * @param {Entity} entity
*/ */
invalidateCache(entity) { checkForCacheInvalidation(entity) {
if (!this.root.gameInitialized) {
return;
}
if (!entity.components.StaticMapEntity) { if (!entity.components.StaticMapEntity) {
return; return;
} }
this.cacheNeedsUpdate = true;
// Optimize for the common case: adding or removing one building at a time. Clicking // Optimize for the common case: adding or removing one building at a time. Clicking
// and dragging can cause up to 4 add/remove signals. // and dragging can cause up to 4 add/remove signals.
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
const bounds = staticComp.getTileSpaceBounds(); const bounds = staticComp.getTileSpaceBounds();
const expandedBounds = bounds.expandedInAllDirections(2); 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 * Precomputes the cache, which makes up for a huge performance improvement
*/ */
recomputeCache() { recomputeCache() {
logger.log("Recomputing cache"); if (this.areaToRecompute) {
logger.log("Recomputing cache using rectangle");
let entryCount = 0; this.recomputeAreaCache(this.areaToRecompute);
if (this.smallCacheAreas.length <= 4) { this.areaToRecompute = null;
// Only recompute caches of entities inside the rectangles.
for (let i = 0; i < this.smallCacheAreas.length; i++) {
entryCount += this.recomputeAreaCaches(this.smallCacheAreas[i]);
}
} else { } else {
logger.log("Full cache recompute");
// Try to find acceptors for every ejector // Try to find acceptors for every ejector
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];
entryCount += this.recomputeSingleEntityCache(entity); this.recomputeSingleEntityCache(entity);
} }
} }
logger.log("Found", entryCount, "entries to update");
this.smallCacheAreas = [];
} }
/** /**
* *
* @param {Rectangle} area * @param {Rectangle} area
*/ */
recomputeAreaCaches(area) { recomputeAreaCache(area) {
let entryCount = 0; 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 x = area.x; x < area.right(); ++x) {
for (let y = area.y; y < area.bottom(); ++y) { for (let y = area.y; y < area.bottom(); ++y) {
const entity = this.root.map.getTileContentXY(x, y); const entity = this.root.map.getTileContentXY(x, y);
if (entity && entity.components.ItemEjector) { if (entity) {
entryCount += this.recomputeSingleEntityCache(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 * @param {Entity} entity
*/ */
recomputeSingleEntityCache(entity) { recomputeSingleEntityCache(entity) {
@ -97,8 +106,6 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
// Clear the old cache. // Clear the old cache.
ejectorComp.cachedConnectedSlots = null; ejectorComp.cachedConnectedSlots = null;
// For every ejector slot, try to find an acceptor
let entryCount = 0;
for (let ejectorSlotIndex = 0; ejectorSlotIndex < ejectorComp.slots.length; ++ejectorSlotIndex) { for (let ejectorSlotIndex = 0; ejectorSlotIndex < ejectorComp.slots.length; ++ejectorSlotIndex) {
const ejectorSlot = ejectorComp.slots[ejectorSlotIndex]; const ejectorSlot = ejectorComp.slots[ejectorSlotIndex];
@ -144,14 +151,11 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
} }
ejectorSlot.cachedTargetEntity = targetEntity; ejectorSlot.cachedTargetEntity = targetEntity;
ejectorSlot.cachedDestSlot = matchingSlot; ejectorSlot.cachedDestSlot = matchingSlot;
entryCount += 1;
} }
return entryCount;
} }
update() { update() {
if (this.cacheNeedsUpdate) { if (this.areaToRecompute) {
this.cacheNeedsUpdate = false;
this.recomputeCache(); this.recomputeCache();
} }