Koda spel i Ruby

Lektion 10: Lite finputsning

Sorgligt nog är vi framme vid den allra sista lektionen. I den ska du få lära dig ytterligare några saker som jag tror att du kan ha nytta av när du skapar egna spel. Först ett par småsaker som är mycket enkla att fixa till.

Jag vet inte om du har tänkt på det, men vårt spel har ännu inget namn. Det brukar datorspel ha. Så till att börja med tycker jag att vi döper spelet. Du får kalla din version precis vad du vill – jag ska kalla min för Asteroid Storm!

För att namnet ska skrivas ut i spelfönstrets överkant behöver vi bara lägga till denna rad i Window-klassens initialize-metod:


self.caption = "---=== ASTEROID STORM ===---"
	  

De där bindestrecken och likamedtecknen behöver du inte ha med om du inte vill – det är bara pynt.

Och så borde vi ha en bakgrundsbild i spelfönstret, en som är 640 pixlar bred och 480 pixlar hög. Det gör du enkelt i ett ritprogram. Annars kan du använda den som ligger här nedanför. Som vanligt är det bara att högerklicka och spara bilden på din egen dator.

Vi måste se till så att datorn vet vilken bakgrundsbild som ska användas, så i initialize-metoden lägger vi till ytterligare en rad:


@background_image = Gosu::Image.new(self, "stars.png", true)	  
	  

Och överst i Window-klassens draw-metod skriver vi:


@background_image.draw(0, 0, 0)	  
	  

Observera att bilden ska ritas i lager 0, det vill säga under både laserskott, rymdskepp och asteroider.

Sedan ska vi lägga till en poängräknare, och det kräver lite mer kod än de två första åtgärderna. Först går vi till Hero-klassen där vi lägger till den nya variabeln @score, som i initialize-metoden sätts till 0:


def initialize(window)
  @window = window
  @icon = Gosu::Image.new(@window, "spaceship.png", true)
  @x = 100
  @y = 215
  @score = 0
end	  
	  

Den här variabeln ska gå att läsa av utifrån, så vi lägger till den i raden som följer direkt efter class Hero:


attr_reader :x, :y, :score	  
	  

Vi måste också ha en metod som ökar poängen, och som ska anropas när rymdskeppet har "klarat av" en asteroid – antingen genom att skjuta den eller undvika den.


def add_score
  @score = @score + 10
end  
	  

Låt oss sedan gå till klassen Asteroid, som borde vara ett bra ställe att anropa add_score från. I update-metoden, allra sist i den if-sats som kontrollerar om asteroiden har lämnat fönstret eller blivit bortskjuten, lägger vi till detta anrop:


if @x < -@icon.width || @hits >= 7
  @x = @window.width
  @y = rand(@window.height - @icon.height)
  @speed = 3 + rand(5)
  @hits = 0
  @hero.add_score
end	  
	  

För att det här ska fungera så måste asteroiden "känna till" vårt rymdskepp. Vi gör ett par ändringar i Asteroid-klassens initialize-metod:


def initialize(window, laser, hero)
  @window = window
  @laser = laser
  @hero = hero
  @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	  
	  

På första raden står det nu även hero inom parenteserna och på fjärde raden har vi lagt till @hero = hero.

Och så måste vi ändra ytterligare i Window-klassen. Så här ska initialize-metoden hädanefter se ut:


def initialize
  super(640, 480, false)
  self.caption = "---=== ASTEROID STORM ===---"
  @background_image = Gosu::Image.new(self, "stars.png", true)
  @font = Gosu::Font.new(self, Gosu::default_font_name, 20)
  @hero = Hero.new(self)
  @laser = Laser.new(self, @hero)
  @asteroids = 5.times.map {Asteroid.new(self, @laser, @hero)}
  @running = true
end	  
	  

Här finns två nyheter: När asteroidsvärmen skapas skickar vi nu även med en hänvisning till vårt rymdskepp (@hero). Och så har vi, på femte raden, deklarerat vilket typsnitt som ska användas och vilken storlek det ska ha. Alltså, hur text som skrivs i spelfönstret ska se ut.

Vår poängräknare är nu i stort sett klar. Vi behöver bara lägga till en rad sist i Window-klassens draw-metod:


@font.draw("Score: #{@hero.score}", 10, 10, 3, 1.0, 1.0, 0xffffff00)
	  

Denna kod skriver ut poängen uppe i vänstra hörnet, med gul färg. Klart! Nu kan du hålla koll på ditt highscore i spelet.

Till sist så irriterar jag mig lite på att man måste starta om spelet varje gång man har krockat med en asteroid. Visst vore det schysst om man bara kunde trycka på en viss tangent för att börja om? Det ordnar vi snabbt.

Vi går till Window-klassens update-metod. All kod i update-metoden är ju "inramad" i en if-sats som börjar med if @running och slutar med end. I denna if-sats ska vi nu lägga till ett alternativ. För att alla if och end ska hamna på rätt ställen är det lika bra att jag visar hur hela metoden ska se ut:


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
  else
    if button_down? Gosu::Button::KbEscape
      start_new_game
    end
  end  
end	  
	  

Efter else följer nu en ny if-sats som bara körs om spelet har stoppats. Den kontrollerar om escape-tangenten är nedtryckt. I så fall anropas metoden start_new_game.

Denna metod lägger vi till direkt efter initialize-metoden. Och så flyttar vi över de fyra sista raderna från initialize till start_new_game:


def start_new_game
  @hero = Hero.new(self)
  @laser = Laser.new(self, @hero)
  @asteroids = 5.times.map {Asteroid.new(self, @laser, @hero)}
  @running = true
end
      

När start_new_game anropas skapas alltså ett nytt rymdskepp, ett nytt laserskott och en ny asteroidsvärm. Och så sätts @running till true, vilket innebär att spelet startar.

Allra sist måste vi lägga till ett anrop till start_new_game i initialize-metoden, i stället för de rader som togs bort:


def initialize
  super(640, 480, false)
  self.caption = "---=== ASTEROID STORM ===---"
  @background_image = Gosu::Image.new(self, "stars.png", true)
  @font = Gosu::Font.new(self, Gosu::default_font_name, 20)    
  start_new_game
end
	  

Det var allt. Här följer nu vårt spel i sin helhet:


require 'gosu'

class Window < Gosu::Window
  def initialize
    super(640, 480, false)
    self.caption = "---=== ASTEROID STORM ===---"
    @background_image = Gosu::Image.new(self, "stars.png", true)
    @font = Gosu::Font.new(self, Gosu::default_font_name, 20)
    start_new_game
  end
  
  def start_new_game
    @hero = Hero.new(self)
    @laser = Laser.new(self, @hero)
    @asteroids = 5.times.map {Asteroid.new(self, @laser, @hero)}
    @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
    else
      if button_down? Gosu::Button::KbEscape
        start_new_game
      end
    end  
  end
  
  def draw
    @background_image.draw(0, 0, 0)
    @hero.draw
    @laser.draw
    @asteroids.each {|asteroid| asteroid.draw}
    @font.draw("Score: #{@hero.score}", 10, 10, 3, 1.0, 1.0, 0xffffff00)
  end
end 

class Hero
  attr_reader :x, :y, :score
  
  def initialize(window)
    @window = window
    @icon = Gosu::Image.new(@window, "spaceship.png", true)
    @x = 100
    @y = 215
    @score = 0
  end
  
  def add_score
    @score = @score + 10
  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, hero)
    @window = window
    @laser = laser
    @hero = hero
    @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
      @hero.add_score
    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	  
	  

Vårt spel är nu helt färdigt – och din utbildning i spelskolan är därmed slutförd! Jag hoppas att du har haft kul, och att du har fått smak för det här med programmering.

Ett litet tips på vägen: Själva stommen i det här rymdspelet, allt utom bilderna, kan användas för att skapa helt andra spel. Till exempel ett spel som heter Shark Attack, och som handlar om en dykare som försöker undvika blodtörstiga hajar. Det är bara din fantasi som sätter gränserna!