amethyst / specs

Specs - Parallel ECS
https://amethyst.github.io/specs/
Apache License 2.0
2.51k stars 221 forks source link

Some way to make a snapshot of part of the world #690

Open vorner opened 4 years ago

vorner commented 4 years ago

Description

I'd like to have some way to get a lightweight snapshot of part of the world state for later use. I'm not entirely sure about the form which would be best, though (I'm not really talking about full saves).

Motivation

I have two things in mind.

First is, I'd like to separate the game logic update from rendering. Right now, the canonical usage of specs seems to be something like:

let mut dispatcher = DispatcherBuilder::new()
    .with(PhysicsSystem, "physics", &[])
    .with_thread_local(RenderSystem)
    .build();

loop {
  dispatcher.dispatch(&mut world);
}

However, I'd like to be able to have one thread that does only the rendering while some other thread (or the whole threadpool) does the updates. That way, if rendering is slow, the game logic would still be able to keep up nevertheless, just some frames would be skipped. So I'd like to take a snapshot of all the entities that can be drawn and their positions after each logic-update, store this snapshot somewhere (eg. in a global Arc). The renderer thread would take the latest exported thread and render that.

Another use case I see is a game where communication would be slow. You'd see the events that happened far away only if a unit came from there, but it would only be able to show the state at the time it was there, not the current one. Such thing would need the units to „remember“ some older states (small parts of them, actually).

I'm thinking something in lines of having component storages based on the im crate might help with that, and being able to somehow take the snapshot and import it into another copy of the world… but I don't have concrete ideas. Or are there some other ways I'm overlooking?

Drawbacks

If it was only adding few more storage types, possibly behind a feature flag, then it shouldn't be breaking.

Probably, but only when these snapshots would be used ‒ at least when used, such storages might need more memory and be slower.

Unresolved questions

Well, how it would actually look like specifically 😇


Please indicate here if you'd like to work on this ticket once it's been approved. Feel free to delete this section if not.

Yes, but I'd probably need some help/guidance with designing the API and approval first.

OvermindDL1 commented 4 years ago

You can have multiple Dispatcher's for note. :-)

A snapshot doesn't sound terribly efficient, but if you are wanting to keep, say, two worlds between logic and render then I'd probably do exactly that, have changes and updates on the logic send send across data to the render thread that the renderer needs to be able to render (and no more), crossbeam channels are useful for that in my tests, though abstracting it out more to be fully client/server internally might not be a bad idea as it would make setting up multiplayer more easy in the future.

vorner commented 4 years ago

I don't think crossbeam channels are the appropriate thing here (because they can be set up to lose the newest, but not the oldest value, you need just one value and you don't need wake up the other side). But I'm sure there are appropriate synchronization mechanisms for that.

So your proposal is to somehow take the data from one world, transfer them and inject them into another world. How would I do that? Somehow try to read the raw storage(s) + all the entities and plug them in into the other world?

OvermindDL1 commented 4 years ago

So your proposal is to somehow take the data from one world, transfer them and inject them into another world. How would I do that?

Same way you'd do it in multiplayer. Think of one side as the 'server' and the other side as the 'client'. The client only needs to know enough to represent the world visually, the server is what holds the actual processing data, but doesn't actually need to hold model information or so. A layer between them to pass data directly is the usual thing to do, you just pretend it's packet transfers over tcp/udp/whatever. A common pattern is to actually send packets between the 'client' and 'server' code in the same program first, then abstract it out to actually send memory later instead.

vorner commented 4 years ago

Right, that's what I mostly wanted to avoid. I don't know if it's just my sense of engineering elegance, but having to do some kind of export to other format and then import somewhere else seems like needless work (both for me and the computer). For this to work, there needs to be some kind of mapping between entity IDs in the first and second world, going over all the data at least two more times…

Then, you can either send the whole state each frame, even if most of it doesn't change, or you need to track and generate diffs. But then you can't just skip some if you don't keep up.

My idea was more in the sense of this (omitting lifetimes and other letter soup):

impl System for ExportSystem {
    type SystemData = (Entities, ReadStorage<Position>, ReadStorage<Image>);
   fn run(&mut self, data: SystemData) {
     *global_storage.lock() = Arc::new(Export {
        entities: data.0.clone(),
        positions: data.1.clone(),
        images: data.2.clone(),
     });
   }
}

The other world would have something like this:

let current_export = Arc::clone(global_storage.lock());
world.insert(current_export.entities.clone());
world.insert(current_export...);

draw_dispacter.dispatch(&mut world);

For this to work, several things would be needed, though:

I believe both could be tweaked somehow (eg. having a WoldImpl<EntityStorage> generic somewhere and World being a type alias for World<Entities> or whatever the real type is right now). I'm thinking having the entity as generic parameter might allow flexibility for other reasons too ‒ like using some other type than u32 as the ID (which might be too little or too much for some use cases). Or allowing to have tracking storage for entities too (currently you can track components, but not entities, or at least I haven't found a way).

If I'm not explaining myself clearly, I might try putting some kind of PoC together, but currently I don't have an idea how much work that would be.

OvermindDL1 commented 4 years ago

or you need to track and generate diffs

Doing this via mapping the events identically on all sides is generally known as the lockstep, or Deadman's simulation, and is also quite popular for certain types of game or keeping multiple simulations concurrently at different 'steps' for interpolation and synchronization. The nice bit about this is you only need to record the 'inputs' into the systems, as long as your systems are deterministic (so fake 'noise' randomness, not any normal random calls and so forth, careful float usage, etc...) then it will be identical output every time. As an ancient example the game Doom's replay files are tiny because it only need to record the level ID, the level seed, and the user inputs to work. :-)

My idea was more in the sense of this (omitting lifetimes and other letter soup):

You are actually getting a style very close to what I've been experimenting with in an ECS for a week now. ^.^

If I'm not explaining myself clearly, I might try putting some kind of PoC together, but currently I don't have an idea how much work that would be.

Making at least a pseudo-PoC for specs would be very useful and help flesh out the idea, I'm all for you looking in to it!

vorner commented 4 years ago

Doing this via mapping the events identically on all sides is generally known as the lockstep, or Deadman's simulation, and is also quite popular for certain types of game or keeping multiple simulations concurrently at different 'steps' for interpolation and synchronization.

That is all very nice, but I'm not really trying to do a multiplayer here 😇.

Making at least a pseudo-PoC for specs would be very useful and help flesh out the idea, I'm all for you looking in to it!

I've tried looking into it. However, my attempt at making the EntitiesRes type clonable or, to make it a different range than i32 or to identify what the behaviour of the type needs to satisfy hasn't been successful yet, mostly because it seems quite interlinked with non-trivial assumptions across all the bitsets and how it is used. I might get to it eventually and give it some more time, but it doesn't seem doable as 1-2 hour PoC :-(.

Nevertheless, I wonder about one thing. If I need the entities at all. I'll try experimenting if a world without entities in it (or with entities not matching the actual components) behave in some sane way.