I'm going to be dealing with loading/saving soon and was wondering what the different options for it were. My game will have persistent levels.
Depends heavily upon the language you are using. Most modern languages have relatively good serialization support which allow you to save game state in a portable form (JSON for example). If you're not worried about exchanging save files between different machines/OS's, then you may be able to use a raw binary dump, again depends on language. If the language you are using doesn't have good serialization support, or if you've drank too much OOP-juice, it may be a worthwhile option to use a lightweight database for storage such as SQLite. This may let you use an ORM to untangle your objects in a class heavy implementation.
There are a couple gotchas. Some languages have very crappy PRNG implementations. Regardless of their performance characteristics, if you cannot easily save and restore the state of the PRNG, you're looking at a potential problem. There are two main solutions, either use a 3rd party PRNG that has save/restore state capability or write a wrapper around the standard library PRNG that saves both the initial seed and a counter that tracks how many iterations the PRNG performs. Save both the seed and counter along with the game state during saves, then during a load, reseed with the loaded seed and run however many iterations that the loaded counter indicates to bring the PRNG into sync with your saved state.
Another gotcha involves multi-threading. I'll not go into detail but basically you need a 'stop the world' save event that, at the least, makes an in-memory copy of the current state, and then saves it. There are other techniques but you need to be very careful to not end up with an inconsistent save state.
I honestly winced a bit when I read your first line above. The easiest way to implement save/load is to build it in from the very start. Leaving it till later is... non-optimal at best, potentially catastrophic at worst. If you find yourself in a catastrophic situation due to too much OOP, poor language support, etc, and if you are willing to abandon the ability to backtrack, you could always just use stairs as save points. Much easier since you only need to store player state, map level, and a seed to generate the next level. If you are recording player moves, you *can* use this to create a persistent world which does allow backtracking - just keep track of each save point.
I realise I could save the exact state of the game and all the actors in it, but I've noticed some games allow you to replay the game so it obviously records transitions as well somehow. How exactly does this work? I guess you could save the players actions and random seed, but then you'd have to replay the whole game up to that point when you reload if you use the recording as a game save as well.
Conceptually playback is nothing more than a combination of a saved starting point and a 'undo/redo' stack. It is easier than doing an 'undo/redo' stack in that you only need a simple list of translated player input. It gets almost trivial to implement if you have a clean separation between your world model and your screen presentation, especially if you have already added the ability for player remapping of commands. The only 'tricky' part is mouse input; you need to capture the translated coordinates to avoid ugly coupling with the view state.
Now for using it to restore state as part of a persistent game, playing back a list of player input from beginning of game to the latest state would be one obvious way to do it, but... a better way is to use the 'savepoint at stairs' approach I noted above and
combine it with the playback data. This allows you to regenerate any level at any turn.
During restore (when using the playback record to restore state), you simply do not call render. You just restore the model to the stairs savepoint, and then update it with the playback record in a simple loop. Since you're not having to wait for the player to actually input anything and you're not calling render to show anything, it should be quite fast unless you have extremely large levels or an extremely involved AI implementation.
Or is the loading/saving normally kept separate from the record/playback?
I don't think there is a 'normally' here, depending on language and your chosen architecture, one may be much easier and simpler than the other. For those who have drank too deeply of the well of OOP-juice, it may actually be easier to implement a savepoint + record/playback solution than to deal with all the tangled relationships. On the other hand, if you use a data-centric approach or a hybrid, you will likely find the state save/load approach to be easier.
[EDIT]Given the relative ease of implementing save/load in terms of record/playback, I'd expect those who implement both would combine them unless there were some mitigating factors.
[/EDIT]
Hope this helps,
Brian aka Omnivore