Saving previous gamestates

Started by
9 comments, last by SmkViper 8 years, 5 months ago

Hi,

One feature of the engine for the game I'm developing is it will record the previous 32 (or so) full states for dynamic objects in a looping buffer. That way, systems like the renderer and quicksave can function without blocking the simulation thread (assuming they finish their job before it comes around the loop again).

This seems like a great idea (which has been successfully implemented in many games), but I'm having trouble figuring out how to implement it myself. Since my game engine is written in C++, performing a straight memcopy would be very dangerous. Some elements need more intelligent behavior when performing the copy as well, outside of the standard copy-assignment operator (ex, pointers to other objects in the scene need to be adjusted to point to the new version, even if it's just a simple +1 increment).

The first solution that comes to mind would just have a virtual function on the "GameObject" and "Component" classes, so that the author of each class can just manually copy over the relevant data (and exclude data that doesn't need to be copied), performing any adjustments themselves. While from an implementation standpoint this is probably the safest, simplest, and most efficient solution, it leaves a lot of room for user error and will quickly become very tedious. To solve that I could have the default implementation operate via the reflection system.

Complicating things, there are cases where a field simply is not copyable due to its type. I could either ignore copying that field entirely, or perform an intelligent deep-copy for that type (ex, a UniquePtr would have its pointed value copied, while a standard pointer would not).

Another solution to that problem would be to simply forbid the use of non-copyable types on GameObjects or Components. This would shift a lot of memory-management responsibility onto the scene manager itself (one thing that would have to change is how Components are currently owned by the GameObjects they are attached to. While conceptually this makes sense, requiring GameObjects to be fully copyable would entail moving ownership to the scene, which would complicate the question of when to detach/destroy components).

There are a lot of facets to this problem and there's a very good chance that I'm overthinking it. Ultimately, I'd like the solution I settle on to be safe and efficient, while also not making too many assumptions about how the user-defined Component/GameObject operates. There would of course have to be a few compromises on the part of the author of such types, but if I can leave things as open as possible that would be great. Whatever the solution is, I just can't think of it right now so I'd love to hear your ideas.

Edit:

What I've decided on is making this a special case of serialization (serializing to a working portion of memory, as opposed to a file or something). With a little modification, I could add support for this kind of thing to my existing serialization system. I'm still open to suggestions though.

Advertisement

Rule of thumb: use IDs (integers, whatever) instead of raw pointers (or references, when using a higher level language instead of C++). This way it doesn't matter if you just do a dumb copy because IDs will not become invalid (while pointers would). It can also be useful later on if you're in need of some huge optimization and need the ability to rearrange the order of objects in memory or whatever.

Just make sure to have a quick way to map IDs to pointers for when the need arises (i.e. a function that takes an ID and returns a pointer), then have everything use IDs.

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.

Do you need it to be a special case of serialisation? If your objects were able to serialise and deserialise their state through a byte array or a memory stream, then that data can be saved to disk, sent via the network, or kept in memory.

[size="2"]Currently working on an open world survival RPG - For info check out my Development blog:[size="2"] ByteWrangler

Rule of thumb: use IDs (integers, whatever)

Good idea, what I'll probably do is implement GameObjectPtr<T> and ComponentPtr<T> types, which internally holds the pointer and requires a frame ID to dereference (since frames for the object will be stored contiguously, the frame ID could simply serve as an index). The standard way of dereferencing would be like "somePtr->Get(this->GetFrameID())". Note that by "Frame" I'm specifically referring to game logic frames.

One thing I dislike about that is how verbose it is. Since the vast majority of the code you write for a game is GameObject/Component classes, that could get real ugly real quickly. A solution to that would be to have a thread-local global frame ID to serve as the default; this would allow you to use the standard '*' and '->' syntax (while still exposing the more verbose ".Get()" syntax for retrieving specific frames). Of course, that does add some room for error if you haven't kept track of what frame your thread is currently bound to, but since 99% of the dereferences performed will be within the same frame, I don't know if that's really a problem.

If your objects were able to serialise and deserialise their state through a byte array or a memory stream, then that data can be saved to disk, sent via the network, or kept in memory.

Yup, that's serialization all right.

Edit: I should have been more specific, by "special case of serialization" I meant serializing directly from one set of live objects to another. That's not quite the same thing as serializing to a byte array, IMO.

When I do serialization I don't bother with IDs. I just write the pointer address out since that's guaranteed to be unique at time of saving. Then you just need to either set up a table to regenerate each object (mapping pointer to type for a factory to regenerate them on load) or just have each object start with writing it's pointer value and then running a post-load pass that resolves all those pointer values to the real pointers once all the objects are re-created.

If you want to get real fancy, you can set up a simple ID interface that you pass around that will return an ID for a pointer value. For speed, just return the pointer value, and for debugging you can return an incrementing integer, storing the integer and pointer in a map for later retrieval.

The downsides to the above approach is it assumes that you won't be deleting and allocating objects between writing and saving time (otherwise the pointer can be re-used) and your saves won't be compatible across 32- and 64-bit versions (though that's easily solved by making sure you up-cast to 64 bits on a 32-bit machine).


When I do serialization I don't bother with IDs. I just write the pointer address out since that's guaranteed to be unique at time of saving.

In theory pointers work fine, but in practice I've found them to be problematic because they can change between saves, and it makes debugging your save/load code a lot more difficult. A simple ID abstraction solves this and doesn't suffer from any platform compatibility issues either.

I guess ID's also hold an advantage in open-world games, where the object in question might not actually exist in memory all the time.

Since my game engine is written in C++, performing a straight memcopy would be very dangerous. Some elements need more intelligent behavior when performing the copy as well, outside of the standard copy-assignment operator (ex, pointers to other objects in the scene need to be adjusted to point to the new version, even if it's just a simple +1 increment).

Most of your engine code is not "game state objects", so the style of the majority of it doesn't matter.
For the small amount of game-state data, you could choose to write it in a different style... A few games that I've worked on have created a "giant bucket of bits" datastructure for holding gamestate data, which is capable of being backed up or restored with a single memcpy call. They actually had the game logic in Lua OOP components, which read/wrote to this pile-o-bits as necessary.

I guess ID's also hold an advantage in open-world games, where the object in question might not actually exist in memory all the time.

This too (not even need to be open-world, some engines are very conservative and only retain objects in the current room and its contiguous ones). Also you have more control over an ID, i.e. you can design them to be less likely for there to be a clash (while a pointer could end up being reused to point to a different entity, screwing up everything - not relevant to saving, but relevant if destroying and creating entities often).

EDIT: by "pointer being reused" I mean that when an entity gets destroyed, a new entity could end up being created in the same address, thereby making the pointer valid again but not to the entity it was meant to point.

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.

Another style, which may work well for some types of games but not others, would be to have an immutable game state, akin to what you might find in a functional paradigm. Within the update pass of your game loop, you build a new game state from your current game state, instead of modifying the current game state in place. This way there's no need to make a copy of your current game state before you update it. You just hold on to your old game state for as long as needed even after building the new game state.

Of course, if you do a whole lot of complex memory allocations in the course of building an updated game state, this probably won't work very efficiently. But if a large bulk of what you're doing is simply appending updated values to new arrays, while iterating over data in your prior game state or intermediate state that was computed from your prior game state, then it could work really well.

It also has the advantage of making it much easier to use multiple cores, since multiple threads can read from a fully built game state all it wants, without fear of the data getting changed right out from underneath it.

"We should have a great fewer disputes in the world if words were taken for what they are, the signs of our ideas only, and not for things themselves." - John Locke

This topic is closed to new replies.

Advertisement