LoginSignup
9

More than 1 year has passed since last update.

posted at

updated at

DragonRuby GTK (Game Toolkit) で遊んでみた

先日、Twitterで以下のチャリティー企画を知りました。

最低5ドルの寄付でインディーゲーム1000タイトルを遊べるキャンペーンが開始 反人種差別運動の一環として - ねとらぼ

PCゲームはあまりやらないけど、素材とかも入ってるし、寄付がてら買っておくか…と眺めていたらDragonRuby GTKの文字が。

Rubyでクロスプラットフォームなゲームが作れるやつのようです。
(一斉を風靡したRubyMotionも今はこのファミリーなのだとか)

これは面白そう、ということで即ポチって週末に少し遊んでみました。

以下、macOS版を試した記録です。

※ とりあえずどんなものか試したい人にはブラウザ版があります。
 http://fiddle.dragonruby.org/

Hello World

まずは動かしてみます。

$ unzip dragonruby-gtk-macos.zip 
$ cd dragonruby-macos/
$ ./dragonruby
# ./dragonruby mygame でも同じ

Screen Shot 2020-06-14 at 22.55.54.png

動きました。
mygame/app/main.rb が実行されています。

僕の環境だと、しばらく動かしててもCPUファンが唸らなくて良い感じです。
(わりと本体が熱くはなる)

四角形を描く

先程はzipを展開したディレクトリの中で作業しましたが、別の場所で作業してみます。
(git管理などがしやすいように)

$ mkdir -p hello-dragonruby-gtk/mygame/app
$ cd hello-dragonruby-gtk/

$ cp ~/Downloads/dragonruby-macos/dragonruby .
$ cp ~/Downloads/dragonruby-macos/font.ttf .

dragonrubyコマンドは、PATHを設定して他の場所から呼び出すようなことを想定されていなそうな感じがしたので、コピーしてきています。
font.ttfも必要でした。
(合わせて5.9MB程度)

実行するといろいろディレクトリやファイルが作られるので、それらと合わせてgitignoreしておきます。

.gitignore
# DragonRuby
/dragonruby
/font.ttf
/logs
/tmp
/exceptions
console_history.txt

これで準備はできたので、四角形を描くソースコードを書きます。

mygame/app/main.rb
def tick args
  args.outputs.solids << [args.grid.center_x - 32, args.grid.h * 0.1, 64, 64]
end

実行します。

./dragonruby

Screen Shot 2020-06-14 at 23.07.44.png

描けました。

args.outputs.solidsに追加すると図形が描画されます。
また、args.gridに画面サイズの情報などが入っているのでそちらを利用しています。

少し独特ですが、特に難しいところはないと思います。

基本的な仕様は以下のようになっているようです。

  • 画面は1280x720ピクセル
    • 画面サイズを変更すると縦横比を保って自動リサイズされる
  • 左下が原点
  • FPSは60固定
    • 先程も書いたtickがフレームごとに呼び出される

配列の順番覚えるのが辛い問題

公式的にはこの記法を推してる感じがしますが、正直なところ、初見殺しで辛い感じがします…。

ちゃんと他の書き方も用意されていました。

ハッシュ

これならまぁ初めて読んでも大丈夫そうです。

mygame/app/main.rb
  args.outputs.solids << { x: args.grid.center_x - 32, y: args.grid.h * 0.1, w: 64, h: 64 }

クラス

以下のようなクラスを定義するとOKのようです。

  • primitive_markerメソッドで:solidなど種類を返す
  • ハッシュ記法のキーと同じ属性を持つ

僕は今のところこの方法を使っています。

mygame/app/primitives.rb
class Primitive
  def initialize(attributes)
    attr_keys.each { |key| send("#{key}=", attributes[key]) }
  end

  def primitive_marker
    self.class.name.downcase
  end

  def attr_keys
    self.class.class_variable_get(:@@attr_keys)
  end

  def serialize
    attr_keys.map { |key| [key, send(key)] }.to_h
  end

  def inspect
    serialize.to_s
  end

  def to_s
    serialize.to_s
  end
end

class Solid < Primitive
  @@attr_keys = %i[x y w h r g b a]
  attr_accessor(*@@attr_keys)
end

serializeinspectto_sを定義していないと、エラー発生時にコンソールがその警告で埋め尽くされてしまうので定義しています。
(そのために@@attr_keysを持っている…)

あとはこれをmain.rbから読み込みます。

mygame/app/main.rb
require 'app/primitives.rb'

def tick args
  args.outputs.solids << Solid.new(x: args.grid.center_x - 32, y: args.grid.h * 0.1, w: 64, h: 64)
end

線を引く

Solid以外の例として、Lineを使って真ん中に十字線を線を描いてみます。
args.outputs.linesに追加することで描画されます。

argsを毎回打つのが面倒なので、outputsgridを変数に入れています。

mygame/app/primitives.rb
class Line < Primitive
  @@attr_keys = %i[x y x2 y2 r g b a]
  attr_accessor(*@@attr_keys)
end
mygame/app/main.rb
require 'app/primitives.rb'

def tick args
  outputs, grid = args.outputs, args.grid

  outputs.solids << Solid.new(x: grid.center_x - 32, y: grid.h * 0.1, w: 64, h: 64)

  outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
  outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
end

Screen Shot 2020-06-14 at 23.53.59.png

四角が左右中央に描画できていることが確認できました。

ゲームもクラス化

tickメソッドが複雑化する前に、ゲーム本体もクラス化しておきます。

mygame/app/main.rb
require 'app/primitives.rb'

class Game
  attr_accessor :state, :outputs, :grid

  def tick
    output
  end

  def output
    outputs.solids << Solid.new(x: grid.center_x - 32, y: grid.h * 0.1, w: 64, h: 64)

    outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
    outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
  end

  def serialize
    {}
  end

  def inspect
    serialize.to_s
  end

  def to_s
    serialize.to_s
  end
end

$game = Game.new

def tick args
  $game.state = args.state
  $game.outputs = args.outputs
  $game.grid = args.grid
  $game.tick
end

attr_gtkというヘルパーが用意されていますが、stateなどに直接アクセスしたかったのでここではattr_accessorを使っています。

キーボードで動かす

左右のカーソルキーで四角(player)を動かします。

フレームをまたいで保持したい情報はstateに持たせます。
キーボード入力はinputs.keyboard.キー名で検知できます。(inputs.keyboard.key_held.キー名で押しっぱなしを拾ったりもできるようですが、使っていません)

main.rbに以下のような変更を加えました。

mygame/app/main.rb
 require 'app/primitives.rb'

 class Game
-  attr_accessor :state, :outputs, :grid
+  attr_accessor :state, :outputs, :grid, :inputs

   def tick
+    set_defaults
+    handle_inputs
+    update_state
     output
   end

+  def set_defaults
+    state.player_x ||= grid.center_x - 32
+    state.player_dx ||= 0
+  end
+
+  def handle_inputs
+    if inputs.keyboard.right
+      state.player_dx = 5
+    elsif inputs.keyboard.left
+      state.player_dx = -5
+    else
+      state.player_dx = 0
+    end
+  end
+
+  def update_state
+    state.player_x += state.player_dx
+  end
+
   def output
-    outputs.solids << Solid.new(x: grid.center_x - 32, y: grid.h * 0.1, w: 64, h: 64)
+    outputs.solids << Solid.new(x: state.player_x, y: grid.h * 0.1, w: 64, h: 64)

     outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
     outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
@@ -26,12 +50,13 @@ class Game
     serialize.to_s
   end
 end

 $game = Game.new

 def tick args
   $game.state = args.state
   $game.outputs = args.outputs
   $game.grid = args.grid
+  $game.inputs = args.inputs
   $game.tick
 end

これで四角形を動かせるようになります。

move.gif

敵を作る

左右に動くグレーのSolidを追加します。
特に新しい要素は使っていません。

mygame/app/main.rb
   def set_defaults
     state.player_x ||= grid.center_x - 32
     state.player_dx ||= 0
+
+    state.enemy_x ||= grid.center_x - 32
+    state.enemy_dx ||= 5
   end

   def update_state
     state.player_x += state.player_dx
+
+    state.enemy_x += state.enemy_dx
+    if state.enemy_x < 0 || state.enemy_x > grid.w - 64
+      state.enemy_dx *= -1
+    end
   end

   def output
     outputs.solids << Solid.new(x: state.player_x, y: grid.h * 0.1, w: 64, h: 64)
+ 
+    outputs.solids << Solid.new(x: state.enemy_x, y: grid.h * 0.7, w: 64, h: 64, r: 150, g: 150, b: 150)

     outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
     outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
   end

enemy.gif

弾を撃つ

state.new_entityでエンティティを追加できます。
エンティティに情報を持たせておいて、その情報を使ってSolidなどで画面表示する使い方になります。
(エンティティがなくてもできるとは思いますが、せっかく用意されているので使ってみます。)

押しっぱなしで撃ちまくれないようにkey_upにを使っています。

また、弾が画面の端に到達したらdeadフラグを立てて削除しています。
deadフラグがあるものはrejectする)

mygame/app/main.rb
   def set_defaults
     state.player_x ||= grid.center_x - 32
     state.player_dx ||= 0

     state.enemy_x ||= grid.center_x - 32
     state.enemy_dx ||= 5
+
+    state.bullets ||= []
   end

   def handle_inputs
     if inputs.keyboard.right
       state.player_dx = 5
     elsif inputs.keyboard.left
       state.player_dx = -5
     else
       state.player_dx = 0
     end
+
+    if inputs.keyboard.key_up.space
+      state.bullets << state.new_entity(:bullet) do |bullet|
+        bullet.y = player_rect[:y]
+        bullet.x = player_rect[:x] + 16
+        bullet.size = 32
+        bullet.dy = 10
+        bullet.solid = { x: bullet.x, y: bullet.y, w: bullet.size, h: bullet.size, r: 255, g: 100, b: 100 }
+      end
+    end
   end

   def update_state
     state.player_x += state.player_dx

     state.enemy_x += state.enemy_dx
     if state.enemy_x < 0 || state.enemy_x > grid.w - 64
       state.enemy_dx *= -1
     end
+
+    state.bullets.each do |bullet|
+      bullet.y += bullet.dy
+      bullet.solid[:y] = bullet.y
+
+      if bullet.y > grid.h
+        bullet.dead = true
+      end
+    end
+    state.bullets = state.bullets.reject(&:dead)
   end

   def output
-    outputs.solids << Solid.new(x: state.player_x, y: grid.h * 0.1, w: 64, h: 64)
+    outputs.solids << Solid.new(player_rect)

     outputs.solids << Solid.new(x: state.enemy_x, y: grid.h * 0.7, w: 64, h: 64, r: 150, g: 150, b: 150)
+ 
+    outputs.solids << state.bullets.map(&:solid)

     outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
     outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
   end
+
+  private
+
+  def player_rect
+    { x: state.player_x, y: grid.h * 0.1, w: 64, h: 64 }
+  end

bullet.gif

弾が撃てるようになりました。

弾を消す(敵に当たったら)

当たり判定にはintersect_rectを使えます。

mygame/app/main.rb
   def update_state
     state.player_x += state.player_dx

     state.enemy_x += state.enemy_dx
     if state.enemy_x < 0 || state.enemy_x > grid.w - 64
       state.enemy_dx *= -1
     end

     state.bullets.each do |bullet|
       bullet.y += bullet.dy
       bullet.solid[:y] = bullet.y

       if bullet.y > grid.h
         bullet.dead = true
       end
+      if bullet.solid.intersect_rect?(enemy_rect)
+        bullet.dead = true
+      end
     end
     state.bullets = state.bullets.reject(&:dead)
   end

   def output
     outputs.solids << Solid.new(player_rect)

-    outputs.solids << Solid.new(x: state.enemy_x, y: grid.h * 0.7, w: 64, h: 64, r: 150, g: 150, b: 150)
+    outputs.solids << Solid.new(enemy_rect.merge(r: 150, g: 150, b: 150))

     outputs.solids << state.bullets.map(&:solid)

     outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
     outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
   end
+
+  def enemy_rect
+    { x: state.enemy_x, y: grid.h * 0.7, w: 64, h: 64 }
+  end

collision.gif

少しわかりづらいですが、敵に当たると弾が消えています。

スコア

弾が敵に当たると+2点、当たらなかったら-1点とし、合計を右上に表示します。

テキストの表示にはLabelを使います。
ドットっぽいフォントにするため、Press Start 2P - Google Fontsを利用させてもらいました。(ダウンロード&展開したものを、mygame/fontsに配置

mygame/app/primitives.rb
class Label < Primitive
  @@attr_keys = %i[x y text size_enum alignment_enum font r g b a]
  attr_accessor(*@@attr_keys)
end
mygame/app/main.rb
   def set_defaults
     state.player_x ||= grid.center_x - 32
     state.player_dx ||= 0

     state.enemy_x ||= grid.center_x - 32
     state.enemy_dx ||= 5

     state.bullets ||= []
+
+    state.score ||= 0
   end

   def update_state
     state.player_x += state.player_dx

     state.enemy_x += state.enemy_dx
     if state.enemy_x < 0 || state.enemy_x > grid.w - 64
       state.enemy_dx *= -1
     end

     state.bullets.each do |bullet|
       bullet.y += bullet.dy
       bullet.solid[:y] = bullet.y

       if bullet.y > grid.h
         bullet.dead = true
+        state.score -= 1
       end
       if bullet.solid.intersect_rect?(enemy_rect)
         bullet.dead = true
+        state.score += 2
       end
     end
     state.bullets = state.bullets.reject(&:dead)
   end

   def output
     outputs.solids << Solid.new(player_rect)

     outputs.solids << Solid.new(enemy_rect.merge(r: 150, g: 150, b: 150))

     outputs.solids << state.bullets.map(&:solid)
+
+    outputs.labels << Label.new(
+      x: grid.w * 0.99,
+      y: grid.h * 0.98,
+      text: state.score,
+      alignment_enum: 2,
+      font: 'fonts/Press_Start_2P/PressStart2P-Regular.ttf'
+    )

     outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
     outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
   end

score.gif

スコアが出るようになったので(一応)遊べるようになりました。

画像を使う

四角形のままだと寂しいので画像を使います。
(画像は以前僕が描いたやつなのでクオリティはあれですが…)

mygame/spritesplayer.pngenemy.pngbullet.pngを置きます。

あとは、Solidの代わりにSpriteを使うようにコードを変更します。
十字線はもう不要なので消しておきます。

mygame/app/primitives.rb
class Sprite < Primitive
  @@attr_keys = %i[
    x y w h path angle a r g b
    source_x source_y source_w source_h
    flip_horizontally flip_vertically
    angle_anchor_x angle_anchor_y
    tile_x tile_y tile_w tile_h
  ]
  attr_accessor(*@@attr_keys)
end
mygame/app/main.rb
   def handle_inputs
     if inputs.keyboard.right
       state.player_dx = 5
     elsif inputs.keyboard.left
       state.player_dx = -5
     else
       state.player_dx = 0
     end

     if inputs.keyboard.key_up.space
       state.bullets << state.new_entity(:bullet) do |bullet|
         bullet.y = player_rect[:y]
         bullet.x = player_rect[:x] + 16
         bullet.size = 32
         bullet.dy = 10
-        bullet.solid = { x: bullet.x, y: bullet.y, w: bullet.size, h: bullet.size, r: 255, g: 100, b: 100 }
+        bullet.sprite = { x: bullet.x, y: bullet.y, w: bullet.size, h: bullet.size, r: 255, g: 100, b: 100, path: 'sprites/bullet.png' }
       end
     end
   end

   def update_state
     state.player_x += state.player_dx

     state.enemy_x += state.enemy_dx
     if state.enemy_x < 0 || state.enemy_x > grid.w - 64
       state.enemy_dx *= -1
     end

     state.bullets.each do |bullet|
       bullet.y += bullet.dy
-      bullet.solid[:y] = bullet.y
+      bullet.sprite[:y] = bullet.y

       if bullet.y > grid.h
         bullet.dead = true
         state.score -= 1
       end
-      if bullet.solid.intersect_rect?(enemy_rect)
+      if bullet.sprite.intersect_rect?(enemy_rect)
         bullet.dead = true
         state.score += 2
       end
     end
     state.bullets = state.bullets.reject(&:dead)
   end

   def output
-    outputs.solids << Solid.new(player_rect)
+    outputs.sprites << Sprite.new(player_rect.merge(path: 'sprites/player.png'))

-    outputs.solids << Solid.new(enemy_rect.merge(r: 150, g: 150, b: 150))
+    outputs.sprites << Sprite.new(enemy_rect.merge(r: 150, g: 150, b: 150, path: 'sprites/enemy.png'))

-    outputs.solids << state.bullets.map(&:solid)
+    outputs.sprites << state.bullets.map(&:sprite)

     outputs.labels << Label.new(
       x: grid.w * 0.99,
       y: grid.h * 0.98,
       text: state.score,
       alignment_enum: 2,
       font: 'fonts/Press_Start_2P/PressStart2P-Regular.ttf'
     )
-    outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
-    outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)     
   end

sprite.gif

最初に比べると、だいぶそれらしくなりました!

パッケージ化

メタデータを用意します。
iconはplayer.pngをコピーしておきます。

mygame/metadata/game_metadata.txt
devid=hello-dragonruby-gtk
devtitle=Hello DragonRuby GTK
gameid=hello-dragonruby-gtk
gametitle=Hello DragonRuby GTK
version=0.1
icon=metadata/icon.png

dragonruby-publish をコピーしてきてもなぜか動かなかったので、
DragonRubyをダウンロード&展開したディレクトリの中でやります。
(コピーして動かす方法はこの後の追記参照)

$ cp mygame/sprites/player.png mygame/metadata/icon.png
$ cp -r mygame/ ~/Downloads/dragonruby-macos/hello-dragonruby-gtk
$ cd ~/Downloads/dragonruby-macos
$ ./dragonruby-publish --only-package hello-dragonruby-gtk

これで他のプラットフォーム用のファイルと共にHTML5版も出力されます。
builds/hello-dragonruby-gtk-html5-0.1

これを公開すればブラウザでプレイできます。
せっかくなので、GitHub Pagesに置いてみました。

dragonruby-publishをコピーして動かす(追記)

dragonruby-publish以外にもいろいろとコピーしてきたら動きました。

$ cp ~/Downloads/dragonruby-macos/dragonruby-publish .
$ cp ~/Downloads/dragonruby-macos/.dragonruby .
$ cp ~/Downloads/dragonruby-macos/*.png .
$ cp ~/Downloads/dragonruby-macos/open-source-licenses.txt .

$ ./dragonruby-publish --only-package

.gitignoreするものもいろいろと増えました。

.gitignore
# DragonRuby
/dragonruby*
/font.ttf
/logs
/tmp
/exceptions
/console_history.txt
/.dragonruby
/builds
/console-logo.png
/open-source-licenses.txt

ソースコード

以下で公開しています。
(DragonRubyが別途必要です)

感想

特に大きなハマりどころはなく遊べました。

Productionで使えるのか?と言われたら、どうなのかわかりませんが、
ちょっとしたものを作るのには十分使えそうでした。

書き慣れたRubyでクリエイティブコーディング的なことはやりたかったので、そのツールとして使えないかな?などと考えています。
(そのためには、物理エンジン搭載&図形描画を充実させてほしいところですが)

また時間を見つけて触ってみようと思います。

参考文献

APIリファレンスがない(?)ので、この辺りを見ながら探り探りやりました。

  • README.md
    • まずはこちらを読む
  • CHEATSHEET.md
    • こちらも読む
  • DragonRuby Game Toolkit by DragonRuby
    • EntityなどREADMEにはない記述あり
  • mygame/documantion
    • Solid、Spriteなど要素ごとにそれなりに説明してくれる
  • samples
    • 67個もサンプルがある(MITライセンス!)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
9