-
Notifications
You must be signed in to change notification settings - Fork 115
How to use ecsy with fixed time step physics and variable rate render game loops ? #136
Comments
After researching this over the past few days, I have concluded that there is no clean easy way to do this with ECSY's current architecture. That's unfortunate because one of ECSY's "selling" feature is that it is supposed to be "framework independant". As such, it should be relatively easy to use in any kind of game engine. Many engines use game loops that feature updates that run on fixed time steps (usually for physics simulation) along with variable rate updates (for rendering, among other things). To use an ECS architecture with such a game engine, one requires a means to designate in which update each system should be executed. At the moment, here is an example of what I have to do to make this work with my game loop: const loop = new GameLoop({ input, update, render });
const world = new World()
...
// register components and systems
// create entities
...
const inputSystem = world.getSystem(InputSystem);
const renderSystem = world.getSystem(RenderSystem);
const updateSystems = world.getSystems().filter(system => !(system instanceof RenderSystem) && !(system instanceof InputSystem));
function input() {
inputSystem.execute();
inputSystem.clearEvents();
}
function update(step: number) {
updateSystems.forEach(system => {
system.execute(step)
system.clearEvents();
})
}
function render(interpolation: number) {
renderSystem.execute(interpolation);
renderSystem.clearEvents();
}
loop.start(); I'm sure you can see how this could quickly get problematic as the number and type of systems grows. Not to mention the maintenance difficulty as systems are added, removed and modified. Unity's new ECS engine has solved this issue by using groups of systems. I'd like to propose implementing a similar, simplified, feature in ECSY that would make it possible to do something along the lines of const loop = new GameLoop({ input, update, render });
const world = new World()
...
// register components and systems
// create entities
...
function input() {
world.inputSystems.execute();
}
function update(step: number) {
world.updateSystems.execute(step);
}
function render(interpolation: number) {
world.renderSystems.execute(interpolation);
}
loop.start(); Of course, it would still be possible to do I have many ideas on how to implement this simply and would love to work on it and submit a PR but before I move ahead with further discussion and work on the issue, @fernandojsg I'd like to know if you'd be interested in integrating this feature into ECSY at this moment and if it would be worth it for me to begin work on this. |
I think you may be looking at this wrong way. The logic for handling different fixed timesteps in certain systems belongs in the systems themselves, not at the (framework agnostic) world/engine layer. Generally speaking, you should use Most of your systems will update on this same timestep (including a RenderSystem) so nothing is needed for these. For things like the PhysicsSystem you will probably want to run this at a fixed 60hz due to performance and consistency (most physics engines work better with fixed timesteps). For a NetworkSystem you will probably want to run this at around 10hz in a browser environment. I used to think things like this needed happen in "actual time", meaning i would need multiple timers to land each update call based on its timestep, but time is relative and in actual fact they just need to be dilated and spaced out correctly. In order to do this, you can use an accumulator to track delta time and land updates in your systems in whatever timesteps you want. Here's how you create a fixed timestep update: function createFixedTimestep(timestep, callback) {
let accumulator = 0
return delta => {
accumulator += delta
while (accumulator >= timestep) {
callback(accumulator)
accumulator -= timestep
}
}
} And here is how you might use it in ECSY: class PhysicsSystem extends System {
constructor() {
this.fixedUpdate = createFixedTimestep(1 / 60, this.onFixedUpdate)
}
execute(delta) {
this.fixedUpdate(delta)
}
onFixedUpdate(delta) {
// simulate physics...
}
} What this does is essentially creates a buffer of deltas. When there is enough in the buffer it will call the callback with the new delta. If your Hopefully this helps you out :) PS: I've been building my own ECS (likely for the same reasons Mozilla needed this one) and i'm only here to have a look around and trade notes. |
@ashconnell Very interesting, thanks for taking the time to share. Personally though, I am not a fan of having this kind of logic inside each systems. It feels like a cross-cutting concern to me and unnecessary coupling. This may work fine in a project that only has a few systems and a simple loop but imagine you are working on a complex game, using a game engine similar to Unity. Do you really want to have to implement this logic in tens or hundreds of your systems? What if you change your implementation and want a bunch of systems that were previously running on a fixed time step to now run at variable rate? You'd have to go through all of them and modify their code. What if you had your timestamp running at 1/60 but now want to change to 1/30? Again, you'd have to hunt through the systems and modify the code. I think it makes more sense to keep the update timing concern outside of systems and centralized in your game engine / loop. Game engines like Unity and others already have a game loop offering update functions with various timings. It seems to me that if I want to use ECSY in game engines like that, I would need a way to group my systems together and execute them in groups. Then again I'm new to all this and I may be completely wrong. That's why I'm waiting for @fernandojsg 's input before working on this. |
The code i pasted above actually originates from some Unity and Unreal forums where they were doing the same thing. I simplified it a bit. Up to you if you want to use it, but IMO systems are meant to be independent of each other. If you change the timestep of one system and another system breaks, then your systems are too coupled. The only reason you should really need fixed timestep is for physics, snapshots and networking. The rest can just update on the normal requestAnimationFrame which is powering your World.execute(). I don't think its a good idea to put fixed timesteps on absolutely everything anyway. |
This idea appeals to me but I think that no matter if we keep the timing logic inside or outside, systems are inherently coupled. They are basically part of a pipeline. Unless you want huge systems that do everything, your systems will read and write from the same data sources and need to do it in a very specific order for your game to work. That's why ECSY gives us the ability to set that order. I'm saying it should go a bit further and do like Unity's ECS by giving us the ability to group systems and execute those groups independently.
Completely agree. But if you have a complex simulation and want to have small specific systems, you can easily end up with hundreds of them nonetheless. We're discussing whether one should put timing logic inside systems or not and honestly I can see advantages and disadvantages to both. What I'm advocating for is the ability to easily do it one way or another. My game engine already supplies a centralized system for updating game state at different moments during the game loop, it feels silly to basically duplicate part of that systems's logic inside many of my ECS's systems. Your suggestions seem based on relatively simple use cases, and I'm sure it works great, but if you work on something complex and use a game engine that offers something similar to Unity's game loop surely you can see the advantages of being able to group, control and execute systems together. Just to be clear, I'm not saying ECSY should manage the various update timings. I'm only saying it should give us the ability to do it ourselves, outside of ECSY's systems. A simple way to group systems together would do the trick. It would allow me to truly use ECSY with my existing game engine, the way it's meant to be used, without having to essentially rewrite parts of the engine inside various ECS systems. |
Hi @snowfrogdev @ashconnell sorry for the long delay answering, I've been quite busy recently :)
For the first one I thought initially about providing an API to create a group based on several systems, but I feel it's more interesting to be able to add tags to the systems, so each system could have more than one tag if needed. And then you could have a way to query the systems based on a specific tag. For the second one, things are a bit trickier to provide a generic solution so probably, in the meantime, you should be using a combination of the fixed time step proposal by @ashconnell that I agree is commonly used in many engines when dealing with fixed time steps, or using the tags functionality to help implementing the @snowfrogdev proposal |
@ashconnell How would you handle the entity interpolation in the Render system? I am having a hard time with smooth movement. It's super jittery for me. Demo: https://nickyvanurk.com/void/ EDIT: It seems to work OK, the jitter is from my camera follow code (lerp) I believe. Still curious if there is a better way to get the interpolation value to the renderer. Here I return the normalized value of how much I am into the next frame. I save this value in the NextFrameNormal component in order to carry it to the render system later. export default function createFixedTimestep(timestep: number, callback: Function) {
let lag = 0;
return (delta: number) => {
lag += delta;
while (lag >= timestep) {
callback(timestep);
lag -= timestep;
}
return lag / timestep;
};
}; I use this fixed update loop in my physics system like so: init() {
const timestep = 1000/60;
this.fixedUpdate = createFixedTimestep(timestep, this.handleFixedUpdate.bind(this));
}
execute(delta: number) {
const nextFrameNormal = this.fixedUpdate(delta);
this.queries.nextFrameNormal.results.forEach((entity: any) => {
const _nextFrameNormal = entity.getMutableComponent(NextFrameNormal);
_nextFrameNormal.value = nextFrameNormal;
});
}
handleFixedUpdate(delta: number) {
this.queries.players.results.forEach((entity: any) => {
const input = entity.getMutableComponent(PlayerInputState);
const transform = entity.getMutableComponent(Transform);
const physics = entity.getMutableComponent(Physics);
physics.velocity.x += physics.acceleration*delta * input.movementX;
physics.velocity.y += physics.acceleration*delta * input.movementY;
physics.velocity.z += physics.acceleration*delta * input.movementZ;
transform.position.x += physics.velocity.x*delta;
transform.position.y += physics.velocity.y*delta;
transform.position.z += physics.velocity.z*delta;
physics.velocity.x *= Math.pow(physics.damping, delta/1000);
physics.velocity.y *= Math.pow(physics.damping, delta/1000);
physics.velocity.z *= Math.pow(physics.damping, delta/1000);
});
} Once in the RenderSystem I use the NextFrameNormal value to interpolate the correct position: const nextFrameNormalEntity = this.queries.nextFrameNormal.results[0];
const nextFrameNormal = nextFrameNormalEntity.getComponent(NextFrameNormal).value;
this.queries.object3d.results.forEach((entity: any) => {
const mesh = entity.getMutableComponent(Object3d).value;
if (entity.hasComponent(Transform)) {
const transform = entity.getComponent(Transform);
mesh.position.x = transform.position.x;
mesh.position.y = transform.position.y;
mesh.position.z = transform.position.z;
if (entity.hasComponent(Physics)) {
const physics = entity.getComponent(Physics);
mesh.position.x += physics.velocity.x*(1000/60)*nextFrameNormal;
mesh.position.y += physics.velocity.y*(1000/60)*nextFrameNormal;
mesh.position.z += physics.velocity.z*(1000/60)*nextFrameNormal;
}
mesh.rotation.x = transform.rotation.x;
mesh.rotation.y = transform.rotation.y;
mesh.rotation.z = transform.rotation.z;
}
}); |
@nickyvanurk it seems pretty smooth to me. If it's the camera movement that is jittery I probably wouldn't put that in a fixed timestep and instead have it lerp to its target location independently, at full framerate. Also, that NextFrameNormal stuff feels like it might be a hack on top of a deeper problem somewhere in your systems, but its hard to tell. (looking neat though!) |
How would you split the PhysicsSystem physics and collision code? this.queries.collisions.results.forEach... doesn't seem to work properly in the fixedUpdate method, I think such queries are supposed to be only called from execute? Oh and I 'solved' the NextFrameNormal code to save a new variable in my transform component: renderPosition/renderRotation..still feels hacky but whatever :D |
From the docs I can see that the usual way to run a tick through the registered systems is to call World.execute(dt, time) but that doesn’t seem compatible with the way my engine works. Some of the systems need to be run in the update function of my game loop and some in the render function. Is there a preferred way to deal with this?
The text was updated successfully, but these errors were encountered: