Major performance improvements

This commit is contained in:
tobspr 2020-05-18 17:40:20 +02:00
parent 260ba892c8
commit 2c48cb72aa
18 changed files with 194 additions and 60 deletions

View File

@ -7,4 +7,5 @@
display: flex; display: flex;
line-height: 15px; line-height: 15px;
flex-direction: column; flex-direction: column;
color: #fff;
} }

View File

@ -92,7 +92,7 @@ body.uiHidden {
body.modalDialogActive, body.modalDialogActive,
body.ingameDialogOpen { body.ingameDialogOpen {
> *:not(.ingameDialog):not(.modalDialogParent) { > *:not(.ingameDialog):not(.modalDialogParent):not(.loadingDialog) {
filter: blur(5px) !important; filter: blur(5px) !important;
} }
} }

View File

@ -71,7 +71,7 @@ export const globalConfig = {
debug: { debug: {
/* dev:start */ /* dev:start */
fastGameEnter: true, // fastGameEnter: true,
noArtificialDelays: true, noArtificialDelays: true,
// disableSavegameWrite: true, // disableSavegameWrite: true,
showEntityBounds: false, showEntityBounds: false,
@ -80,11 +80,13 @@ export const globalConfig = {
disableMusic: true, disableMusic: true,
doNotRenderStatics: false, doNotRenderStatics: false,
disableZoomLimits: false, disableZoomLimits: false,
showChunkBorders: false, // showChunkBorders: true,
rewardsInstant: false, rewardsInstant: false,
allBuildingsUnlocked: true, allBuildingsUnlocked: true,
upgradesNoCost: true, upgradesNoCost: true,
disableUnlockDialog: true, disableUnlockDialog: true,
// disableLogicTicks: true,
// testClipping: true,
// framePausesBetweenTicks: 40, // framePausesBetweenTicks: 40,
// testTranslations: true, // testTranslations: true,
/* dev:end */ /* dev:end */

View File

@ -1,4 +1,5 @@
import { Rectangle } from "./rectangle"; import { Rectangle } from "./rectangle";
import { globalConfig } from "./config";
/* typehints:start */ /* typehints:start */
import { GameRoot } from "../game/root"; import { GameRoot } from "../game/root";
@ -21,5 +22,9 @@ export class DrawParameters {
// FIXME: Not really nice // FIXME: Not really nice
/** @type {GameRoot} */ /** @type {GameRoot} */
this.root = root; this.root = root;
if (G_IS_DEV && globalConfig.debug.testClipping) {
this.visibleRect = this.visibleRect.expandedInAllDirections(-100);
}
} }
} }

View File

@ -145,6 +145,57 @@ export class StaticMapEntityComponent extends Component {
return this.unapplyRotationToVector(localUnrotated); 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 * Draws a sprite over the whole space of the entity
* @param {DrawParameters} parameters * @param {DrawParameters} parameters
@ -156,6 +207,10 @@ export class StaticMapEntityComponent extends Component {
const worldX = this.origin.x * globalConfig.tileSize; const worldX = this.origin.x * globalConfig.tileSize;
const worldY = this.origin.y * globalConfig.tileSize; const worldY = this.origin.y * globalConfig.tileSize;
if (!this.shouldBeDrawn(parameters)) {
return;
}
if (this.rotation === 0) { if (this.rotation === 0) {
// Early out, is faster // Early out, is faster
sprite.drawCached( sprite.drawCached(
@ -164,7 +219,7 @@ export class StaticMapEntityComponent extends Component {
worldY - extrudePixels * this.tileSize.y, worldY - extrudePixels * this.tileSize.y,
globalConfig.tileSize * this.tileSize.x + 2 * extrudePixels * this.tileSize.x, globalConfig.tileSize * this.tileSize.x + 2 * extrudePixels * this.tileSize.x,
globalConfig.tileSize * this.tileSize.y + 2 * extrudePixels * this.tileSize.y, globalConfig.tileSize * this.tileSize.y + 2 * extrudePixels * this.tileSize.y,
clipping false
); );
} else { } else {
const rotationCenterX = worldX + globalConfig.halfTileSize; const rotationCenterX = worldX + globalConfig.halfTileSize;

View File

@ -274,6 +274,11 @@ export class GameCore {
root.dynamicTickrate.beginTick(); root.dynamicTickrate.beginTick();
if (G_IS_DEV && globalConfig.debug.disableLogicTicks) {
root.dynamicTickrate.endTick();
return true;
}
this.duringLogicUpdate = true; this.duringLogicUpdate = true;
// Update entities, this removes destroyed entities // Update entities, this removes destroyed entities
@ -327,6 +332,8 @@ export class GameCore {
return; return;
} }
this.root.dynamicTickrate.onFrameRendered();
if (!this.shouldRender()) { if (!this.shouldRender()) {
// Always update hud tho // Always update hud tho
root.hud.update(); root.hud.update();
@ -341,6 +348,9 @@ export class GameCore {
// Gather context and save all state // Gather context and save all state
const context = root.context; const context = root.context;
context.save(); 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 // Compute optimal zoom level and atlas scale
const zoomLevel = root.camera.zoomLevel; const zoomLevel = root.camera.zoomLevel;
@ -383,7 +393,6 @@ export class GameCore {
// ----- // -----
root.map.drawBackground(params); root.map.drawBackground(params);
// systems.mapResources.draw(params);
if (!this.root.camera.getIsMapOverlayActive()) { if (!this.root.camera.getIsMapOverlayActive()) {
systems.itemAcceptor.drawUnderlays(params); systems.itemAcceptor.drawUnderlays(params);

View File

@ -6,6 +6,8 @@ import { round3Digits } from "../core/utils";
const logger = createLogger("dynamic_tickrate"); const logger = createLogger("dynamic_tickrate");
const fpsAccumulationTime = 1000;
export class DynamicTickrate { export class DynamicTickrate {
/** /**
* *
@ -18,9 +20,27 @@ export class DynamicTickrate {
this.capturedTicks = []; this.capturedTicks = [];
this.averageTickDuration = 0; this.averageTickDuration = 0;
this.accumulatedFps = 0;
this.accumulatedFpsLastUpdate = 0;
this.averageFps = 60;
this.setTickRate(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 * Sets the tick rate to N updates per second
* @param {number} rate * @param {number} rate
@ -36,14 +56,16 @@ export class DynamicTickrate {
* Increases the tick rate marginally * Increases the tick rate marginally
*/ */
increaseTickRate() { 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 * Decreases the tick rate marginally
*/ */
decreaseTickRate() { 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; 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; this.averageTickDuration = average;
if (average < maxTickDuration) { const desiredFps = this.root.app.settings.getDesiredFps();
if (this.averageFps > desiredFps * 0.9) {
// if (average < maxTickDuration) {
this.increaseTickRate(); this.increaseTickRate();
} else { } else if (this.averageFps < desiredFps * 0.7) {
this.decreaseTickRate(); this.decreaseTickRate();
} }

View File

@ -1,19 +1,33 @@
import { BaseHUDPart } from "../base_hud_part"; 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 { export class HUDDebugInfo extends BaseHUDPart {
createElements(parent) { createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_DebugInfo", []); this.element = makeDiv(parent, "ingame_HUD_DebugInfo", []);
this.tickRateElement = makeDiv(this.element, null, ["tickRate"], "Ticks /s: 120"); 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"); this.tickDurationElement = makeDiv(this.element, null, ["tickDuration"], "Update time: 0.5ms");
} }
initialize() {} initialize() {
this.lastTick = 0;
}
update() { update() {
this.tickRateElement.innerText = "Tickrate: " + this.root.dynamicTickrate.currentTickRate; const now = this.root.time.realtimeNow();
this.tickDurationElement.innerText = if (now - this.lastTick > 0.25) {
"Avg. Dur: " + round3Digits(this.root.dynamicTickrate.averageTickDuration) + "ms"; 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";
}
} }
} }

View File

@ -243,6 +243,7 @@ export class MapChunk {
// Determine how likely it is that there is a color patch // Determine how likely it is that there is a color patch
const colorPatchChance = 0.9 - clamp(distanceToOriginInChunks / 25, 0, 1) * 0.5; const colorPatchChance = 0.9 - clamp(distanceToOriginInChunks / 25, 0, 1) * 0.5;
if (rng.next() < colorPatchChance) { if (rng.next() < colorPatchChance) {
const colorPatchSize = Math_max(2, Math_round(1 + clamp(distanceToOriginInChunks / 8, 0, 4))); const colorPatchSize = Math_max(2, Math_round(1 + clamp(distanceToOriginInChunks / 8, 0, 4)));
this.internalGenerateColorPatch(rng, colorPatchSize, distanceToOriginInChunks); this.internalGenerateColorPatch(rng, colorPatchSize, distanceToOriginInChunks);

View File

@ -149,19 +149,7 @@ export class MapChunkView extends MapChunk {
-this.tileX * globalConfig.tileSize, -this.tileX * globalConfig.tileSize,
-this.tileY * 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); this.internalDrawBackgroundSystems(parameters);
// parameters.context.restore();
} }
/** /**
@ -187,24 +175,11 @@ export class MapChunkView extends MapChunk {
zoomLevel, zoomLevel,
root: this.root, 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( parameters.context.translate(
-this.tileX * globalConfig.tileSize, -this.tileX * globalConfig.tileSize,
-this.tileY * globalConfig.tileSize -this.tileY * globalConfig.tileSize
); );
this.internalDrawForegroundSystems(parameters); this.internalDrawForegroundSystems(parameters);
// parameters.context.restore();
} }
/** /**

View File

@ -331,6 +331,10 @@ export class BeltSystem extends GameSystemWithFilter {
return; return;
} }
if (!staticComp.shouldBeDrawn(parameters)) {
return;
}
for (let i = 0; i < items.length; ++i) { for (let i = 0; i < items.length; ++i) {
const itemAndProgress = items[i]; const itemAndProgress = items[i];

View File

@ -41,6 +41,10 @@ export class HubSystem extends GameSystemWithFilter {
const context = parameters.context; const context = parameters.context;
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
if (!staticComp.shouldBeDrawn(parameters)) {
return;
}
const pos = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); const pos = staticComp.getTileSpaceBounds().getCenter().toWorldSpace();
// Background // Background

View File

@ -59,6 +59,10 @@ export class ItemAcceptorSystem extends GameSystemWithFilter {
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
const acceptorComp = entity.components.ItemAcceptor; const acceptorComp = entity.components.ItemAcceptor;
if (!staticComp.shouldBeDrawn(parameters)) {
return;
}
for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) { for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) {
const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[ const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[
animIndex animIndex
@ -88,6 +92,10 @@ export class ItemAcceptorSystem extends GameSystemWithFilter {
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
const acceptorComp = entity.components.ItemAcceptor; const acceptorComp = entity.components.ItemAcceptor;
if (!staticComp.shouldBeDrawn(parameters)) {
return;
}
const underlays = acceptorComp.beltUnderlays; const underlays = acceptorComp.beltUnderlays;
for (let i = 0; i < underlays.length; ++i) { for (let i = 0; i < underlays.length; ++i) {
const { pos, direction } = underlays[i]; const { pos, direction } = underlays[i];

View File

@ -141,6 +141,10 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
const ejectorComp = entity.components.ItemEjector; const ejectorComp = entity.components.ItemEjector;
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
if (!staticComp.shouldBeDrawn(parameters)) {
return;
}
for (let i = 0; i < ejectorComp.slots.length; ++i) { for (let i = 0; i < ejectorComp.slots.length; ++i) {
const slot = ejectorComp.slots[i]; const slot = ejectorComp.slots[i];
const ejectedItem = slot.item; const ejectedItem = slot.item;

View File

@ -13,23 +13,34 @@ export class MapResourcesSystem extends GameSystem {
const renderItems = parameters.zoomLevel >= globalConfig.mapChunkOverviewMinZoom; const renderItems = parameters.zoomLevel >= globalConfig.mapChunkOverviewMinZoom;
parameters.context.globalAlpha = 0.5; parameters.context.globalAlpha = 0.5;
const layer = chunk.lowerLayer; const layer = chunk.lowerLayer;
for (let x = 0; x < globalConfig.mapChunkSize; ++x) { for (let x = 0; x < globalConfig.mapChunkSize; ++x) {
const row = layer[x]; const row = layer[x];
const worldX = (chunk.tileX + x) * globalConfig.tileSize;
for (let y = 0; y < globalConfig.mapChunkSize; ++y) { for (let y = 0; y < globalConfig.mapChunkSize; ++y) {
const lowerItem = row[y]; const lowerItem = row[y];
if (lowerItem) { 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.fillStyle = lowerItem.getBackgroundColorAsResource();
parameters.context.fillRect( parameters.context.fillRect(worldX, worldY, globalConfig.tileSize, globalConfig.tileSize);
(chunk.tileX + x) * globalConfig.tileSize,
(chunk.tileY + y) * globalConfig.tileSize,
globalConfig.tileSize,
globalConfig.tileSize
);
if (renderItems) { if (renderItems) {
lowerItem.draw( lowerItem.draw(
(chunk.tileX + x + 0.5) * globalConfig.tileSize, worldX + globalConfig.halfTileSize,
(chunk.tileY + y + 0.5) * globalConfig.tileSize, worldY + globalConfig.halfTileSize,
parameters parameters
); );
} }

View File

@ -103,6 +103,10 @@ export class MinerSystem extends GameSystemWithFilter {
if (entity && entity.components.Miner) { if (entity && entity.components.Miner) {
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
if (!staticComp.shouldBeDrawn(parameters)) {
return;
}
const lowerLayerItem = this.root.map.getLowerLayerContentXY( const lowerLayerItem = this.root.map.getLowerLayerContentXY(
staticComp.origin.x, staticComp.origin.x,
staticComp.origin.y staticComp.origin.y

View File

@ -102,6 +102,19 @@ export const allApplicationSettings = [
document.body.setAttribute("data-theme", id); 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) { export function getApplicationSettingById(id) {
@ -116,6 +129,7 @@ class SettingsStorage {
this.soundsMuted = false; this.soundsMuted = false;
this.musicMuted = false; this.musicMuted = false;
this.theme = "light"; this.theme = "light";
this.refreshRate = "60";
} }
} }
@ -168,6 +182,10 @@ export class ApplicationSettings extends ReadWriteProxy {
return this.getAllSettings().uiScale; return this.getAllSettings().uiScale;
} }
getDesiredFps() {
return parseInt(this.getAllSettings().refreshRate);
}
getInterfaceScaleValue() { getInterfaceScaleValue() {
const id = this.getInterfaceScaleId(); const id = this.getInterfaceScaleId();
for (let i = 0; i < uiScales.length; ++i) { for (let i = 0; i < uiScales.length; ++i) {
@ -234,7 +252,7 @@ export class ApplicationSettings extends ReadWriteProxy {
} }
getCurrentVersion() { getCurrentVersion() {
return 3; return 4;
} }
migrate(data) { migrate(data) {

View File

@ -73,7 +73,7 @@ mainMenu:
# This is shown when using firefox and other browsers which are not supported. # This is shown when using firefox and other browsers which are not supported.
browserWarning: >- 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: dialogs:
buttons: buttons:
@ -325,3 +325,8 @@ settings:
title: Game theme title: Game theme
description: >- description: >-
Choose the game theme which mainly affects the map background. Notice that everything except the light theme may lead to graphical issues. 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.