TeamPorcupine / ProjectPorcupine

Project Porcupine: A Base-Building Game...in Space!
GNU General Public License v3.0
484 stars 279 forks source link

TimeManager, Updates, and offscreen updates #1726

Closed koosemose closed 7 years ago

koosemose commented 7 years ago

Presently FurnitureManager shuffles furnitures between two collections when the camera moves, depending on rather or not they're on screen, this is so that less time is spent updating things that aren't visible, and therefore don't need to update as fast to keep their animations smooth. This of course causes more time being taken maintain those lists every time the camera moves (and creating garbage to be collected). Additionally, there is the possibility of a given furniture losing or gaining a little bit of time depending on when it is moved relative to the slow updates.

The first can be reduced by managing the furniture in chunks, so it takes less processing (as @BraedonWooding has done in #1695 ) or by not calling updates (and not managing on the visible/invisible lists) on things that don't actually need updated, such as walls (as I have done in #1721 ). Neither of these do anything about the second (and mine may in fact make it more likely to happen as it spreads the slow updates out).

However, I think with my reduction to the number of objects updating (most of our time is spent calling updates on the very large number of walls, particularly asteroids, which don't actually do anything) may eliminate the need of slowing updates on nonvisible objects, which in turn, of course, means no time used on camera move to shuffle things between collections, and no time irregularities from things moving between them.

Additionally it would allow me to make my system in the experiment in #1721, which I intend to be merged once it is completed, more intelligently handle ensuring each object (IUpdatable) is updated once and only once every cycle. Otherwise the more complex structures required to track if an IUpdatable has been updated leads to the switching between visible and invisible take a lot more time, and the whole system require a great deal more complexity, and even then I don't think it can operate with complete accuracy as the camera moves around.

So I would like thoughts and opinions on removing the whole visible/invisible separation, on the basis that if we can reduce time wasted calling update on things that don't actually do anything, we have a lot more time available to update things that do even at full speed. In my experience (both playing and watching others play) the bulk of objects built in a base building game are walls or other non active things, with a relatively small number of active objects (probably little more than 50-100 on a larger game), and since most of it is going to be in a single base, that means it's likely to all be on screen at the same time (or within 1 screen of scrolling for a very large base), and if we can't handle updating everything the player builds at one time at full speed (since even with a large base it's easy to get it all on screen by zooming out) without performance suffering, we need alternate methods to reduce strain on the system than something that won't help in the most common circumstances.

BraedonWooding commented 7 years ago

Just wanna say that thats what chunking does it has visible/non-visible chunks but its a flag.  Personally its really just moving the updates out of the furniture manager (in my PR there are no updates in furniture manager it hooks right into chunk manager), and I feel that one could merge the changes with the chunks and your changes as well quite successfully 😀.  That way it could further optimise?

koosemose commented 7 years ago

The potential issue, and this may come from me just not fully understanding the underlying bits of your chunk code, is that the updates need to be applied evenly, and not per chunk, since things are likely to cluster in a few chunks (particularly with not updating furniture not even being in the collections to be updated), so if it were to do, for example, 10 chunks per cycle (If I remember correctly you chunk into 10 by 10, so 100 chunks spread across 10 frames), it's quite likely on one of the frames, we are going to update chunks that contain the bulk of the furniture that needs updating, no longer spreading it out evenly.

The other issue is that to spread the updates out optimally, without missing or redoing some, we need to keep track of what has and hasn't updated in a given cycle, because just taking a 10th of the collection starting from a certain point as things are added or removed the relevant numbers will move about and the position in the list isn't reliable, so something could either miss a whole cycle of updates or gain an extra cycle, as things are added or removed (either due to new objects being created or being moved about), so optimally we need to set a flag as to rather or not it was updated. And moving a fast updating updatable back and forth between fast and slow updating, rather or not it was updated becomes less meaningful. I'm honestly not certain how to implement tracking rather or not an object has been updated in a meaningful way with visible/invisible furniture.

Additionally I want to (and in fact am in the latest experimental PR) apply the IUpdatable thing to Characters, for which the current visible/invisible thing doesn't work so well, since if you pan them off the screen so they go to the invisible list, and then they walk back on screen they'll still be slow updating (so moving chunkier), and it would need further ways to handle if it's visible/invisible.

I may continue my evolving my experiment towards what I'm talking about (and removing the visible/invisible thing), possibly in a separate fork of my main experimental branch, so you can more clearly see what I'm talking about (and so I can develop the core idea without having to work awkwardly around the visible/invisible thing), and then you can see if you can layer chunking over top of something closer to the final version.

BraedonWooding commented 7 years ago

Fair point and yes it does basically do that however what you would do is say update 10% of total chunks (so if you had 100 chunks you would do 10% each time) every frame iterating through.  Now you still would have heavily populated chunks but in reality a 10x10 isn't that large and in rimworld often you have fewer then 6 machines in that area (due to them being multi-tile and often just around the edges), now i still recognise there being the potential problem of mass machines but i feel that it wouldn't effect the speed anyway and it would be offset by easier chunks later on. Essentially if the game took longer to process one chunk, it would speed up the next frame essentially only causing a single frame dip rarely.  Also it ignores empty chunks anyway so you could further spread it out idk I'm not really to on the side of chunking but i feel it could further be a performance boost. 

BraedonWooding commented 7 years ago

And I also want to point out that in reality missing an update or getting an extra one is fine in contrast of having an overly complicated system.  Since its only 1/6th of a second and happens so rarely.

NogginBops commented 7 years ago

I think that it's a little bit weird that we decided to slow the updates down instead of fixing the code (I do understand why though). I think that @koosemose has the most logical solution; don't spend time on tiles that don't need it. I think that if we wanted to that solution and maybe some more performance thinking (coroutines?) could get updates back to real time.

I haven't been that involved with the code lately so I don't know how much of a hassle coroutines would be to implement.

BraedonWooding commented 7 years ago

Just wanted to point out that we did fix the code.  Just these were ways to further optimise also both options to avoid tiles/updates not needed to be done, its just either do you take a percentage of furniture as a whole, meaning that you need to store positions and it has more problems with indexes (where my concerns lie) or do you chunk the map at the beginning and take a percentage of those chunks which provides more 'safety' with a small bottleneck maybe 1 in 10 frames causing at most a frame every 10 frames to be slower which at most could result in it dipping every few seconds by a frame or two (at max).  Now you can combine the ideas but it probably is then too much. Like currently there is a PR for chunking and it works well providing a measurable difference.  So maybe we pull that in then when this experimental change happens later we can just remove the chunking if deemed necessary?  @koosemose maybe thats a reasonable decision?  Also coroutines are a whole other ballpark and often can lead to other problems too (not necessarily making them bad for this purpose just maybe making them less preferred, though thats my opinion).

dusho commented 7 years ago

I think chunking code from @BraedonWooding is a good starting point.. If he will include that copying only when adding/removing furniture, then it should be quite optimized.. That spreading of update through several ticks should be tested further, can also bring some balance.. just not sure about having fixed frames 1-10 instead of some highly performant timers with various update periods, but anything that will work and is fast will do. As long as implementations is clean, transparent and doesn't bring any issues to gameplay code that needs to be still added.

BraedonWooding commented 7 years ago

Im still a little cautious about it since you are taking up double the memory for this section for a under O(n) ish operation (remember that isn't indicative, of speed and is actually an insanely fast operation in practice).

dusho commented 7 years ago

well double the memory in this case is keeping array with references to all furniture, so say 160 furniture items (160 * 4/8 bytes) in 100 chunks is ~64-128kB. I think it's worth it, if other option is recreating array 60 times a second instead of doing it when player action (adding/removing furniture) is taken.

dusho commented 7 years ago

Might as well describe my idea of furniture timers - the simplest one. Every furniture would contain in definition something like update frequency, that would contain time in ms(?) after which it will be called with Update(). This could be implicit or explicit. When you have a workshop or other component attached to furniture, this update frequency may be set for you. Explicit imagine maybe some custom event (LUA or C# script) requesting to be updated with certain frequency, or just property in XML/LUA of furniture. Now e.g. wall doesn't need any updates or may need them sporadic (say initial frequency will be set to 10000ms), If the wall is on fire or is being damaged, you may want to change the frequency (to say 1000ms) for that wall furniture in order to change the sprites or update the HP. So in main update in e.g. TimeManager imagine looping though all furniture and increasing their internal time values, if it's bigger than update frequency requested, call Update(float deltaTime). This can also work with chunks optimization - imagine just increasing update frequency for furniture that are offscreen, but honestly I wouldn't use it for that - that chunk optimization is already good enough.

BraedonWooding commented 7 years ago

Yes, however what I think may be better is straight up use the original array/list. Rather than take a copy either way. This way we avoid memory and speed problems.  There was a reason it originally needed to copy but that reason is no longer valid since chunking exists (it was a convoluted bug that it was only after looking at old documentation on the bug I realised it was not relevant any more, it essentially was a GC thing, but not relevant since we have a strong reference with chunking and a few other relevant things) 😀. 

BraedonWooding commented 7 years ago

Note about my previous comment this will be done on my return to a computer on the 4th. On your second point, I have mixed feelings about it because I feel that keeping the system simpler is preferable.  Essentially people writing mods may or may not be able to code or have limited knowledge in coding. Or just simply may not know what update they need for smooth transitions.  However lets say we force them to use one of a few presets.  Like 'Immutable Object' (i.e. wall), 'Interacting Object' (high update speed when interacted with, low update speed when idle).  Umm and so on idk. Still i feel that this could lead to us micro-optimising a little (any of this time manager stuff is getting kinda close). 

koosemose commented 7 years ago

The TimeManager stuff is far from microoptimizations, between the two major changes I've made (spread updates, and not updating certain furniture, the game goes from flopping between 30 and 60 fps (depending on if it's doing a slow update that frame) to a smooth 120 fps (all on starting generation).

Honestly the furniture definition shouldn't need to tell us explicitly what kind of updating, if any, it needs. It can check if it has an appropriate eventAction for a fast or slow update, and if components can have a function or property for each, it can check if it has any components, and only then add it to the list to receive the proper updates. My current method of simply checking for the presence of a Wall typetag is just a hack to get by the lack of needed properties in components to remove the most pervasive non-updating furniture to test the general idea.

For something like a wall, there's no need to have it update ever, even if it's on fire, that wouldn't be done by an update, but instead by a Change event (similar to how tiles, which have no update, change after they've been walked on so much).

Adding multiple timers doesn't do anything except complicate the current system. And not having the chunk manager change the timers makes it pointless, that's all it does, it just improves the fps when the camera is moved, with the whole visible/invisible furniture thing. And different timers are only going to help if the player happens to have the right mix of objects that are updating on different timers, but with evenly timed timers (for example if it was 10ms, 100ms, 500ms, 1000ms) it's still going to pile up on the big timers.

BraedonWooding commented 7 years ago

Yes currently, I'm just warning about if we get too heavy it may just result in micro optimizations. Remember that you are slowing things down to 1/10th of the speed, so in your 120 fps each furniture is experiencing '12 fps', since they are updated 12 times a second; with delta time it may not matter but it will result in more jittery animations and if you have like a work counter then that'll become jumpy somewhat. The chunk manager however still maintained a high fps (off my head I don't remember but it was atleast 80fps) without having to lose those update cycles, so each furniture was updating 80 times a second (when it was visible, less when not). So really a conversation should also take place about the impact of slowing these updates down; there are no inherit problems and it just means that small things will be more jittery and the speed bonus is noticeable. Chunk manager really was just a solution to the bad camera fps so what I would say however, is the removal of invisible/visible lists is questionable, because furniture is a class (and therefore a reference rather than a value type), it has the bonus of being quite cheap memory wise (and PP is no where near a memory issue, it uses around 20-30 mb currently nothing at all basically).

So what I say is we maybe do this; have a 'every frame' which we can call animate or something similar, where animation can occur (if not using component system) we can also use it for things with a counter that needs accurate and constant updating; then we have the 'fixed freq.' update changed to your suggested method of 1/10 frames that way the chunk manager only performs the every frame update, and NOT the 1 in 10 updates this way it can see and update all visible chunks (sure it'll over render chunks but personally that isn't an issue since it means when you move its more accurate), then time manager can do the 1 in 10 update cycle that you suggested not updating if not visible. This means that we 'get best of both worlds' a fast and real time updating system for visible components (like maybe you want accurate bullet tracking which would need every frame or something else, many uses of course) and the 1/10 update cycle for just general stuff that less accuracy is needed.

This is just what I was thinking up today, its more the idea of having the two updates then anything else :D. Obviously all furnitures (including visible) would get the 1 in 10 (or whatever) frame update its just the visible ones getting the special every frame update.