ecsyjs / ecsy

Entity Component System for javascript
https://ecsyjs.github.io/ecsy/
MIT License
1.11k stars 115 forks source link

How to use ecsy with fixed time step physics and variable rate render game loops ? #136

Open snowfrogdev opened 4 years ago

snowfrogdev commented 4 years ago

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?

snowfrogdev commented 4 years ago

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 world.execute(dt) for simpler cases that only have one update loop.

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.

ashconnell commented 4 years ago

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 window.requestAnimationFrame (or XRSession.requestAnimationFrame in WebXR) to power your World.execute() calls.

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 World.execute() runs at 60hz and you have a fixedUpdate of 30hz then it would be triggered once every second execute. If it runs at 60hz and you have a fixedUpdate of 120hz it will run twice per execute. Another benefit is that if your framerate speeds up or down, the accumulator correctly spaces things out for you in order to maintain your fixed timestep.

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.

snowfrogdev commented 4 years ago

@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.

ashconnell commented 4 years ago

@snowfrogdev

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.

snowfrogdev commented 4 years ago

@ashconnell

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.

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.

I don't think its a good idea to put fixed timesteps on absolutely everything anyway.

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.

fernandojsg commented 4 years ago

Hi @snowfrogdev @ashconnell sorry for the long delay answering, I've been quite busy recently :) This is an issue we have been discussing internally several times. It can be split in two features I believe:

  1. Having the possibility to group systems just for the sake of simplify your app code, eg: if you want to enable or disable a group of systems based on your app state. This could be taken as syntactic sugar as is really something that you can do by yourself but still it makes your life easier and eventually it could be used to order the system execution, execute each group independently or whatever. Initially I thought about having creating a group where you could add systems
  2. Provide a flexible enough solution to how to schedule your systems execution.

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. This is a feature that I believe we could be landing quite soon even if initially we may not provide helper functions for all the use cases related to tags though.

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

nickyvanurk commented 4 years ago

@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/ Controls: W A S D ControlLeft Space

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;
      }
    });
ashconnell commented 4 years ago

@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!)

nickyvanurk commented 4 years ago

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