Koda spel i Ruby

Lektion 9: Skjuta och träffa

Välkommen till den näst sista lektionen i spelskolan! I den ska vi uträtta två saker. Dels ska vi se till så att laserkanonen, som nu är laddad, går att fyra av. Dels ska vi fixa så att asteroiderna försvinner om de blir träffade ett visst antal gånger.

För att kunna avlossa laserskottet så ska vi lägga till en ny variabel i Laser-klassen. Låt oss kalla den för @shooting. Från början ska @shooting ha värdet false. Det ordnar vi i initialize-metoden:


def initialize(window, hero)
  @window = window
  @hero = hero
  @icon = Gosu::Image.new(@window, "laser.png", true)
  laser_home
  @shooting = false
end
      

Sedan lägger vi till en metod, shoot, som bara gör en enda sak: ändrar värdet på @shooting till true:


def shoot
  @shooting = true
end
      

Och så ska vi göra så att skottet flyger iväg om @shooting är true och stannar kvar om @shooting är false. Vi lägger till två stycken if-satser i laserskottets update-metod:


def update
  if @shooting
    @x = @x + 30
    if @x > @window.width
      @shooting = false
    end
  else
    laser_home
  end
end	  
	  

Den första if-satsen kollar värdet på @shooting. Om det är true så avfyras kanonen. Notera att skottet flyttas 30 pixlar åt gången, och därmed flyger mycket snabbare än både asteroider och rymdskepp. Om @shooting är false händer ingenting alls, utan skottet håller sig "hemma" under laserkanonen, tack vare metoden laser_home.

Den andra if-satsen ligger inuti den första och kontrollerar om @x har blivit större än fönstrets bredd, det vill säga om skottet har flygit ut ur fönstret. I så fall ändras värdet på @shooting till false, och nästa gång update-metoden körs kommer skottet därför att flyttas tillbaka till ursprungsläget.

Vi måste också se till så att metoden shoot anropas när man trycker på en viss tangent, förslagsvis space. Så vi går till Window-klassens update-metod. Under de if-satser som kontrollerar om piltangenterna är nedtryckta skriver vi:


if button_down? Gosu::Button::KbSpace
  @laser.shoot
end	  
	  

Nu kan rymdskeppet skjuta laserstrålar! Prova så får du se. Men fortfarande händer ingenting när asteroiderna träffas. Vi går till Asteroid-klassen och skriver en hit_by_laser?-metod. Så här ska den se ut:


def hit_by_laser?
  @laser.shooting && Gosu::distance(@x, @y, @laser.x, @laser.y) < 50
end	  
	  

Den här metoden ska vi använda för att kontrollera två saker: om laserskottet har fyrats av och om avståndet mellan skottet och asteroiden är mindre än 50 pixlar. När man vill kolla om två saker är sanna används som du ser två stycken och-tecken, "&&".

För att hålla reda på hur många gånger asteroiden har träffats ska vi använda en ny variabel, @hits, som i initialize-metoden får värdet 0:


def initialize(window, laser)
  @window = window
  @laser = laser
  @icon = Gosu::Image.new(@window, "asteroid.png", true)
  @x = @window.width + rand(1000)
  @y = rand(@window.height - @icon.height)
  @speed = 3 + rand(5)
  @hits = 0
end	  
	  

I metoden ovan har vi också lagt till lite laser-kod på första och tredje raden. Det är för att asteroiden ska "lära känna" laserskottet, och kunna läsa av dess position.

Dessutom ska vi göra ett par ändringar i Asteroid-klassens update-metod:


def update
  if hit_by_laser?
    @hits = @hits + 1
  end
    
  @x = @x - @speed
    
  if @x < -@icon.width || @hits >= 7
    @x = @window.width
    @y = rand(@window.height - @icon.height)
    @speed = 3 + rand(5)
    @hits = 0
  end
end	  
	  

Den första if-satsen anropar hit_by_laser?-metoden. Om asteroiden träffats ökas värdet på @hits med 1.

Och så har vi ändrat den andra if-satsen lite grann. Den kontrollerar nu om asteroiden har åkt ut ur fönstret eller om antalet träffar är större än, eller lika med, 7. När man vill göra en sådan kontroll används två lodräta streck, "||". Om minst ett av dessa påståenden är sanna så får asteroiden ny startposition och hastighet och @hits nollställs.

Det här innebär att om en asteroid blir träffad av laserskottet sju gånger så kommer den att försvinna! Detta ska strax bevisas. Vi måste bara göra ytterligare två små tillägg först. I Laser-klassen ska vi, direkt efter class Laser lägga till följande rad:


attr_reader :x, :y, :shooting	  
	  

Den gör så att variablerna @x, @y och @shooting kan läsas av utifrån.

Till sist en liten justering i Window-klassens initialize-metod. När asteroidsvärmen bildas ska vi även skicka med en hänvisning till vårt laserskott, så här:

	  
@asteroids = 5.times.map {Asteroid.new(self, @laser)}
	  

Sådär! Vårt spel ska nu, i sin helhet, se ut så som följer:


require 'gosu'

class Window < Gosu::Window
  def initialize
    super(640, 480, false)
    @hero = Hero.new(self)
    @laser = Laser.new(self, @hero)
    @asteroids = 5.times.map {Asteroid.new(self, @laser)}
    @running = true
  end
  
  def update
    if @running
      if button_down? Gosu::Button::KbRight
        @hero.move_forward
      end
  
      if button_down? Gosu::Button::KbLeft
        @hero.move_back
      end
  
      if button_down? Gosu::Button::KbUp
        @hero.move_up
      end
  
      if button_down? Gosu::Button::KbDown
        @hero.move_down
      end
         
      if button_down? Gosu::Button::KbSpace
        @laser.shoot
      end
      
      @laser.update
      @asteroids.each {|asteroid| asteroid.update}
      
      if @hero.hit_by?(@asteroids)
        @running = false
      end
    end  
  end
  
  def draw
    @hero.draw
    @laser.draw
    @asteroids.each {|asteroid| asteroid.draw}
  end
end 

class Hero
  attr_reader :x, :y
  
  def initialize(window)
    @window = window
    @icon = Gosu::Image.new(@window, "spaceship.png", true)
    @x = 100
    @y = 215
  end
  
  def move_forward
    @x = @x + 5
    if @x > @window.width - @icon.width
      @x = @window.width - @icon.width
    end
  end
  
  def move_back
    @x = @x - 5
    if @x < 0
      @x = 0
    end
  end

  def move_up
    @y = @y - 5
    if @y < 0
      @y = 0
    end
  end

  def move_down
    @y = @y + 5
    if @y > @window.height - @icon.height
      @y = @window.height - @icon.height
    end
  end 
  
  def draw
    @icon.draw(@x, @y, 2)
  end
  
  def hit_by?(asteroids)
    asteroids.any? {|asteroid| Gosu::distance(@x, @y, asteroid.x, asteroid.y) < 50} 
  end  
end

class Asteroid
  attr_reader :x, :y
  
  def initialize(window, laser)
    @window = window
    @laser = laser
    @icon = Gosu::Image.new(@window, "asteroid.png", true)
    @x = @window.width + rand(1000)
    @y = rand(@window.height - @icon.height)
    @speed = 3 + rand(5)
    @hits = 0
  end  
  
  def hit_by_laser?
    @laser.shooting && Gosu::distance(@x, @y, @laser.x, @laser.y) < 50
  end
  
  def update
    if hit_by_laser?
      @hits = @hits + 1
    end
    
    @x = @x - @speed
    
    if @x < -@icon.width || @hits >= 7
      @x = @window.width
      @y = rand(@window.height - @icon.height)
      @speed = 3 + rand(5)
      @hits = 0
    end
  end

  def draw
    @icon.draw(@x, @y, 2)
  end  
end

class Laser
  attr_reader :x, :y, :shooting
  
  def initialize(window, hero)
    @window = window
    @hero = hero
    @icon = Gosu::Image.new(@window, "laser.png", true)
    laser_home
    @shooting = false
  end
  
  def shoot
    @shooting = true
  end
  
  def laser_home
    @x = @hero.x + 46
    @y = @hero.y + 3
  end
  
  def update
    if @shooting
      @x = @x + 30
      if @x > @window.width
        @shooting = false
      end
    else
      laser_home
    end
  end
  
  def draw
    @icon.draw(@x, @y, 1)
  end
end

window = Window.new
window.show	  
	  

Dags att testa spelet! Det ska nu gå att skjuta bort asteroiderna. Om du tycker att det är för svårt kan du minska antalet träffar som krävs för att en asteroid kan försvinna. Jag är säker på att du klarar det på egen hand!

Vårt spel är nu i stort sett färdigt. I den tionde och sista lektionen ska vi bara ägna oss åt finputsning. Till exempel ska vi lägga till poängräkning, så att man kan sätta rekord i spelet!