Koda spel i Ruby

Lektion 7: En hel svärm med asteroider!

Du har nu klarat av hela sex lektioner i den här spelskolan. Det tyder på att du är både uthållig och har talang för programmering. Nu återstår att se om du är en lika talangfull spelare – när den här lektionen är över kommer vårt spel att vara betydligt svårare!

Än så länge är det bara en enda asteroid som svävar omkring i spelfönstret. Vi ska nu lägga till ytterligare ett antal asteroider, så att det blir en hel svärm. Det är inte särskilt svårt, eftersom vi redan har en färdig beskrivning av en asteroid: vår Asteroid-klass.

Men hur ska vi skriva i programmet för att skapa, säg, fem olika asteroider? Ett sätt vore att skapa dem en och en, på precis samma sätt som vi skapade den första:


def initialize
  super(640, 480, false)
  @hero = Hero.new(self)
  @asteroid1 = Asteroid.new(self)
  @asteroid2 = Asteroid.new(self)
  @asteroid3 = Asteroid.new(self)
  @asteroid4 = Asteroid.new(self)
  @asteroid5 = Asteroid.new(self)
  @running = true 
end		  
	  

Men det är inte ett särskilt bra sätt. Då skulle vi få upprepa all kod som rör asteroiderna fem gånger om. Till exempel skulle vi, i stället för bara ett @asteroid.update, få skriva @asteroid1.update, @asteroid2.update, @asteroid3.update och så vidare. Det skulle bli rörigt!

Nej, i stället ska vi ta och lägga alla asteroiderna i samma "låda", det vill säga samma variabel. Så i Window-klassen tar vi och döper om variabeln @asteroid (som betyder just "asteroid") till @asteroids (som betyder "asteroider"). Och så skapar vi alla fem asteroiderna på en enda rad, på det här viset:


def initialize
  super(640, 480, false)
  @hero = Hero.new(self)
  @asteroids = 5.times.map {Asteroid.new(self)}
  @running = true 
end	  
      

De där konstiga parenteserna, "{" och "}", runt Asteroid.new(self) brukar kallas för klammerparenteser, krullparenteser eller måsvingar. Här ska det vara just sådana – det funkar inte med vanliga parenteser.

Sedan ska vi ändra i Window-klassens metoder update och draw. Först i update, där vi i stället för @asteroid.update skriver så här:


@asteroids.each {|asteroid| asteroid.update}	  
	  

"Each" betyder "varje", och den här koden gör just så att varje asteroid i svärmen uppdateras. Vi måste också göra motsvarande ändring i draw-metoden:


@asteroids.each {|asteroid| asteroid.draw}	  
	  

Dessutom måste vi ändra @asteroid till @asteroids i den if-sats i update-metoden som kontrollerar om rymdskeppet har träffats av en asteroid:


if @hero.hit_by?(@asteroids)
  @running = false
end	  
	  

Bara en ändring till innan vi testar spelet! Vi går till Player-klassen och skriver om metoden hit_by? på följande sätt:


def hit_by?(asteroids)
  asteroids.any? {|asteroid| Gosu::distance(@x, @y, asteroid.x, asteroid.y) < 50} 
end	  
	  

(Observera att du måste scrolla till höger för att se all kod!)

Sådär. Spara och provkör. Om allting funkar kommer det att se ut ungefär så här:

Hm. Jag vet inte vad du säger, men jag är inte helt nöjd. Det här liknar ju mer en vägg av asteroider än en svärm. Det beror på att alla asteroider har samma position i sidled när de startar, 640, och att de rör sig med samma hastighet, fem pixlar i taget.

Låt oss testa att ge asteroiderna olika startpositioner, och olika hastigheter, så att de inte kommer in i fönstret samtidigt. För att göra det ska vi återigen ta hjälp av slumpen. Vi går till Asteroid-klassen och gör några ändringar i initialize-metoden:


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

Tidigare har vi använt rand-metoden för att slumpa fram ett värde på @y, som bestämmer asteroidens position i höjdled. Nu använder vi samma metod för att få ett värde på @x.

När man skriver rand(1000) så levererar metoden ett slumptal mellan 0 och 999. Det slumptalet lägger vi ihop med 640, som är fönstrets totala bredd. Så @x kommer som minst att få ett värde på 640, vilket ger en position precis utanför högerkanten, och som mest ett värde på 1 639, vilket motsvarar en position en bra bit utanför fönstret.

Och så har vi lagt till en ny variabel, @speed ("hastighet"), som med hjälp av rand får ett värde mellan 3 och 7. Denna variabel ska vi även använda i Asteroid-klassens update-metod, på det här viset:


def update
  @x = @x - @speed
  if @x < -@icon.width
    @x = @window.width
    @y = rand(@window.height - @icon.height)
    @speed = 3 + rand(5)
  end
end
      

Provkör nu spelet igen. Nu får vi verkligen en svärm av asteroider. Jag hoppas du håller med mig om att spelet blev bättre på det här sättet. Är det inte tillräckligt svårt? Prova gärna att lägga till ännu fler asteroider! Det är bara att byta ut siffran i 5.times.map i Window-klassens initialize-metod.

Vårt program börjar nu bli riktigt långt. Här har du det i sin helhet:


require 'gosu'

class Window < Gosu::Window
  def initialize
    super(640, 480, false)
    @hero = Hero.new(self)
    @asteroids = 5.times.map {Asteroid.new(self)}
    @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
  
      @asteroids.each {|asteroid| asteroid.update}
      
      if @hero.hit_by?(@asteroids)
        @running = false
      end
    end  
  end
  
  def draw
    @hero.draw
    @asteroids.each {|asteroid| asteroid.draw}
  end
end 

class Hero
  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)
    @window = window
    @icon = Gosu::Image.new(@window, "asteroid.png", true)
    @x = @window.width + rand(1000)
    @y = rand(@window.height - @icon.height)
    @speed = 3 + rand(5)
  end
  
  def update
    @x = @x - @speed
    if @x < -@icon.width
      @x = @window.width
      @y = rand(@window.height - @icon.height)
      @speed = 3 + rand(5)
    end
  end

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

window = Window.new
window.show	  
	  

Ännu en lektion är avklarad – bravo! Det här börjar verkligen likna ett riktigt spel. Fast visst vore det rätt coolt om man inte behövde undvika asteroiderna, utan kunde skjuta sönder dem? I nästa lektion ska vi ladda laserkanonen!