This is not directly related to NES, but more of a general game programming question.
I want to map an action to a double-tap of the A button. Of course, it needs to handle different speeds of taps. Also, id like to have an action mapped to A+B. Again, it needs some tolerance since it's possible the player wont press both on exactly the same frame.
So that got me thinking : what's the simplest/most efficient way of detecting these kind of complex button combos/sequences? How do fighting games do it usually?
Any suggestions are welcome. Thanks!
Every frame you'd scan all sequences, and test if the current input allows them to advance to the next step. If the input is wrong, reset the step counter to the start of the sequence. If the step counter ever reaches the end, that means the sequence has been completed and you can carry out the action associated with it. If you want to reject slow input, you can count the time since the last input change and reset all sequences if that reaches a certain threshold.
You can do the same to detect B and wait for B+A, etc.
Though, maybe worth pointing out that you have to wait for the timeout to resolve the cases where it's not one of those, which may add a lot of lag to your input. You could do something like generically wait for a few frames of commitment on any input (e.g. keep a buffer of last few pad reads and AND them or something), but it may not be appropriate to wait that long for all actions.
For example, if the B+A action can just cancel the B or A action already started, then you don't need to do the timeout at all. You start the B action immediately when pressed, and cancel it when they get to B+A. No lag on either case, but whatever B does has to be designed to be cancellable like this. (Could also make B only cancellable for a few frames, etc.)
If you want to detect longer sequences, it generically becomes a problem of navigating a finite state graph. For one sequence this might just be implemented with a single array, where you increment the index if you get a confirmation of the next input, and restart the sequence if you don't. (...and like above you might allow 1 or 2 frames of "error" between correct entries, just another timeout situation.)
For multiple sequences either you're running that same thing many times in parallel, or you have a big finite state graph with all the loops of the sequences connected together, it can get pretty complicated to do this efficiently (but it might not be necessary to do it efficiently -- for a small enough set of moves, a separate linear detector for each move that runs every frame might be sufficient).
- Formerly ~J-@D!~
- Posts: 521
- Joined: Sun Mar 12, 2006 12:36 am
- Location: Rive nord de Montréal
Also, you can make an upward Smash attack with (↑-tap)+A, but (↑-tap) is also used to jump. So, if you carefully tilt and hold up (so the fighter doesn't jump) then press (Jump) and within a few frames A, you'll still do an upward Smash attack. Also, normally you can't run and make anything else than skidding, dash-attach, dash-grab or jump, but you can still run and do an upward Smash attack because of the "fail-safe" ↑+Jump+A decaying to (↑-tap)+A. The games don't seem to differentiate or care how you do a grab or a jump.
Soul Calibur II has even more variations in its moves: you can do for example ↓ + (Vert. Attack), but also (Vert. Attack) + ↓ which means you have to press, say X, and then the joystick down. I remember Cassandra having such a move; if you do it slowly enough (the game seems to have very tolerant timings except in a few places) you clearly see the regular vertical sword slash beginning, then practically half-way through changing to a down-stab to the opponent's foot.
So this shows that you can make moves with key combinations, with timeouts and without introducing lag, but it can introduce quirks visually or in the move itself, but IMHO it is perfectly acceptable.
For example, your state graph might look like the following:
Code: Select all
+-----------+ +---------> A --> | High jump | | +-----------+ | +------+ +-----------+ +---------+ | Idle | --> Down --> | Crouching | --> B --> | Kicking | +---+--+ +---------+-+ +----+----+ ^ | | | +-- Timeout or wrong button +
Here's an example of how you could translate the above state graph into lookup tables:
Code: Select all
ACTION_NONE = 0 ACTION_A = 1 ACTION_B = 2 ACTION_UP = 3 ACTION_DOWN = 4 ACTION_TIMEOUT = 5 STATE_IDLE = 0 STATE_CROUCHING = 1 STATE_KICKING = 2 STATE_HIGHJUMP = 3 stateTable: .dw idleTable ; state 0 (idle) .dw crouchingTable ; state 1 (crouching) .dw kickingTable ; state 2 (kicking) .dw highjumpTable ; state 3 (high jump) idleTable: .db STATE_IDLE ; No action .db STATE_IDLE ; A press .db STATE_IDLE ; B press .db STATE_IDLE ; Up press .db STATE_CROUCHING ; Down press .db STATE_IDLE ; Timeout crouchingTable: .db STATE_CROUCHING ; No action .db STATE_HIGHJUMP ; A press .db STATE_KICKING ; B press .db STATE_IDLE ; Up press .db STATE_CROUCHING ; Down press .db STATE_IDLE ; Timeout kickingTable: .db STATE_KICKING ; No action .db STATE_IDLE ; A press .db STATE_KICKING ; B press .db STATE_IDLE ; Up press .db STATE_IDLE ; Down press .db STATE_IDLE ; Timeout highjumpTable: .db STATE_HIGHJUMP ; No action .db STATE_HIGHJUMP ; A press .db STATE_IDLE ; B press .db STATE_IDLE ; Up press .db STATE_IDLE ; Down press .db STATE_IDLE ; Timeout
Code: Select all
ChangeActionState: ; Get the current state as an index in the state table LDA currentState ASL TAX ; Point to the current state's action table LDA stateTable, x STA pointer LDA stateTable+1, x STA pointer+1 ; Change state based on the current action LDY currentAction LDA (pointer), y STA currentState
A simple way to think about it might be like this:
1. Startup -- before the hitbox for the move comes out, there is a startup animation to telegraph that move will happen and give the other player a chance to react
2. Attack -- the actual hitboxes appear and the move is performed
3. Cooldown -- after the move finishes, the player must wait before performing another move
Move cancelling is common in fighting games. During the startup phase, button combinations are resolved, so pressing A will start the A move immediately, but allow canceling into A+A or A+B for a few frames. Sometimes the attack phase can cancel into a dodge or shield. In Street Fighter, I believe, performing certain combos of moves allows skipping the cooldown phase and starting the new move immediately. Super Smash Bros L-Cancels allow cancelling an aerial move much faster on landing.
With move cancelling implemented, if you have multiple moves that might start with the same button press, it's not a big deal to take a few frames to decide. Simply start the move it looks like based on the button press in the startup phase. For a few more frames (you'll have to play around to tell how much), allow cancelling into different moves. You can decide whether the couple of frames of the first move startup count towards the startup of the new move. This is a good way to handle button combos like A+B.
Another thing to consider is move buffering. Separate presses, like your A+A example, might be better handled using move buffering. Many games allow you to input your next move while the previous move is still going. Even some NES platformers allow you to buffer a jump a few frames before you land, I believe. Rather than cancelling the current move, it will buffer it and do the move once you are able. In this case, you can define rules not on button presses, but on moves. Say A is the button for a basic punch. You would add a rule that says if the player buffers A while performing basic punch, then buffer left-hand punch next. Then you don't have to introduce lag to decide which move to do, but create a one-two punch move if the player presses A twice in succession.
Instead of just storing the button state for this frame and last frame, you instead make a relatively large circular buffer and put the new button state in it each frame, overwriting the oldest sample, and wrapping around at the end. Then each frame search backward through the buffer for an allowed combo. For example, at 60Hz a 120-sample buffer gives the player at most 2 seconds to enter a combo. Your exact comparison method may vary, and you would still need an FSM for your character so the player can't do moves that aren't legal for their current state, like doing air moves on the ground or when they're in hit stun, etc.
So if, to use an extreme example, you were implementing a combo identical to the Konami code, then every frame you start looking backwards through your input buffer (ignoring duplicate entries.) If you read these entries, in this order, then they successfully performed the combo: $80, $0, $40, $0, $1, $0, $2, $0, $1, $0, $2, $0, $4, $0, $4, $0, $8, $0, $8, $0 (which is the Konami code backwards.)
For double-tapping A and A+B specifically: you can know if the player was within the tolerance by the distance between the two button presses in the buffer -- A and A, or A and B, respectively.
P.S. Hello NesDev!