memorandums

日々の生活で問題解決したこと、知ってよかったことなどを自分が思い出すために記録しています。

gosuチュートリアルを試してみた(作業ログ)

■背景(お急ぎの方は本題へどうぞ)

1年生向けの共通科目に基礎ゼミという科目があります。

大学によって呼び名は違うと思いますが、要はこれから大学生として勉強するにあたり、必要なことがらを勉強してもらいましょうという科目です。学科の専門に応じて、図書館の利用方法、読書から(感想文ではなく)要約文を作成&他者への推薦、ノートの取り方、など様々な取り組みがされています。少人数教育であるため、ある程度、研究室ごとの工夫がいれられるようになっています。

当研究室では、プログラミングはまだ習いたてなので、CEDECのペラコンに応募するためのアイデアを考える練習をしてみたこともあります。昨年は、論文を書くときなどに必要になる技術文章の書き方トレーニングをしました。教材は以下を利用させていただきました。私のやり方が悪かったのでしょうね。。。PDFを配布して一緒に読み合わせしながら練習問題などやってみたのですが、学生の反応は今ひとつでした。

http://tomi0730.com/tomi_blog/study/わかりやすい文章のためのテキストブックver1.pdf

で、今年は何をしようかと。今年の1年生から導入のプログラミング言語Rubyになりました(これまではJava、その前はC言語)。文法を勉強している最中ですが、Ruby使うと(あまり細かいことは抜きにしても)こんなことができるんだぞー。。。というのを経験するのは悪くないように思いました。で、Rubyで簡単なゲームを開発できるライブラリがないか探しました。ちょっと昔にSDLを使ったRubyゲーム本があって、それを買ってゼミ室に置いたこともあったのですが。。。いまどき、もっといいライブラリがあるんじゃないか?と探したのですがあまりない。あっても情報が古い。その中で唯一見つけたのがgosuでした。(たぶん他にもあるんでしょうけど見つけられませんでした)

gosuのホームページはこちらGithubのページもあり以下です。

GitHub - gosu/gosu: 2D game development library for the desktop (Ruby and C++) and iOS (C++ only). MIT licensed.

一応、本も出てるみたい。

pragprog.com

いまどきスマホじゃなくてPCゲームか。。。と学生に思われるかもしれませんが。。。とりあえずRubyっぽく書けるようなので候補として。まず僕が勉強しないと、ということで以下のチュートリアルをトレースしてみたのが下記の本題です。

Ruby Tutorial · gosu/gosu Wiki · GitHub


■本題

以下の内容は既に日本語の翻訳ページが下記にありました。さきほど気づきました。そちらをご覧になってわからなければ以下をお読みいただければと思います。

gist.github.com



Rubyはまったく勉強したことがない、という人には以下は難しいかもしれませんが。。。とりあえず自分の作業ログを以下に書きたいと思います。誰得なのか?僕だけですかね。。。以下、チュートリアルのコードには個人的な解説というか感想をコメント文で入れています。よろしければ参考にしてください。

とりあえず完成すると以下のようなゲームができあがります。

youtu.be


0.インストール
環境はMacです。OSX10.11です。Rubyはrbenvで2.2.2p95がインストール済みの環境です。ターミナルを開き以下2行を実行します。

brew install sdl2
gem install gosu


1.概要

以下、入力してgosu0.rbというファイルに保存します。実行は、ruby gosu0.rb。終了は⌘-qかターミナルで⌘-c

  • gosu0.rb
require 'gosu'

class GameWindow < Gosu::Window
  def initialize
    super 640, 480 #ウィンドウサイズを指定する、フルスクリーンにしたければ、第3パラメータに:fullscreen => trueを追加する

    self.caption = "Gosu Tutorial Game"
  end

  def update #60fpsで呼ばれる
  end

  def draw #必要になったときに呼ばれる
  end
end

window = GameWindow.new
window.show

上記の動作を示すフローチャートこちらにあるらしいので詳しく知りたい人は参照してちょ。


2.イメージを表示する

以下、入力してgosu1.rbというファイルに保存します。実行は、ruby gosu1.rb。実行前にmediaフォルダを作成しそこにダウンロードしたspace.png(←ここをクリックすると画像ファイルが表示されます)を置くことを忘れないでね。

  • gosu1.rb
require 'gosu'

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

    @background_image = Gosu::Image.new("media/space.png", :tileable => true) #第2引数はオプション。指定するとタイル表示してくれる。小さい画像でも敷き詰めてくれるってこと。この辺の基本はhttps://github.com/gosu/gosu/wiki/Basic-Conceptsにあるらしい。

  end

  def update
  end

  def draw
    @background_image.draw(0, 0, 0) #これが画像表示。第1、2がx、y座標、第3パラはZ値。重ねる順番だね。
  end
end

window = GameWindow.new
window.show


2.1 プレイヤークラスを作る

以下、入力してplayer.rbというファイルに保存しましょう。このファイルは部品なのでrubyで実行しても何も表示されまへん。mediaフォルダにダウンロードしたstarfighter.bmp を置くことを忘れないでね。

  • player.rb
class Player
  def initialize
    @image = Gosu::Image.new("media/starfighter.bmp")
    @x = @y = @vel_x = @vel_y = @angle = 0.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 += Gosu::offset_x(@angle, 0.5) #x方向の増分量。0.5 * cos(@angle)を求めてくれるらしい。
    @vel_y += Gosu::offset_y(@angle, 0.5)  #y方向の増分量。 0.5 * sin(@angle)を求めてくれるらしい。
  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) #画像を@angle度回転して表示してくれるメソッドらしい。プレイヤーは一番手前に表示したいのでZ値は1にするってさ。
  end
end


2.2 プレイヤークラスを使ってみるお

以下、入力してgosu2.rbというファイルに保存してちょ。キーボードの矢印キー(上下左右)を押すとPlayerが動くぞ。

以下では、update()で何のキーが押されたのか判定して、左右回転と前進メソッドを呼び出している。button_down というメソッドはキーが押下されたときに呼ばれるメソッド。引数idに押されたキーのキーコードが入ってくると思われる。ESCが押されたら終了するようになったというわけ。そのほか、button_up(id)というのもあるらしい。

  • gosu2.rb
require 'gosu'
require './player'

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

    @background_image = Gosu::Image.new("media/space.png", :tileable => true)

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

  def update
    if Gosu::button_down? Gosu::KbLeft or Gosu::button_down? Gosu::GpLeft then
      @player.turn_left
    end
    if Gosu::button_down? Gosu::KbRight or Gosu::button_down? Gosu::GpRight then
      @player.turn_right
    end
    if Gosu::button_down? Gosu::KbUp or Gosu::button_down? Gosu::GpButton0 then
      @player.accelerate
    end
    @player.move
  end

  def draw
    @player.draw
    @background_image.draw(0, 0, 0);
  end

  def button_down(id)
    if id == Gosu::KbEscape
      close
    end
  end
end

window = GameWindow.new
window.show


3 アニメーションやるぞ

アニメーションのまえに、ちょっと。

背景やプレイヤーを表示するたびにZ値を数字で指定してきたけど、どれが何の値なのかわからなくなるので、ラベルをつけた方がいいよとのこと。指定の仕方は以下。これは初めてみたんだけど。。。Backgroundに0を、Starsに1を、UIに3を代入することができる。以下、zorder.rbというファイルに入力して保存しておこう。あとで使う。

  • zorder.rb
module ZOrder
  Background, Stars, Player, UI = *0..3
end

さて、アニメーションの本題に入ろう。

以下、入力してstart.rbというファイルに保存する。このファイルは部品なのでrubyで実行しても何も表示されない。mediaフォルダにダウンロードしたstar.png を置くことを忘れないでね。

ちょっと説明が複雑になるのですが、とりあえずがんばって文字にすると。。。このStarクラスのコンストラクタで与えるanimationはイメージデータ(タイル状)になっていて25x25の星の画像が10個連なっている。で、drawメソッド内のimg = @animation[Gosu::milliseconds / 100 % @animation.size];で、ゲーム起動から0.1秒ごとに加算(Gosu::milliseconds / 100)される値を画像のタイル数(@animation.size)で割った剰余を求めている。つまり。。。0,1,2,3,4,5,6,7,8,9,0,1,2,3,4...と画像タイルがループ表示されるわけ。ZOrder::Starsは上記で作った定数?だね。具体的には1が入っているはず。星の色と位置をコンストラクタで決定して、そこで星が明滅する感じを実現するのが以下のStarクラスってわけ。初心者にはこれを理解するのはなかなか難しいと思うけど。。。:addって何かな。。。あとで調べてみないとわからない。

  • star.rb
class Star
  attr_reader :x, :y

  def initialize(animation)
    @animation = animation
    @color = Gosu::Color.new(0xff_000000)
    @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,
        ZOrder::Stars, 1, 1, @color, :add)
  end
end

ここでStarクラスを作ったので、これまで作った他のクラスを変更しなければならない。

player.rbは以下の部分を追加&修正。ちょっとね。。。どこを追加したのかわかりにくいでしょう?あとで全部のファイルを載せるのでここでは眺める程度にしましょう。

とりあえず解説。def scoreはゲッターだね。def collect_stars(stars)は引数starsがArrayの変数なので、playerの座標(@x,@y)とArray内にあるStarの座標( star.x, star.y)の距離をGosu::distanceで求めて、その距離が35ピクセル内ならスコア(@score)を1点加算する処理をしている。また、reject!メソッドなので「35ピクセル内であれば」が真であればstarsからそのstarを取り除く処理もしている(http://ref.xaio.jp/ruby/classes/array/reject_bang)。rubyはこういうの簡単に書けるからいいね。。。

class Player
  ...
  def score
    @score
  end

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

gosu2.rbは以下の部分を追加&修正。ここも直すの面倒だから眺める程度ね。

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

    @background_image = Gosu::Image.new("media/space.png", :tileable => true)

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

    @star_anim = Gosu::Image::load_tiles("media/star.png", 25, 25) #star.pngは250x25の画像、それを25x25で10個に分割して配列化する。それがload_tilesメソッドのようだ。

    @stars = Array.new #表示する星を入れる入れ物(配列)
  end

  def update
    ...
    @player.move #プレイヤーの移動処理を呼び出す
    @player.collect_stars(@stars) #プレイヤーと星群の当たり判定

    if rand(100) < 4 and @stars.size < 25 then #画面上の星の数が25個未満になって、0.04の確率のときに、星を1つ追加するようだね。画面上にはいつまでも一定数の星が存在することになる。
      @stars.push(Star.new(@star_anim))
    end
  end

  def draw
    @background_image.draw(0, 0, ZOrder::Background)
    @player.draw
    @stars.each { |star| star.draw }
  end
  ...

で、変更したplayer.rbとgosu2.rbの全体は以下です。これらをコピペしてください。たぶん動くはずです。

  • player.rb
class Player
  def initialize
    @image = Gosu::Image.new("media/starfighter.bmp")
    @x = @y = @vel_x = @vel_y = @angle = 0.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 += Gosu::offset_x(@angle, 0.5) #x方向の増分量。0.5 * cos(@angle)を求めてくれるらしい。
    @vel_y += Gosu::offset_y(@angle, 0.5)  #y方向の増分量。 0.5 * sin(@angle)を求めてくれるらしい。
  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) #画像を@angle度回転して表示してくれるメソッドらしい。プレイヤーは一番手前に表示したいのでZ値は1にするってさ。
  end

  def score
    @score
  end

  def collect_stars(stars)
    if stars.reject! {|star| Gosu::distance(@x, @y, star.x, star.y) < 35 } then
      @score += 1
    end
  end
end
  • gosu2.rb
require 'gosu'
require './player'
require './star'
require './zorder'

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

    @background_image = Gosu::Image.new("media/space.png", :tileable => true)

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

    @star_anim = Gosu::Image::load_tiles("media/star.png", 25, 25) #star.pngは250x25の画像、それを25x25で10個に分割して配列化する。それがload_tilesメソッドのようだ。
    @stars = Array.new #表示する星を入れる入れ物(配列)
  end

  def update
    if Gosu::button_down? Gosu::KbLeft or Gosu::button_down? Gosu::GpLeft then
      @player.turn_left
    end
    if Gosu::button_down? Gosu::KbRight or Gosu::button_down? Gosu::GpRight then
      @player.turn_right
    end
    if Gosu::button_down? Gosu::KbUp or Gosu::button_down? Gosu::GpButton0 then
      @player.accelerate
    end
    @player.move #プレイヤーの移動処理を呼び出す
    @player.collect_stars(@stars) #プレイヤーと星群の当たり判定

    if rand(100) < 4 and @stars.size < 25 then #画面上の星の数が25個未満になって、0.04の確率のときに、星を1つ追加するようだね。画面上にはいつまでも一定数の星が存在することになる。
      @stars.push(Star.new(@star_anim))
    end
  end

  def draw
    @background_image.draw(0, 0, ZOrder::Background)
    @player.draw
    @stars.each { |star| star.draw }
  end

  def button_down(id)
    if id == Gosu::KbEscape
      close
    end
  end
end

window = GameWindow.new
window.show


4.やっぱりゲームといえばスコアそして音だよね

スコアを表示するためにgosu2.rbに以下の変更を加える。音声ファイルbeep.wavはダウンロードしてmediaフォルダにいれておく。

class Window < Gosu::Window
  def initialize
    ...
    @font = Gosu::Font.new(20) #20ポイントのフォントデータを生成
  end

  ...

  def draw
    @background_image.draw(0, 0, ZOrder::Background)
    @player.draw
    @stars.each { |star| star.draw }
    @font.draw("Score: #{@player.score}", 10, 10, ZOrder::UI, 1.0, 1.0, 0xff_ffff00) #スコアを表示。(10,10)の場所に横と縦を等倍(1.0)で、色は黄色(RGB)を指定。
  end
end

音は当たり判定のあるPlayerクラスにいれるよー。具体的には以下。

class Player
  attr_reader :score #上記でスコアを表示するときに@player.scoreとしたので読み取り用のアクセッサ〜を追加。

  def initialize
    @image = Gosu::Image.new("media/starfighter.bmp")
    @beep = Gosu::Sample.new("media/beep.wav") #効果音データを読み込み

    @x = @y = @vel_x = @vel_y = @angle = 0.0
    @score = 0
  end

  ...

  def collect_stars(stars)
    stars.reject! do |star|
      if Gosu::distance(@x, @y, star.x, star.y) < 35 then
        @score += 10
        @beep.play #効果音を再生
        true
      else
        false
      end
    end
  end
end

で、最終的なplayer.rbとgosu2.rbは以下。

  • player.rb
class Player
  def initialize
    @image = Gosu::Image.new("media/starfighter.bmp")
    @beep = Gosu::Sample.new("media/beep.wav")
    @x = @y = @vel_x = @vel_y = @angle = 0.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 += Gosu::offset_x(@angle, 0.5) #x方向の増分量。0.5 * cos(@angle)を求めてくれるらしい。
    @vel_y += Gosu::offset_y(@angle, 0.5)  #y方向の増分量。 0.5 * sin(@angle)を求めてくれるらしい。
  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) #画像を@angle度回転して表示してくれるメソッドらしい。プレイヤーは一番手前に表示したいのでZ値は1にするってさ。
  end

  def score
    @score
  end

  def collect_stars(stars)
    stars.reject! do |star|
      if Gosu::distance(@x, @y, star.x, star.y) < 35 then
        @score += 10
        @beep.play
        true
      else
        false
      end
    end
  end
end
  • gosu2.rb
require 'gosu'
require './player'
require './star'
require './zorder'

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

    @background_image = Gosu::Image.new("media/space.png", :tileable => true)

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

    @star_anim = Gosu::Image::load_tiles("media/star.png", 25, 25) #star.pngは250x25の画像、それを25x25で10個に分割して配列化する。それがload_tilesメソッドのようだ。
    @stars = Array.new #表示する星を入れる入れ物(配列)
    @font = Gosu::Font.new(20) #20ポイントのフォントデータを生成
  end

  def update
    if Gosu::button_down? Gosu::KbLeft or Gosu::button_down? Gosu::GpLeft then
      @player.turn_left
    end
    if Gosu::button_down? Gosu::KbRight or Gosu::button_down? Gosu::GpRight then
      @player.turn_right
    end
    if Gosu::button_down? Gosu::KbUp or Gosu::button_down? Gosu::GpButton0 then
      @player.accelerate
    end
    @player.move #プレイヤーの移動処理を呼び出す
    @player.collect_stars(@stars) #プレイヤーと星群の当たり判定

    if rand(100) < 4 and @stars.size < 25 then #画面上の星の数が25個未満になって、0.04の確率のときに、星を1つ追加するようだね。画面上にはいつまでも一定数の星が存在することになる。
      @stars.push(Star.new(@star_anim))
    end
  end

  def draw
    @background_image.draw(0, 0, ZOrder::Background)
    @player.draw
    @stars.each { |star| star.draw }
    @font.draw("Score: #{@player.score}", 10, 10, ZOrder::UI, 1.0, 1.0, 0xff_ffff00) #スコアを表示。(10,10)の場所に横と縦を等倍(1.0)で、色は黄色(RGB)を指定
  end

  def button_down(id)
    if id == Gosu::KbEscape
      close
    end
  end
end

window = GameWindow.new
window.show

1年生でこれが理解できてオリジナル作品とか作れたらいいな。。。とりあえず今年はこれでチャレンジしてみよう。