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!