1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2026-03-02 03:39:21 +00:00

Properly process belt dependencies and fix items not travelling linear on belts

This commit is contained in:
tobspr
2020-05-18 16:08:33 +02:00
parent ca0e17f3dd
commit 260ba892c8
7 changed files with 199 additions and 847 deletions

View File

@@ -60,7 +60,7 @@ export class BeltComponent extends Component {
/**
* Returns if the belt can currently accept an item from the given direction
*/
canAcceptNewItem() {
canAcceptNewItem(leftoverProgress = 0.0) {
const firstItem = this.sortedItems[0];
if (!firstItem) {
return true;
@@ -73,8 +73,19 @@ export class BeltComponent extends Component {
* Pushes a new item to the belt
* @param {BaseItem} item
*/
takeNewItem(item) {
this.sortedItems.unshift([0, item]);
takeNewItem(item, leftoverProgress = 0.0) {
if (G_IS_DEV) {
assert(
this.sortedItems.length === 0 ||
leftoverProgress <= this.sortedItems[0][0] - globalConfig.itemSpacingOnBelts + 0.001,
"Invalid leftover: " +
leftoverProgress +
" items are " +
this.sortedItems.map(item => item[0])
);
assert(leftoverProgress < 1.0, "Invalid leftover: " + leftoverProgress);
}
this.sortedItems.unshift([leftoverProgress, item]);
}
/**

View File

@@ -14,15 +14,11 @@ export class DynamicTickrate {
constructor(root) {
this.root = root;
this.setTickRate(120);
this.currentTickStart = null;
this.capturedTicks = [];
this.averageTickDuration = 0;
// Exposed
this.deltaSeconds = 0;
this.deltaMs = 0;
this.setTickRate(60);
}
/**
@@ -40,14 +36,14 @@ export class DynamicTickrate {
* Increases the tick rate marginally
*/
increaseTickRate() {
this.setTickRate(Math_round(Math_min(globalConfig.maximumTickRate, this.currentTickRate * 1.1)));
this.setTickRate(Math_round(Math_min(globalConfig.maximumTickRate, this.currentTickRate * 1.2)));
}
/**
* Decreases the tick rate marginally
*/
decreaseTickRate() {
this.setTickRate(Math_round(Math_min(globalConfig.maximumTickRate, this.currentTickRate * 0.9)));
this.setTickRate(Math_round(Math_max(globalConfig.minimumTickRate, this.currentTickRate * 0.8)));
}
/**
@@ -57,7 +53,7 @@ export class DynamicTickrate {
assert(this.currentTickStart === null, "BeginTick called twice");
this.currentTickStart = performanceNow();
if (this.capturedTicks.length > this.currentTickRate * 4) {
if (this.capturedTicks.length > this.currentTickRate * 2) {
// Take only a portion of the ticks
this.capturedTicks.sort();
this.capturedTicks.splice(0, 10);

View File

@@ -12,9 +12,14 @@ import { gMetaBuildingRegistry } from "../../core/global_registries";
import { MetaBeltBaseBuilding } from "../buildings/belt_base";
import { defaultBuildingVariant } from "../meta_building";
import { GameRoot } from "../root";
import { createLogger } from "../../core/logging";
const BELT_ANIM_COUNT = 6;
const logger = createLogger("belt");
/** @typedef {Array<{ entity: Entity, followUp: Entity }>} BeltCache */
export class BeltSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [BeltComponent]);
@@ -26,7 +31,7 @@ export class BeltSystem extends GameSystemWithFilter {
[enumDirection.left]: Loader.getSprite("sprites/belt/left_0.png"),
[enumDirection.right]: Loader.getSprite("sprites/belt/right_0.png"),
};
/**
/**b
* @type {Object.<enumDirection, Array<AtlasSprite>>}
*/
this.beltAnimations = {
@@ -58,6 +63,11 @@ export class BeltSystem extends GameSystemWithFilter {
this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this);
this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this);
this.cacheNeedsUpdate = true;
/** @type {BeltCache} */
this.beltCache = [];
}
/**
@@ -74,6 +84,10 @@ export class BeltSystem extends GameSystemWithFilter {
return;
}
if (entity.components.Belt) {
this.cacheNeedsUpdate = true;
}
const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding);
// Compute affected area
@@ -98,6 +112,7 @@ export class BeltSystem extends GameSystemWithFilter {
);
targetStaticComp.rotation = rotation;
metaBelt.updateVariants(targetEntity, rotationVariant, defaultBuildingVariant);
this.cacheNeedsUpdate = true;
}
}
}
@@ -110,97 +125,155 @@ export class BeltSystem extends GameSystemWithFilter {
}
/**
* Updates a given entity
* Finds the follow up entity for a given belt. Used for building the dependencies
* @param {Entity} entity
* @param {Set} processedEntities
*/
updateBelt(entity, processedEntities) {
if (processedEntities.has(entity.uid)) {
return;
}
processedEntities.add(entity.uid);
// Divide by item spacing on belts since we use throughput and not speed
const beltSpeed =
this.root.hubGoals.getBeltBaseSpeed() *
this.root.dynamicTickrate.deltaSeconds *
globalConfig.itemSpacingOnBelts;
const beltComp = entity.components.Belt;
findFollowUpEntity(entity) {
const staticComp = entity.components.StaticMapEntity;
const items = beltComp.sortedItems;
const beltComp = entity.components.Belt;
if (items.length === 0) {
// Fast out for performance
const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction);
const followUpVector = enumDirectionToVector[followUpDirection];
const followUpTile = staticComp.origin.add(followUpVector);
const followUpEntity = this.root.map.getTileContent(followUpTile);
if (followUpEntity) {
const followUpBeltComp = followUpEntity.components.Belt;
if (followUpBeltComp) {
return followUpEntity;
}
}
return null;
}
/**
* Adds a single entity to the cache
* @param {Entity} entity
* @param {BeltCache} cache
* @param {Set} visited
*/
computeSingleBeltCache(entity, cache, visited) {
// Check for double visit
if (visited.has(entity.uid)) {
return;
}
visited.add(entity.uid);
const ejectorComp = entity.components.ItemEjector;
let maxProgress = 1;
// When ejecting, we can not go further than the item spacing since it
// will be on the corner
if (ejectorComp.isAnySlotEjecting()) {
maxProgress = 1 - globalConfig.itemSpacingOnBelts;
} else {
// Find follow up belt to make sure we don't clash items
const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction);
const followUpVector = enumDirectionToVector[followUpDirection];
const followUpTile = staticComp.origin.add(followUpVector);
const followUpEntity = this.root.map.getTileContent(followUpTile);
if (followUpEntity) {
const followUpBeltComp = followUpEntity.components.Belt;
if (followUpBeltComp) {
// Update follow up belt first
this.updateBelt(followUpEntity, processedEntities);
const spacingOnBelt = followUpBeltComp.getDistanceToFirstItemCenter();
maxProgress = Math_min(1, 1 - globalConfig.itemSpacingOnBelts + spacingOnBelt);
}
}
const followUp = this.findFollowUpEntity(entity);
if (followUp) {
// Process followup first
this.computeSingleBeltCache(followUp, cache, visited);
}
let speedMultiplier = 1;
if (beltComp.direction !== enumDirection.top) {
// Shaped belts are longer, thus being quicker
speedMultiplier = 1.41;
cache.push({ entity, followUp });
}
computeBeltCache() {
logger.log("Updating belt cache");
let cache = [];
let visited = new Set();
for (let i = 0; i < this.allEntities.length; ++i) {
this.computeSingleBeltCache(this.allEntities[i], cache, visited);
}
assert(
cache.length === this.allEntities.length,
"Belt cache mismatch: Has " + cache.length + " entries but should have " + this.allEntities.length
);
for (let itemIndex = items.length - 1; itemIndex >= 0; --itemIndex) {
const itemAndProgress = items[itemIndex];
const newProgress = itemAndProgress[0] + speedMultiplier * beltSpeed;
if (newProgress >= 1.0) {
// Try to give this item to a new belt
const freeSlot = ejectorComp.getFirstFreeSlot();
if (freeSlot === null) {
// So, we don't have a free slot - damned!
itemAndProgress[0] = 1.0;
maxProgress = 1 - globalConfig.itemSpacingOnBelts;
} else {
// We got a free slot, remove this item and keep it on the ejector slot
if (!ejectorComp.tryEject(freeSlot, itemAndProgress[1])) {
assert(false, "Ejection failed");
}
items.splice(itemIndex, 1);
maxProgress = 1;
}
} else {
itemAndProgress[0] = Math_min(newProgress, maxProgress);
maxProgress = itemAndProgress[0] - globalConfig.itemSpacingOnBelts;
}
}
this.beltCache = cache;
}
update() {
const processedEntities = new Set();
if (this.cacheNeedsUpdate) {
this.cacheNeedsUpdate = false;
this.computeBeltCache();
}
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
this.updateBelt(entity, processedEntities);
for (let i = 0; i < this.beltCache.length; ++i) {
const { entity, followUp } = this.beltCache[i];
// Divide by item spacing on belts since we use throughput and not speed
const beltSpeed =
this.root.hubGoals.getBeltBaseSpeed() *
this.root.dynamicTickrate.deltaSeconds *
globalConfig.itemSpacingOnBelts;
const beltComp = entity.components.Belt;
const items = beltComp.sortedItems;
if (items.length === 0) {
// Fast out for performance
continue;
}
const ejectorComp = entity.components.ItemEjector;
let maxProgress = 1;
// When ejecting, we can not go further than the item spacing since it
// will be on the corner
if (ejectorComp.isAnySlotEjecting()) {
maxProgress = 1 - globalConfig.itemSpacingOnBelts;
} else {
// Otherwise our progress depends on the follow up
if (followUp) {
const spacingOnBelt = followUp.components.Belt.getDistanceToFirstItemCenter();
maxProgress = Math_min(2, 1 - globalConfig.itemSpacingOnBelts + spacingOnBelt);
}
}
let speedMultiplier = 1;
if (beltComp.direction !== enumDirection.top) {
// Shaped belts are longer, thus being quicker
speedMultiplier = 1.41;
}
// Not really nice. haven't found the reason for this yet.
if (items.length > 2 / globalConfig.itemSpacingOnBelts) {
logger.error("Fixing broken belt:", entity, items);
beltComp.sortedItems = [];
}
for (let itemIndex = items.length - 1; itemIndex >= 0; --itemIndex) {
const progressAndItem = items[itemIndex];
progressAndItem[0] = Math_min(maxProgress, progressAndItem[0] + speedMultiplier * beltSpeed);
if (progressAndItem[0] >= 1.0) {
if (followUp) {
const followUpBelt = followUp.components.Belt;
if (followUpBelt.canAcceptNewItem()) {
followUpBelt.takeNewItem(progressAndItem[1], progressAndItem[0] - 1.0);
items.splice(itemIndex, 1);
} else {
// Well, we couldn't really take it to a follow up belt, keep it at
// max progress
progressAndItem[0] = 1.0;
maxProgress = 1 - globalConfig.itemSpacingOnBelts;
}
} else {
// Try to give this item to a new belt
const freeSlot = ejectorComp.getFirstFreeSlot();
if (freeSlot === null) {
// So, we don't have a free slot - damned!
progressAndItem[0] = 1.0;
maxProgress = 1 - globalConfig.itemSpacingOnBelts;
} else {
// We got a free slot, remove this item and keep it on the ejector slot
if (!ejectorComp.tryEject(freeSlot, progressAndItem[1])) {
assert(false, "Ejection failed");
}
items.splice(itemIndex, 1);
// Do not override max progress at all
// maxProgress = 1;
}
}
} else {
// We just moved this item forward, so determine the maximum progress of other items
maxProgress = progressAndItem[0] - globalConfig.itemSpacingOnBelts;
}
}
}
}
@@ -212,7 +285,6 @@ export class BeltSystem extends GameSystemWithFilter {
drawChunk(parameters, chunk) {
if (parameters.zoomLevel < globalConfig.mapChunkOverviewMinZoom) {
return;
1;
}
const speedMultiplier = this.root.hubGoals.getBeltBaseSpeed();

View File

@@ -112,10 +112,14 @@ export class GameTime extends BasicSerializableObject {
}
// Check for too big pile of updates -> reduce it to 1
const maxLogicSteps = Math_max(
let maxLogicSteps = Math_max(
3,
(this.speed.getMaxLogicStepsInQueue() * this.root.dynamicTickrate.currentTickRate) / 60
);
if (G_IS_DEV && globalConfig.debug.framePausesBetweenTicks) {
maxLogicSteps *= 1 + globalConfig.debug.framePausesBetweenTicks;
}
if (this.logicTimeBudget > this.root.dynamicTickrate.deltaMs * maxLogicSteps) {
// logger.warn("Skipping logic time steps since more than", maxLogicSteps, "are in queue");
this.logicTimeBudget = this.root.dynamicTickrate.deltaMs * maxLogicSteps;
@@ -132,9 +136,14 @@ export class GameTime extends BasicSerializableObject {
const speedAtStart = this.root.time.getSpeed();
let effectiveDelta = this.root.dynamicTickrate.deltaMs;
if (G_IS_DEV && globalConfig.debug.framePausesBetweenTicks) {
effectiveDelta += globalConfig.debug.framePausesBetweenTicks * this.root.dynamicTickrate.deltaMs;
}
// Update physics & logic
while (this.logicTimeBudget >= this.root.dynamicTickrate.deltaMs) {
this.logicTimeBudget -= this.root.dynamicTickrate.deltaMs;
while (this.logicTimeBudget >= effectiveDelta) {
this.logicTimeBudget -= effectiveDelta;
if (!updateMethod()) {
// Gameover happened or so, do not update anymore

View File

@@ -16,19 +16,19 @@ export const UPGRADES = {
},
{
required: [{ shape: "CpCpCpCp", amount: 15000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 40000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 40000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: finalGameShape, amount: 150000 }],
improvement: 4,
improvement: 5,
excludePrevious: true,
},
],
@@ -46,19 +46,19 @@ export const UPGRADES = {
},
{
required: [{ shape: "ScScScSc", amount: 20000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 40000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 40000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: finalGameShape, amount: 150000 }],
improvement: 4,
improvement: 5,
excludePrevious: true,
},
],
@@ -76,19 +76,19 @@ export const UPGRADES = {
},
{
required: [{ shape: "CgScScCg", amount: 25000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 40000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 40000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: finalGameShape, amount: 150000 }],
improvement: 4,
improvement: 5,
excludePrevious: true,
},
],
@@ -106,19 +106,19 @@ export const UPGRADES = {
},
{
required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 30000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 40000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 40000 }],
improvement: 4,
improvement: 2,
},
{
required: [{ shape: finalGameShape, amount: 150000 }],
improvement: 4,
improvement: 5,
excludePrevious: true,
},
],