MotoGP had the ability to store and replay race events, and could save replays to disk. Players used this to view races after they finished, and in timetrial mode to race against a "ghost bike" that was playing back their previous best lap.
There are several ways to implement replays:
MotoGP used a mixture of techniques #2 and #3. We stored the entire physics simulation state at periodic intervals, and also stored controller inputs for the gaps in between these keyframes. This hybrid approach allowed us to jump back and forth between keyframes (so we could include forward and rewind controls in the replay viewer) while keeping the overall data rate quite low.
Remember how predictable memory usage is important in console games? This applies to replay data, too. We allocated a fixed size (one megabyte) buffer for the replay system, which was enough to store a default 3 lap race with keyframes every 10 seconds. But the game also had an option that let hard-core race fanatics replicate the 20+ lap races from the real MotoGP sport. To find room for such long races, we varied the keyframe frequency:
In a 20 lap race with a full pack of bikes, you got no keyframes at all. You could play back the replay in order from the start, but couldn’t jump back and forth through it. In a 10 lap race, you could jump in units of roughly 1 minute. In a 1 lap race, you got a keyframe every couple of seconds. In each case we tried to fit as many keyframes as possible into memory. If you messed around and deliberately failed to finish the race in a sensible amount of time, the replay would just stop recording when the buffer filled up, aka "ran out of film for the camera".
It is important to understand that replay keyframes only stored the state of the core bike physics. They did not store any data to do with rider animations, sounds, cameras, particles, skidmarks, etc. These other features did not affect the core gameplay simulation, so they did not have to be 100% identical during replays. As long as the bikes did the same thing, these other systems would stay at least similar to the original. We didn’t care if individual particles ended up in slightly different locations, as long as there was still a cloud of smoke in the right place.
The challenge was what to do when you jumped to a different keyframe. This would leave things like particles and rider animation in the wrong state, since they were not stored as part of the keyframe. To avoid obvious artifacts during these jumps, we deleted all active particles and skidmarks, restored bike state from the keyframe, then ran a single update before rendering a frame to the screen. This update gave the camera and rider animation a chance to catch up with the modified bike position, but left us without any particles or skidmarks. If you let the replay continue forward, new particles would soon appear, but any skidmarks from earlier in the race were gone for good. Ah well, such is the price you pay for making things work with limited memory!
The most painful part of implementing replays was making sure we got the same results each time we played back the same controller inputs. This technique only works if the game simulation is 100% deterministic. Over time, even the tiniest deviation will be magnified until you end up with objects flying around in crazy directions, running into walls, etc.
We discovered (and spend much time fixing) several reasons why a game may not be entirely deterministic: