mirror of
				https://github.com/tobspr/shapez.io.git
				synced 2025-06-13 13:04:03 +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