Koda spel i Ruby

Lektion 5: Något att undvika

När vi nu ska lägga till ytterligare en sak i vårt spel, ett nytt objekt, så kommer mycket att kännas bekant. Vi börjar med att skapa en ny klass, som vi kallar för Asteroid:


class Asteroid
  def initialize(window)
    @window = window
  end
  
  def update
  end
  
  def draw
  end
end	  
	  

I metoden initialize talar vi om för asteroiden att den hör hemma i vårt spelfönster. Metoderna update och draw får vara tomma ett tag till.

Precis som när det gällde rymdskeppet så behöver vi en bild, en ikon, för att asteroiden ska synas på skärmen. Här har du en lämplig bild:

Det är en kille som heter Jonas Wagner som har skapat asteroidbilden. Jag hämtade den här:
http://opengameart.org/content/asteroid-explosions-rocket-mine-and-laser

Högerklicka på bilden, spara den på din dator och flytta den till rätt mapp. Gör sedan följande tillägg i Asteroid-klassens initialize-metod, så att datorn vet vilken bild som ska användas:


def initialize(window)
  @window = window
  @icon = Gosu::Image.new(@window, "asteroid.png", true)
end	  
	  

Sedan ska vi ge asteroiden en startposition, och då måste vi tänka till ordentligt. Asteroiden ska komma in i spelfönstret från högerkanten, röra sig genom fönstret och försvinna ut på den vänstra sidan. Därför måste x-värdet – positionen i sidled – från början vara samma som fönstrets bredd, det vill säga 640 pixlar.

Och y-värdet, som bestämmer positionen i höjdled? För att få fram det ska vi ta hjälp av slumpen. Tänk dig att du har en hatt, full med ihopvikta papperslappar. På lapparna står olika nummer. För att få ett slumptal kan du helt enkelt låta någon dra en lapp ur hatten.

Men vi behöver inte använda någon hatt. I Ruby finns det en färdig metod som heter rand, och som levererar ett slumptal när man ber den om det. Vi ska ta och använda rand i vår initialize-metod:


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

Här får @x inledningsvis värdet 640, vilket betyder att asteroiden ligger och lurar precis utanför spelfönstrets högerkant. Värdet på @y blir ett slumptal mellan 0 och spelfönstrets höjd, som ju är 480, minus asteroidbildens höjd, som är 77 pixlar. Vi drar bort asteroidens höjd eftersom vi inte vill att den ska hamna utanför fönstret.

Det var mycket siffror på en gång! Vi ska strax se hur det här fungerar i praktiken. Men först ska vi lägga till lite kod i Asteroid-klassens båda metoder update och draw:


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

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

Koden i draw-metoden ritar helt enkelt bilden av asteroiden. I update minskas @x med 5 varje gång metoden anropas. Det betyder att asteroiden rör sig åt vänster, fem pixlar åt gången. När den har svävat ut genom fönstrets vänsterkant så startar den om från högerkanten igen, nu med en ny y-position.

Titta gärna en extra gång på if-satsen, där vi kontrollerar om @x har blivit mindre än -81 (asteroidbilden är 81 pixlar bred, så -@icon.width är här samma sak som -81). Ett minusvärde? Ja, vid fönstrets vänstra kant är ju x-värdet 0. Så när asteroiden precis har försvunnit ut ur fönstret så har @x faktiskt värdet -81.

Vi ska också lägga till lite asteroid-kod på några ställen i Window-klassen. Först i initialize-metoden, där vi nu även vill skapa en ny asteroid:


def initialize
  super(640, 480, false)
  @hero = Hero.new(self)
  @asteroid = Asteroid.new(self)
end
      

I update och draw ska vi bara anropa de metoder i Asteroid-klassen som har samma namn. Så i slutet av update-metoden skriver vi @asteroid.update och i draw-metoden ska det stå @asteroid.draw (nedan ser du hur det ska se ut).

Här följer, återigen, programmet i sin helhet:


require 'gosu'

class Window < Gosu::Window
  def initialize
    super(640, 480, false)
    @hero = Hero.new(self)
    @asteroid = Asteroid.new(self)
  end
  
  def update
    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
  
    @asteroid.update
  end
  
  def draw
    @hero.draw
    @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
end

class Asteroid
  def initialize(window)
    @window = window
    @icon = Gosu::Image.new(@window, "asteroid.png", true)
    @x = @window.width
    @y = rand(@window.height - @icon.height)
  end
  
  def update
    @x = @x - 5
    if @x < -@icon.width
      @x = @window.width
      @y = rand(@window.height - @icon.height)
    end
  end

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

window = Window.new
window.show	  
	  

Du kanske minns att spelfönstrets båda metoder update och draw körs automatiskt, 60 gånger i sekunden, tack vare en loop. Nu har vi gjort så att även asteroidens update- och draw-metoder körs automatiskt. Därmed kommer asteroiden att flyttas "av sig själv". Provkör så får du se!

Men att undvika en ensam asteroid är inte särskilt svårt. Och om rymdskeppet mot förmodan krockar med asteroiden så händer ingenting alls! Det ska vi ändra på i nästa lektion.