■背景(お急ぎの方は本題へどうぞ)
1年生向けの共通科目に基礎ゼミという科目があります。
大学によって呼び名は違うと思いますが、要はこれから大学生として勉強するにあたり、必要なことがらを勉強してもらいましょうという科目です。学科の専門に応じて、図書館の利用方法、読書から(感想文ではなく)要約文を作成&他者への推薦、ノートの取り方、など様々な取り組みがされています。少人数教育であるため、ある程度、研究室ごとの工夫がいれられるようになっています。
当研究室では、プログラミングはまだ習いたてなので、CEDECのペラコンに応募するためのアイデアを考える練習をしてみたこともあります。昨年は、論文を書くときなどに必要になる技術文章の書き方トレーニングをしました。教材は以下を利用させていただきました。私のやり方が悪かったのでしょうね。。。PDFを配布して一緒に読み合わせしながら練習問題などやってみたのですが、学生の反応は今ひとつでした。
http://tomi0730.com/tomi_blog/study/わかりやすい文章のためのテキストブックver1.pdf
で、今年は何をしようかと。今年の1年生から導入のプログラミング言語がRubyになりました(これまではJava、その前はC言語)。文法を勉強している最中ですが、Ruby使うと(あまり細かいことは抜きにしても)こんなことができるんだぞー。。。というのを経験するのは悪くないように思いました。で、Rubyで簡単なゲームを開発できるライブラリがないか探しました。ちょっと昔にSDLを使ったRubyゲーム本があって、それを買ってゼミ室に置いたこともあったのですが。。。いまどき、もっといいライブラリがあるんじゃないか?と探したのですがあまりない。あっても情報が古い。その中で唯一見つけたのがgosuでした。(たぶん他にもあるんでしょうけど見つけられませんでした)
gosuのホームページはこちら。Githubのページもあり以下です。
一応、本も出てるみたい。
いまどきスマホじゃなくてPCゲームか。。。と学生に思われるかもしれませんが。。。とりあえずRubyっぽく書けるようなので候補として。まず僕が勉強しないと、ということで以下のチュートリアルをトレースしてみたのが下記の本題です。
Ruby Tutorial · gosu/gosu Wiki · GitHub
■本題
以下の内容は既に日本語の翻訳ページが下記にありました。さきほど気づきました。そちらをご覧になってわからなければ以下をお読みいただければと思います。
Rubyはまったく勉強したことがない、という人には以下は難しいかもしれませんが。。。とりあえず自分の作業ログを以下に書きたいと思います。誰得なのか?僕だけですかね。。。以下、チュートリアルのコードには個人的な解説というか感想をコメント文で入れています。よろしければ参考にしてください。
とりあえず完成すると以下のようなゲームができあがります。
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年生でこれが理解できてオリジナル作品とか作れたらいいな。。。とりあえず今年はこれでチャレンジしてみよう。