From 2c48cb72aafbd8b32541d8fe18b612d61188a7b9 Mon Sep 17 00:00:00 2001 From: tobspr Date: Mon, 18 May 2020 17:40:20 +0200 Subject: [PATCH] Major performance improvements --- src/css/ingame_hud/debug_info.scss | 1 + src/css/main.scss | 2 +- src/js/core/config.js | 6 ++- src/js/core/draw_parameters.js | 5 ++ src/js/game/components/static_map_entity.js | 57 ++++++++++++++++++++- src/js/game/core.js | 11 +++- src/js/game/dynamic_tickrate.js | 44 ++++++++++------ src/js/game/hud/parts/debug_info.js | 24 +++++++-- src/js/game/map_chunk.js | 1 + src/js/game/map_chunk_view.js | 25 --------- src/js/game/systems/belt.js | 4 ++ src/js/game/systems/hub.js | 4 ++ src/js/game/systems/item_acceptor.js | 8 +++ src/js/game/systems/item_ejector.js | 4 ++ src/js/game/systems/map_resources.js | 27 +++++++--- src/js/game/systems/miner.js | 4 ++ src/js/profile/application_settings.js | 20 +++++++- translations/base-en.yaml | 7 ++- 18 files changed, 194 insertions(+), 60 deletions(-) diff --git a/src/css/ingame_hud/debug_info.scss b/src/css/ingame_hud/debug_info.scss index 2e8c2732..37e3a07c 100644 --- a/src/css/ingame_hud/debug_info.scss +++ b/src/css/ingame_hud/debug_info.scss @@ -7,4 +7,5 @@ display: flex; line-height: 15px; flex-direction: column; + color: #fff; } diff --git a/src/css/main.scss b/src/css/main.scss index e998ae05..2bb2ff4d 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -92,7 +92,7 @@ body.uiHidden { body.modalDialogActive, body.ingameDialogOpen { - > *:not(.ingameDialog):not(.modalDialogParent) { + > *:not(.ingameDialog):not(.modalDialogParent):not(.loadingDialog) { filter: blur(5px) !important; } } diff --git a/src/js/core/config.js b/src/js/core/config.js index 9bf7847a..aa8fe90c 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -71,7 +71,7 @@ export const globalConfig = { debug: { /* dev:start */ - fastGameEnter: true, + // fastGameEnter: true, noArtificialDelays: true, // disableSavegameWrite: true, showEntityBounds: false, @@ -80,11 +80,13 @@ export const globalConfig = { disableMusic: true, doNotRenderStatics: false, disableZoomLimits: false, - showChunkBorders: false, + // showChunkBorders: true, rewardsInstant: false, allBuildingsUnlocked: true, upgradesNoCost: true, disableUnlockDialog: true, + // disableLogicTicks: true, + // testClipping: true, // framePausesBetweenTicks: 40, // testTranslations: true, /* dev:end */ diff --git a/src/js/core/draw_parameters.js b/src/js/core/draw_parameters.js index 5e06c59f..dcdf6d13 100644 --- a/src/js/core/draw_parameters.js +++ b/src/js/core/draw_parameters.js @@ -1,4 +1,5 @@ import { Rectangle } from "./rectangle"; +import { globalConfig } from "./config"; /* typehints:start */ import { GameRoot } from "../game/root"; @@ -21,5 +22,9 @@ export class DrawParameters { // FIXME: Not really nice /** @type {GameRoot} */ this.root = root; + + if (G_IS_DEV && globalConfig.debug.testClipping) { + this.visibleRect = this.visibleRect.expandedInAllDirections(-100); + } } } diff --git a/src/js/game/components/static_map_entity.js b/src/js/game/components/static_map_entity.js index ff45b0f1..6f9abb87 100644 --- a/src/js/game/components/static_map_entity.js +++ b/src/js/game/components/static_map_entity.js @@ -145,6 +145,57 @@ export class StaticMapEntityComponent extends Component { return this.unapplyRotationToVector(localUnrotated); } + /** + * Returns whether the entity should be drawn for the given parameters + * @param {DrawParameters} parameters + */ + shouldBeDrawn(parameters) { + let x = 0; + let y = 0; + let w = 0; + let h = 0; + + switch (this.rotation) { + case 0: { + x = this.origin.x; + y = this.origin.y; + w = this.tileSize.x; + h = this.tileSize.y; + break; + } + case 90: { + x = this.origin.x - this.tileSize.y + 1; + y = this.origin.y; + w = this.tileSize.y; + h = this.tileSize.x; + break; + } + case 180: { + x = this.origin.x - this.tileSize.x + 1; + y = this.origin.y - this.tileSize.y + 1; + w = this.tileSize.x; + h = this.tileSize.y; + break; + } + case 270: { + x = this.origin.x; + y = this.origin.y - this.tileSize.x + 1; + w = this.tileSize.y; + h = this.tileSize.x; + break; + } + default: + assert(false, "Invalid rotation"); + } + + return parameters.visibleRect.containsRect4Params( + x * globalConfig.tileSize, + y * globalConfig.tileSize, + w * globalConfig.tileSize, + h * globalConfig.tileSize + ); + } + /** * Draws a sprite over the whole space of the entity * @param {DrawParameters} parameters @@ -156,6 +207,10 @@ export class StaticMapEntityComponent extends Component { const worldX = this.origin.x * globalConfig.tileSize; const worldY = this.origin.y * globalConfig.tileSize; + if (!this.shouldBeDrawn(parameters)) { + return; + } + if (this.rotation === 0) { // Early out, is faster sprite.drawCached( @@ -164,7 +219,7 @@ export class StaticMapEntityComponent extends Component { worldY - extrudePixels * this.tileSize.y, globalConfig.tileSize * this.tileSize.x + 2 * extrudePixels * this.tileSize.x, globalConfig.tileSize * this.tileSize.y + 2 * extrudePixels * this.tileSize.y, - clipping + false ); } else { const rotationCenterX = worldX + globalConfig.halfTileSize; diff --git a/src/js/game/core.js b/src/js/game/core.js index b9e50856..d800b602 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -274,6 +274,11 @@ export class GameCore { root.dynamicTickrate.beginTick(); + if (G_IS_DEV && globalConfig.debug.disableLogicTicks) { + root.dynamicTickrate.endTick(); + return true; + } + this.duringLogicUpdate = true; // Update entities, this removes destroyed entities @@ -327,6 +332,8 @@ export class GameCore { return; } + this.root.dynamicTickrate.onFrameRendered(); + if (!this.shouldRender()) { // Always update hud tho root.hud.update(); @@ -341,6 +348,9 @@ export class GameCore { // Gather context and save all state const context = root.context; context.save(); + if (G_IS_DEV && globalConfig.debug.testClipping) { + context.clearRect(0, 0, window.innerWidth * 3, window.innerHeight * 3); + } // Compute optimal zoom level and atlas scale const zoomLevel = root.camera.zoomLevel; @@ -383,7 +393,6 @@ export class GameCore { // ----- root.map.drawBackground(params); - // systems.mapResources.draw(params); if (!this.root.camera.getIsMapOverlayActive()) { systems.itemAcceptor.drawUnderlays(params); diff --git a/src/js/game/dynamic_tickrate.js b/src/js/game/dynamic_tickrate.js index f70a084e..b8d2d0d4 100644 --- a/src/js/game/dynamic_tickrate.js +++ b/src/js/game/dynamic_tickrate.js @@ -6,6 +6,8 @@ import { round3Digits } from "../core/utils"; const logger = createLogger("dynamic_tickrate"); +const fpsAccumulationTime = 1000; + export class DynamicTickrate { /** * @@ -18,9 +20,27 @@ export class DynamicTickrate { this.capturedTicks = []; this.averageTickDuration = 0; + this.accumulatedFps = 0; + this.accumulatedFpsLastUpdate = 0; + + this.averageFps = 60; + this.setTickRate(60); } + onFrameRendered() { + ++this.accumulatedFps; + + const now = performanceNow(); + const timeDuration = now - this.accumulatedFpsLastUpdate; + if (timeDuration > fpsAccumulationTime) { + const avgFps = (this.accumulatedFps / fpsAccumulationTime) * 1000; + this.averageFps = avgFps; + this.accumulatedFps = 0; + this.accumulatedFpsLastUpdate = now; + } + } + /** * Sets the tick rate to N updates per second * @param {number} rate @@ -36,14 +56,16 @@ export class DynamicTickrate { * Increases the tick rate marginally */ increaseTickRate() { - this.setTickRate(Math_round(Math_min(globalConfig.maximumTickRate, this.currentTickRate * 1.2))); + const desiredFps = this.root.app.settings.getDesiredFps(); + this.setTickRate(Math_round(Math_min(desiredFps, this.currentTickRate * 1.2))); } /** * Decreases the tick rate marginally */ decreaseTickRate() { - this.setTickRate(Math_round(Math_max(globalConfig.minimumTickRate, this.currentTickRate * 0.8))); + const desiredFps = this.root.app.settings.getDesiredFps(); + this.setTickRate(Math_round(Math_max(desiredFps / 2, this.currentTickRate * 0.8))); } /** @@ -65,22 +87,14 @@ export class DynamicTickrate { } 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) { + const desiredFps = this.root.app.settings.getDesiredFps(); + + if (this.averageFps > desiredFps * 0.9) { + // if (average < maxTickDuration) { this.increaseTickRate(); - } else { + } else if (this.averageFps < desiredFps * 0.7) { this.decreaseTickRate(); } diff --git a/src/js/game/hud/parts/debug_info.js b/src/js/game/hud/parts/debug_info.js index 74ce7fe6..745fbbd5 100644 --- a/src/js/game/hud/parts/debug_info.js +++ b/src/js/game/hud/parts/debug_info.js @@ -1,19 +1,33 @@ import { BaseHUDPart } from "../base_hud_part"; -import { makeDiv, round3Digits } from "../../../core/utils"; +import { makeDiv, round3Digits, round2Digits } from "../../../core/utils"; +import { Math_round } from "../../../core/builtins"; 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.fpsElement = makeDiv(this.element, null, ["fps"], "FPS: 60"); this.tickDurationElement = makeDiv(this.element, null, ["tickDuration"], "Update time: 0.5ms"); } - initialize() {} + initialize() { + this.lastTick = 0; + } update() { - this.tickRateElement.innerText = "Tickrate: " + this.root.dynamicTickrate.currentTickRate; - this.tickDurationElement.innerText = - "Avg. Dur: " + round3Digits(this.root.dynamicTickrate.averageTickDuration) + "ms"; + const now = this.root.time.realtimeNow(); + if (now - this.lastTick > 0.25) { + this.lastTick = now; + this.tickRateElement.innerText = "Tickrate: " + this.root.dynamicTickrate.currentTickRate; + this.fpsElement.innerText = + "FPS: " + + Math_round(this.root.dynamicTickrate.averageFps) + + " (" + + round2Digits(1000 / this.root.dynamicTickrate.averageFps) + + " ms)"; + this.tickDurationElement.innerText = + "Tick Dur: " + round3Digits(this.root.dynamicTickrate.averageTickDuration) + "ms"; + } } } diff --git a/src/js/game/map_chunk.js b/src/js/game/map_chunk.js index b56836e8..ee0ff12a 100644 --- a/src/js/game/map_chunk.js +++ b/src/js/game/map_chunk.js @@ -243,6 +243,7 @@ export class MapChunk { // Determine how likely it is that there is a color patch const colorPatchChance = 0.9 - clamp(distanceToOriginInChunks / 25, 0, 1) * 0.5; + if (rng.next() < colorPatchChance) { const colorPatchSize = Math_max(2, Math_round(1 + clamp(distanceToOriginInChunks / 8, 0, 4))); this.internalGenerateColorPatch(rng, colorPatchSize, distanceToOriginInChunks); diff --git a/src/js/game/map_chunk_view.js b/src/js/game/map_chunk_view.js index 411bfba8..cc0734d8 100644 --- a/src/js/game/map_chunk_view.js +++ b/src/js/game/map_chunk_view.js @@ -149,19 +149,7 @@ export class MapChunkView extends MapChunk { -this.tileX * globalConfig.tileSize, -this.tileY * globalConfig.tileSize ); - // parameters.context.save(); - // parameters.context.transform( - // 1, - // 0, - // 0, - // zoomLevel, - // this.tileX * globalConfig.tileSize, - // this.tileY * globalConfig.tileSize - // ); - this.internalDrawBackgroundSystems(parameters); - - // parameters.context.restore(); } /** @@ -187,24 +175,11 @@ export class MapChunkView extends MapChunk { zoomLevel, root: this.root, }); - // parameters.context.save(); - // parameters.context.save(); - // parameters.context.transform( - // zoomLevel, - // 0, - // 0, - // zoomLevel, - // this.tileX * globalConfig.tileSize, - // this.tileY * globalConfig.tileSize - // ); - parameters.context.translate( -this.tileX * globalConfig.tileSize, -this.tileY * globalConfig.tileSize ); this.internalDrawForegroundSystems(parameters); - - // parameters.context.restore(); } /** diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index cd228bce..1654834e 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -331,6 +331,10 @@ export class BeltSystem extends GameSystemWithFilter { return; } + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + for (let i = 0; i < items.length; ++i) { const itemAndProgress = items[i]; diff --git a/src/js/game/systems/hub.js b/src/js/game/systems/hub.js index 539a0981..93fbcb0d 100644 --- a/src/js/game/systems/hub.js +++ b/src/js/game/systems/hub.js @@ -41,6 +41,10 @@ export class HubSystem extends GameSystemWithFilter { const context = parameters.context; const staticComp = entity.components.StaticMapEntity; + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + const pos = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); // Background diff --git a/src/js/game/systems/item_acceptor.js b/src/js/game/systems/item_acceptor.js index 6a3bec0f..69209ca9 100644 --- a/src/js/game/systems/item_acceptor.js +++ b/src/js/game/systems/item_acceptor.js @@ -59,6 +59,10 @@ export class ItemAcceptorSystem extends GameSystemWithFilter { const staticComp = entity.components.StaticMapEntity; const acceptorComp = entity.components.ItemAcceptor; + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) { const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[ animIndex @@ -88,6 +92,10 @@ export class ItemAcceptorSystem extends GameSystemWithFilter { const staticComp = entity.components.StaticMapEntity; const acceptorComp = entity.components.ItemAcceptor; + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + const underlays = acceptorComp.beltUnderlays; for (let i = 0; i < underlays.length; ++i) { const { pos, direction } = underlays[i]; diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index da3e5c7c..25ca7e37 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -141,6 +141,10 @@ export class ItemEjectorSystem extends GameSystemWithFilter { const ejectorComp = entity.components.ItemEjector; const staticComp = entity.components.StaticMapEntity; + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + for (let i = 0; i < ejectorComp.slots.length; ++i) { const slot = ejectorComp.slots[i]; const ejectedItem = slot.item; diff --git a/src/js/game/systems/map_resources.js b/src/js/game/systems/map_resources.js index 7edb827c..6504d457 100644 --- a/src/js/game/systems/map_resources.js +++ b/src/js/game/systems/map_resources.js @@ -13,23 +13,34 @@ export class MapResourcesSystem extends GameSystem { const renderItems = parameters.zoomLevel >= globalConfig.mapChunkOverviewMinZoom; parameters.context.globalAlpha = 0.5; + const layer = chunk.lowerLayer; for (let x = 0; x < globalConfig.mapChunkSize; ++x) { const row = layer[x]; + const worldX = (chunk.tileX + x) * globalConfig.tileSize; for (let y = 0; y < globalConfig.mapChunkSize; ++y) { const lowerItem = row[y]; if (lowerItem) { + const worldY = (chunk.tileY + y) * globalConfig.tileSize; + + if ( + !parameters.visibleRect.containsRect4Params( + worldX, + worldY, + globalConfig.tileSize, + globalConfig.tileSize + ) + ) { + // Clipped + continue; + } + parameters.context.fillStyle = lowerItem.getBackgroundColorAsResource(); - parameters.context.fillRect( - (chunk.tileX + x) * globalConfig.tileSize, - (chunk.tileY + y) * globalConfig.tileSize, - globalConfig.tileSize, - globalConfig.tileSize - ); + parameters.context.fillRect(worldX, worldY, globalConfig.tileSize, globalConfig.tileSize); if (renderItems) { lowerItem.draw( - (chunk.tileX + x + 0.5) * globalConfig.tileSize, - (chunk.tileY + y + 0.5) * globalConfig.tileSize, + worldX + globalConfig.halfTileSize, + worldY + globalConfig.halfTileSize, parameters ); } diff --git a/src/js/game/systems/miner.js b/src/js/game/systems/miner.js index ffc1617a..f42e46c9 100644 --- a/src/js/game/systems/miner.js +++ b/src/js/game/systems/miner.js @@ -103,6 +103,10 @@ export class MinerSystem extends GameSystemWithFilter { if (entity && entity.components.Miner) { const staticComp = entity.components.StaticMapEntity; + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + const lowerLayerItem = this.root.map.getLowerLayerContentXY( staticComp.origin.x, staticComp.origin.y diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index b45d35b8..1b784063 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -102,6 +102,19 @@ export const allApplicationSettings = [ document.body.setAttribute("data-theme", id); }, }), + + new EnumSetting("refreshRate", { + options: ["60", "100", "144", "165"], + valueGetter: rate => rate, + textGetter: rate => rate + " Hz", + category: categoryGame, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => {}, + }), ]; export function getApplicationSettingById(id) { @@ -116,6 +129,7 @@ class SettingsStorage { this.soundsMuted = false; this.musicMuted = false; this.theme = "light"; + this.refreshRate = "60"; } } @@ -168,6 +182,10 @@ export class ApplicationSettings extends ReadWriteProxy { return this.getAllSettings().uiScale; } + getDesiredFps() { + return parseInt(this.getAllSettings().refreshRate); + } + getInterfaceScaleValue() { const id = this.getInterfaceScaleId(); for (let i = 0; i < uiScales.length; ++i) { @@ -234,7 +252,7 @@ export class ApplicationSettings extends ReadWriteProxy { } getCurrentVersion() { - return 3; + return 4; } migrate(data) { diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 3b57d93e..82ef29d6 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -73,7 +73,7 @@ mainMenu: # This is shown when using firefox and other browsers which are not supported. browserWarning: >- - This game is optimized for Google Chrome. Your browser is not supported or slow! + Sorry, but the game is known to run slow on your browser! Get the steam version or download chrome for the full experience. dialogs: buttons: @@ -325,3 +325,8 @@ settings: title: Game theme description: >- Choose the game theme which mainly affects the map background. Notice that everything except the light theme may lead to graphical issues. + + refreshRate: + title: Simulation Target + description: >- + If you have a 144hz monitor, change the refresh rate here so the game will properly simulate at higher refresh rates.