# a bit ugly... INFINITY = Float::MAX * 1.1 VZERO = vec2(0.0, 0.0) class Game WIDTH = 800.0 HEIGHT = 500.0 FLOOR_Y = -0.25 * HEIGHT PHYSICS_FPS = 60.0 PHYSICS_INTERVAL = 1.0 / PHYSICS_FPS SERVE_SPEED = 384.0 GRAVITY = -256.0 BUCKET_SPEED = 128.0 BALL_RADIUS = 8.0 MAX_TRACE = 60 TYPE_WALL = 1 TYPE_PEG = 2 TYPE_BALL = 3 TYPE_FLOOR = 4 TYPE_TRACER = 5 TYPE_BUCKET = 6 RED_COUNT = 20 BALL_COUNT = 10 BOREDOM_TIME = 1.0 SHADOW_COLOR = [ 0.00, 0.00, 0.00 ].pack('f*') RED_PEG_COLOR = [ 0.25, 0.25, 1.00 ].pack('f*') BALL_COLOR = [ 1.00, 1.00, 0.10 ].pack('f*') BUCKET_COLOR = [ 0.50, 1.00, 0.50 ].pack('f*') BUCKET_INNER_COLOR = [ 0.10, 0.20, 0.10 ].pack('f*') IMAGE_NAMES = [ :evilUFO, :brick, :earth, :mars, :background, :brickEarth, :beam, :stone, :bucket_inner ] EXPLOSION_SOUND_NAMES = [ 'explosion', 'explosion2', 'explosion3', 'explosion4', 'explosion5', 'explosion6', #'explosion7', 'explosion8' # my WAV parser barfs at these... ] TADA_SOUND_NAMES = [ 'tada' ] BOING_SOUND_NAMES = [ 'boing1', 'boing2', 'boing3' ] def initialize @leftover_dt = 0.0 @boredom_timer = 0.0 end def opengl_context_created @audio = Audio.new @explosion_sounds = _load_sounds(EXPLOSION_SOUND_NAMES) @tada_sounds = _load_sounds(TADA_SOUND_NAMES) @boing_sounds = _load_sounds(BOING_SOUND_NAMES) @images = {} @textures = {} IMAGE_NAMES.each do |name| image = Image.new( NSBundle.mainBundle.pathForImageResource(name.to_s)) @images[name] = image @textures[name] = texturize(image) end @level_index = 0 _new_game end def opengl_context_resized(w, h) @width, @height = w, h end def display(dt) _update(dt) _draw() end def text_entered(text) end def key_pressed(keycode) end def key_released(keycode) end def mouse_pressed(button) if @mode == :serve then @ball.v = _serve_vector * SERVE_SPEED @mode = :play @boredom_timer = 0.0 end end def mouse_released(button) end def mouse_moved_by(dx, dy) end def mouse_moved_to(x, y) @serve_angle = -Math::PI * (1.0 - (x / WIDTH)) end def _load_sounds(names) names.map { |n| Sound.new(NSBundle.mainBundle.pathForSoundResource(n + '.wav')) } end def _new_game @mode = :serve @serve_angle = -0.5 * Math::PI @space = CP::Space.new @space.elastic_iterations = 5 @space.gravity = vec2(0.0, GRAVITY) static_body = CP::Body.new(INFINITY, INFINITY) _make_wall(static_body, vec2(0.0, FLOOR_Y), vec2(0.0, 2 * HEIGHT)) _make_wall(static_body, vec2(WIDTH, 2 * HEIGHT), vec2(WIDTH, FLOOR_Y)) _make_floor(static_body) @pegs = [] LEVELS[@level_index].call( LevelFactory.new(@space, static_body, TYPE_PEG), @pegs) red = RED_COUNT while red > 0 do peg = @pegs[rand(@pegs.size)] if peg.color == :blue then peg.color = :red red -= 1 end end _update_red_left _update_balls_left(BALL_COUNT) @ball = _make_ball(TYPE_BALL) @tracer = _make_ball(TYPE_TRACER) @bucket = _make_bucket @bucket.p = vec2(0.5 * WIDTH, 0.0) @space.add_collision_func(TYPE_PEG, TYPE_BALL) do |shape_a, shape_b| @delayed << lambda { pan = 2.0 * shape_a.p.x / WIDTH - 1.0 if !shape_a.lit then shape_a.lit = true @boredom_timer = 0.0 fantastic = shape_a.color == :red #confusing, red == earth == more points @explosions << Explosion.new(shape_a.p, fantastic) @audio.play(@explosion_sounds[@explosion_sound_index], pan) @explosion_sound_index = (@explosion_sound_index + 1) % @explosion_sounds.size if shape_a.color == :red then @audio.play(@tada_sounds[@tada_sound_index], pan) @tada_sound_index = (@tada_sound_index + 1) % @tada_sounds.size end else if shape_b.body.v.length > 50.0 then #todo should be related to bounce... need to get contacts from chipmunk, not posible with current bindings. @audio.play(@boing_sounds[@boing_sound_index], pan) @boing_sound_index = (@boing_sound_index + 1) % @boing_sounds.size end end } end @space.add_collision_func(TYPE_FLOOR, TYPE_BALL) do |shape_a, shape_b| @delayed << lambda { _dead_ball(false) } end @space.add_collision_func(TYPE_PEG, TYPE_TRACER) do |shape_a, shape_b| @tracing = false end @space.add_collision_func(TYPE_BUCKET, TYPE_BALL) do |shape_a, shape_b| @delayed << lambda { _dead_ball(true) } end @explosions = [] _hack_objects @explosion_sound_index = 0 @tada_sound_index = 0 @boing_sound_index = 0 end def _serve @mode = :serve end def _update(dt) @leftover_dt += dt while @leftover_dt >= PHYSICS_INTERVAL do @delayed = [] @space.step(PHYSICS_INTERVAL) @boredom_timer += PHYSICS_INTERVAL @delayed.each do |f| f.call() end if @boredom_timer > BOREDOM_TIME && @ball.v.y.abs < 0.1 then _remove_lit_pegs end _hack_objects removeArray = [] @explosions.delete_if do |explosion| explosion.update(PHYSICS_INTERVAL) end @leftover_dt -= PHYSICS_INTERVAL end end def _draw() glClear(GL_COLOR_BUFFER_BIT) # TODO handle aspect glMatrixMode(GL_PROJECTION) glLoadIdentity() glOrtho(0.0, WIDTH, 0.0, HEIGHT, 1.0, -1.0) glMatrixMode(GL_MODELVIEW) draw_quad(@images[:background], @textures[:background], 0.0, 0.0, WIDTH, HEIGHT) draw_quad( @images[:stone], @textures[:stone], @bucket.p.x - 70.0, @bucket.p.y, 20.0, 20.0) draw_quad( @images[:stone], @textures[:stone], @bucket.p.x + 50.0, @bucket.p.y, 20.0, 20.0) draw_quad( @images[:bucket_inner], @textures[:bucket_inner], @bucket.p.x - 50.0, @bucket.p.y, 100.0, 20.0) @pegs.each do |peg| if peg.lit then glColor3f(0.2, 0.2, 0.2) else glColor3f(1.0, 1.0, 1.0) end case peg when Peg if peg.color == :red then image = @images[:earth] texture = @textures[:earth] else image = @images[:mars] texture = @textures[:mars] end draw_centered_quad( image, texture, peg.p.x, peg.p.y, 2 * Peg::RADIUS, 2 * Peg::RADIUS) when Brick if peg.color == :red then image = @images[:brickEarth] texture = @textures[:brickEarth] else image = @images[:brick] texture = @textures[:brick] end draw_centered_quad( image, texture, peg.p.x, peg.p.y, Brick::WIDTH, Brick::HEIGHT) when Curve if peg.color == :red then image = @images[:brickEarth] texture = @textures[:brickEarth] else image = @images[:brick] texture = @textures[:brick] end glEnable(GL_TEXTURE_RECTANGLE_ARB) glBindTexture(GL_TEXTURE_RECTANGLE_ARB, texture) glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) glEnable(GL_BLEND) glMatrixMode(GL_TEXTURE) glPushMatrix() glScalef(image.width, image.height, 1.0) glMatrixMode(GL_MODELVIEW) glVertexPointer(2, GL_FLOAT, 16, peg.va) glTexCoordPointer(2, GL_FLOAT, 16, peg.va[8..-1]) glEnableClientState(GL_VERTEX_ARRAY) glEnableClientState(GL_TEXTURE_COORD_ARRAY) glDrawArrays(GL_POLYGON, 0, peg.vs.size) glDisableClientState(GL_VERTEX_ARRAY) glDisableClientState(GL_TEXTURE_COORD_ARRAY) glMatrixMode(GL_TEXTURE) glPopMatrix() glMatrixMode(GL_MODELVIEW) glDisable(GL_BLEND) glDisable(GL_TEXTURE_RECTANGLE_ARB) end end glColor3fv(BALL_COLOR) draw_centered_quad( @images[:evilUFO], @textures[:evilUFO], @ball.p.x, @ball.p.y, 2 * BALL_RADIUS, 2 * BALL_RADIUS) _draw_shadowed_text( @red_left_image, @red_left_texture, 0.0, HEIGHT - @red_left_image.height, RED_PEG_COLOR) _draw_shadowed_text( @balls_left_image, @balls_left_texture, WIDTH - @balls_left_image.width, HEIGHT - @balls_left_image.height, BALL_COLOR) if @mode == :serve glColor3fv(BALL_COLOR) right = vec2(0.0,1.0) if @trace.size > 1 then up = @trace[0] - @trace[1] up = up.normalize right = vec2(-up.y, up.x) end texY = 0.0 line_width = 2.0 glEnable(GL_TEXTURE_RECTANGLE_ARB) glBindTexture(GL_TEXTURE_RECTANGLE_ARB, @textures[:beam]) glBlendFunc(GL_ONE, GL_ONE) glEnable(GL_BLEND) glBegin(GL_TRIANGLE_STRIP) @trace.each_with_index do |v, index| if index == 0 then texY = 0.0 elsif index == @trace.size - 1 then texY = 0.0 else texY = 0.5 end glTexCoord2f(0.0, texY * @images[:beam].height) glVertex2f(v.x - right.x * line_width, v.y - right.y * line_width) glTexCoord2f(1.0* @images[:beam].width,texY * @images[:beam].height) glVertex2f(v.x + right.x * line_width, v.y + right.y * line_width) end glEnd() glDisable(GL_BLEND) glDisable(GL_TEXTURE_RECTANGLE_ARB) glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) end @explosions.each do |explosion| explosion.draw() end glColor3f(1.0, 1.0, 1.0) end def _draw_shadowed_text(image, texture, x, y, color) glColor3fv(SHADOW_COLOR) draw_quad( image, texture, x + 2.0, y - 2.0, image.width, image.height) glColor3fv(color) draw_quad( image, texture, x, y, image.width, image.height) end def _make_wall(body, p0, p1) shape = CP::Shape::Segment.new(body, p0, p1, 0.0) shape.e = 1.0 shape.u = 0.0 shape.collision_type = TYPE_WALL @space.add_static_shape(shape) end def _make_floor(body) shape = CP::Shape::Segment.new(body, vec2(0.0, FLOOR_Y), vec2(WIDTH, FLOOR_Y), 0.0) shape.e = 0.0 shape.u = 0.0 shape.collision_type = TYPE_FLOOR @space.add_static_shape(shape) end def _make_ball(type) body = CP::Body.new(1.0, INFINITY) @space.add_body(body) shape = CP::Shape::Circle.new(body, BALL_RADIUS, VZERO) shape.e = 1.0 shape.u = 0.0 shape.collision_type = type @space.add_shape(shape) body end def _make_bucket body = CP::Body.new(1000000.0, INFINITY) @space.add_body(body) left = CP::Shape::Poly.new(body, _make_rect(-70, -50, 20, 70), VZERO) left.e = 0.75 left.u = 0.0 left.collision_type = TYPE_WALL @space.add_shape(left) right = CP::Shape::Poly.new(body, _make_rect(50, -50, 20, 70), VZERO) right.e = 0.75 right.u = 0.0 right.collision_type = TYPE_WALL @space.add_shape(right) bottom = CP::Shape::Poly.new(body, _make_rect(-70, -70, 140, 20), VZERO) bottom.e = 0.75 bottom.u = 0.0 bottom.collision_type = TYPE_BUCKET @space.add_shape(bottom) body end def _make_rect(x, y, w, h) [ vec2(x, y), vec2(x, y + h), vec2(x + w, y + h), vec2(x + w, y) ] end def _draw_untextured_quad(x, y, w, h) glBegin(GL_QUADS) glVertex2f(x, y) glVertex2f(x + w, y) glVertex2f(x + w, y + h) glVertex2f(x, y + h) glEnd() end def _hack_objects if @mode == :serve then p = vec2(0.5 * WIDTH, HEIGHT) + _serve_vector * 50.0 bucket_p = vec2(@bucket.p.x, @bucket.p.y) bucket_v = vec2(@bucket.v.x, @bucket.v.y) @tracer.p = p @tracer.v = _serve_vector * SERVE_SPEED @trace = [] count = 0 @tracing = true while @tracing && count < MAX_TRACE do @ball.p = p @ball.v = VZERO @space.step(PHYSICS_INTERVAL) @bucket.p = bucket_p @bucket.v = bucket_v @trace << vec2(@tracer.p.x, @tracer.p.y) count += 1 end @ball.p = p @ball.v = VZERO else @tracer.p = vec2(2 * WIDTH, 2 * HEIGHT) @tracer.v = VZERO if @ball.p.y < FLOOR_Y then _dead_ball(false) end end @bucket.p = vec2(@bucket.p.x, 0.0) @bucket.v = if @bucket.p.x >= WIDTH - 70.0 then vec2(-BUCKET_SPEED, 0.0) elsif @bucket.p.x <= 70.0 then vec2(BUCKET_SPEED, 0.0) elsif @bucket.v.x < 0.0 then vec2(-BUCKET_SPEED, 0.0) else vec2(BUCKET_SPEED, 0.0) end end def _serve_vector vec2(Math::cos(@serve_angle), Math::sin(@serve_angle)) end def _update_red_left red_left = @pegs.find_all { |p| p.color == :red }.size if @red_left != red_left then @red_left = red_left @red_left_image = Text.new( 128.0, 'Lucida Grande', 36.0, :left, @red_left.to_s) if @red_left_texture then glDeleteTextures(1, [ @red_left_texture ].pack('i')) end @red_left_texture = texturize(@red_left_image) end end def _update_balls_left(balls_left) if @balls_left != balls_left then @balls_left = balls_left @balls_left_image = Text.new( 128.0, 'Lucida Grande', 36.0, :right, @balls_left.to_s) if @balls_left_texture then glDeleteTextures(1, [ @balls_left_texture ].pack('i')) end @balls_left_texture = texturize(@balls_left_image) end end def _dead_ball(free) _remove_lit_pegs if @red_left == 0 then @level_index = (@level_index + 1) % LEVELS.size _new_game elsif @balls_left == 0 && !free _new_game else _update_balls_left(@balls_left - 1) unless free _serve end end def _remove_lit_pegs @pegs = @pegs.find_all { |p| if p.lit then @space.remove_static_shape(p) end !p.lit } _update_red_left end end $game = Game.new