kervinck / gigatron-rom

System, apps and tooling for the Gigatron TTL microcomputer

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support alternative game controllers

kervinck opened this issue · comments

Not all famicom-clone game controllers give the same signals. The standard ones are 4021-chip based. We had an earlier type that was 74165 based (which isn't supported yet). Now a third type emerges that behaves different again. Question is: is it possible to map "non-standard" signals to the 4021-based signals that application software expects to see?

Reported signals from "type 3" controller:

                   Standard 4021-based
00000000 Right  -> 11111110 Right
00000001 Left   -> 11111101 Left
00000011 Down   -> 11111011 Down
00000111 Up     -> 11110111 Up
00001111 Start  -> 11101111 Start
00011111 Select -> 11011111 Select
01111111 B(?)   -> 10111111 B
00111111 A(?)   -> 01111111 A
11111111 None(?)-> 11111111 None

If we blindly map the new type values to standard values, then [Ctrl-C] and '?' (abbreviation for PRINT) won't work in Tiny BASIC anymore (for nobody):

00000011 Down
      ^^ this is also ASCII for ^C (3)

01111111 B(?)
 ^^^^^^^ this is also ASCII for DEL (127). This is the same as standard controller [A]

00111111 A(?)
  ^^^^^^ this is also ASCII for '?' (63)

First let's assume that the A and B buttons are swapped on the new controller. We then have a much more regular mapping between new codes and expected codes:

                       Standard 4021-based
  0 00000000 Right  -> 11111110 Right
  1 00000001 Left   -> 11111101 Left
  3 00000011 Down   -> 11111011 Down   (3 is also Ctrl-C)
  7 00000111 Up     -> 11110111 Up
 15 00001111 Start  -> 11101111 Start
 31 00011111 Select -> 11011111 Select
 63 00111111 B      -> 10111111 B      (63 is also '?')
127 01111111 A      -> 01111111 A      (same code)
255 11111111 None   -> 11111111 None   (same code)

The formula for the mapping is: standardCode = 254 - weirdCode (can be used both ways, actually)

In the video loop we can then do something like this, after fetching the input port:

if serialRaw in [0,1,7,15,31]: # new codes for Right,Left,Up,Start,Select
  detected = True
if serialRaw not in [0,1,3,7,15,31,63,127,255]: # can't be the new controller
  detected = False
if detected:
  serialRaw = 254 - serialRaw

This will automatically detect the new controller type and convert to the expected bit pattern. And once a keyboard is plugged in (or a default controller), it will see that also and disable the conversion.

The new [Down] (3) and [B] (63) codes are ambiguous, as they could be valid 'Ctrl-C' and '?' bytes coming from a keyboard as well. That's why the detected state is required in the first place. The main menu suffers, because the video loop can't know if a (3) comes from a controller or from a keyboard, so it doesn't change state.

For this we can let Main.gcl help out a bit, by accepting this special case as a way to move the arrow down.

With the above, Snake still suffers slightly: if the game is started, the [Down] button will only respond after one of the other arrow buttons have been pressed.

Another idea is to reorder the main menu application such that the user is forced to use one of the unambiguous buttons before the ambiguous ones. The stupid way to do that is let the arrow point at `Loader' first. Or ask explicitly for a momentary press of [Start], something like that. (But that's obnoxious).

We have to find a place to store the detected bit, somewhere in the 0..2F address range of zero page, preferably without consuming a whole new unallocated byte for this. Candidates are:

  1. $10 serialLast (don't know if it can work)
  2. $21 romType (repurpose one of the low bits)
  3. $2c soundTimer (repurpose the low bit)
  4. $2d ledTimer (repurpose the low bit)

Alternatively, we can use the page 1 memory between vReset and Channel 1. There are two unused bytes at $1f8 and $1f9

When we switch state we can consider a visual indicator? Shaking the screen, colouring a pixel... Flashing the blinkenlights...

@gigawalt

  1. Can A and B be swapped? Are they labeled or unlabeled?
  2. What is the signal with simulaneous press, for example Right+A
  3. What is the value when idle (255?)
  4. Is there an auto-fire mode? Does it do anything?

Overall impact will be that user must be instructed how to switch between controller types, and Racer and Mandelbrot will not work as-written and documented, because simultaneous button presses can't be detected with this encoding.

  1. A and B unlabeled, but located horizontally beside each other, so the left one would normally be A.
  2. The lowest value wins. As Right is 0 and thus the lower value, pressing Right+A gives 0.
  3. Idle value is 255.
  4. Auto-fire works as expected (on/off loop for that button giving the same value as when pressed).

On 2: that makes Racer still sort of playable. You can hold [A]cceletate and steer, just the acceleration will pause whilst steering. That's better than losing the steering. [B]reaking also only works when not steering however. Normally when you break and steer, you really need the break (and steer).

This gives an alternative method to update buttonState (the logic in scanline 40):

If in this mode:

  1. Clear the bit belonging to the button (at least: if serialRaw < serialLast, because that indicates a press-down event instead of just a hold)
  2. Keep the higher bits untouched, heuristically assuming their state hasn't changed
  3. Set the lower bits, because we know those buttons can't be pressed any longer
; On line 40

; If serialRaw >= serialLast: goto skip
  ld   [serialRaw]
  adda 1
  anda [serialLast]
  beq  skip
  nop

; Clear only the selected bit
  ld   254
  suba [serialRaw]
  anda [buttonState]
  st   [buttonState]

skip:

; Set the lower bits
  ld   [serialRaw]
  ora  [buttonState]
  st   [buttonState]

Arrows aren't mutually exclusive, and some combinations are not detectable: specifically holding Right or Left followed by (without release) Up or Down. (The other order should be detected: Holding Up or Down followed by Left or Right.)

This can be annoying in many types of games that allow movement in 8 directions: half of the transitions don't respond. We can add a filter that makes the arrow buttons mutually exclusive (only on these controller types), restricting movement to just 4 directions. But lets not do that now.

The same holds for the A/B buttons in shooting games: they won't be detected as long as an arrow button is pressed (walking, moving, ...).

Other applications are less affected: the Mandelbrot clock is settable with the arrow buttons while holding [A], and steering is possible in Racer while [A]ccelerating and [B]reaking. The release of [A] won't be detected in a sharp corner however....

Furthermore, programs in TinyBASIC that look at the buttonState variable (with peek(17)), will be interrupted when the down arrow is pressed on these controllers (because that's still a Ctrl-C).

Alpha-testing Ok after d8ec131

Note that no state keeping is required, because we only change the way buttonState gets updated for the new type of values. serialRaw remains untouched. This means that programs have to be conscious about which of the two they look at: serialRaw is for mainly for keyboards, buttonState for game controller buttons.