mirror of
https://github.com/tobspr/shapez.io.git
synced 2024-10-27 20:34:29 +00:00
Support dynamic tick rates
This commit is contained in:
parent
65529cce1a
commit
ca0e17f3dd
10
src/css/ingame_hud/debug_info.scss
Normal file
10
src/css/ingame_hud/debug_info.scss
Normal file
@ -0,0 +1,10 @@
|
||||
#ingame_HUD_DebugInfo {
|
||||
position: absolute;
|
||||
@include S(bottom, 5px);
|
||||
@include S(left, 5px);
|
||||
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
line-height: 15px;
|
||||
flex-direction: column;
|
||||
}
|
@ -39,6 +39,7 @@
|
||||
@import "ingame_hud/pinned_shapes";
|
||||
@import "ingame_hud/notifications";
|
||||
@import "ingame_hud/settings_menu";
|
||||
@import "ingame_hud/debug_info";
|
||||
|
||||
// prettier-ignore
|
||||
$elements:
|
||||
@ -57,6 +58,7 @@ ingame_HUD_GameMenu,
|
||||
ingame_HUD_KeybindingOverlay,
|
||||
ingame_HUD_Notifications,
|
||||
ingame_HUD_MassSelector,
|
||||
ingame_HUD_DebugInfo,
|
||||
|
||||
// Overlays
|
||||
ingame_HUD_BetaOverlay,
|
||||
|
@ -23,13 +23,8 @@ export const globalConfig = {
|
||||
statisticsGraphSlices: 100,
|
||||
analyticsSliceDurationSeconds: 10,
|
||||
|
||||
// [Calculated] physics step size
|
||||
physicsDeltaMs: 0,
|
||||
physicsDeltaSeconds: 0,
|
||||
|
||||
// Update physics at N fps, independent of rendering
|
||||
// physicsUpdateRate: 55,
|
||||
physicsUpdateRate: 120,
|
||||
minimumTickRate: 30,
|
||||
maximumTickRate: 500,
|
||||
|
||||
// Map
|
||||
mapChunkSize: 32,
|
||||
@ -76,7 +71,7 @@ export const globalConfig = {
|
||||
|
||||
debug: {
|
||||
/* dev:start */
|
||||
fastGameEnter: true,
|
||||
// fastGameEnter: true,
|
||||
noArtificialDelays: true,
|
||||
// disableSavegameWrite: true,
|
||||
showEntityBounds: false,
|
||||
@ -111,7 +106,4 @@ export const IS_MOBILE = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
|
||||
// Automatic calculations
|
||||
|
||||
globalConfig.physicsDeltaMs = 1000.0 / globalConfig.physicsUpdateRate;
|
||||
globalConfig.physicsDeltaSeconds = 1.0 / globalConfig.physicsUpdateRate;
|
||||
|
||||
globalConfig.minerSpeedItemsPerSecond = globalConfig.beltSpeedItemsPerSecond / 5;
|
||||
|
@ -62,7 +62,7 @@ export class UndergroundBeltComponent extends Component {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.pendingItems.push([item, 1 / beltSpeed / globalConfig.itemSpacingOnBelts]);
|
||||
this.pendingItems.push([item, 0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -88,7 +88,8 @@ export class UndergroundBeltComponent extends Component {
|
||||
// NOTICE:
|
||||
// This corresponds to the item ejector - it needs 0.5 additional tiles to eject the item.
|
||||
// So instead of adding 1 we add 0.5 only.
|
||||
const travelDuration = (travelDistance + 0.5) / beltSpeed / globalConfig.itemSpacingOnBelts;
|
||||
// Additionally it takes 1 tile for the acceptor which we just add on top.
|
||||
const travelDuration = (travelDistance + 1.5) / beltSpeed / globalConfig.itemSpacingOnBelts;
|
||||
|
||||
this.pendingItems.push([item, travelDuration]);
|
||||
|
||||
|
@ -32,6 +32,7 @@ import { GameTime } from "./time/game_time";
|
||||
import { ProductionAnalytics } from "./production_analytics";
|
||||
import { randomInt } from "../core/utils";
|
||||
import { defaultBuildingVariant } from "./meta_building";
|
||||
import { DynamicTickrate } from "./dynamic_tickrate";
|
||||
|
||||
const logger = createLogger("ingame/core");
|
||||
|
||||
@ -53,16 +54,6 @@ export class GameCore {
|
||||
/** @type {GameRoot} */
|
||||
this.root = null;
|
||||
|
||||
/**
|
||||
* Time budget (seconds) for logic updates
|
||||
*/
|
||||
this.logicTimeBudget = 0;
|
||||
|
||||
/**
|
||||
* Time budget (seconds) for user interface updates
|
||||
*/
|
||||
this.uiTimeBudget = 0;
|
||||
|
||||
/**
|
||||
* Set to true at the beginning of a logic update and cleared when its finished.
|
||||
* This is to prevent doing a recursive logic update which can lead to unexpected
|
||||
@ -97,6 +88,9 @@ export class GameCore {
|
||||
// This isn't nice, but we need it right here
|
||||
root.gameState.keyActionMapper = new KeyActionMapper(root, this.root.gameState.inputReciever);
|
||||
|
||||
// Needs to come first
|
||||
root.dynamicTickrate = new DynamicTickrate(root);
|
||||
|
||||
// Init classes
|
||||
root.camera = new Camera(root);
|
||||
root.map = new MapView(root);
|
||||
@ -250,17 +244,6 @@ export class GameCore {
|
||||
// Perform logic ticks
|
||||
this.root.time.performTicks(deltaMs, this.boundInternalTick);
|
||||
|
||||
// Update UI particles
|
||||
this.uiTimeBudget += deltaMs;
|
||||
const maxUiSteps = 3;
|
||||
if (this.uiTimeBudget > globalConfig.physicsDeltaMs * maxUiSteps) {
|
||||
this.uiTimeBudget = globalConfig.physicsDeltaMs;
|
||||
}
|
||||
while (this.uiTimeBudget >= globalConfig.physicsDeltaMs) {
|
||||
this.uiTimeBudget -= globalConfig.physicsDeltaMs;
|
||||
// root.uiParticleMgr.update();
|
||||
}
|
||||
|
||||
// Update analytics
|
||||
root.productionAnalytics.update();
|
||||
|
||||
@ -288,6 +271,9 @@ export class GameCore {
|
||||
|
||||
updateLogic() {
|
||||
const root = this.root;
|
||||
|
||||
root.dynamicTickrate.beginTick();
|
||||
|
||||
this.duringLogicUpdate = true;
|
||||
|
||||
// Update entities, this removes destroyed entities
|
||||
@ -296,6 +282,8 @@ export class GameCore {
|
||||
// IMPORTANT: At this point, the game might be game over. Stop if this is the case
|
||||
if (!this.root) {
|
||||
logger.log("Root destructed, returning false");
|
||||
root.dynamicTickrate.endTick();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -303,7 +291,7 @@ export class GameCore {
|
||||
// root.particleMgr.update();
|
||||
|
||||
this.duringLogicUpdate = false;
|
||||
|
||||
root.dynamicTickrate.endTick();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
104
src/js/game/dynamic_tickrate.js
Normal file
104
src/js/game/dynamic_tickrate.js
Normal file
@ -0,0 +1,104 @@
|
||||
import { GameRoot } from "./root";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { globalConfig } from "../core/config";
|
||||
import { performanceNow, Math_min, Math_round, Math_max } from "../core/builtins";
|
||||
import { round3Digits } from "../core/utils";
|
||||
|
||||
const logger = createLogger("dynamic_tickrate");
|
||||
|
||||
export class DynamicTickrate {
|
||||
/**
|
||||
*
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
|
||||
this.setTickRate(120);
|
||||
|
||||
this.currentTickStart = null;
|
||||
this.capturedTicks = [];
|
||||
this.averageTickDuration = 0;
|
||||
|
||||
// Exposed
|
||||
this.deltaSeconds = 0;
|
||||
this.deltaMs = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tick rate to N updates per second
|
||||
* @param {number} rate
|
||||
*/
|
||||
setTickRate(rate) {
|
||||
logger.log("Applying tick-rate of", rate);
|
||||
this.currentTickRate = rate;
|
||||
this.deltaMs = 1000.0 / this.currentTickRate;
|
||||
this.deltaSeconds = 1.0 / this.currentTickRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the tick rate marginally
|
||||
*/
|
||||
increaseTickRate() {
|
||||
this.setTickRate(Math_round(Math_min(globalConfig.maximumTickRate, this.currentTickRate * 1.1)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decreases the tick rate marginally
|
||||
*/
|
||||
decreaseTickRate() {
|
||||
this.setTickRate(Math_round(Math_min(globalConfig.maximumTickRate, this.currentTickRate * 0.9)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Call whenever a tick began
|
||||
*/
|
||||
beginTick() {
|
||||
assert(this.currentTickStart === null, "BeginTick called twice");
|
||||
this.currentTickStart = performanceNow();
|
||||
|
||||
if (this.capturedTicks.length > this.currentTickRate * 4) {
|
||||
// Take only a portion of the ticks
|
||||
this.capturedTicks.sort();
|
||||
this.capturedTicks.splice(0, 10);
|
||||
this.capturedTicks.splice(this.capturedTicks.length - 11, 10);
|
||||
|
||||
let average = 0;
|
||||
for (let i = 0; i < this.capturedTicks.length; ++i) {
|
||||
average += this.capturedTicks[i];
|
||||
}
|
||||
average /= this.capturedTicks.length;
|
||||
|
||||
// Calculate tick duration to cover X% of the frame
|
||||
const ticksPerFrame = this.currentTickRate / 60;
|
||||
const maxFrameDurationMs = 8;
|
||||
const maxTickDuration = maxFrameDurationMs / ticksPerFrame;
|
||||
// const maxTickDuration = (1000 / this.currentTickRate) * 0.75;
|
||||
logger.log(
|
||||
"Average time per tick:",
|
||||
round3Digits(average) + "ms",
|
||||
"allowed are",
|
||||
maxTickDuration
|
||||
);
|
||||
this.averageTickDuration = average;
|
||||
|
||||
if (average < maxTickDuration) {
|
||||
this.increaseTickRate();
|
||||
} else {
|
||||
this.decreaseTickRate();
|
||||
}
|
||||
|
||||
this.capturedTicks = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call whenever a tick ended
|
||||
*/
|
||||
endTick() {
|
||||
assert(this.currentTickStart !== null, "EndTick called without BeginTick");
|
||||
const duration = performanceNow() - this.currentTickStart;
|
||||
this.capturedTicks.push(duration);
|
||||
this.currentTickStart = null;
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import { HUDPinnedShapes } from "./parts/pinned_shapes";
|
||||
import { ShapeDefinition } from "../shape_definition";
|
||||
import { HUDNotifications, enumNotificationType } from "./parts/notifications";
|
||||
import { HUDSettingsMenu } from "./parts/settings_menu";
|
||||
import { HUDDebugInfo } from "./parts/debug_info";
|
||||
|
||||
export class GameHUD {
|
||||
/**
|
||||
@ -57,6 +58,7 @@ export class GameHUD {
|
||||
settingsMenu: new HUDSettingsMenu(this.root),
|
||||
|
||||
// betaOverlay: new HUDBetaOverlay(this.root),
|
||||
debugInfo: new HUDDebugInfo(this.root),
|
||||
};
|
||||
|
||||
this.signals = {
|
||||
|
19
src/js/game/hud/parts/debug_info.js
Normal file
19
src/js/game/hud/parts/debug_info.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { makeDiv, round3Digits } from "../../../core/utils";
|
||||
|
||||
export class HUDDebugInfo extends BaseHUDPart {
|
||||
createElements(parent) {
|
||||
this.element = makeDiv(parent, "ingame_HUD_DebugInfo", []);
|
||||
|
||||
this.tickRateElement = makeDiv(this.element, null, ["tickRate"], "Ticks /s: 120");
|
||||
this.tickDurationElement = makeDiv(this.element, null, ["tickDuration"], "Update time: 0.5ms");
|
||||
}
|
||||
|
||||
initialize() {}
|
||||
|
||||
update() {
|
||||
this.tickRateElement.innerText = "Tickrate: " + this.root.dynamicTickrate.currentTickRate;
|
||||
this.tickDurationElement.innerText =
|
||||
"Avg. Dur: " + round3Digits(this.root.dynamicTickrate.averageTickDuration) + "ms";
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ import { ProductionAnalytics } from "./production_analytics";
|
||||
import { Entity } from "./entity";
|
||||
import { ShapeDefinition } from "./shape_definition";
|
||||
import { BaseItem } from "./base_item";
|
||||
import { DynamicTickrate } from "./dynamic_tickrate";
|
||||
/* typehints:end */
|
||||
|
||||
const logger = createLogger("game/root");
|
||||
@ -115,6 +116,9 @@ export class GameRoot {
|
||||
/** @type {ProductionAnalytics} */
|
||||
this.productionAnalytics = null;
|
||||
|
||||
/** @type {DynamicTickrate} */
|
||||
this.dynamicTickrate = null;
|
||||
|
||||
this.signals = {
|
||||
// Entities
|
||||
entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
|
||||
|
@ -109,21 +109,30 @@ export class BeltSystem extends GameSystemWithFilter {
|
||||
this.forEachMatchingEntityOnScreen(parameters, this.drawEntityItems.bind(this));
|
||||
}
|
||||
|
||||
update() {
|
||||
/**
|
||||
* Updates a given entity
|
||||
* @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() *
|
||||
globalConfig.physicsDeltaSeconds *
|
||||
this.root.dynamicTickrate.deltaSeconds *
|
||||
globalConfig.itemSpacingOnBelts;
|
||||
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
const beltComp = entity.components.Belt;
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const items = beltComp.sortedItems;
|
||||
|
||||
if (items.length === 0) {
|
||||
// Fast out for performance
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
const ejectorComp = entity.components.ItemEjector;
|
||||
@ -144,6 +153,9 @@ export class BeltSystem extends GameSystemWithFilter {
|
||||
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);
|
||||
}
|
||||
@ -182,6 +194,14 @@ export class BeltSystem extends GameSystemWithFilter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
const processedEntities = new Set();
|
||||
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
this.updateBelt(entity, processedEntities);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,7 +31,7 @@ export class ItemAcceptorSystem extends GameSystemWithFilter {
|
||||
for (let animIndex = 0; animIndex < aceptorComp.itemConsumptionAnimations.length; ++animIndex) {
|
||||
const anim = aceptorComp.itemConsumptionAnimations[animIndex];
|
||||
anim.animProgress +=
|
||||
globalConfig.physicsDeltaSeconds *
|
||||
this.root.dynamicTickrate.deltaSeconds *
|
||||
this.root.hubGoals.getBeltBaseSpeed() *
|
||||
2 *
|
||||
globalConfig.itemSpacingOnBelts;
|
||||
|
@ -14,7 +14,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
|
||||
update() {
|
||||
const effectiveBeltSpeed = this.root.hubGoals.getBeltBaseSpeed() * globalConfig.itemSpacingOnBelts;
|
||||
const progressGrowth = (effectiveBeltSpeed / 0.5) * globalConfig.physicsDeltaSeconds;
|
||||
const progressGrowth = (effectiveBeltSpeed / 0.5) * this.root.dynamicTickrate.deltaSeconds;
|
||||
|
||||
// Try to find acceptors for every ejector
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
|
@ -23,7 +23,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
// First of all, process the current recipe
|
||||
processorComp.secondsUntilEject = Math_max(
|
||||
0,
|
||||
processorComp.secondsUntilEject - globalConfig.physicsDeltaSeconds
|
||||
processorComp.secondsUntilEject - this.root.dynamicTickrate.deltaSeconds
|
||||
);
|
||||
|
||||
// Check if we have any finished items we can eject
|
||||
|
@ -31,7 +31,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter {
|
||||
// Decrease remaining time of all items in belt
|
||||
for (let k = 0; k < undergroundComp.pendingItems.length; ++k) {
|
||||
const item = undergroundComp.pendingItems[k];
|
||||
item[1] = Math_max(0, item[1] - globalConfig.physicsDeltaSeconds);
|
||||
item[1] = Math_max(0, item[1] - this.root.dynamicTickrate.deltaSeconds);
|
||||
}
|
||||
|
||||
if (undergroundComp.mode === enumUndergroundBeltMode.sender) {
|
||||
|
@ -6,7 +6,7 @@ import { types, BasicSerializableObject } from "../../savegame/serialization";
|
||||
import { RegularGameSpeed } from "./regular_game_speed";
|
||||
import { BaseGameSpeed } from "./base_game_speed";
|
||||
import { PausedGameSpeed } from "./paused_game_speed";
|
||||
import { performanceNow } from "../../core/builtins";
|
||||
import { performanceNow, Math_max } from "../../core/builtins";
|
||||
import { FastForwardGameSpeed } from "./fast_forward_game_speed";
|
||||
import { gGameSpeedRegistry } from "../../core/global_registries";
|
||||
import { globalConfig } from "../../core/config";
|
||||
@ -102,7 +102,7 @@ export class GameTime extends BasicSerializableObject {
|
||||
* Internal method to generate new logic time budget
|
||||
* @param {number} deltaMs
|
||||
*/
|
||||
înternalAddDeltaToBudget(deltaMs) {
|
||||
internalAddDeltaToBudget(deltaMs) {
|
||||
// Only update if game is supposed to update
|
||||
if (this.root.hud.shouldPauseGame()) {
|
||||
this.logicTimeBudget = 0;
|
||||
@ -112,9 +112,13 @@ export class GameTime extends BasicSerializableObject {
|
||||
}
|
||||
|
||||
// Check for too big pile of updates -> reduce it to 1
|
||||
const maxLogicSteps = this.speed.getMaxLogicStepsInQueue();
|
||||
if (this.logicTimeBudget > globalConfig.physicsDeltaMs * maxLogicSteps) {
|
||||
this.logicTimeBudget = globalConfig.physicsDeltaMs * maxLogicSteps;
|
||||
const maxLogicSteps = Math_max(
|
||||
3,
|
||||
(this.speed.getMaxLogicStepsInQueue() * this.root.dynamicTickrate.currentTickRate) / 60
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,13 +128,13 @@ export class GameTime extends BasicSerializableObject {
|
||||
* @param {function():boolean} updateMethod
|
||||
*/
|
||||
performTicks(deltaMs, updateMethod) {
|
||||
this.înternalAddDeltaToBudget(deltaMs);
|
||||
this.internalAddDeltaToBudget(deltaMs);
|
||||
|
||||
const speedAtStart = this.root.time.getSpeed();
|
||||
|
||||
// Update physics & logic
|
||||
while (this.logicTimeBudget >= globalConfig.physicsDeltaMs) {
|
||||
this.logicTimeBudget -= globalConfig.physicsDeltaMs;
|
||||
while (this.logicTimeBudget >= this.root.dynamicTickrate.deltaMs) {
|
||||
this.logicTimeBudget -= this.root.dynamicTickrate.deltaMs;
|
||||
|
||||
if (!updateMethod()) {
|
||||
// Gameover happened or so, do not update anymore
|
||||
@ -138,7 +142,7 @@ export class GameTime extends BasicSerializableObject {
|
||||
}
|
||||
|
||||
// Step game time
|
||||
this.timeSeconds = quantizeFloat(this.timeSeconds + globalConfig.physicsDeltaSeconds);
|
||||
this.timeSeconds = quantizeFloat(this.timeSeconds + this.root.dynamicTickrate.deltaSeconds);
|
||||
|
||||
// Game time speed changed, need to abort since our logic steps are no longer valid
|
||||
if (speedAtStart.getId() !== this.speed.getId()) {
|
||||
|
@ -179,8 +179,13 @@ export class MainMenuState extends GameState {
|
||||
this.trackClicks(qs(".mainContainer .importButton"), this.requestImportSavegame);
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
|
||||
const games = this.app.savegameMgr.getSavegamesMetaData();
|
||||
if (games.length > 0) {
|
||||
this.resumeGame(games[0]);
|
||||
} else {
|
||||
this.onPlayButtonClicked();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize video
|
||||
this.videoElement = this.htmlElement.querySelector("video");
|
||||
|
Loading…
Reference in New Issue
Block a user