2019年9月17日火曜日

Ruby Gosu でゲームを開発入門

概要

gosu は Ruby or C++ で 2D ゲーム開発ができるライブラリです
今回は Ruby ライブラリを使って簡単なチュートリアルゲームを作成してみました

環境

  • macOS 10.14.6
  • Ruby 2.6.2p47
    • gosu 0.14.5

準備

  • brew install sdl2
  • bundle init
  • vim Gemfile
gem "gosu"
  • bundle install --path vendor

とりあえずウィンドウを表示する

  • vim app.rb
require 'gosu'

class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"
  end

  def update
  end

  def draw
  end
end

Tutorial.new.show
  • bundle exec ruby app.rb

実行すると真っ暗なウィンドウだけが表示されます

背景画像の設定

次にゲームの背景画像を設定します
何でも良いので画像をダウンロードしておきましょう
公式のチュートリアルで使用している画像はここにあります 

require 'gosu'

class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"
    @bg = Gosu::Image.new("bg.png", :tileable => true)
  end

  def update
  end

  def draw
    @bg.draw(0, 0, 0)
  end
end

Tutorial.new.show

Gosu::Image.new で背景に指定する画像のパスを指定します
また draw で実際に画像をどの座標に描画するのか指定します
座標は左上が起点になります数字が増えるほど右下に移動します

実行すると以下のようにダウンロードした画像が背景として設定されています

プレイヤーの作成と配置

ゲームに必須の要素になります
キーボードで移動可能なプレイヤーを設置してみます

player.rb

プレイヤーを管理するクラスを別途作成します
少し長いですがあとで解説します

  • vim player.rb
require 'gosu'

class Player
  def initialize
    @image = Gosu::Image.new("starfighter.bmp")
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    @score = 0
  end

  def warp(x, y)
    @x = x
    @y = y
  end

  def turn_left
    @angle -= 4.5
  end

  def turn_right
    @angle += 4.5
  end

  def accelerate
    @vel_x += Gosu.offset_x(@angle, 0.5)
    @vel_y += Gosu.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, 1, @angle)
  end
end

キーボードの右を押した時に右に回転し左を押した時に左に回転するようにします
キーボードの上を押した際にプレイヤーが前に動くようにします
それぞれ turn_right, turn_left, accelerate メソッドが対応しています

move メソッドは実際に設定された座標にプレイヤーを移動するメソッドです
@x %= 640@y %= 480 はプレイヤーが画面からはみ出さないようにするための設定です
@vel_x *= 0.95, @vel_y *= 0.95 は摩擦係数のようなものでこれを設定しないと一度 accelerate したプレイヤーが永遠に動き続けてしまいます

少し難しいのは Gosu.offset_xGosu.offset_y になります
これは現在の位置からどの方向にどれだけ動かすかを指定することができるメソッドです
@angle で調整した角度に対して 0.5 だけ動かします
つまり上ボタンが押されている間徐々に動くような挙動になります

プレイヤー自体を動かす場合は draw_rot で動かします
draw メソッドはプレイヤーが固定されて伸縮するような表現に使います

warp は初期配置用のメソッドです

メインにプレイヤーを配置する

キーボードの入力のハンドリングはメインとなる app.rb 側の update で処理します

  • vim app.rb
require 'gosu'
require './player.rb'

class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"
    @bg = Gosu::Image.new("bg.png", :tileable => true)
    @player = Player.new
    @player.warp(320, 240)
  end

  def update
    if Gosu::button_down?(Gosu::KB_LEFT) || Gosu::button_down?(Gosu::GP_LEFT)
      @player.turn_left
    end
    if Gosu::button_down?(Gosu::KB_RIGHT) || Gosu::button_down?(Gosu::GP_RIGHT)
      @player.turn_right
    end
    if Gosu::button_down?(Gosu::KB_UP) || Gosu::button_down?(Gosu::GP_BUTTON_0)
      @player.accelerate
    end
    @player.move
  end

  def draw
    @bg.draw(0, 0, 0)
    @player.draw
  end

  def button_down(id)
    if id == Gosu::KB_ESCAPE
      close
    else
      super
    end
  end
end

Tutorial.new.show

キーボードの入力は Gosu::button_down? でハンドリングできます
ハンドリング可能なキーボードは定数で定義されています

updatedraw メソッドは 60fps でコールされ続けるので内部的にはキーボードに移動と再描画を繰り返すことであたかもプレイヤーが動いているような表現を実現しています

button_down は他のキーボードの入力をハンドリングするためのメソッドです
close を呼び出すことでわざわざ左上のバツボタン押さなくてもゲームが終了するようにしています

これで実行してみましょう
マシンのキーボードを使って以下のようにプレイヤーが操縦できるようになります

星を追加する

star.rb

少しゲーム要素を追加します
星を取得するとスコアがカウントアップするようにします
また星は連続したタイル画像を使います (これ)
なので星が回転するような動きになるようなアニメーションを実装します

  • vim star.rb
require 'gosu'

class Star
  attr_reader :x, :y

  def initialize(animation)
    @animation = animation
    @color = Gosu::Color::BLACK.dup
    @color.red = rand(256 - 40) + 40
    @color.green = rand(256 - 40) + 40
    @color.blue = rand(256 - 40) + 40
    @x = rand * 640
    @y = rand * 480
  end

  def draw
    img = @animation[Gosu.milliseconds / 100 % @animation.size]
    img.draw(@x - img.width / 2.0, @y - img.height / 2.0, 1, 1, 1, @color, :add)
  end
end

@animation はタイルに上に切り分けられた連続した画像になります
色や場所をランダムで決定します
そして draw が呼ばれる際に適切なタイルの情報を取得することであたかも星が回転しているようなアニメーションを実現してます
draw はいろいろと引数を指定していますがそれぞれ「X 座標」「Y 座標」「Z 座標」「X 方向の画像のスケール」「Y 方向の画像のスケール」「色」「追加モード」になります

プレイヤーが星を集められるようにする

だいぶ長くなってきました
player.rb を修正します
追加しているのは下部にある scorecollect_star メソッドになります

  • vim player.rb
require 'gosu'

class Player
  def initialize
    @image = Gosu::Image.new("starfighter.bmp")
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    @score = 0
  end

  def warp(x, y)
    @x = x
    @y = y
  end

  def turn_left
    @angle -= 4.5
  end

  def turn_right
    @angle += 4.5
  end

  def accelerate
    @vel_x += Gosu.offset_x(@angle, 0.5)
    @vel_y += Gosu.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, 1, @angle)
  end

  def score
    @score
  end

  def collect_stars(stars)
    stars.reject! { |star| Gosu.distance(@x, @y, star.x, star.y) < 35 }
  end
end

ポイントは プレイヤーと星の距離に応じて stars 配列から reject! している Gosu.distance になります
これで 2 つのオブジェクトの距離を計算することができます
今回は 35 以下であれば集めたと判定して星を管理する配列から星を削除します

メインで星を描画する

あとはメインで星を描画する処理を実装します
星は連続するタイル画像になります
そういった画像を使う場合は Gosu::Image.load_tiles を使います
これに各タイルのサイズをしていすることで配列として扱うことができます

  • vim app.rb
require 'gosu'
require './player.rb'
require './star.rb'

class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"
    @bg = Gosu::Image.new("bg.png", :tileable => true)
    @player = Player.new
    @player.warp(320, 240)
    @star_anim = Gosu::Image.load_tiles("star.png", 25, 25)
    @stars = Array.new
  end

  def update
    if Gosu::button_down?(Gosu::KB_LEFT) || Gosu::button_down?(Gosu::GP_LEFT)
      @player.turn_left
    end
    if Gosu::button_down?(Gosu::KB_RIGHT) || Gosu::button_down?(Gosu::GP_RIGHT)
      @player.turn_right
    end
    if Gosu::button_down?(Gosu::KB_UP) || Gosu::button_down?(Gosu::GP_BUTTON_0)
      @player.accelerate
    end
    @player.move
    @player.collect_stars(@stars)
    if rand(100) < 4 && @stars.size < 25
      @stars.push(Star.new(@star_anim))
    end
  end

  def draw
    @bg.draw(0, 0, 0)
    @player.draw
    @stars.each do |star|
      star.draw
    end
  end

  def button_down(id)
    if id == Gosu::KB_ESCAPE
      close
    else
      super
    end
  end
end

Tutorial.new.show

また先程紹介しましたが星の数は配列で管理しています
集めると reject! されるので追加していく必要があります
update されるたびに stars の状態を確認して減ってきたり 4% の確率で追加するようにします
また星を集めているかどうかも update@player.collect_stars(@stars) を呼び出すことで実現します
各星との距離を毎回計算するので少し重い処理になりそうですが今回はチュートリアル通りに進めます

これで実行すると以下のように星を集められるようになります
だいぶゲームっぽくなってきました

スコアの表示を行う

最後にスコアの表示を行う処理を実装します
@fontinitialize で初期化して draw で描画しています

  • vim app.rb
require 'gosu'
require './player.rb'
require './star.rb'

class Tutorial < Gosu::Window
  def initialize
    super 640, 480
    self.caption = "Tutorial Game"
    @bg = Gosu::Image.new("bg.png", :tileable => true)
    @player = Player.new
    @player.warp(320, 240)
    @star_anim = Gosu::Image.load_tiles("star.png", 25, 25)
    @stars = Array.new
    @font = Gosu::Font.new(20)
  end

  def update
    if Gosu::button_down?(Gosu::KB_LEFT) || Gosu::button_down?(Gosu::GP_LEFT)
      @player.turn_left
    end
    if Gosu::button_down?(Gosu::KB_RIGHT) || Gosu::button_down?(Gosu::GP_RIGHT)
      @player.turn_right
    end
    if Gosu::button_down?(Gosu::KB_UP) || Gosu::button_down?(Gosu::GP_BUTTON_0)
      @player.accelerate
    end
    @player.move
    @player.collect_stars(@stars)
    if rand(100) < 4 && @stars.size < 25
      @stars.push(Star.new(@star_anim))
    end
  end

  def draw
    @bg.draw(0, 0, 0)
    @player.draw
    @stars.each do |star|
      star.draw
    end
    @font.draw("Score: #{@player.score}", 10, 10, 3, 1.0, 1.0, Gosu::Color::YELLOW)
  end

  def button_down(id)
    if id == Gosu::KB_ESCAPE
      close
    else
      super
    end
  end
end

Tutorial.new.show

あとは星を収集した際にスコアのカウントを行うだけです
collect_stars を少し改修しています

  • vim player.rb
require 'gosu'

class Player
  def initialize
    @image = Gosu::Image.new("starfighter.bmp")
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    @score = 0
  end

  def warp(x, y)
    @x = x
    @y = y
  end

  def turn_left
    @angle -= 4.5
  end

  def turn_right
    @angle += 4.5
  end

  def accelerate
    @vel_x += Gosu.offset_x(@angle, 0.5)
    @vel_y += Gosu.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, 1, @angle)
  end

  def score
    @score
  end

  def collect_stars(stars)
    stars.reject! do |star|
      if Gosu.distance(@x, @y, star.x, star.y) < 35
        @score += 10
      else
        false
      end
    end
  end
end

これで実行すれば完成です
ちゃんと星を取得するとスコアがカウントアップするのが確認できます

Tips

Gosu は背景が #ff00ff の色を自動的に透過にしてくれます

最後に

Gosu で Ruby を使って 2D ゲーム開発に入門してみました
キーボードを使ったゲームですが簡単にできました

物理エンジンなどおそらくないので重力加速などが必要な場合は自分で実装する必要がありそうです
当たり判定なども今回自分で実装したので距離などの情報を使って自分で実装するのかなと思います
基本はキーボードが入力になるのでマウスは考慮しなくても良さそうですがマウスの入力もできるっぽいです
詳しくは参考サイトにある Github の Wiki や Rdoc を見てください

Windows と Mac 用にエクスポートできるようなので次回紹介したいと思います

参考サイト

1 件のコメント: