Skip to content
Dominic Muller edited this page May 6, 2015 · 10 revisions

Ruby Tutorial

##Disclaimer

This is a modified version of Gosu's Tutorial wiki page. Please go check out that gem!

Live Demo

If you're impatient, here's a link to a live demo of this game: http://domgetter.github.io/dare/tutorial.html

Source code

This and other example games are included with the rest of the library.

If you don't have an editor that supports direct execution (TextMate, SciTE, ViM, SublimeText, etc.), cd into the directory and create the js file with rake build and then open tutorial.html in your favorite browser.

Down to Business

Installation

Install the dare gem using rubygems

$ gem install dare

After that's done, use dare to create a new directory with some staring files

$ dare new game
    create  game/Gemfile
    create  game/Rakefile
    create  game/game.rb
    create  game/game.html
$ cd game
game$ rake build
game$

You can now open game.html in your browser!

Overriding Window's methods

The easiest way to create a complete Dare application is to write a new class that inherits from Dare::Window (see the reference for a complete description of its interface). Here's how a minimal Game class might look:

require 'dare'

class Game < Dare::Window
  def initialize
    super width: 640, height: 480, border: true
    self.caption = "Dare Tutorial Game"
  end
  
  def update
  end
  
  def draw
  end
end

Game.new.run!

The constructor initializes the Dare::Window base class. The parameters shown here create a 640x480 pixels-large canvas in the browser. The :border option adds a 1px black border around the canvas. self.caption= will set the window's title to whatever you like (self.title= works too).

update and draw are overrides of Dare::Window's methods. update is called 60 times per second (by default) and should contain the main game logic: move objects, handle collisions, etc.

draw is called afterwards and whenever the window needs repainting for other reasons, and may also be skipped every other time if the FPS go too low. It should contain the code to redraw the whole screen, and no logic whatsoever.

Then follows the main program. A window is created and its run! method is called, which does not return until the window/tab has been closed by the user or its own code. Tada! - go run rake build and refresh your browser and now you have a small white canvas with a title of your choice, and a nice little border!

Using Images

require 'dare'

class Game < Dare::Window
  def initialize
    super width: 640, height: 480, border: true
    self.caption = "Dare Tutorial Game"
    
    @background_image = Dare::Image.new("assets/Space.png")
  end
  
  def update
  end
  
  def draw
    @background_image.draw # same as @background_image.draw(0, 0, canvas: Dare.default_canvas)
  end
end

Game.new.run!

Dare::Image#initialize takes one argument (plus an options hash). Like all drawable media resources, it is tied to a canvas, and by default, this is the one you already have (but you can use the :canvas option to pass a different one). All of Dare's drawable resources need a canvas for initialization and will hold an internal reference to that canvas. Second, the file name of the image file is given. You can either pass an absolute URL, or a relative path to the resulting js file.

As mentioned in the last lesson, Game's draw method is the place to draw everything, so this is the place for us to draw our background image.

There are no required arguments since we'll be drawing with the defaults (starting at the top left on the default canvas). Don't forget to rake build and refresh!

Player & movement

Here comes a simple player class:

class Player
  def initialize
    @image = Dare::Image.new("assets/Starfighter.png")
    @x = @y = @vel_x = @vel_y = 0.0
    @angle = 90.0
    @score = 0
  end

  def warp(x, y)
    @x, @y = x, y
  end
  
  def turn_left
    @angle += 4.5
  end
  
  def turn_right
    @angle -= 4.5
  end
  
  def accelerate
    @vel_x += Dare.offset_x(@angle, 0.5)
    @vel_y += Dare.offset_y(@angle, 0.5)
  end
  
  def move
    @x += @vel_x
    @y += @vel_y
    @x %= 640
    @y %= 480
    
    @vel_x *= 0.95
    @vel_y *= 0.95
  end

  def draw
    @image.draw_rot(@x, @y, @angle)
  end
end

There are a couple of things to say about this:

Angles in Dare

  • Player#accelerate makes use of the offset_x/offset_y methods. They are similar to what some people use sin/cos for: For example, if something moved 100 pixels at an angle of 60°, it would pass offset_x(60, 100) pixels horizontally and offset_y(60, 100) pixels vertically.
  • Note that draw_rot puts the center of the image at (x; y) - not the upper left corner as draw does!
  • The player is drawn on top of the background image because it was drawn after the background image. We'll figure out a better scheme for draw order later.
  • Also, see the rdoc for all drawing methods and arguments.

Integrating Player with the Window

class Game < Dare::Window
  def initialize
    super width: 640, height: 480
    self.caption = "Dare Tutorial Game"

    @background_image = Dare::Image.new("media/Space.png")

    @player = Player.new
    @player.warp(320, 240)
  end

  def update
    if button_down? Dare::KbLeft
      @player.turn_left
    end
    if button_down? Dare::KbRight
      @player.turn_right
    end
    if button_down? Dare::KbUp
      @player.accelerate
    end
    @player.move
  end

  def draw
    @player.draw
    @background_image.draw
  end
end

Game.new.run!

As you can see, we have introduced keyboard input! Similar to update and draw, Dare::Window provides the method button_down?(id) which returns true if the id is the same as a key currently being pressed. (For a list of predefined button constants, see rdoc). If you rake build and refresh your browser, you should be able to fly around!

Simple animations

What is an animation? A sequence of images - so we'll use Ruby's built in Arrays to store them. (For a real game, there is no way around writing some classes that fit the game's individual needs, but we'll get away with this simple solution for now.)

Let's introduce the stars which are the central object of this lesson. Stars appear out of nowhere at a random place on the screen and live their animated lives until the player collects them. The definition of the Star class is rather simple:

class Star
  attr_reader :x, :y

  def initialize(animation)
    @animation = animation
    @creation_time = Dare.ms
    @x = rand 640
    @y = rand 480
  end

  def draw  
    img = @animation[((Dare.ms-@creation_time)/100).to_i % @animation.size];
    img.draw_rot(@x, @y)
  end
end

Since we don't want each and every star to load the animation again, we can't do that in its constructor, but rather pass it in from somewhere else. (The Window will load the animation in about three paragraphs.)

To show a different frame of the stars' animation every 100 milliseconds, the time returned by Dare::ms is divided by 100 and then modulo-ed down to the number of frames (we also use to_i since javascript will treat the /100 like a float). We also want to subtract the time at which the star was born, so that they all spin separately. This very image is then drawn centered at the star's position.

Now let's add easy code to the player to collect away stars from an array:

class Player
  ...
  def score
    @score
  end

  def collect_stars(stars)
    if stars.reject! {|star| Dare.distance(@x, @y, star.x, star.y) < 35 }
      @score += 1
    end
  end
end

Now let's modify Game to load the animation, spawn new stars, have the player collect them and draw the remaining ones:

...
class Game < Dare::Window
  def initialize
    super width: 640, height: 480
    self.caption = "Dare Tutorial Game"

    @background_image = Dare::Image.new("assets/Space.png")

    @player = Player.new
    @player.warp(320, 240)

    @star_animation = Dare::Image.load_tiles("assets/Star.png", width: 25)
    @stars = []
  end

  def update
    ...
    @player.move
    @player.collect_stars(@stars)

    if rand(100) < 4 and @stars.size < 25
      @stars << Star.new(@star_animation)
    end
  end

  def draw
    @background_image.draw
    @player.draw
    @stars.each(&:draw)
  end
  ...

Done! After a rake build and a refresh, you can now collect stars!

Text and sound

Finally, we want to draw the current score using a font and play a 'beep' sound every time the player collects a star. The Window will handle the text part, loading a font 20 pixels high:

class Game < Dare::Window
  def initialize
    ...
    @font = Dare::Font.new(font: "Arial", size: 20, color: 'yellow')
  end

  ...

  def draw
    @background_image.draw
    @player.draw
    @stars.each(&:draw)
    @font.draw("Score: #{@player.score}", 10, 10) # you could have passed color: 'yellow' here too
  end
end

What's left for the player? Right: A counter for the score, loading the sound and playing it.

class Player

  def initialize
    @image = Dare::Image.new("assets/Starfighter.png")
    @grab_star_beep = Dare::Sound.new("assets/Beep.wav", overlap: 3)
    @x = @y = @vel_x = @vel_y = 0.0
    @angle = 90.0
    @score = 0
  end

  ...

  def collect_stars(stars)
    if stars.reject! {|star| Dare.distance(@x, @y, star.x, star.y) < 35}
      @score += 10
      @grab_star_beep.play
    end
  end
end

As you can see, loading and playing sound effects couldn't be easier! The overlap option lets more than one of the same sound play at once. Don't worry, you can play two different sounds at once without specifying the overlap! See the rdoc for other ways of playing back sounds - volume, looping, etc.

That's it! Everything else is up to your imagination. Also, to see a live demo of this tutorial, check out this page: http://domgetter.github.io/dare/tutorial.html