Feature/Refactor to use ecs architecture (#14)
Reviewed-on: #14 Co-authored-by: Juan Sebastian Montoya <juansmm@outlook.com> Co-committed-by: Juan Sebastian Montoya <juansmm@outlook.com>
This commit is contained in:
parent
e0436e7769
commit
cec1fccc22
23 changed files with 1709 additions and 650 deletions
157
src/game/EntityFactory.js
Normal file
157
src/game/EntityFactory.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { Transform } from '../components/Transform.js';
|
||||
import { Velocity } from '../components/Velocity.js';
|
||||
import { MeshComponent } from '../components/MeshComponent.js';
|
||||
import { Collidable } from '../components/Collidable.js';
|
||||
import { Health } from '../components/Health.js';
|
||||
import { PlayerTag, CoinTag, ObstacleTag, BoundaryConstrained } from '../components/Tags.js';
|
||||
|
||||
/**
|
||||
* EntityFactory - creates pre-configured game entities with appropriate components.
|
||||
* Centralizes entity creation logic for consistency.
|
||||
*
|
||||
* @typedef {import('../ecs/World.js').EntityId} EntityId
|
||||
*/
|
||||
export class EntityFactory {
|
||||
/**
|
||||
* @param {import('../ecs/World.js').World} world - The ECS world
|
||||
* @param {THREE.Scene} scene - The Three.js scene
|
||||
*/
|
||||
constructor(world, scene) {
|
||||
/** @type {import('../ecs/World.js').World} */
|
||||
this.world = world;
|
||||
|
||||
/** @type {THREE.Scene} */
|
||||
this.scene = scene;
|
||||
|
||||
/** @type {number} Size of the game ground/play area */
|
||||
this.groundSize = 30;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the player entity
|
||||
* @returns {EntityId} The player entity ID
|
||||
*/
|
||||
createPlayer() {
|
||||
const entity = this.world.createEntity();
|
||||
|
||||
// Create mesh
|
||||
const geometry = new window.THREE.BoxGeometry(1, 1, 1);
|
||||
const material = new window.THREE.MeshStandardMaterial({
|
||||
color: 0x4169E1,
|
||||
metalness: 0.3,
|
||||
roughness: 0.4
|
||||
});
|
||||
const mesh = new window.THREE.Mesh(geometry, material);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
this.scene.add(mesh);
|
||||
|
||||
// Add components
|
||||
this.world.addComponent(entity, new Transform(0, 0.5, 0));
|
||||
this.world.addComponent(entity, new Velocity());
|
||||
this.world.addComponent(entity, new MeshComponent(mesh));
|
||||
this.world.addComponent(entity, new Collidable(0, 'player')); // Player center point (original behavior)
|
||||
this.world.addComponent(entity, new Health(100));
|
||||
this.world.addComponent(entity, new PlayerTag());
|
||||
this.world.addComponent(entity, new BoundaryConstrained(this.groundSize));
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a collectible coin entity
|
||||
* @param {number} [index=0] - Unique index for animation offset
|
||||
* @returns {EntityId} The coin entity ID
|
||||
*/
|
||||
createCoin(index = 0) {
|
||||
const entity = this.world.createEntity();
|
||||
|
||||
// Create mesh
|
||||
const geometry = new window.THREE.SphereGeometry(0.3, 16, 16);
|
||||
const material = new window.THREE.MeshStandardMaterial({
|
||||
color: 0xFFD700,
|
||||
metalness: 0.8,
|
||||
roughness: 0.2,
|
||||
emissive: 0xFFD700,
|
||||
emissiveIntensity: 0.3
|
||||
});
|
||||
const mesh = new window.THREE.Mesh(geometry, material);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
this.scene.add(mesh);
|
||||
|
||||
// Random position
|
||||
const x = (Math.random() - 0.5) * (this.groundSize - 4);
|
||||
const z = (Math.random() - 0.5) * (this.groundSize - 4);
|
||||
|
||||
// Add components
|
||||
this.world.addComponent(entity, new Transform(x, 0.5, z));
|
||||
this.world.addComponent(entity, new MeshComponent(mesh));
|
||||
this.world.addComponent(entity, new Collidable(0.8, 'coin'));
|
||||
this.world.addComponent(entity, new CoinTag(index));
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an obstacle entity
|
||||
* @returns {EntityId} The obstacle entity ID
|
||||
*/
|
||||
createObstacle() {
|
||||
const entity = this.world.createEntity();
|
||||
|
||||
// Create mesh
|
||||
const geometry = new window.THREE.BoxGeometry(1.5, 2, 1.5);
|
||||
const material = new window.THREE.MeshStandardMaterial({
|
||||
color: 0xFF4500,
|
||||
metalness: 0.3,
|
||||
roughness: 0.7
|
||||
});
|
||||
const mesh = new window.THREE.Mesh(geometry, material);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
this.scene.add(mesh);
|
||||
|
||||
// Random position (away from center)
|
||||
let posX, posZ;
|
||||
do {
|
||||
posX = (Math.random() - 0.5) * (this.groundSize - 4);
|
||||
posZ = (Math.random() - 0.5) * (this.groundSize - 4);
|
||||
} while (Math.abs(posX) < 3 && Math.abs(posZ) < 3);
|
||||
|
||||
// Random velocity
|
||||
const velocity = new Velocity(
|
||||
(Math.random() - 0.5) * 0.05,
|
||||
0,
|
||||
(Math.random() - 0.5) * 0.05
|
||||
);
|
||||
|
||||
// Add components
|
||||
this.world.addComponent(entity, new Transform(posX, 1, posZ));
|
||||
this.world.addComponent(entity, velocity);
|
||||
this.world.addComponent(entity, new MeshComponent(mesh));
|
||||
this.world.addComponent(entity, new Collidable(1.5, 'obstacle'));
|
||||
this.world.addComponent(entity, new ObstacleTag());
|
||||
this.world.addComponent(entity, new BoundaryConstrained(this.groundSize));
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entity and its mesh from scene
|
||||
* @param {EntityId} entityId - The entity to destroy
|
||||
*/
|
||||
destroyEntity(entityId) {
|
||||
// Remove mesh from scene if it exists
|
||||
const meshComp = this.world.getComponent(entityId, MeshComponent);
|
||||
if (meshComp) {
|
||||
this.scene.remove(meshComp.mesh);
|
||||
meshComp.mesh.geometry.dispose();
|
||||
meshComp.mesh.material.dispose();
|
||||
}
|
||||
|
||||
// Remove entity from world
|
||||
this.world.removeEntity(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
486
src/game/Game.js
Normal file
486
src/game/Game.js
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
import { World } from '../ecs/World.js';
|
||||
import { EntityFactory } from './EntityFactory.js';
|
||||
|
||||
// Systems
|
||||
import { InputSystem } from '../systems/InputSystem.js';
|
||||
import { PlayerControlSystem } from '../systems/PlayerControlSystem.js';
|
||||
import { MovementSystem } from '../systems/MovementSystem.js';
|
||||
import { BoundarySystem } from '../systems/BoundarySystem.js';
|
||||
import { CoinSystem } from '../systems/CoinSystem.js';
|
||||
import { ObstacleSystem } from '../systems/ObstacleSystem.js';
|
||||
import { CollisionSystem } from '../systems/CollisionSystem.js';
|
||||
import { RenderSystem } from '../systems/RenderSystem.js';
|
||||
|
||||
// Components
|
||||
import { Transform } from '../components/Transform.js';
|
||||
import { Health } from '../components/Health.js';
|
||||
|
||||
/**
|
||||
* Main Game class - manages the game loop and coordinates all systems.
|
||||
* Orchestrates the ECS architecture and Three.js rendering.
|
||||
*
|
||||
* @typedef {import('../ecs/World.js').EntityId} EntityId
|
||||
*/
|
||||
export class Game {
|
||||
constructor() {
|
||||
/** @type {number} Size of the game play area */
|
||||
this.groundSize = 30;
|
||||
|
||||
/** @type {number} Current game score */
|
||||
this.score = 0;
|
||||
|
||||
/** @type {boolean} Whether the game is currently active */
|
||||
this.gameActive = true;
|
||||
|
||||
/** @type {EntityId|null} The player entity ID */
|
||||
this.playerEntity = null;
|
||||
|
||||
/** @type {EntityId[]} Array of coin entity IDs */
|
||||
this.coins = [];
|
||||
|
||||
/** @type {EntityId[]} Array of obstacle entity IDs */
|
||||
this.obstacles = [];
|
||||
|
||||
/** @type {number} Last frame timestamp for deltaTime calculation */
|
||||
this.lastTime = performance.now();
|
||||
|
||||
/** @type {number} Maximum deltaTime cap (in seconds) to prevent huge jumps */
|
||||
this.maxDeltaTime = 0.1; // 100ms cap
|
||||
|
||||
/** @type {number} Smoothed FPS for display */
|
||||
this.smoothedFPS = 60;
|
||||
|
||||
/** @type {number} Last time performance monitor was updated */
|
||||
this.lastPerfUpdate = performance.now();
|
||||
|
||||
/** @type {boolean} Whether the game is paused (e.g., tab not visible) */
|
||||
this.isPaused = false;
|
||||
|
||||
/** @type {boolean} Whether performance monitor is visible */
|
||||
this.perfMonitorVisible = false;
|
||||
|
||||
/** @type {Object} Shake detection state */
|
||||
this.shakeDetection = {
|
||||
lastX: 0,
|
||||
lastY: 0,
|
||||
lastZ: 0,
|
||||
shakeThreshold: 15,
|
||||
shakeCount: 0,
|
||||
lastShakeTime: 0
|
||||
};
|
||||
|
||||
this.init();
|
||||
this.setupEventListeners();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the game (ECS, Three.js, entities)
|
||||
*/
|
||||
init() {
|
||||
// Initialize ECS
|
||||
this.world = new World();
|
||||
|
||||
// Setup Three.js
|
||||
this.setupScene();
|
||||
this.setupCamera();
|
||||
this.setupRenderer();
|
||||
this.setupLights();
|
||||
this.setupGround();
|
||||
|
||||
// Create entity factory
|
||||
this.entityFactory = new EntityFactory(this.world, this.scene);
|
||||
|
||||
// Initialize systems
|
||||
this.setupSystems();
|
||||
|
||||
// Create game entities
|
||||
this.createGameEntities();
|
||||
}
|
||||
|
||||
setupScene() {
|
||||
this.scene = new window.THREE.Scene();
|
||||
this.scene.background = new window.THREE.Color(0x87CEEB);
|
||||
this.scene.fog = new window.THREE.Fog(0x87CEEB, 0, 50);
|
||||
}
|
||||
|
||||
setupCamera() {
|
||||
this.camera = new window.THREE.PerspectiveCamera(
|
||||
75,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
this.camera.position.set(0, 10, 15);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
}
|
||||
|
||||
setupRenderer() {
|
||||
this.renderer = new window.THREE.WebGLRenderer({ antialias: true });
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
this.renderer.shadowMap.enabled = true;
|
||||
this.renderer.shadowMap.type = window.THREE.PCFSoftShadowMap;
|
||||
|
||||
document.body.appendChild(this.renderer.domElement);
|
||||
}
|
||||
|
||||
setupLights() {
|
||||
// Increased ambient light for brighter scene (was 0.6)
|
||||
const ambientLight = new window.THREE.AmbientLight(0xffffff, 0.6);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
// Increased directional light for better clarity (was 0.8)
|
||||
const directionalLight = new window.THREE.DirectionalLight(0xffffff, 3.0);
|
||||
directionalLight.position.set(10, 20, 10);
|
||||
directionalLight.castShadow = true;
|
||||
directionalLight.shadow.mapSize.width = 2048;
|
||||
directionalLight.shadow.mapSize.height = 2048;
|
||||
directionalLight.shadow.camera.left = -20;
|
||||
directionalLight.shadow.camera.right = 20;
|
||||
directionalLight.shadow.camera.top = 20;
|
||||
directionalLight.shadow.camera.bottom = -20;
|
||||
directionalLight.shadow.camera.near = 0.5;
|
||||
directionalLight.shadow.camera.far = 50;
|
||||
this.scene.add(directionalLight);
|
||||
}
|
||||
|
||||
setupGround() {
|
||||
const groundGeometry = new window.THREE.PlaneGeometry(this.groundSize, this.groundSize);
|
||||
const groundMaterial = new window.THREE.MeshStandardMaterial({
|
||||
color: 0x90EE90,
|
||||
roughness: 0.8
|
||||
});
|
||||
const ground = new window.THREE.Mesh(groundGeometry, groundMaterial);
|
||||
ground.rotation.x = -Math.PI / 2;
|
||||
ground.receiveShadow = true;
|
||||
this.scene.add(ground);
|
||||
|
||||
const gridHelper = new window.THREE.GridHelper(this.groundSize, 20, 0x000000, 0x000000);
|
||||
gridHelper.material.opacity = 0.2;
|
||||
gridHelper.material.transparent = true;
|
||||
this.scene.add(gridHelper);
|
||||
}
|
||||
|
||||
setupSystems() {
|
||||
// Input system (must be first)
|
||||
this.inputSystem = new InputSystem();
|
||||
this.world.addSystem(this.inputSystem);
|
||||
|
||||
// Player control
|
||||
this.world.addSystem(new PlayerControlSystem(this.inputSystem));
|
||||
|
||||
// Movement and physics
|
||||
this.world.addSystem(new MovementSystem());
|
||||
this.world.addSystem(new BoundarySystem());
|
||||
this.world.addSystem(new ObstacleSystem());
|
||||
|
||||
// Game-specific behavior
|
||||
this.world.addSystem(new CoinSystem());
|
||||
|
||||
// Collision detection
|
||||
this.collisionSystem = new CollisionSystem();
|
||||
this.collisionSystem.onCollision((entity1, entity2, layer1, layer2) => {
|
||||
this.handleCollision(entity1, entity2, layer1, layer2);
|
||||
});
|
||||
this.world.addSystem(this.collisionSystem);
|
||||
|
||||
// Rendering (must be last to sync transforms)
|
||||
this.world.addSystem(new RenderSystem(this.scene));
|
||||
}
|
||||
|
||||
createGameEntities() {
|
||||
// Create player
|
||||
this.playerEntity = this.entityFactory.createPlayer();
|
||||
|
||||
// Create coins
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const coin = this.entityFactory.createCoin(this.coins.length);
|
||||
this.coins.push(coin);
|
||||
}
|
||||
|
||||
// Create obstacles
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const obstacle = this.entityFactory.createObstacle();
|
||||
this.obstacles.push(obstacle);
|
||||
}
|
||||
}
|
||||
|
||||
handleCollision(entity1, entity2, layer1, layer2) {
|
||||
if (!this.gameActive) return;
|
||||
|
||||
// Player-Coin collision
|
||||
if ((layer1 === 'player' && layer2 === 'coin') || (layer1 === 'coin' && layer2 === 'player')) {
|
||||
const coinEntity = layer1 === 'coin' ? entity1 : entity2;
|
||||
this.collectCoin(coinEntity);
|
||||
}
|
||||
|
||||
// Player-Obstacle collision
|
||||
if ((layer1 === 'player' && layer2 === 'obstacle') || (layer1 === 'obstacle' && layer2 === 'player')) {
|
||||
const playerEntity = layer1 === 'player' ? entity1 : entity2;
|
||||
const obstacleEntity = layer1 === 'obstacle' ? entity1 : entity2;
|
||||
this.handleObstacleCollision(playerEntity, obstacleEntity);
|
||||
}
|
||||
}
|
||||
|
||||
collectCoin(coinEntity) {
|
||||
// Remove coin
|
||||
this.entityFactory.destroyEntity(coinEntity);
|
||||
const index = this.coins.indexOf(coinEntity);
|
||||
if (index > -1) {
|
||||
this.coins.splice(index, 1);
|
||||
}
|
||||
|
||||
// Update score
|
||||
this.score += 10;
|
||||
this.updateUI();
|
||||
|
||||
// Spawn new coin
|
||||
const newCoin = this.entityFactory.createCoin(this.coins.length);
|
||||
this.coins.push(newCoin);
|
||||
}
|
||||
|
||||
handleObstacleCollision(playerEntity, obstacleEntity) {
|
||||
const health = this.world.getComponent(playerEntity, Health);
|
||||
const playerTransform = this.world.getComponent(playerEntity, Transform);
|
||||
const obstacleTransform = this.world.getComponent(obstacleEntity, Transform);
|
||||
|
||||
// Damage player
|
||||
const isDead = health.damage(1);
|
||||
|
||||
// Push player back
|
||||
const pushDirection = playerTransform.position.clone().sub(obstacleTransform.position);
|
||||
pushDirection.y = 0;
|
||||
pushDirection.normalize();
|
||||
playerTransform.position.add(pushDirection.multiplyScalar(0.3));
|
||||
playerTransform.position.y = 0.5;
|
||||
|
||||
this.updateUI();
|
||||
|
||||
if (isDead) {
|
||||
this.gameOver();
|
||||
}
|
||||
}
|
||||
|
||||
updateCamera() {
|
||||
if (!this.playerEntity) return;
|
||||
|
||||
const playerTransform = this.world.getComponent(this.playerEntity, Transform);
|
||||
if (playerTransform) {
|
||||
this.camera.position.x = playerTransform.position.x;
|
||||
this.camera.position.z = playerTransform.position.z + 15;
|
||||
this.camera.lookAt(playerTransform.position);
|
||||
}
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
document.getElementById('score').textContent = this.score;
|
||||
|
||||
const health = this.world.getComponent(this.playerEntity, Health);
|
||||
if (health) {
|
||||
document.getElementById('health').textContent = Math.max(0, health.currentHealth);
|
||||
}
|
||||
}
|
||||
|
||||
gameOver() {
|
||||
this.gameActive = false;
|
||||
document.getElementById('finalScore').textContent = this.score;
|
||||
document.getElementById('gameOver').style.display = 'block';
|
||||
}
|
||||
|
||||
restart() {
|
||||
// Clean up old entities
|
||||
[...this.coins].forEach(coin => this.entityFactory.destroyEntity(coin));
|
||||
[...this.obstacles].forEach(obstacle => this.entityFactory.destroyEntity(obstacle));
|
||||
if (this.playerEntity) {
|
||||
this.entityFactory.destroyEntity(this.playerEntity);
|
||||
}
|
||||
|
||||
this.coins = [];
|
||||
this.obstacles = [];
|
||||
|
||||
// Reset game state
|
||||
this.score = 0;
|
||||
this.gameActive = true;
|
||||
this.lastTime = performance.now(); // Reset timer to prevent deltaTime spike
|
||||
|
||||
// Recreate entities
|
||||
this.createGameEntities();
|
||||
|
||||
// Hide game over screen
|
||||
document.getElementById('gameOver').style.display = 'none';
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
window.addEventListener('resize', () => this.onWindowResize());
|
||||
document.getElementById('restartBtn').addEventListener('click', () => this.restart());
|
||||
|
||||
// Toggle performance monitor with 'T' key
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key.toLowerCase() === 't') {
|
||||
this.togglePerformanceMonitor();
|
||||
}
|
||||
});
|
||||
|
||||
// Shake detection for mobile
|
||||
if (window.DeviceMotionEvent) {
|
||||
window.addEventListener('devicemotion', (e) => this.handleDeviceMotion(e), false);
|
||||
}
|
||||
|
||||
// Pause game when tab loses focus
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
this.isPaused = true;
|
||||
console.log('Game paused (tab hidden)');
|
||||
} else {
|
||||
this.isPaused = false;
|
||||
// Reset timer to prevent deltaTime spike
|
||||
this.lastTime = performance.now();
|
||||
console.log('Game resumed');
|
||||
}
|
||||
});
|
||||
|
||||
// Also handle window blur/focus as fallback
|
||||
window.addEventListener('blur', () => {
|
||||
this.isPaused = true;
|
||||
});
|
||||
|
||||
window.addEventListener('focus', () => {
|
||||
if (!document.hidden) {
|
||||
this.isPaused = false;
|
||||
this.lastTime = performance.now();
|
||||
}
|
||||
});
|
||||
|
||||
// Load version
|
||||
this.loadVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle performance monitor visibility
|
||||
*/
|
||||
togglePerformanceMonitor() {
|
||||
this.perfMonitorVisible = !this.perfMonitorVisible;
|
||||
const monitor = document.getElementById('perfMonitor');
|
||||
if (this.perfMonitorVisible) {
|
||||
monitor.classList.add('visible');
|
||||
console.log('Performance monitor enabled');
|
||||
} else {
|
||||
monitor.classList.remove('visible');
|
||||
console.log('Performance monitor disabled');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle device motion for shake detection
|
||||
* @param {DeviceMotionEvent} event
|
||||
*/
|
||||
handleDeviceMotion(event) {
|
||||
const acceleration = event.accelerationIncludingGravity;
|
||||
if (!acceleration) return;
|
||||
|
||||
const currentTime = Date.now();
|
||||
const timeDiff = currentTime - this.shakeDetection.lastShakeTime;
|
||||
|
||||
if (timeDiff > 100) { // Check every 100ms
|
||||
const { x = 0, y = 0, z = 0 } = acceleration;
|
||||
|
||||
const deltaX = Math.abs(x - this.shakeDetection.lastX);
|
||||
const deltaY = Math.abs(y - this.shakeDetection.lastY);
|
||||
const deltaZ = Math.abs(z - this.shakeDetection.lastZ);
|
||||
|
||||
if (deltaX + deltaY + deltaZ > this.shakeDetection.shakeThreshold) {
|
||||
this.shakeDetection.shakeCount++;
|
||||
|
||||
// Toggle after 2 shakes within 500ms
|
||||
if (this.shakeDetection.shakeCount >= 2) {
|
||||
this.togglePerformanceMonitor();
|
||||
this.shakeDetection.shakeCount = 0;
|
||||
}
|
||||
} else {
|
||||
this.shakeDetection.shakeCount = 0;
|
||||
}
|
||||
|
||||
this.shakeDetection.lastX = x;
|
||||
this.shakeDetection.lastY = y;
|
||||
this.shakeDetection.lastZ = z;
|
||||
this.shakeDetection.lastShakeTime = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
loadVersion() {
|
||||
fetch('/version.json')
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error('Version file not found');
|
||||
})
|
||||
.then(data => {
|
||||
const versionElement = document.getElementById('versionNumber');
|
||||
if (versionElement && data.version) {
|
||||
versionElement.textContent = data.version;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.debug('Version information not available:', error.message);
|
||||
});
|
||||
}
|
||||
|
||||
onWindowResize() {
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main game loop - calculates deltaTime and updates all systems
|
||||
* @param {number} [currentTime] - Current timestamp from requestAnimationFrame
|
||||
*/
|
||||
animate(currentTime = performance.now()) {
|
||||
requestAnimationFrame((time) => this.animate(time));
|
||||
|
||||
// If paused, skip updates but keep rendering
|
||||
if (this.isPaused) {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate deltaTime in seconds
|
||||
const deltaTime = Math.min((currentTime - this.lastTime) / 1000, this.maxDeltaTime);
|
||||
this.lastTime = currentTime;
|
||||
|
||||
// Update performance monitor with smoothed values
|
||||
if (this.perfMonitorVisible) {
|
||||
// Calculate instant FPS from deltaTime
|
||||
const instantFPS = 1 / deltaTime;
|
||||
|
||||
// Smooth FPS using exponential moving average for stability
|
||||
this.smoothedFPS = this.smoothedFPS * 0.9 + instantFPS * 0.1;
|
||||
|
||||
// Update display every 100ms for real-time feel without flickering
|
||||
if (currentTime - this.lastPerfUpdate >= 100) {
|
||||
const frameTime = (deltaTime * 1000).toFixed(1);
|
||||
const entityCount = this.world.entities.size;
|
||||
|
||||
document.getElementById('fps').textContent = Math.round(this.smoothedFPS);
|
||||
document.getElementById('frameTime').textContent = frameTime;
|
||||
document.getElementById('entityCount').textContent = entityCount;
|
||||
|
||||
this.lastPerfUpdate = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.gameActive) {
|
||||
// Update ECS world with actual deltaTime
|
||||
this.world.update(deltaTime);
|
||||
|
||||
// Update camera
|
||||
this.updateCamera();
|
||||
}
|
||||
|
||||
// Render scene
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue