1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-13 13:04:03 +00:00

Lots of belt optimizations, ~15% performance boost

This commit is contained in:
tobspr 2021-04-29 11:04:27 +02:00
parent 1f22327679
commit cba91de114
7 changed files with 8811 additions and 7732 deletions

View File

@ -11,6 +11,7 @@ export const itemTypes = ["shape", "color", "boolean"];
export class BaseItem extends BasicSerializableObject {
constructor() {
super();
this._type = this.getItemType();
}
static getId() {

View File

@ -13,8 +13,6 @@ import { GameRoot } from "./root";
const logger = createLogger("belt_path");
// Helpers for more semantic access into interleaved arrays
const _nextDistance = 0;
const _item = 1;
const DEBUG = G_IS_DEV && false;
@ -174,7 +172,7 @@ export class BeltPath extends BasicSerializableObject {
* Recomputes cache variables once the path was changed
*/
onPathChanged() {
this.acceptorTarget = this.computeAcceptingEntityAndSlot();
this.boundAcceptor = this.computeAcceptingEntityAndSlot();
/**
* How many items past the first item are compressed
@ -192,7 +190,7 @@ export class BeltPath extends BasicSerializableObject {
/**
* Finds the entity which accepts our items
* @param {boolean=} debug_Silent Whether debug output should be silent
* @return {{ entity: Entity, slot: number, direction?: enumDirection }}
* @return { (BaseItem, number) => boolean }
*/
computeAcceptingEntityAndSlot(debug_Silent = false) {
DEBUG && !debug_Silent && logger.log("Recomputing acceptor target");
@ -214,55 +212,142 @@ export class BeltPath extends BasicSerializableObject {
"regular"
);
if (targetEntity) {
DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid);
const targetStaticComp = targetEntity.components.StaticMapEntity;
const targetBeltComp = targetEntity.components.Belt;
if (!targetEntity) {
return;
}
// Check for belts (special case)
if (targetBeltComp) {
const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top);
DEBUG &&
!debug_Silent &&
logger.log(
" Entity is accepting items from",
ejectSlotWsDirection,
"vs",
beltAcceptingDirection,
"Rotation:",
targetStaticComp.rotation
const noSimplifiedBelts = !this.root.app.settings.getAllSettings().simplifiedBelts;
DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid);
const targetStaticComp = targetEntity.components.StaticMapEntity;
const targetBeltComp = targetEntity.components.Belt;
// Check for belts (special case)
if (targetBeltComp) {
const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top);
DEBUG &&
!debug_Silent &&
logger.log(
" Entity is accepting items from",
ejectSlotWsDirection,
"vs",
beltAcceptingDirection,
"Rotation:",
targetStaticComp.rotation
);
if (ejectSlotWsDirection === beltAcceptingDirection) {
return item => {
const path = targetBeltComp.assignedPath;
assert(path, "belt has no path");
return path.tryAcceptItem(item);
};
}
}
// Check for item acceptors
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
if (!targetAcceptorComp) {
// Entity doesn't accept items
return;
}
const ejectingDirection = targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection);
const matchingSlot = targetAcceptorComp.findMatchingSlot(
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
ejectingDirection
);
if (!matchingSlot) {
// No matching slot found
return;
}
const matchingSlotIndex = matchingSlot.index;
const passOver = this.computePassOverFunctionWithoutBelts(targetEntity, matchingSlotIndex);
if (!passOver) {
return;
}
const matchingDirection = enumInvertedDirections[ejectingDirection];
const filter = matchingSlot.slot.filter;
return function (item, remainingProgress = 0.0) {
// Check if the acceptor has a filter
if (filter && item._type !== filter) {
return false;
}
// Try to pass over
if (passOver(item, matchingSlotIndex)) {
// Trigger animation on the acceptor comp
if (noSimplifiedBelts) {
targetAcceptorComp.onItemAccepted(
matchingSlotIndex,
matchingDirection,
item,
remainingProgress
);
if (ejectSlotWsDirection === beltAcceptingDirection) {
return {
entity: targetEntity,
direction: null,
slot: 0,
};
}
return true;
}
return false;
};
}
// Check for item acceptors
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
if (!targetAcceptorComp) {
// Entity doesn't accept items
return;
}
/**
* Computes a method to pass over the item to the entity
* @param {Entity} entity
* @param {number} matchingSlotIndex
* @returns {(item: BaseItem, slotIndex: number) => boolean | void}
*/
computePassOverFunctionWithoutBelts(entity, matchingSlotIndex) {
const systems = this.root.systemMgr.systems;
const hubGoals = this.root.hubGoals;
const ejectingDirection = targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection);
const matchingSlot = targetAcceptorComp.findMatchingSlot(
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
ejectingDirection
);
// NOTICE: THIS IS COPIED FROM THE ITEM EJECTOR SYSTEM FOR PEROFMANCE REASONS
if (!matchingSlot) {
// No matching slot found
return;
}
const itemProcessorComp = entity.components.ItemProcessor;
if (itemProcessorComp) {
// Its an item processor ..
return function (item) {
// Check for potential filters
if (!systems.itemProcessor.checkRequirements(entity, item, matchingSlotIndex)) {
return;
}
return itemProcessorComp.tryTakeItem(item, matchingSlotIndex);
};
}
return {
entity: targetEntity,
slot: matchingSlot.index,
direction: enumInvertedDirections[ejectingDirection],
const undergroundBeltComp = entity.components.UndergroundBelt;
if (undergroundBeltComp) {
// Its an underground belt. yay.
return function (item) {
return undergroundBeltComp.tryAcceptExternalItem(
item,
hubGoals.getUndergroundBeltBaseSpeed()
);
};
}
const storageComp = entity.components.Storage;
if (storageComp) {
// It's a storage
return function (item) {
if (storageComp.canAcceptItem(item)) {
storageComp.takeItem(item);
return true;
}
};
}
const filterComp = entity.components.Filter;
if (filterComp) {
// It's a filter! Unfortunately the filter has to know a lot about it's
// surrounding state and components, so it can't be within the component itself.
return function (item) {
if (systems.filter.tryAcceptItem(entity, matchingSlotIndex, item)) {
return true;
}
};
}
}
@ -365,17 +450,17 @@ export class BeltPath extends BasicSerializableObject {
for (let i = 0; i < this.items.length; ++i) {
const item = this.items[i];
if (item[_nextDistance] < 0 || item[_nextDistance] > this.totalLength + 0.02) {
if (item[0 /* nextDistance */] < 0 || item[0 /* nextDistance */] > this.totalLength + 0.02) {
return fail(
"Item has invalid offset to next item: ",
item[_nextDistance],
item[0 /* nextDistance */],
"(total length:",
this.totalLength,
")"
);
}
currentPos += item[_nextDistance];
currentPos += item[0 /* nextDistance */];
}
// Check the total sum matches
@ -387,7 +472,7 @@ export class BeltPath extends BasicSerializableObject {
this.spacingToFirstItem,
") and items does not match total length (",
this.totalLength,
") -> items: " + this.items.map(i => i[_nextDistance]).join("|")
") -> items: " + this.items.map(i => i[0 /* nextDistance */]).join("|")
);
}
@ -399,43 +484,14 @@ export class BeltPath extends BasicSerializableObject {
// Check acceptor
const acceptor = this.computeAcceptingEntityAndSlot(true);
if (!!acceptor !== !!this.acceptorTarget) {
return fail("Acceptor target mismatch, acceptor", !!acceptor, "vs stored", !!this.acceptorTarget);
}
if (acceptor) {
if (this.acceptorTarget.entity !== acceptor.entity) {
return fail(
"Mismatching entity on acceptor target:",
acceptor.entity.uid,
"vs",
this.acceptorTarget.entity.uid
);
}
if (this.acceptorTarget.slot !== acceptor.slot) {
return fail(
"Mismatching entity on acceptor target:",
acceptor.slot,
"vs stored",
this.acceptorTarget.slot
);
}
if (this.acceptorTarget.direction !== acceptor.direction) {
return fail(
"Mismatching direction on acceptor target:",
acceptor.direction,
"vs stored",
this.acceptorTarget.direction
);
}
if (!!acceptor !== !!this.boundAcceptor) {
return fail("Acceptor target mismatch, acceptor", !!acceptor, "vs stored", !!this.boundAcceptor);
}
// Check first nonzero offset
let firstNonzero = 0;
for (let i = this.items.length - 2; i >= 0; --i) {
if (this.items[i][_nextDistance] < globalConfig.itemSpacingOnBelts + 1e-5) {
if (this.items[i][0 /* nextDistance */] < globalConfig.itemSpacingOnBelts + 1e-5) {
++firstNonzero;
} else {
break;
@ -483,11 +539,11 @@ export class BeltPath extends BasicSerializableObject {
DEBUG &&
logger.log(
" Extended spacing of last item from",
lastItem[_nextDistance],
lastItem[0 /* nextDistance */],
"to",
lastItem[_nextDistance] + additionalLength
lastItem[0 /* nextDistance */] + additionalLength
);
lastItem[_nextDistance] += additionalLength;
lastItem[0 /* nextDistance */] += additionalLength;
}
// Assign reference
@ -618,7 +674,7 @@ export class BeltPath extends BasicSerializableObject {
DEBUG &&
logger.log(
"Old items are",
this.items.map(i => i[_nextDistance])
this.items.map(i => i[0 /* nextDistance */])
);
// Create second path
@ -628,7 +684,7 @@ export class BeltPath extends BasicSerializableObject {
let itemPos = this.spacingToFirstItem;
for (let i = 0; i < this.items.length; ++i) {
const item = this.items[i];
const distanceToNext = item[_nextDistance];
const distanceToNext = item[0 /* nextDistance */];
DEBUG && logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next");
@ -643,7 +699,7 @@ export class BeltPath extends BasicSerializableObject {
// Check if its on the second path (otherwise its on the removed belt and simply lost)
if (itemPos >= secondPathStart) {
// Put item on second path
secondPath.items.push([distanceToNext, item[_item]]);
secondPath.items.push([distanceToNext, item[1 /* item */]]);
DEBUG &&
logger.log(
" Put item to second path @",
@ -672,7 +728,7 @@ export class BeltPath extends BasicSerializableObject {
"to",
clampedDistanceToNext
);
item[_nextDistance] = clampedDistanceToNext;
item[0 /* nextDistance */] = clampedDistanceToNext;
}
}
@ -683,13 +739,13 @@ export class BeltPath extends BasicSerializableObject {
DEBUG &&
logger.log(
"New items are",
this.items.map(i => i[_nextDistance])
this.items.map(i => i[0 /* nextDistance */])
);
DEBUG &&
logger.log(
"And second path items are",
secondPath.items.map(i => i[_nextDistance])
secondPath.items.map(i => i[0 /* nextDistance */])
);
// Adjust our total length
@ -776,9 +832,17 @@ export class BeltPath extends BasicSerializableObject {
continue;
}
DEBUG && logger.log("Item", i, "is at", itemOffset, "with next offset", item[_nextDistance]);
DEBUG &&
logger.log(
"Item",
i,
"is at",
itemOffset,
"with next offset",
item[0 /* nextDistance */]
);
lastItemOffset = itemOffset;
itemOffset += item[_nextDistance];
itemOffset += item[0 /* nextDistance */];
}
// If we still have an item, make sure the last item matches
@ -805,7 +869,7 @@ export class BeltPath extends BasicSerializableObject {
this.totalLength,
")"
);
this.items[this.items.length - 1][_nextDistance] = lastDistance;
this.items[this.items.length - 1][0 /* nextDistance */] = lastDistance;
} else {
DEBUG && logger.log(" Removed all items so we'll update spacing to total length");
@ -893,7 +957,7 @@ export class BeltPath extends BasicSerializableObject {
DEBUG &&
logger.log(
" Items:",
this.items.map(i => i[_nextDistance])
this.items.map(i => i[0 /* nextDistance */])
);
// Find offset to first item
@ -912,7 +976,7 @@ export class BeltPath extends BasicSerializableObject {
// This item must be dropped
this.items.splice(i, 1);
i -= 1;
itemOffset += item[_nextDistance];
itemOffset += item[0 /* nextDistance */];
continue;
} else {
// This item can be kept, thus its the first we know
@ -990,9 +1054,13 @@ export class BeltPath extends BasicSerializableObject {
// Now, update the distance of our last item
if (this.items.length !== 0) {
const lastItem = this.items[this.items.length - 1];
lastItem[_nextDistance] += otherPath.spacingToFirstItem;
lastItem[0 /* nextDistance */] += otherPath.spacingToFirstItem;
DEBUG &&
logger.log(" Add distance to last item, effectively being", lastItem[_nextDistance], "now");
logger.log(
" Add distance to last item, effectively being",
lastItem[0 /* nextDistance */],
"now"
);
} else {
// Seems we have no items, update our first item distance
this.spacingToFirstItem = oldLength + otherPath.spacingToFirstItem;
@ -1012,7 +1080,7 @@ export class BeltPath extends BasicSerializableObject {
// Aaand push the other paths items
for (let i = 0; i < otherPath.items.length; ++i) {
const item = otherPath.items[i];
this.items.push([item[_nextDistance], item[_item]]);
this.items.push([item[0 /* nextDistance */], item[1 /* item */]]);
}
// Update bounds
@ -1046,6 +1114,11 @@ export class BeltPath extends BasicSerializableObject {
this.debug_checkIntegrity("pre-update");
}
// Skip empty belts
if (this.items.length === 0) {
return;
}
// Divide by item spacing on belts since we use throughput and not speed
let beltSpeed =
this.root.hubGoals.getBeltBaseSpeed() *
@ -1074,30 +1147,40 @@ export class BeltPath extends BasicSerializableObject {
lastItemProcessed === this.items.length - 1 ? 0 : globalConfig.itemSpacingOnBelts;
// Compute how much we can advance
const clampedProgress = Math.max(
0,
Math.min(remainingVelocity, nextDistanceAndItem[_nextDistance] - minimumSpacing)
);
let clampedProgress = nextDistanceAndItem[0 /* nextDistance */] - minimumSpacing;
// Make sure we don't advance more than the remaining velocity has stored
if (remainingVelocity < clampedProgress) {
clampedProgress = remainingVelocity;
}
// Make sure we don't advance back
if (clampedProgress < 0) {
clampedProgress = 0;
}
// Reduce our velocity by the amount we consumed
remainingVelocity -= clampedProgress;
// Reduce the spacing
nextDistanceAndItem[_nextDistance] -= clampedProgress;
nextDistanceAndItem[0 /* nextDistance */] -= clampedProgress;
// Advance all items behind by the progress we made
this.spacingToFirstItem += clampedProgress;
// If the last item can be ejected, eject it and reduce the spacing, because otherwise
// we lose velocity
if (isFirstItemProcessed && nextDistanceAndItem[_nextDistance] < 1e-7) {
if (isFirstItemProcessed && nextDistanceAndItem[0 /* nextDistance */] < 1e-7) {
// Store how much velocity we "lost" because we bumped the item to the end of the
// belt but couldn't move it any farther. We need this to tell the item acceptor
// animation to start a tad later, so everything matches up. Yes I'm a perfectionist.
const excessVelocity = beltSpeed - clampedProgress;
// Try to directly get rid of the item
if (this.tryHandOverItem(nextDistanceAndItem[_item], excessVelocity)) {
if (
this.boundAcceptor &&
this.boundAcceptor(nextDistanceAndItem[1 /* item */], excessVelocity)
) {
this.items.pop();
const itemBehind = this.items[lastItemProcessed - 1];
@ -1108,11 +1191,11 @@ export class BeltPath extends BasicSerializableObject {
// Also see #999
const fixupProgress = Math.max(
0,
Math.min(remainingVelocity, itemBehind[_nextDistance])
Math.min(remainingVelocity, itemBehind[0 /* nextDistance */])
);
// See above
itemBehind[_nextDistance] -= fixupProgress;
itemBehind[0 /* nextDistance */] -= fixupProgress;
remainingVelocity -= fixupProgress;
this.spacingToFirstItem += fixupProgress;
}
@ -1145,8 +1228,8 @@ export class BeltPath extends BasicSerializableObject {
// Check if we have an item which is ready to be emitted
const lastItem = this.items[this.items.length - 1];
if (lastItem && lastItem[_nextDistance] === 0 && this.acceptorTarget) {
if (this.tryHandOverItem(lastItem[_item])) {
if (lastItem && lastItem[0 /* nextDistance */] === 0) {
if (this.boundAcceptor && this.boundAcceptor(lastItem[1 /* item */])) {
this.items.pop();
this.numCompressedItemsAfterFirstItem = Math.max(
0,
@ -1160,50 +1243,6 @@ export class BeltPath extends BasicSerializableObject {
}
}
/**
* Tries to hand over the item to the end entity
* @param {BaseItem} item
*/
tryHandOverItem(item, remainingProgress = 0.0) {
if (!this.acceptorTarget) {
return;
}
const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor;
// Check if the acceptor has a filter for example
if (targetAcceptorComp && !targetAcceptorComp.canAcceptItem(this.acceptorTarget.slot, item)) {
// Well, this item is not accepted
return false;
}
// Try to pass over
if (
this.root.systemMgr.systems.itemEjector.tryPassOverItem(
item,
this.acceptorTarget.entity,
this.acceptorTarget.slot
)
) {
// Trigger animation on the acceptor comp
const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor;
if (targetAcceptorComp) {
if (!this.root.app.settings.getAllSettings().simplifiedBelts) {
targetAcceptorComp.onItemAccepted(
this.acceptorTarget.slot,
this.acceptorTarget.direction,
item,
remainingProgress
);
}
}
return true;
}
return false;
}
/**
* Computes a world space position from the given progress
* @param {number} progress
@ -1270,11 +1309,11 @@ export class BeltPath extends BasicSerializableObject {
parameters.context.font = "6px GameFont";
parameters.context.fillStyle = "#111";
parameters.context.fillText(
"" + round4Digits(nextDistanceAndItem[_nextDistance]),
"" + round4Digits(nextDistanceAndItem[0 /* nextDistance */]),
worldPos.x + 5,
worldPos.y + 2
);
progress += nextDistanceAndItem[_nextDistance];
progress += nextDistanceAndItem[0 /* nextDistance */];
if (this.items.length - 1 - this.numCompressedItemsAfterFirstItem === i) {
parameters.context.fillStyle = "red";
@ -1370,7 +1409,7 @@ export class BeltPath extends BasicSerializableObject {
const centerPos = staticComp.localTileToWorld(centerPosLocal).toWorldSpaceCenterOfTile();
parameters.context.globalAlpha = 0.5;
firstItem[_item].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters);
firstItem[1 /* item */].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters);
parameters.context.globalAlpha = 1;
}
@ -1402,7 +1441,7 @@ export class BeltPath extends BasicSerializableObject {
const distanceAndItem = this.items[currentItemIndex];
distanceAndItem[_item].drawItemCenteredClipped(
distanceAndItem[1 /* item */].drawItemCenteredClipped(
worldPos.x,
worldPos.y,
parameters,
@ -1410,7 +1449,7 @@ export class BeltPath extends BasicSerializableObject {
);
// Check for the next item
currentItemPos += distanceAndItem[_nextDistance];
currentItemPos += distanceAndItem[0 /* nextDistance */];
++currentItemIndex;
if (currentItemIndex >= this.items.length) {

View File

@ -71,6 +71,8 @@ export class ItemAcceptorComponent extends Component {
/**
* Returns if this acceptor can accept a new item at slot N
*
* NOTICE: The belt path ignores this for performance reasons and does his own check
* @param {number} slotIndex
* @param {BaseItem=} item
*/

View File

@ -239,6 +239,14 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
return false;
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
//
// NOTICE ! THIS CODE IS DUPLICATED IN THE BELT PATH FOR PERFORMANCE REASONS
//
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
const itemProcessorComp = receiver.components.ItemProcessor;
if (itemProcessorComp) {
// Check for potential filters

View File

@ -224,13 +224,16 @@ export class UndergroundBeltSystem extends GameSystemWithFilter {
update() {
this.staleAreaWatcher.update();
const sender = enumUndergroundBeltMode.sender;
const now = this.root.time.now();
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const undergroundComp = entity.components.UndergroundBelt;
if (undergroundComp.mode === enumUndergroundBeltMode.sender) {
if (undergroundComp.mode === sender) {
this.handleSender(entity);
} else {
this.handleReceiver(entity);
this.handleReceiver(entity, now);
}
}
}
@ -327,14 +330,15 @@ export class UndergroundBeltSystem extends GameSystemWithFilter {
/**
*
* @param {Entity} entity
* @param {number} now
*/
handleReceiver(entity) {
handleReceiver(entity, now) {
const undergroundComp = entity.components.UndergroundBelt;
// Try to eject items, we only check the first one because it is sorted by remaining time
const nextItemAndDuration = undergroundComp.pendingItems[0];
if (nextItemAndDuration) {
if (this.root.time.now() > nextItemAndDuration[1]) {
if (now > nextItemAndDuration[1]) {
const ejectorComp = entity.components.ItemEjector;
const nextSlotIndex = ejectorComp.getFirstFreeSlot();

1208
translations/base-he.yml Normal file

File diff suppressed because it is too large Load Diff

14957
yarn.lock

File diff suppressed because it is too large Load Diff