As has been detailed elsewhere, many fighting games offer a choice of gameplay speeds in the form of normal vs. turbo. Normal is the base, and higher speeds work by skipping frames at certain intervals. A skipped frame skips everything: movement frames, startup time, recovery time, background animations, etc. (Music and sound effects are unaffected though.) The effect is that everything gets faster smoothly and constantly as frameskip is increased.
The downside is that lost input opportunities may screw up sensitive combos. If the frame you needed for a one frame link was skipped, your combo will fail. This problem also affects me, since I'm TASing VSav at Turbo 3. When I'm trying a combo that should work but doesn't I want to know if it's because of frameskip.
To reiterate: The complete animation of the gameplay action is represented by the internal (or pre-skip) frames. But you can modify input and see changes on-screen only between display frames. They are equal at normal, but at turbo speeds the internal framerate is increased, whereas the display framerate never changes.
(Frameskip will hopefully never see the light of day again because it makes execution more failure-prone through no fault of the player. Besides that, it's a self-defeating choice: Competitive players always prefer the highest available speed for the increased legitimate challenge, despite the increased randomness.)
I devised an experiment to determine the frameskip interval and made it as simple as possible: examine the countdown behavior of the game's own timer under different game speeds. First I did a search the for on-screen timer. It wasn't hard to find because it changes so predictably; plus it's in all the cheat code collections. Next I needed to find the number of frames that must elapse before the count decreases. I looked at the next byte and sure enough, there it was.
Address | Function |
---|---|
0xFF8109 | time limit counts |
0xFF810A | time limit frames |
Next, the game settings.
The timer speed setting determines how many pre-skip frames must elapse between countdowns. The number starts at the value given and decreases by one (or more) per displayed frame. When it reaches zero, the on-screen count is decremented and the value returns to max. Actually it skips zero, so these values are more like one less than shown.
TIMER SPEED | Internal frames per count |
---|---|
1 SLOW | 134 |
2 NORMAL | 109 |
3 FAST | 89 |
4 FASTEST | 74 |
This means each timer speed setting is ~20-23% faster than the last.
Next, the interesting part. I made Lua script that checks when the frame count decreases by more than one and prints out how many frames elapsed since the last time it happened. As a bonus it also displays on-screen how many internal frames elapsed within the last display frame; i.e., it shows whether the last frame was skipped or not. There's no need to do anything but start a match and run the script.
The results:
GAME SPEED | Frameskip pattern |
---|---|
1 NORMAL | n/a |
2 TURBO 1 | 4,4,5,4,4,5,... |
3 TURBO 2 | 4,3,4,3,4,4,3,4,3,4,3,4,4,3... |
4 TURBO 3 | 3,3,3,3,... |
The turbo speeds can also be accessed by their corresponding FREE SELECT levels.
A few observations:
- As expected, higher speeds have shorter interval between skipped frames.
- Turbo 2 has a strange skip pattern.
- At Turbo 3, a quarter of the action is not being shown.
- At Turbo 3, if you are trying something that should work but doesn't, it should only be necessary to shift the start point by one frame.
- Likewise, shifting the start by a multiple of 4 frames should give the same results.
The may be other consequences revealed later.
This Lua script doesn't depend on savestates once the experiment is begun. That's good because FBA-rr currently has trouble loading them via Lua. The script can easily be modified to study other games by adjusting the addr and full parameters.
local full=134 --set timer speed to slow
local old,new={},{}
old.count=memory.readbyte(addr.count)
old.frame=memory.readbyte(addr.frame)
old.skip=old.frame
emu.registerafter(function()
new.count=memory.readbyte(addr.count)
new.frame=memory.readbyte(addr.frame)
--don't lose count when the frame count resets
if old.count>new.count then
old.frame=old.frame+full
old.skip=old.skip+full
end
local diff=old.frame-new.frame
emu.message(diff)
if diff>1 then
new.skip=new.frame
--subtract 1 because of the lost frame
print(old.skip-new.skip-1)
old.skip=new.skip
elseif diff<0 then
print("error: diff =",diff)
end
old.count=new.count
old.frame=new.frame
end)
Recent Comments