As a certain recently released fighting game added rollbacks and then completely screwed up the ability to handle sound effects, I think now’s a good time to go over how RollCaster‘s sound engine works.
Before I get in on it, I want to get something out of the way though. This process is not in any way CPU intensive and anyone claiming this sort of thing would be hugely draining on a computer’s resources just so happens to be full of crap.
The case of doing this in a modern game that is being run directly on the native system is often somewhat different from the case of an emulator running an old game, so GGPO itself is not perfectly applicable here. Namely, you have to deal with the sound APIs instead of just saving the state of everything, which is a somewhat more complicated affair.
Alright! So, with that done, let’s phrase the problem here.
Something to understand is that sound usually works on buffers, and when you tell a sound buffer to play, it usually starts over from the beginning. This way, the same sound doesn’t play twice, it just clips and starts from the beginning. This is important.
Assume a situation where one sound is played at a given frame, and a rollback occurs over it. There’s a few different conditions that can occur to the sound:
- The frames play out exactly the same as the first time. The sound should not be canceled.
- The sound did not get played after the rollback, so it should be canceled.
- The sound was played a frame or two later, which means it should not be canceled. You might even want to play it again.
- The sound was played a frame or two earlier, which also means it should not be canceled.
- The sound was played twice, and the first occurrence was canceled while the second one wasn’t. Nope, shouldn’t cancel this either.
- The sound was played twice, and the second time was canceled while the first one wasn’t. You might think this would be worth stopping, but this is one of the ones where it must not be canceled. You need to let it keep playing, because it just sounds weird if a series gets cut short but nobody really notices an extra sound once in awhile.
So there’s exactly one situation where the sound should be stopped, and a whole messload of cases where it shouldn’t be.
This is actually a pretty simple problem when you get down to it.
For RollCaster, I started by hooking every function that played a sound. For each frame, if any of them activated, I stored the sound that was played and incremented a counter for how many times it was played. This was put in a list attached to that frame’s save state.
The interesting thing about this hook, is that if it found that there was already an entry for the sound for that frame, even if it’s at a zero play count, it will not play the sound. This means that it won’t double-play the same sound on the same frame, which normally has no effect, but during rollbacks is another story.
When a rollback occurs, before processing each frame, it goes through and resets the counters for all played sounds to zero. This is only for that frame, it doesn’t change any others. So when it goes to play a sound that had already been played for that frame? Yeah, it doesn’t restart it on accident.
It just lets that process for the entire rollback period before it gets to a post-processing phase. This is where it tries to figure out what sounds should be canceled, by going backwards across the stored rollback states, checking sounds as it goes.
If it sees that a sound has a play count of zero, then it knows that it got canceled out. This starts another check that begins a few frames later than the canceled sound, and goes backwards down the state chain to see if it got played at any other time recently. If it finds an entry that has the same sound and a non-zero play count, it knows that it shouldn’t bother to cancel the sound out, and bails out right there.
So in the end, if none of the other conditions are met, it will forcefully stop the sound. Additionally, it removes that sound entry from the list entirely, rather than just zeroing out the play count. This means that if another rollback occurs where it starts playing again, it will let it go through unhindered. This actually does happen occasionally.
This is not really a complicated process and I’m probably making this sound like more of a big deal than it is, but you can see it isn’t really doing anything expensive here, and the results are just about as close to perfect as you can get with netcode.
I thought about making silly graphs for this but I’m lazy tonight, so this is all you get. Hope it was educational!