Playing Squash with the Wii Remote
14 Feb 2011I 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: