import { SystemSettings } from "../configs.js";
import { GameStageData } from "./GameStageData.js";
import { CONST } from "../constants.js";
import { Warning } from "./Exception.js";
import { WARNING_CODES } from "../constants.js";
import { DrawTiledLayer } from "./2d/DrawTiledLayer.js";
import { DrawImageObject } from "./2d/DrawImageObject.js";
import { DrawCircleObject } from "./2d/DrawCircleObject.js";
import { DrawConusObject } from "./2d/DrawConusObject.js";
import { DrawLineObject } from "./2d/DrawLineObject.js";
import { DrawPolygonObject } from "./2d/DrawPolygonObject.js";
import { DrawRectObject } from "./2d/DrawRectObject.js";
import { DrawTextObject } from "./2d/DrawTextObject.js";
import { WebGlEngine } from "./WebGl/WebGlEngine.js";
import { RenderLoopDebug } from "./RenderLoopDebug.js";
import { utils } from "../index.js";
/**
* Class represents the render loop,
* on each time stage start, a new RenderLoop class instance created,
* after stage stop, RenderLoop stops and its instance removed
* @see {@link IRender} a part of iRender
* @hideconstructor
*/
export class RenderLoop {
/**
* @type {boolean}
*/
#isActive;
/**
* @type {boolean}
*/
#isCleared;
/**
* @type {RenderLoopDebug}
*/
#renderLoopDebug;
#fpsAverageCountTimer;
/**
*
* @param {GameStageData} stageData
*/
#stageData;
/**
* @param { WebGlEngine }
*/
#webGlEngine;
/**
*
* @param {SystemSettings} systemSettings
*/
#systemSettings;
/**
* @type {EventTarget}
*/
#emitter = new EventTarget();
constructor(systemSettings, stageData, WebGlEngine) {
this.#systemSettings = systemSettings;
this.#stageData = stageData;
this.#renderLoopDebug = new RenderLoopDebug(this.#systemSettings.gameOptions.render.cyclesTimeCalc.averageFPStime);
this.#webGlEngine = WebGlEngine;
this.#webGlEngine._initDrawCallsDebug(this.renderLoopDebug);
if (this.#systemSettings.gameOptions.render.cyclesTimeCalc.check === CONST.OPTIMIZATION.CYCLE_TIME_CALC.AVERAGES) {
this.#fpsAverageCountTimer = setInterval(() => this.#countFPSaverage(), this.#systemSettings.gameOptions.render.cyclesTimeCalc.averageFPStime);
}
}
/**
* @type { GameStageData }
*/
get stageData() {
return this.#stageData;
}
/**
* @type { RenderLoopDebug }
*/
get renderLoopDebug() {
return this.#renderLoopDebug;
}
/**
* @ignore
*/
set _isCleared(value) {
this.#isCleared = value;
}
/**
* @ignore
*/
get _isCleared() {
return this.#isCleared;
}
_start() {
this.#isActive = true;
requestAnimationFrame(this.#runRenderLoop);
}
_stop() {
this.#isActive = false;
this.#stageData = null;
this.renderLoopDebug.cleanupTempVars();
clearInterval(this.#fpsAverageCountTimer);
//this.#fpsAverageCountTimer = null;
}
/**
*
* @param {Number} drawTimestamp - end time of previous frame's rendering
*/
#runRenderLoop = (drawTimestamp) => {
if (!this.#isActive) {
return;
}
const currentDrawTime = this.renderLoopDebug.currentDrawTime(drawTimestamp);
this.renderLoopDebug.prevDrawTime = drawTimestamp;
const timeStart = performance.now(),
isCyclesTimeCalcCheckCurrent = this.#systemSettings.gameOptions.render.cyclesTimeCalc.check === CONST.OPTIMIZATION.CYCLE_TIME_CALC.CURRENT;
this.emit(CONST.EVENTS.SYSTEM.RENDER.START);
this.#stageData._clearBoundaries();
this.#clearContext();
this.render().then(() => {
const currentRenderTime = performance.now() - timeStart,
//r_time_less = minCycleTime - currentRenderTime,
wait_time = 0, // нужна ли вообще возможность контролировать время отрисовки?
cycleTime = currentRenderTime + wait_time;
if (isCyclesTimeCalcCheckCurrent) {
console.log("current draw take: ", (currentDrawTime), " ms");
console.log("current render() time: ", currentRenderTime);
console.log("draw calls: ", this.renderLoopDebug.drawCalls);
} else {
this.renderLoopDebug.tempRCircleT = currentDrawTime;
this.renderLoopDebug.incrementTempRCircleTPointer();
}
this.emit(CONST.EVENTS.SYSTEM.RENDER.END);
if (this.#isActive) {
setTimeout(() => requestAnimationFrame(this.#runRenderLoop), wait_time);
}
}).catch((errors) => {
if (errors.forEach) {
errors.forEach((err) => {
Warning(WARNING_CODES.UNHANDLED_DRAW_ISSUE, err);
});
} else {
Warning(WARNING_CODES.UNHANDLED_DRAW_ISSUE, errors.message);
}
this._stop();
});
};
/**
* @returns {Promise<void>}
*/
async render() {
const renderObjects = this.#stageData.renderObjects;
let errors = [],
isErrors = false,
len = renderObjects.length,
renderObjectsPromises = new Array(len),
// for v1.5.2, each object has its own render method
drawCalls = len;
if (len !== 0) {
//this.#checkCollisions(view.renderObjects);
for (let i = 0; i < len; i++) {
const object = renderObjects[i];
if (object.isRemoved) {
renderObjects.splice(i, 1);
i--;
len--;
continue;
}
if (object.hasAnimations) {
object._processActiveAnimations();
}
const promise = await this.#bindRenderObject(object)
.catch((err) => Promise.reject(err));
renderObjectsPromises[i] = promise;
}
if (this.#systemSettings.gameOptions.debug.boundaries.drawLayerBoundaries) {
renderObjectsPromises.push(this.#drawBoundariesWebGl()
.catch((err) => Promise.reject(err)));
}
//const bindResults = await Promise.allSettled(renderObjectsPromises);
//bindResults.forEach((result) => {
// if (result.status === "rejected") {
// reject(result.reason);
// }
//});
//await this.#webGlEngine._executeImagesDraw();
//this.#postRenderActions();
}
const bindResults = await Promise.allSettled(renderObjectsPromises);
bindResults.forEach((result) => {
if (result.status === "rejected") {
Promise.reject(result.reason);
isErrors = true;
errors.push(result.reason);
}
});
this.#postRenderActions();
this._isCleared = false;
if (isErrors === false) {
return Promise.resolve(drawCalls);
} else {
return Promise.reject(errors);
}
}
/**
*
* @param {string} eventName
* @param {*} listener
* @param {*=} options
*/
addEventListener = (eventName, listener, options) => {
this.#emitter.addEventListener(eventName, listener, options);
};
/**
*
* @param {string} eventName
* @param {*} listener
* @param {*=} options
*/
removeEventListener = (eventName, listener, options) => {
this.#emitter.removeEventListener(eventName, listener, options);
};
/**
*
* @param {string} eventName
* @param {...any} eventParams
*/
emit = (eventName, ...eventParams) => {
const event = new Event(eventName);
event.data = [...eventParams];
this.#emitter.dispatchEvent(event);
};
/**
* @ignore
* @param {DrawImageObject | DrawCircleObject | DrawConusObject | DrawLineObject | DrawPolygonObject | DrawRectObject | DrawTextObject | DrawTiledLayer} renderObject
* @returns {Promise<void>}
*/
#bindRenderObject(renderObject) {
return this.#webGlEngine._bindRenderObject(renderObject, this.stageData);
}
#clearContext() {
this.#webGlEngine._clearView();
}
#postRenderActions() {
//const images = this.stageData.getObjectsByInstance(DrawImageObject);
//for (let i = 0; i < images.length; i++) {
// const object = images[i];
// if (object.isAnimations) {
// object._processActiveAnimations();
// }
//}
}
/**
*
* @returns {Promise<void>}
*/
#drawBoundariesWebGl() {
return new Promise((resolve) => {
const b = this.stageData.getRawBoundaries(),
eB = this.stageData.getEllipseBoundaries(),
pB = this.stageData.getPointBoundaries(),
len = this.stageData.boundariesLen,
eLen = this.stageData.ellipseBLen,
pLen = this.stageData.pointBLen;
if (len)
this.#webGlEngine._drawLines(b, this.#systemSettings.gameOptions.debug.boundaries.boundariesColor, this.#systemSettings.gameOptions.debug.boundaries.boundariesWidth);
this.renderLoopDebug.incrementDrawCallsCounter();
if (eLen) {
//draw ellipse boundaries
for (let i = 0; i < eLen; i+=4) {
const x = eB[i],
y = eB[i+1],
radX = eB[i+2],
radY = eB[i+3],
vertices = utils.calculateEllipseVertices(x, y, radX, radY);
this.#webGlEngine._drawPolygon({x: 0, y: 0, vertices, isOffsetTurnedOff: true}, this.stageData);
this.renderLoopDebug.incrementDrawCallsCounter();
//this.#webGlEngine._drawLines(vertices, this.systemSettings.gameOptions.debug.boundaries.boundariesColor, this.systemSettings.gameOptions.debug.boundaries.boundariesWidth);
}
}
if (pLen) {
//draw point boundaries
for (let i = 0; i < pLen; i+=2) {
const x = pB[i],
y = pB[i+1],
vertices = [x,y, x+1,y+1];
this.#webGlEngine._drawLines(vertices, this.#systemSettings.gameOptions.debug.boundaries.boundariesColor, this.#systemSettings.gameOptions.debug.boundaries.boundariesWidth);
this.renderLoopDebug.incrementDrawCallsCounter();
}
}
resolve();
});
}
/**
*
* @param {DrawTiledLayer} renderLayer
* @returns {Promise<void>}
*/
#layerBoundariesPrecalculation(renderLayer) {
return new Promise((resolve, reject) => {
/*
if (renderLayer.setBoundaries) {
const tilemap = this.#iLoader.getTileMap(renderLayer.tileMapKey),
tilesets = tilemap.tilesets,
layerData = tilemap.layers.find((layer) => layer.name === renderLayer.layerKey),
{ tileheight:dtheight, tilewidth:dtwidth } = tilemap,
tilewidth = dtwidth,
tileheight = dtheight,
[ settingsWorldWidth, settingsWorldHeight ] = this.stageData.worldDimensions;
let boundaries = [];
if (!layerData) {
Warning(WARNING_CODES.NOT_FOUND, "check tilemap and layers name");
reject();
}
for (let i = 0; i < tilesets.length; i++) {
const layerCols = layerData.width,
layerRows = layerData.height,
worldW = tilewidth * layerCols,
worldH = tileheight * layerRows;
if (worldW !== settingsWorldWidth || worldH !== settingsWorldHeight) {
Warning(WARNING_CODES.UNEXPECTED_WORLD_SIZE, " World size from tilemap is different than settings one, fixing...");
this.stageData._setWorldDimensions(worldW, worldH);
}
if (renderLayer.setBoundaries && this.#systemSettings.gameOptions.render.boundaries.mapBoundariesEnabled) {
this.stageData._setWholeWorldMapBoundaries();
}
//calculate boundaries
let mapIndex = 0;
for (let row = 0; row < layerRows; row++) {
for (let col = 0; col < layerCols; col++) {
let tile = layerData.data[mapIndex],
mapPosX = col * tilewidth,
mapPosY = row * tileheight;
if (tile !== 0) {
tile -= 1;
boundaries.push([mapPosX, mapPosY, mapPosX + tilewidth, mapPosY]);
boundaries.push([mapPosX + tilewidth, mapPosY, mapPosX + tilewidth, mapPosY + tileheight]);
boundaries.push([mapPosX + tilewidth, mapPosY + tileheight, mapPosX, mapPosY + tileheight]);
boundaries.push([mapPosX, mapPosY + tileheight, mapPosX, mapPosY ]);
}
mapIndex++;
}
}
}
this.stageData._setWholeMapBoundaries(boundaries);
this.stageData._mergeBoundaries(true);
resolve();
} else {
resolve();
}*/
});
}
#countFPSaverage() {
const timeLeft = this.#systemSettings.gameOptions.render.cyclesTimeCalc.averageFPStime,
steps = this.renderLoopDebug.tempRCircleTPointer;
let fullTime = 0;
for (let i = 0; i < steps; i++) {
const timeStep = this.renderLoopDebug.tempRCircleT[i];
fullTime += timeStep;
}
console.log("FPS average for", timeLeft/1000, "sec, is ", (1000 / (fullTime / steps)).toFixed(2));
console.log("Last loop webgl draw calls: ", this.renderLoopDebug.drawCalls);
// cleanup
this.renderLoopDebug.cleanupTempVars();
}
}