Playing Squash with the Wii Remote

Implementing a Wii game with Ruby

I gave a presentation about developing a Squash game using the Nintendo Wii Remote at the Sheffield Ruby User Group (ShRUG).

Here is the source code of the main program

#!/usr/bin/env ruby
require 'rubygems'
require 'hornetseye_rmagick'
require 'hornetseye_alsa'
require 'opengl'
require 'cwiid'
include Hornetseye
WIDTH = 800
HEIGHT = 600
GRAVITY = 9.81
SPEED_PER_VOLUME = 16.0
SIZE_X = 3.2
SIZE_Z = 9.75
BAR_HEIGHT = 0.43
BAR_THICKNESS = 0.1
RADIUS = 0.02025 * 2
REFLECTION = 0.7
AIR_FRICTION = 0.00
ROLL_FRICTION = 0.1
MIN_SPEED = 0.8
ACC_RISING = 20.0
ACC_FALLING = 0.0
MIN_DELAY = 0.3
MIN_HEIGHT = 0.15
OBSERVER_Y = -2.4
OBSERVER_Z = -10.5
DIST_Z = 6.0
X0 = -2.0
H0 = 1.5
SERVE_SPEED = 5.0
V_MIN = 8.0
V_MAX = 20.0
Z0 = DIST_Z - SIZE_Z
NORM_Z = -1.5
L = 1.0
# switch on lights with WiiMote
# http://www.paulsprojects.net/opengl/shadowmap/shadowmap.html
# http://bitwiseor.com/gl_arb_shadow/3/
puts 'Put Wiimote in discoverable mode now (press 1+2)...'
wiimote = nil
wiimote = WiiMote.new
wiimote.rpt_mode = WiiMote::RPT_BTN | WiiMote::RPT_ACC if wiimote
$floor = MultiArray.load_ubytergb 'floor.png'
$side = MultiArray.load_ubytergb 'side.png'
$back = MultiArray.load_ubytergb 'back.png'
( MultiArray( SINT, 2, 16 ).new * 0.5 ).to_sint
s = File.new( 'wall.wav', 'rb' ).read; s = s[ 44 .. -1 ]
m = Malloc.new s.size; m.write s
$wall = MultiArray( SINT, 2, m.size / 4 ).new m
s = File.new( 'ground.wav', 'rb' ).read; s = s[ 44 .. -1 ]
m = Malloc.new s.size; m.write s
$ground = MultiArray( SINT, 2, m.size / 4 ).new m
s = File.new( 'racket.wav', 'rb' ).read; s = s[ 44 .. -1 ]
m = Malloc.new s.size; m.write s
$racket = MultiArray( SINT, 2, m.size / 4 ).new m
$pos = [ X0, RADIUS, Z0 ]
$v = [ 0.0, 0.0, 0.0 ]
$t = Time.new.to_f
$sign = nil
$strength = 0.0
$delay = Time.new.to_f
$alsa = AlsaOutput.new 'default:0'
$sounds = []
def init
  GL.ClearColor 0.0, 0.0, 0.0, 1.0
  GL.Lightfv GL::LIGHT0, GL::AMBIENT, [ 1.0, 1.0, 1.0, 1.0 ]
  GL.Lightfv GL::LIGHT0, GL::DIFFUSE, [ 1.0, 1.0, 1.0, 1.0 ]
  GL.Lightfv GL::LIGHT0, GL::POSITION, [ 0.0, 6.5 + OBSERVER_Y, -3.0 + OBSERVER_Z, 1.0 ]
  GL.Lightfv GL::LIGHT0, GL::SPOT_DIRECTION, [ 0.0, -1.0, -0.5 ]
  GL.Lightf GL::LIGHT0, GL::SPOT_CUTOFF, 60.0
  GL.Lightf GL::LIGHT0, GL::SPOT_EXPONENT, 1.2
  GL.Enable GL::LIGHT0
  GL.Enable GL::LIGHTING
  GL.DepthFunc GL::LESS
  GL.Enable GL::DEPTH_TEST
  $tex = GL.GenTextures 3
  GL.BindTexture GL::TEXTURE_2D, $tex[0]
  GL.TexParameter GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::NEAREST
  GL.TexImage2D GL::TEXTURE_2D, 0, GL::RGB, 256, 256, 0,
                GL::RGB, GL::UNSIGNED_BYTE, $floor.memory.export
  GL.BindTexture GL::TEXTURE_2D, $tex[1]
  GL.TexParameter GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::NEAREST
  GL.TexImage2D GL::TEXTURE_2D, 0, GL::RGB, 256, 256, 0,
                GL::RGB, GL::UNSIGNED_BYTE, $side.memory.export
  GL.BindTexture GL::TEXTURE_2D, $tex[2]
  GL.TexParameter GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::NEAREST
  GL.TexImage2D GL::TEXTURE_2D, 0, GL::RGB, 256, 256, 0,
                GL::RGB, GL::UNSIGNED_BYTE, $back.memory.export
  $list = GL.GenLists 2
  GL.NewList $list, GL::COMPILE
  GL.Enable GL::TEXTURE_2D
  GL.Material GL::FRONT, GL::AMBIENT, [ 0.2, 0.2, 0.2, 1.0 ]
  GL.Material GL::FRONT, GL::DIFFUSE, [ 0.8, 0.8, 0.8, 1.0 ]
  GL.BindTexture GL::TEXTURE_2D, $tex[0]
  GL.Begin GL::QUADS
  GL.Normal 0.0, 1.0, 0.0
  GL.TexCoord 0.0, 1.0; GL.Vertex -SIZE_X, 0.0, 0.0
  GL.TexCoord 1.0, 1.0; GL.Vertex  SIZE_X, 0.0, 0.0
  GL.TexCoord 1.0, 0.0; GL.Vertex  SIZE_X, 0.0, -SIZE_Z
  GL.TexCoord 0.0, 0.0; GL.Vertex -SIZE_X, 0.0, -SIZE_Z
  GL.End
  GL.BindTexture GL::TEXTURE_2D, $tex[2]
  GL.Begin GL::QUADS
  GL.Normal 0.0, 0.0, 1.0
  GL.TexCoord 0.0, 0.914; GL.Vertex -SIZE_X, BAR_HEIGHT, -SIZE_Z
  GL.TexCoord 1.0, 0.914; GL.Vertex  SIZE_X, BAR_HEIGHT, -SIZE_Z
  GL.TexCoord 1.0, 0.0; GL.Vertex  SIZE_X, 5.0, -SIZE_Z
  GL.TexCoord 0.0, 0.0; GL.Vertex -SIZE_X, 5.0, -SIZE_Z
  GL.Normal 0.0, 1.0, 0.0
  GL.TexCoord 0.0, 0.914; GL.Vertex -SIZE_X, BAR_HEIGHT, -SIZE_Z
  GL.TexCoord 1.0, 0.914; GL.Vertex  SIZE_X, BAR_HEIGHT, -SIZE_Z
  GL.TexCoord 1.0, 0.914; GL.Vertex  SIZE_X, BAR_HEIGHT, BAR_THICKNESS - SIZE_Z
  GL.TexCoord 0.0, 0.914; GL.Vertex -SIZE_X, BAR_HEIGHT, BAR_THICKNESS - SIZE_Z
  GL.Normal 0.0, 0.0, 1.0
  GL.TexCoord 0.0, 0.914; GL.Vertex -SIZE_X, BAR_HEIGHT, BAR_THICKNESS - SIZE_Z
  GL.TexCoord 1.0, 0.914; GL.Vertex  SIZE_X, BAR_HEIGHT, BAR_THICKNESS - SIZE_Z
  GL.TexCoord 1.0, 1.0; GL.Vertex  SIZE_X, 0.0, BAR_THICKNESS - SIZE_Z
  GL.TexCoord 0.0, 1.0; GL.Vertex -SIZE_X, 0.0, BAR_THICKNESS - SIZE_Z
  GL.End
  GL.BindTexture GL::TEXTURE_2D, $tex[1]
  GL.Begin GL::QUADS
  GL.Normal 1.0, 0.0, 0.0
  GL.TexCoord 0.0, 1.0; GL.Vertex -SIZE_X, 0.0,  0.0
  GL.TexCoord 1.0, 1.0; GL.Vertex -SIZE_X, 0.0, -SIZE_Z
  GL.TexCoord 1.0, 0.0; GL.Vertex -SIZE_X, 5.0, -SIZE_Z
  GL.TexCoord 0.0, 0.0; GL.Vertex -SIZE_X, 5.0,  0.0
  GL.Normal -1.0, 0.0, 0.0
  GL.TexCoord 0.0, 1.0; GL.Vertex  SIZE_X, 0.0,  0.0
  GL.TexCoord 1.0, 1.0; GL.Vertex  SIZE_X, 0.0, -SIZE_Z
  GL.TexCoord 1.0, 0.0; GL.Vertex  SIZE_X, 5.0, -SIZE_Z
  GL.TexCoord 0.0, 0.0; GL.Vertex  SIZE_X, 5.0,  0.0
  GL.End
  GL.Disable GL::TEXTURE_2D
  GL.EndList
  GL.NewList $list + 1, GL::COMPILE
  GL.Material GL::FRONT, GL::AMBIENT, [ 0.7, 0.7, 0.0, 1.0 ]
  GL.Material GL::FRONT, GL::DIFFUSE, [ 0.3, 0.3, 0.0, 1.0 ]
  GLUT.SolidSphere RADIUS, 16, 16
  GL.EndList
end
display = proc do
  GL.Clear GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT
  GL.CallList $list
  GL.PushMatrix
  GL.Translate *$pos
  GL.CallList $list + 1
  GL.PopMatrix
  GLUT.SwapBuffers
end
reshape = proc do |w, h|
  GL.Viewport 0, 0, w, h
  GL.MatrixMode GL::PROJECTION
  GL.LoadIdentity
  GLU.Perspective 25.0, w.to_f/h, 1.0, 25.0
  GL.MatrixMode GL::MODELVIEW
  GL.LoadIdentity
  GL.Translate 0.0, OBSERVER_Y, OBSERVER_Z
end
keyboard = proc do |key, x, y|
  case key
  when ?\e
    exit 0
  when ?s
    $pos = [ X0, H0, Z0 ]
    $v = [ 0.0, SERVE_SPEED, 0.0 ]
  when ?\ 
    vz = V_MAX / 2
    t = ( SIZE_Z + $pos[2] + DIST_Z / REFLECTION ) / vz
    vy = 0.5 * GRAVITY * t - $pos[1] / t
    vx = -2 * $pos[0] / t
    $v = [ vx, vy, -vz ]
    #vz = 12.0
    #t = ( DIST_Z + DIST_Z / REFLECTION ) / vz
    #vy = 0.5 * GRAVITY * t - H0 / t
    #vx = -2 * X0 / t
    #$v = [ vx, vy, -vz ]
    #$pos = [ X0, H0, Z0 ]
    $sounds.push( ( $racket * [ 0.2, 0.2 ].min ).to_sint )
  end
end
animate = proc do
  dt = Time.new.to_f - $t
  $t += dt
  g = $pos[1] > RADIUS ? GRAVITY : 0
  $pos[0] += $v[0] * dt
  $pos[1] += $v[1] * dt - 0.5 * g * dt ** 2
  $pos[2] += $v[2] * dt
  v = Math.sqrt $v.inject( 0.0 ) { |a,b| a + b ** 2 }
  if g > 0 or v > MIN_SPEED
    f = g > 0 ? AIR_FRICTION : ROLL_FRICTION
    r = f * v
    $v[0] -= $v[0] * r * dt
    $v[1] -= $v[1] * r * dt + g * dt
    $v[2] -= $v[2] * r * dt
  else
    $v = [ 0, 0, 0 ]
  end
  if $pos[0] < -SIZE_X + RADIUS
    $pos[0] = 2 * ( -SIZE_X + RADIUS ) - $pos[0]
    $v[0] = -$v[0] * REFLECTION
    $sounds.push( ( $wall * [ $v[0].abs / SPEED_PER_VOLUME, 1.0 ].min ).to_sint )
  end
  if $pos[0] > SIZE_X - RADIUS
    $pos[0] = 2 * ( SIZE_X - RADIUS ) - $pos[0]
    $v[0] = -$v[0] * REFLECTION
    $sounds.push( ( $wall * [ $v[0].abs / SPEED_PER_VOLUME, 1.0 ].min ).to_sint )
  end
  if $pos[1] < RADIUS
    if $v[1] < -MIN_SPEED
      $pos[1] = 2 * RADIUS - $pos[1]
      $v[1] = -$v[1] * REFLECTION
      $sounds.push( ( $ground * [ 0.3 * $v[1].abs / SPEED_PER_VOLUME, 0.3 ].min ).to_sint )
    else
      $pos[1] = RADIUS
      $v[1] = 0
    end
  end
  b = $pos[1] > BAR_HEIGHT ? -SIZE_Z + RADIUS : -SIZE_Z + RADIUS + BAR_THICKNESS
  if $pos[2] < b and $v[2] < 0
    $pos[2] = 2 * b - $pos[2]
    $v[2] = -$v[2] * REFLECTION
    $sounds.push( ( $wall * [ $v[2].abs / SPEED_PER_VOLUME, 1.0 ].min ).to_sint )
  end
  if $pos[2] > -RADIUS
    $pos = [ X0, RADIUS, Z0 ]
    $v = [ 0, 0, 0 ]
  end
  if wiimote
    wiimote.get_state
    exit 0 if wiimote.buttons == WiiMote::BTN_HOME
    if wiimote.buttons == WiiMote::BTN_B
      $pos = [ X0, H0, Z0 ]
      $v = [ 0.0, SERVE_SPEED, 0.0 ]
    end
    acc = wiimote.acc.collect { |x| ( x - 120.0 ) / 2.5 }
    if acc[2].abs >= ACC_RISING and Time.new.to_f >= $delay
      $sign = acc[2] > 0 ? +1 : -1 unless $sign
      $strength = [ acc[2].abs, $strength ].max
    elsif $sign
      if acc[2] * $sign <= ACC_FALLING
        if $pos[1] >= MIN_HEIGHT
          # a = Math::PI + 2 * Math.atan2( $v[0], $v[2] ) - Math.atan( ( $pos[2] - NORM_Z ) / L )
          $sounds.push( ( $racket * [ $strength * 0.3 / 50, 0.3 ].min ).to_sint )
          vz = V_MIN + ( V_MAX - V_MIN ) * $strength / 50
          # vz = 12.5
          t = ( SIZE_Z + $pos[2] + DIST_Z / REFLECTION ) / vz
          vy = 0.5 * GRAVITY * t - $pos[1] / t
          vx = -2 * $pos[0] / t
          # vx = Math.tan( a ) * vz
          $v = [ vx, vy, -vz ]
        end
        $sign = nil
        $strength = 0.0
        $delay = Time.new.to_f + MIN_DELAY
      end
    end
  end
  avail = $alsa.avail
  $alsa.write( $sounds.inject( MultiArray.sint( 2, avail ).fill!( 0 ) ) do |x,s|
    n = [ x.shape[1], s.shape[1] ].min
    x[ 0 ... 2, 0 ... n ] + s[ 0 ... 2, 0 ... n ]
  end )
  $sounds = $sounds.select { |s| s.shape[1] > avail }.collect do |s|
    s[ 0 ... 2, avail ... s.shape[1] ]
  end
  GLUT.PostRedisplay
end
GLUT.Init
GLUT.InitDisplayMode GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH
GLUT.InitWindowSize WIDTH, HEIGHT
GLUT.CreateWindow 'Wii Remote'
init
GLUT.DisplayFunc display
GLUT.ReshapeFunc reshape
GLUT.KeyboardFunc keyboard
GLUT.IdleFunc animate
GLUT.MainLoop

See also: