I modified the tutorial game to make the ship point to the mouse cursor. Kept the physics, resulting in an interesting game. Here is the source code:
require 'rubygems' require 'gosu' require 'chipmunk' SCREEN_WIDTH = 640 SCREEN_HEIGHT = 480 # The number of steps to process every Gosu update # The Player ship can get going so fast as to "move through" a # star without triggering a collision; an increased number of # Chipmunk step calls per update will effectively avoid this issue SUBSTEPS = 6 # Convenience methods for converting between Gosu degrees, radians, and Vec2 vectors class Numeric def gosu_to_radians (self - 90) * Math::PI / 180.0 end def radians_to_gosu self * 180.0 / Math::PI + 90 end def radians_to_vec2 CP::Vec2.new(Math::cos(self), Math::sin(self)) end end # Layering of sprites module ZOrder Background, Stars, Player, UI, Cursor = *0..4 end class Cursor attr_reader :img, :visible, :imgObj, :pos_x, :pos_y def initialize(window,img,visible) @img=img @visible=visible @imgObj=Gosu::Image.new(window,img,true) @forced=false @window = window @pos_x = @window.mouse_x @pos_y = @window.mouse_y end def toggleVisible if not @forced @visible=!visible end end def forceVisible @forced=true @visible=true end def unforceVisible @forced=false end def visible? return visible end def draw if @visible @imgObj.draw(@window.mouse_x,@window.mouse_y,ZOrder::Cursor) end end def update @pos_x = @window.mouse_x @pos_y = @window.mouse_y end end # This game will have one Player in the form of a ship class Player attr_reader :shape def initialize(window, shape, cursor) @image = Gosu::Image.new(window, "media/Starfighter.bmp", false) @shape = shape @shape.body.p = CP::Vec2.new(0.0, 0.0) # position @shape.body.v = CP::Vec2.new(0.0, 0.0) # velocity @cursor = cursor # Keep in mind that down the screen is positive y, which means that PI/2 radians, # which you might consider the top in the traditional Trig unit circle sense is actually # the bottom; thus 3PI/2 is the top @shape.body.a = (3*Math::PI/2.0) # angle in radians; faces towards top of screen end # Directly set the position of our Player def warp(vect) @shape.body.p = vect end # Apply negative Torque; Chipmunk will do the rest # SUBSTEPS is used as a divisor to keep turning rate constant # even if the number of steps per update are adjusted def turn_left @shape.body.t -= 400.0/SUBSTEPS end # Apply positive Torque; Chipmunk will do the rest # SUBSTEPS is used as a divisor to keep turning rate constant # even if the number of steps per update are adjusted def turn_right @shape.body.t += 400.0/SUBSTEPS end # Apply forward force; Chipmunk will do the rest # SUBSTEPS is used as a divisor to keep acceleration rate constant # even if the number of steps per update are adjusted # Here we must convert the angle (facing) of the body into # forward momentum by creating a vector in the direction of the facing # and with a magnitude representing the force we want to apply def accelerate @shape.body.apply_force((@shape.body.a.radians_to_vec2 * (3000.0/SUBSTEPS)), CP::Vec2.new(0.0, 0.0)) end # Apply even more forward force # See accelerate for more details def boost @shape.body.apply_force((@shape.body.a.radians_to_vec2 * (3000.0)), CP::Vec2.new(0.0, 0.0)) end # Apply reverse force # See accelerate for more details def reverse @shape.body.apply_force(-(@shape.body.a.radians_to_vec2 * (1000.0/SUBSTEPS)), CP::Vec2.new(0.0, 0.0)) end # Wrap to the other side of the screen when we fly off the edge def validate_position l_position = CP::Vec2.new(@shape.body.p.x % SCREEN_WIDTH, @shape.body.p.y % SCREEN_HEIGHT) @shape.body.p = l_position end def update angle_degrees = Gosu::angle(@shape.body.p.x, @shape.body.p.y, @cursor.pos_x , @cursor.pos_y ) #puts angle_degrees @shape.body.a = angle_degrees.gosu_to_radians # #TODO: apply a smooth rotation (e.g. pontential) that dimish torque when reaching goal # if (angle_degrees > 0) #positive # #turn counter_clock wise # turn_right() # else # turn_left() # end end def draw @image.draw_rot(@shape.body.p.x, @shape.body.p.y, ZOrder::Player, @shape.body.a.radians_to_gosu) end end # See how simple our Star is? # Of course... it just sits around and looks good... class Star attr_reader :shape def initialize(animation, shape) @animation = animation @color = Gosu::Color.new(0xff000000) @color.red = rand(255 - 40) + 40 @color.green = rand(255 - 40) + 40 @color.blue = rand(255 - 40) + 40 @shape = shape @shape.body.p = CP::Vec2.new(rand * SCREEN_WIDTH, rand * SCREEN_HEIGHT) # position @shape.body.v = CP::Vec2.new(0.0, 0.0) # velocity @shape.body.a = (3*Math::PI/2.0) # angle in radians; faces towards top of screen end def draw img = @animation[Gosu::milliseconds / 100 % @animation.size]; img.draw(@shape.body.p.x - img.width / 2.0, @shape.body.p.y - img.height / 2.0, ZOrder::Stars, 1, 1, @color, :additive) end end # The Gosu::Window is always the "environment" of our game # It also provides the pulse of our game class GameWindow < Gosu::Window def initialize super(SCREEN_WIDTH, SCREEN_HEIGHT, false, 16) self.caption = "Gosu & Chipmunk Integration Demo" @background_image = Gosu::Image.new(self, "media/Space.png", true) # Put the beep here, as it is the environment now that determines collision @beep = Gosu::Sample.new(self, "media/Beep.wav") # Put the score here, as it is the environment that tracks this now @score = 0 @font = Gosu::Font.new(self, Gosu::default_font_name, 20) # Time increment over which to apply a physics "step" ("delta t") @dt = (1.0/60.0) # Create our Space and set its damping # A damping of 0.8 causes the ship bleed off its force and torque over time # This is not realistic behavior in a vacuum of space, but it gives the game # the feel I'd like in this situation @space = CP::Space.new @space.damping = 0.8 # Create the Body for the Player body = CP::Body.new(10.0, 150.0) # In order to create a shape, we must first define it # Chipmunk defines 3 types of Shapes: Segments, Circles and Polys # We'll use s simple, 4 sided Poly for our Player (ship) # You need to define the vectors so that the "top" of the Shape is towards 0 radians (the right) shape_array = [CP::Vec2.new(-25.0, -25.0), CP::Vec2.new(-25.0, 25.0), CP::Vec2.new(25.0, 1.0), CP::Vec2.new(25.0, -1.0)] shape = CP::Shape::Poly.new(body, shape_array, CP::Vec2.new(0,0)) # The collision_type of a shape allows us to set up special collision behavior # based on these types. The actual value for the collision_type is arbitrary # and, as long as it is consistent, will work for us; of course, it helps to have it make sense shape.collision_type = :ship @space.add_body(body) @space.add_shape(shape) @mycursor = Cursor.new(self, "media/Starfighter.bmp", true) @player = Player.new(self, shape, @mycursor) @player.warp(CP::Vec2.new(320, 240)) # move to the center of the window @star_anim = Gosu::Image::load_tiles(self, "media/Star.png", 25, 25, false) @stars = Array.new # Here we define what is supposed to happen when a Player (ship) collides with a Star # I create a @remove_shapes array because we cannot remove either Shapes or Bodies # from Space within a collision closure, rather, we have to wait till the closure # is through executing, then we can remove the Shapes and Bodies # In this case, the Shapes and the Bodies they own are removed in the Gosu::Window.update phase # by iterating over the @remove_shapes array # Also note that both Shapes involved in the collision are passed into the closure # in the same order that their collision_types are defined in the add_collision_func call @remove_shapes = [] @space.add_collision_func(:ship, :star) do |ship_shape, star_shape| @score += 10 @beep.play @remove_shapes << star_shape end # Here we tell Space that we don't want one star bumping into another # The reason we need to do this is because when the Player hits a Star, # the Star will travel until it is removed in the update cycle below # which means it may collide and therefore push other Stars # To see the effect, remove this line and play the game, every once in a while # you'll see a Star moving @space.add_collision_func(:star, :star, &nil) end def update # Step the physics environment SUBSTEPS times each update SUBSTEPS.times do # This iterator makes an assumption of one Shape per Star making it safe to remove # each Shape's Body as it comes up # If our Stars had multiple Shapes, as would be required if we were to meticulously # define their true boundaries, we couldn't do this as we would remove the Body # multiple times # We would probably solve this by creating a separate @remove_bodies array to remove the Bodies # of the Stars that were gathered by the Player @remove_shapes.each do |shape| @stars.delete_if { |star| star.shape == shape } @space.remove_body(shape.body) @space.remove_shape(shape) end @remove_shapes.clear # clear out the shapes for next pass # When a force or torque is set on a Body, it is cumulative # This means that the force you applied last SUBSTEP will compound with the # force applied this SUBSTEP; which is probably not the behavior you want # We reset the forces on the Player each SUBSTEP for this reason @player.shape.body.reset_forces # Wrap around the screen to the other side @player.validate_position # Check keyboard if button_down? Gosu::Button::KbLeft @player.turn_left end if button_down? Gosu::Button::KbRight @player.turn_right end if button_down? Gosu::Button::KbUp if ( (button_down? Gosu::Button::KbRightShift) || (button_down? Gosu::Button::KbLeftShift) ) @player.boost else @player.accelerate end elsif button_down? Gosu::Button::KbDown @player.reverse end @mycursor.update @player.update # Perform the step over @dt period of time # For best performance @dt should remain consistent for the game @space.step(@dt) end # Each update (not SUBSTEP) we see if we need to add more Stars if rand(100) < 4 and @stars.size < 25 then body = CP::Body.new(0.0001, 0.0001) shape = CP::Shape::Circle.new(body, 25/2, CP::Vec2.new(0.0, 0.0)) shape.collision_type = :star @space.add_body(body) @space.add_shape(shape) @stars.push(Star.new(@star_anim, shape)) end end def draw @background_image.draw(0, 0, ZOrder::Background) @mycursor.draw @player.draw @stars.each { |star| star.draw } @font.draw("Score: #{@score}", 10, 10, ZOrder::UI, 1.0, 1.0, 0xffffff00) end def button_down(id) if id == Gosu::Button::KbEscape close end end end window = GameWindow.new window.show
Comments