I'm working on a Three.js project with a Tamagotchi-style animated character using GLTF animations and an animation mixer. The project includes a robot, a hatching box, and interactive controls.
Everything runs smoothly until the browser tab is idle for a few minutes. When I return to the window the scene appears frozen and the robot stops animating.
My animation loop is in the class Experience.js
:
this.time.on('tick', () => {
this.update();
});
update() {
const deltaTime = this.time.delta;
this.world.update(deltaTime);
this.camera.update();
this.renderer.update();
}
In the class World.js
the update is:
update(deltaTime) {
if (this.box) {
this.box.update(deltaTime)
}
if (this.robot) {
this.robot.update(deltaTime)
}
}
Inside the Box.js
:
update(deltaTime) {
if (this.mixer) {
this.mixer.update(deltaTime)
}
}
In the Robot.js
:
update(deltaTime) {
if (this.tamagotchiController) {
this.tamagotchiController.update(deltaTime)
}
}
Animation updating in TamagotchiController.js
:
update(deltaTime) {
if (this.mixer) {
this.mixer.update(deltaTime);
}
}
Finally the Camera.js
update is:
update() {
if (this.controls) {
this.controls.update()
}
}
and the Renderer.js
:
update() {
this.instance.render(this.scene, this.camera.instance)
}
The live project is here
I have no idea what is going wrong because no errors appear on the console.
Here is the Time.js
used in the project:
import EventEmitter from './EventEmitter.js'
export default class Time extends EventEmitter {
constructor() {
super()
// Setup
this.start = Date.now()
this.current = this.start
this.elapsed = 0
this.delta = 0.016 // Initialize with a typical frame time in seconds
window.requestAnimationFrame(() => {
this.tick()
})
}
tick() {
const currentTime = Date.now()
this.delta = (currentTime - this.current) / 1000 // Convert milliseconds to seconds
this.current = currentTime
this.elapsed = (this.current - this.start) / 1000 // Convert milliseconds to seconds
this.trigger('tick')
window.requestAnimationFrame(() => {
this.tick()
})
}
}
I'm working on a Three.js project with a Tamagotchi-style animated character using GLTF animations and an animation mixer. The project includes a robot, a hatching box, and interactive controls.
Everything runs smoothly until the browser tab is idle for a few minutes. When I return to the window the scene appears frozen and the robot stops animating.
My animation loop is in the class Experience.js
:
this.time.on('tick', () => {
this.update();
});
update() {
const deltaTime = this.time.delta;
this.world.update(deltaTime);
this.camera.update();
this.renderer.update();
}
In the class World.js
the update is:
update(deltaTime) {
if (this.box) {
this.box.update(deltaTime)
}
if (this.robot) {
this.robot.update(deltaTime)
}
}
Inside the Box.js
:
update(deltaTime) {
if (this.mixer) {
this.mixer.update(deltaTime)
}
}
In the Robot.js
:
update(deltaTime) {
if (this.tamagotchiController) {
this.tamagotchiController.update(deltaTime)
}
}
Animation updating in TamagotchiController.js
:
update(deltaTime) {
if (this.mixer) {
this.mixer.update(deltaTime);
}
}
Finally the Camera.js
update is:
update() {
if (this.controls) {
this.controls.update()
}
}
and the Renderer.js
:
update() {
this.instance.render(this.scene, this.camera.instance)
}
The live project is here
I have no idea what is going wrong because no errors appear on the console.
Here is the Time.js
used in the project:
import EventEmitter from './EventEmitter.js'
export default class Time extends EventEmitter {
constructor() {
super()
// Setup
this.start = Date.now()
this.current = this.start
this.elapsed = 0
this.delta = 0.016 // Initialize with a typical frame time in seconds
window.requestAnimationFrame(() => {
this.tick()
})
}
tick() {
const currentTime = Date.now()
this.delta = (currentTime - this.current) / 1000 // Convert milliseconds to seconds
this.current = currentTime
this.elapsed = (this.current - this.start) / 1000 // Convert milliseconds to seconds
this.trigger('tick')
window.requestAnimationFrame(() => {
this.tick()
})
}
}
Share
Improve this question
asked yesterday
cconsta1cconsta1
8351 gold badge9 silver badges23 bronze badges
2 Answers
Reset to default 1As noted in the documentation:
requestAnimationFrame() calls are paused in most browsers when running in background tabs or hidden <iframe>s, in order to improve performance and battery life.
So, that's why the animation stops at first.
Then there is a possible issue with deltaTime. After a few minutes of idling, deltaTime becomes that few minutes. requestAnimationFrame() will resume after returning the tab, mixer.update() will receive 3-5 minutes of delta and that may break animations. Visibility API may help you handle deltaTime:
export default class Time extends EventEmitter {
constructor() {
super();
// Setup
this.start = Date.now();
this.current = this.start;
this.elapsed = 0;
this.delta = 0.016; // Typical frame time (in seconds)
// For tracking the requestAnimationFrame id
this.animationId = null;
// Bind methods to preserve context
this.tick = this.tick.bind(this);
this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
// Listen for visibility changes
document.addEventListener("visibilitychange", this.handleVisibilityChange);
// Start ticking only if the document is visible
if (!document.hidden) {
this.startTicking();
}
}
startTicking() {
if (!this.animationId) {
this.animationId = window.requestAnimationFrame(this.tick);
}
}
stopTicking() {
if (this.animationId) {
window.cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
handleVisibilityChange() {
if (document.hidden) {
this.stopTicking();
} else {
// Reset the current time to avoid a huge delta when resuming.
this.current = Date.now();
this.startTicking();
}
}
tick() {
const currentTime = Date.now();
// Calculate delta in seconds
this.delta = (currentTime - this.current) / 1000;
this.current = currentTime;
this.elapsed = (this.current - this.start) / 1000;
this.trigger('tick');
// Continue the loop if not paused
this.animationId = window.requestAnimationFrame(this.tick);
}
}
Or something like that
Thanks for the reply. It turns out that the problem was not related to the Time.js
class or the other classes' update methods. I'm working on a Tamagotchi-style robot character using the Expressive Robot from the Three.js examples and an animation mixer. The robot performs actions like feeding, playing, and cleaning. I wrote methods that create waste objects generated periodically, which the user can clean like the classic Tamagotchi game.
The scene froze after the browser tab was idle because the reset()
method (of the class creating the waste) restarted the waste creation loop without clearing the previous interval. This caused multiple intervals to stack, creating excessive waste objects and freezing the scene.
Old code (from reset()
):
this.wasteObjects.forEach(waste => this.scene.remove(waste));
this.wasteObjects = [];
this.startWasteCreation(); // Restarted without clearing old interval
The problem was that startWasteCreation()
was called without clearing the previous interval. So overlapping intervals created too many waste objects over time, causing performance issues.
New code:
if (this.wasteCreationInterval) {
clearInterval(this.wasteCreationInterval); // Clear previous interval
}
this.wasteObjects.forEach(waste => this.scene.remove(waste));
this.wasteObjects = [];
this.startWasteCreation(); // Restart safely
Now the scene no longer freezes after the tab is idle.