In Ramses Game, monsters pop out of holes and the player needs to whack them. There are only 8 buttons on the NES controller, really 7 if you reserve Start for pausing the game. This means that if I want more than 7 holes, I need to assign the holes to combinations of buttons rather than individual buttons.

To me the most obvious way to do this is to look for combinations like up+A, down+A, left+A, right+A. Even if I reserve the Start button for pausing, that still leaves me with 12 combinations (4 each for A, B and Select). 12 holes is plenty for this kind of game, and actually due to screen space limitations I’ve decided to limit myself to 10 holes.

Detecting button combinations on the NES is a little bit tricky. I don’t want to allow the player to hold down B and spam the dpad to hit many holes very quickly. Also, if the player presses up+left at the same time while pressing B, I don’t want two holes to be hit simulataneously. On the other hand, it would be unreasonable to expect the player to hit two buttons at once in the exact same frame. Nobody is that fast. So I have to allow some leeway. With that in mind, here is how my Button Combo routine works.

Button Combo Routine

Output

My button combo routine takes what the user has pressed and outputs a value 0-12. The output possibilities are laid out like this:

  • 0: no combination was pressed.
  • 1-4: B+dpad was pressed (in order: up, down, left, right)
  • 5-8: A+dpad
  • 9-12: Select+dpad

Each of these values is mapped to a hole, and I use the outputted value to index into hole-related tables. For example, here is a table that I use to determine if a valid hole was whacked:

valid_holes:
    .byte 0             ;unused (no combo)
    .byte 1, 1, 1, 1    ;B (up, down, left, right order)
    .byte 1, 1, 1, 1    ;A
    .byte 1, 1, 0, 0    ;Select
    .byte 0, 0, 0       ;unused (select+left, select+right unused)

I have tables for attribute table PPU addresses and the like as well.

Input

When I read the controller every frame, I store two sets of data in RAM. One is the current button states (pressed or not pressed), which I store in a variable called joypad1. The other is off-to-on transitions, ie what was unpressed last frame but is pressed this frame. Off-to-on transitions are stored in a variable called joypad1_pressed.

Each variable is one byte, basically a bitflag with each bit corresponding to a button. My controller reading routine stores button information in this order:

joypad1 and joypad1_pressed bits:

76543210
||||||||
|||||||+- right
||||||+-- left
|||||+--- down
||||+---- up
|||+----- start
||+------ select
|+------- B
+-------- A

Logic

The first thing I do in my button combo routine is to check if A, B or select is pressed. If not, obviously we can’t have a combo, so skip to the end.

;---------------------------------------------------
; check for dpad+button combos
.proc check_combos
    lda joypad1
    and #%11100000      ;isolate A, B, select
    beq @somewhere_near_the_end
    ;....snip

If a button is pressed, I want to grab its combo value and store it in RAM:

.proc check_combos
    lda joypad1
    and #%11100000      ;isolate A, B, select
    beq @clear_btn_val  ;if not pressed, clear btn_val
    lda joypad1_pressed
    and #%11100000      ;if off_to_on, we need to set the flag, else skip to dpad check
    beq @check_dpad
@set_btn_val:
    asl                 ;shift the A button bit to the CF
    bcc @B
    lda #$05            ;if A bit is set, store 5 in btn_flag  (5, 6, 7, 8 = A combo)
    bne @store
@B:
    asl                 ;shift the B button bit to the CF
    bcc @Select
    lda #$01            ;else if B bit is set, store 1 in btn_flag (1, 2, 3, 4 = B combo)
    bne @store
@Select:
    lda #$09            ;else Select bit must be set, so store 9 in btn_flag (9, 10, 11, 12 = Select combo)
@store:
    sta btn_val         ;add dpad value to this to get index
@check_dpad:
    ;.....snip

@clear_btn_val:
    lda #$00
    sta btn_val
@end:
    rts
.endproc
    

Notice that I check if the button is newly pressed or if it was held from last frame. This check is for a special case. Say the user is holding A. Then say a monster they want to smack appears in the up+B hole. The player will press up+B, but there is a chance that there will be several frames of A+B as the player slides their thumb from A to B. I want to catch the newest button press (B), not the older one (A). So I check joypad1_pressed. If a button is newly pressed this frame, I set btn_val. If not, I skip straight to the dpad (btn_val having been set in a previous frame).

To say it another way, I only alter the value of btn_val in two cases: when a new button is pressed, or when no buttons are pressed. This is important for another case too, which I will get to in a moment.

Note that if on the off-chance that B, A or Select are newly pressed simultaneous in the same frame, A takes precedence over B, which takes precedence over Select. Not going to happen often, and when it does it’s not a big deal – the player will know they mashed both buttons.

Finally we check the dpad to complete the combo:

@check_dpad:
    lda btn_val
    beq @end            ;if A, B, Select not pressed, no combo
    lda joypad1
    and #%00001111      ;isolate dpad
    beq @end            ;if no arrows pressed, no combo
    tay
    lda dpad_add, y     ;else, grab arrow value from table (up = 0, down=1, left=2, right=3)
    clc
    adc btn_val         ;add arrow value to button value.  Results in a number 1-12
    sta joypad1_combo
    jsr do_button_combo
@clear_btn_val:
    lda #$00
    sta btn_val
@end:
    rts

First thing I check is if btn_val is 0. But if we made it this far, shouldn’t this be non-zero? The player is definitely holding down either A, B or Select afterall. Yes they are, but btn_val could still be zero. After a button combo is registered, I process the combo via do_button_combo and then I clear btn_val. This check for btn_val == 0 covers the case that a button combination was recently processed (say last frame), but the player hasn’t released the buttons yet. It took me some bug-hunting before I realized I needed this check. Button Combinations are tricky, aren’t they?

The rest is pretty straightforward. I check the dpad. If no arrow is pressed, there is no combo so I return. If an arrow is pressed, I grab its value from a table and add it to btn_val to get the final combo value. The reason for the dpad_add table is to protect against cases of multiple arrows being pressed at the same time. If up+left is pressed, for example, I resolve it to up. If left+right is pressed, I resolve it to left. If you’re curious, my table looks like this:

;this table maps each possible d-pad combination to a single d-pad direction
;   0 = up, 1 = down, 2  = left, 3 = right
dpad_add:
    .byte 0     ;unused (will never be read)
    .byte 3     ;r -> right
    .byte 2     ;l -> left
    .byte 2     ;r+l -> left
    .byte 1     ;d -> down
    .byte 1     ;d+r -> down
    .byte 1     ;d+l -> down
    .byte 1     ;d+r+l -> down
    .byte 0     ;u -> up
    .byte 0     ;u+r -> up
    .byte 0     ;u+l -> up
    .byte 0     ;u+r+l -> up
    .byte 0     ;u+d -> up
    .byte 0     ;u+d+r -> up
    .byte 0     ;u+d+l -> up
    .byte 0     ;u+d+r+l -> up

Having this in table form makes it easy to modify in the future if I need to tweak some of the values (poor right arrow gets overruled a lot, doesn’t he?). If you want to know more about this technique, I have written about protecting against odd dpad combinations using tables before.

That’s it. It’s a little complicated but it works. I haven’t optimized this routine, so it’s possible that I could improve it somehow. I’d rather move forward though, since this works fine the way it is.

Incidentally, the player is allowed to hold a dpad arrow and spam A/B/Select. Doing so is of such limited use that I don’t see it being abusive. If testing reveals otherwise though, I’ll rewrite the routine to protect against that.

Full Routine

Here’s the full routine in case anyone wants to copy/paste it into their game. If you do, I will feel really happy if you mention me in the credits somewhere. :)

;---------------------------------------------------
; check for dpad+button combos
.proc check_combos
    lda joypad1
    and #%11100000      ;isolate A, B, select
    beq @clear_btn_val  ;if not pressed, clear btn_val
    lda joypad1_pressed
    and #%11100000      ;if off_to_on, we need to set the flag, else skip to dpad check
    beq @check_dpad
@set_btn_val:
    asl                 ;shift the A button bit to the CF
    bcc @B
    lda #$05            ;if A bit is set, store 5 in btn_flag  (5, 6, 7, 8 = A combo)
    bne @store
@B:
    asl                 ;shift the B button bit to the CF
    bcc @Select
    lda #$01            ;else if B bit is set, store 1 in btn_flag (1, 2, 3, 4 = B combo)
    bne @store
@Select:
    lda #$09            ;else Select bit must be set, so store 9 in btn_flag (9, 10, 11, 12 = Select combo)
@store:
    sta btn_val         ;add dpad value to this to get index
@check_dpad:
    lda btn_val
    beq @end            ;if A, B, Select not pressed, no combo
    lda joypad1
    and #%00001111      ;isolate dpad
    beq @end            ;if no arrows pressed, no combo
    tay
    lda dpad_add, y     ;else, grab arrow value from table (up = 0, down=1, left=2, right=3)
    clc
    adc btn_val         ;add arrow value to button value.  Results in a number 1-12
    sta joypad1_combo
    jsr do_button_combo
@clear_btn_val:
    lda #$00
    sta btn_val
@end:
    rts
.endproc

;this table maps each possible d-pad combination to a single d-pad direction
;   0 = up, 1 = down, 2  = left, 3 = right
dpad_add:
    .byte 0     ;unused
    .byte 3     ;r -> right
    .byte 2     ;l -> left
    .byte 2     ;r+l ->left
    .byte 1     ;d -> down
    .byte 1     ;d+r -> down
    .byte 1     ;d+l -> down
    .byte 1     ;d+r+l -> down
    .byte 0     ;u -> up
    .byte 0     ;u+r -> up
    .byte 0     ;u+l -> up
    .byte 0     ;u+r+l -> up
    .byte 0     ;u+d -> up
    .byte 0     ;u+d+r -> up
    .byte 0     ;u+d+l -> up
    .byte 0     ;u+d+r+l -> up

Conclusion

So button combinations are done with, and if you watched the video embedded in the last post you will see that they work quite well! This game is really coming along.

3 Responses to “Ramses Game: 2 – Reading NES Button Combinations”

  1. Peter says:

    I would think that expecting the player to hit the select button + dpad would be awkward during a fast paced game. What’s the reason you went with that versus dpad without any other buttons pressed for the third set of 4?

  2. Thomas says:

    That’s a good question. I won’t know how awkward Select+dpad will be until I get my PowerPak. I can’t think of a good way to use dpad only without getting mishits though. The controller is read about 60 times per second, too fast for a human to synchronize their fingers exactly. I envision a lot of misreads as the player tries to press up+B, but gets their input read as up because the B came 20 frames later.

    The dpad+button combination also gives the trigger-happy player a tiny window of opportunity to hesitate and pull back to avoid a mishit. Accepting dpad-only input would take that away from the player. Hitting an empty hole won’t have any consequences in-game, but hitting the wrong monster will. As much as possible I want to avoid players throwing down the controller and screaming “BULLSHIT!” at the TV :)

    Not to mention that it is my personal habit when playing video games to spin circles on the dpad randomly while waiting for something to happen. OK, that’s not really a reason. :)

    Testing will tell. Select+dpad might be awkward, but maybe it won’t be. NES controllers are pretty small. If it proves unwieldy, I could try Start+dpad, or A+B+dpad, or have one hole be Select and the other Start, or just remove the 9th and 10th holes completely. It should be noted that the 9th and 10th holes aren’t unlocked until later in the game when the difficulty level is supposed to be higher.

  3. [...] means that I have a full day at my desk to do whatever I want. I was up late last night writing my last post, so NES programming was on my mind when I woke up in the [...]

Leave a Reply