Gitaroo Man + DDR Pad

As much as I like the long, complicated projects that involve weeks of soldering, gluing, coding, tweaking, re-tweaking and debugging, it’s really refreshing to occasionally do something cool with no more than an hour or two of work.

This mini-project was my boyfriend’s idea. It’s an experiment in cooperative two-player Gitaroo Man, played using a gamepad for attack/charge and DDR pad for defense.

For those who aren’t familiar with it, Gitaroo Man is a rhythm game where you do battle using music. You play music on your gitaroo to attack or to charge your health bar. To defend against the enemy’s attacks, you dodge by pressing buttons in time with icons that fly toward the center of the screen. We thought the dodging phase of the game might map well to a DDR pad.

Videos first, tech details below. My boyfriend is on the gamepad, and one of my friends defends with the DDR pad. Theoretically you can play this with one person, but we suck too much at the moment. When we filmed this, we hadn’t played Gitaroo Man in years.. so we definitely could have done better with some more practice :)

Resurrection

https://youtube.com/watch?v=Cp5wrRF4QPg

Bee Jam Blues, Master mode

https://youtube.com/watch?v=_mB1lZ-WlMg

Implementation Details

This turned out to be such a quick project because it was really just some special-purpose firmware for my old Unicone2 controller emulator project. The firmware combines inputs from the gamepad and DDR pad, maps the DDR arrows to the buttons used for blocking in Gitaroo Man, and it includes a one-shot timer which converts sustained pressure on a dance pad arrow into a brief tap of the corresponding controller button. This way, standing still on the DDR pad won’t affect your ability to use the controller buttons normally during the attack phase.

Overview of all hardware and software components:

gitaroo-ddr-blockdiagram

The Unicone2 hardware:

P8030081

Links:


{{
  Special-purpose firmware for the Unicone2 base station:
  This implements a special DDR pad mode for the game Gitaroo Man.

  We use only port 1 on the base unit, and ports 1 and 2 on the
  first remote port. The first port is intended to be used with a
  normal Dual Shock controller, the second port with a DDR pad.

  The normal controller is passed through 1:1 to the base unit.
  On the DDR pad, the D-pad up, down, left, right are mapped to
  triangle, cross, square, circle respectively. So, you can use
  the controller to play the game normally, plus you can use the
  DDR pad to defend.

  Since you're likely to be standing on the pad while attacking too,
  we need a way to prevent those inputs from interfering with the
  attack phase. For this reason, we detect when you first press a
  button on the DDR pad, and simulate just a momentary tap on the
  corresponding controller button. This means you can't use the pad
  to attack.

    - This is configurable by setting TAP_LENGTH. A very long TAP_LENGTH
      has pretty much the same effect as disabling this one-shot functionality.

  TODO: Make this configurable with a button.
        I like using the pad to attack sometimes :)

  -- Micah Dowty <micah@navi.cx>
}}

CON

  _CLKMODE = XTAL1 + PLL16X
  _XINFREQ = 5_000_000

  SLOT_CONTROLLER = 0
  SLOT_DDR = 1

  TAP_LENGTH_MS = 100
  
OBJ

  Rem : "psx_remote_port"
  RemDummy : "psx_remote_port"
  Emu : "psx_controller_emulator"

VAR
  long  ddr_out_buf[Emu#STATE_BUFFER_LEN]
  long  ddr_stack[64]
  long  tap_length_ticks
                     
PUB Main | remote, slot, emulator, buttons

  ' Start a single remote connection (port 1) and a single
  ' PSX controller emulator core.
  
  Rem.start(0, 1)
  Emu.start(8, 4, Emu#CONTROLLER_DUAL_SHOCK)

  ' Run a dummy remote unit on port 2, just to display FF on
  ' the LEDs to indicate that you're using the wrong port.

  RemDummy.start(2, 3)
  RemDummy.set_led_char(0, $F)
  RemDummy.set_led_char(1, $F)
  
  ' Set up the 1:1 mapping for our controller
  
  Emu.add_state_buffer(Rem.get_state_buffer(SLOT_CONTROLLER))
  Emu.add_actuator_buffer(Rem.get_actuator_buffer(SLOT_CONTROLLER))

  ' For the DDR pad, we use a separate cog to translate the data. That
  ' cog has separate input and output buffers. Tell the emulator about
  ' our output buffer. The emulator cog will merge this with
  ' the data from our controller above.

  Emu.add_state_buffer(@ddr_out_buf)

  ' Start a separate cog to translate the DDR buttons quickly
  cognew(ddr_thread, @ddr_stack)

  ' Main loop for less time-critical things: Send low-frequency data back to
  ' the remote unit (LED and force feedback state).

  repeat
    update_controller_leds
    update_ddr_out_leds
    Rem.poll
    RemDummy.poll


PRI ddr_thread | ddr_in_ptr, ddr_buttons, out_buttons, ddr_buttons_prev, timers[4]

  ' Just a tight loop to translate DDR pad data to appropriate controller data

  tap_length_ticks := clkfreq / 1000 * TAP_LENGTH_MS

  ' Default state for the output buffer (Axes centered, buttons up)
  bytefill(@ddr_out_buf, 0, Emu#STATE_BUFFER_LEN)
  bytefill(@ddr_out_buf, $80, 6)
  bytefill(@ddr_out_buf, $ff, 2)
  
  ddr_in_ptr := Rem.get_state_buffer(SLOT_DDR)
  repeat
    ddr_buttons_prev := ddr_buttons
    ddr_buttons := !WORD[ddr_in_ptr]
    out_buttons := 0

    if button_tap(ddr_buttons, ddr_buttons_prev, Rem#BTN_UP, @timers[0])
      out_buttons |= Rem#BTN_TRIANGLE
  
    if button_tap(ddr_buttons, ddr_buttons_prev, Rem#BTN_DOWN, @timers[1])
      out_buttons |= Rem#BTN_X

    if button_tap(ddr_buttons, ddr_buttons_prev, Rem#BTN_LEFT, @timers[2])
      out_buttons |= Rem#BTN_SQUARE

    if button_tap(ddr_buttons, ddr_buttons_prev, Rem#BTN_RIGHT, @timers[3])
      out_buttons |= Rem#BTN_CIRCLE

    WORD[@ddr_out_buf] := !out_buttons

PRI button_tap(current, prev, mask, timer_ptr) : out | timediff
  ' Utility function to handle emitting a short tap any time an
  ' incoming button press is detected. We release the output button
  ' when TAP_LENGTH elapses, or when the original button is released.

  if current & mask
    if prev & mask
      ' Button held down. Check timer
      timediff := cnt - LONG[timer_ptr]
      if timediff <= 0
        ' Timer hasn't expired
        out~~
      else
        ' Timer expired. Move the timer forward so it doesn't wrap.
        out~
        LONG[timer_ptr] := cnt
    else
      ' Button just pressed. Set timer.
      out~~
      LONG[timer_ptr] := cnt + tap_length_ticks
  else
    ' Button not pressed
      out~
 
PRI update_controller_leds | state, x, y, magnitude, slope, leds

  ' On the controller slot's LEDs, display the approximate angle of the analog stick.

  state := Rem.get_state_buffer(SLOT_CONTROLLER)
  x := BYTE[state + Rem#AXIS_LX] - $80
  y := BYTE[state + Rem#AXIS_LY] - $80

  magnitude := x * x + y * y
  if y <> 0
    slope := (x << 8) / y
  else
    slope := $7FFFFF

  if magnitude < 600
    ' Dead zone. LEDs are blank.
    leds := 0
  elseif x > 0
    ' Right half (+x)
    if slope > $100
      leds := $04   ' Upper right
    elseif slope > 0
      leds := $02   ' Bottom
    elseif slope > -$100
      leds := $20   ' Top
    else
      leds := $10   ' Lower right
  else
    ' Left half (-x)
    if slope > $100
      leds := $40   ' Lower left
    elseif slope > 0
      leds := $20   ' Top
    elseif slope > -$100
      leds := $02   ' Bottom
    else
      leds := $01   ' Upper left

  Rem.set_led_segments(SLOT_CONTROLLER, leds)

PRI update_ddr_in_leds | buttons, leds

  ' On the DDR slot's LEDs, display which pads are pressed.
  ' This version shows the inputs from the pad.
  
  buttons := !WORD[Rem.get_state_buffer(SLOT_DDR)]
  leds := 0

  if buttons & Rem#BTN_UP
    leds |= $20
  if buttons & Rem#BTN_DOWN
    leds |= $02
  if buttons & Rem#BTN_LEFT
    leds |= $41
  if buttons & Rem#BTN_RIGHT
    leds |= $14  

  Rem.set_led_segments(SLOT_DDR, leds)

PRI update_ddr_out_leds | buttons, leds

  ' On the DDR slot's LEDs, display which pads are pressed.
  ' This version shows the outputs after translation.

  buttons := !WORD[@ddr_out_buf]
  leds := 0

  if buttons & Rem#BTN_TRIANGLE
    leds |= $20
  if buttons & Rem#BTN_X
    leds |= $02
  if buttons & Rem#BTN_SQUARE
    leds |= $41
  if buttons & Rem#BTN_CIRCLE
    leds |= $14  

  Rem.set_led_segments(SLOT_DDR, leds)