2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ruby2d が Ruby4.0 と WASM で生き返ったぞ!

2
Last updated at Posted at 2026-08-11

突然ですが・・・

ruby2dの奴が生き返りました!

私、仕事の傍らキッズプログラミングスクールでRubyを使って子供たちにRubyの素晴らしさを日々啓蒙しているのですが、そのツールとして断固Rubyとruby2dを使う事にしております。

Rubyは子供にも書きやすいし、ruby2dはWSL環境でも動かせるので学習コストはもちろん、物理的コストがお安いのです。MacでもWSL環境化でも使えて、Rubyらしさを失わないシンプルで素晴らしいライブラリなので、子供たちにはちょっと小難しい、クラスや継承をゲームで遊びながら学べるという神ツールなのです。

そんなruby2dで、ちょっと授業の調べ物をしようと 公式ページ をみてみた所

スクリーンショット 2026-08-11 22.57.30.png

ずっとメンテナンスされていなかったruby2dの公式ページが更新されている・・・しかもかわいい。

まさかと思って Github ものぞいてみると・・・

なんということでしょう!ruby2dのメジャーバージョンが変わっているではありませんか。

スクリーンショット 2026-08-11 16.01.36.png

気がついたのが更新日翌日だったのでさらに大興奮です!

内容としては↑のSDL3対応が大きなトピックになります。

ということは、私が今まで塩漬けにしていた「子供が作ったゲームを公開する」という事が可能になってくるはず!!

Githubのアカウントさえ持てる年であれば無料で自作ゲームが公開できちゃうというなんとも有難い世の中

ちょっと触ってみたところ

一ヶ所目立つところで、メソッドの破壊的移行がありまして、

text = Text.new("hello")
text.text = "yamada"

これがなくなりまして、

text = Text.new("hello")
text.content = "yamada"

になってるようです。
確かにこれネーミング気になってましたが、まさかの過去資産書き直しw

ruby2d自体元々かなりプレーンなイメージだったので、もしかしたら他に何か便利になったものもあるかもしれませんね。

実際、まだ触ってませんが、Buttonクラスもできていて、

btn = Button.new(x: 0, y: 0, width: 100, height: 40, color: '#333')

btn.on(:mouse_down) { btn.color = '#555' }
btn.on(:mouse_up)   { btn.color = '#333' }
btn.on(:click)      { fire }
btn.on(:hover)      { puts 'enter' }
btn.on(:hover_out)  { puts 'leave' }
btn.on(click: :left)           { puts 'left click' }
btn.on(click: [:left, :right]) { puts 'either click' }

こんな感じでイベントを仕込んだり、
既存のオブジェクトを第一引数に取る、カスタムイベントボタンを実装できるようになっています。

circle = Circle.new(x: 200, y: 200, radius: 30, color: 'tomato')
btn = Button.new(circle) { puts 'Circle clicked!' }

これがSpriteクラスでも使えると、色々と幅が広がりますね。

で、本題のwasm化ですが

現在はWindows、またはMacにのみ公式に対応とのことではありますが、びっくりするくらい簡単で、下準備としてまず

①emsdkのインストール

shell
% git clone https://github.com/emscripten-core/emsdk.git ~/emsdk
% cd ~/emsdk
% ./emsdk install latest
% ./emsdk activate latest
% source ~/emsdk/emsdk_env.sh

これだけやっていれば良いです。
ただ、これはセッション単位で読ませる必要があるようですので、

.zshrc
source ~/emsdk/emsdk_env.sh

こんな感じで仕込んでおくのも良いでしょう。

あとは、

②Ruby4系が必須

とのことで、3系だとコンパイルが通せずアウトです。

確かに、以前のバージョンでもwasm化のコマンド自体はあったのですが、コケ倒してどうにもならない状態でした。

③require を使ったファイル連携は死ぬ

これは mruby の仕様上仕方がないので、最初に試す時は1ファイルで完結したもの・・例えば、

shell
% mkdir ruby2d_kawa
% touch main.rb

で作業ディレクトリにmain.rbだけを置いて、

昔懐かし川下りゲーム(逆走Ver)

main.rb
require "ruby2d"

# 川(背景)
river = Rectangle.new(width: Window.width, height: Window.height, color: "blue")

# ボート
boat = Rectangle.new(width: 60, height: 30, color: "brown")
boat.x = (Window.width - boat.width) / 2
boat.y = Window.height - boat.height * 2

# 岩(3つ)
rock_count = 3
rocks = rock_count.times.map do |i|
  Circle.new(radius: 15, color: "gray", x: rand(0..Window.width), y: -i * 150)
end

status = :playing

# ゲームオーバー画面
message = [
  Text.new("GAME OVER", size: 30, x: 220, y: 200),
  Text.new("press key 'R' to restart", size: 20, x: 190, y: 260)
]
message.each { |text| text.remove }

on :key_down do |event|
  if status == :playing
    case event.key
    when "left"
      boat.x -= 20 if boat.x > 0
    when "right"
      boat.x += 20 if boat.x < Window.width - boat.width
    end
  end

  if event.key == "r" && status == :game_over
    status = :playing
    boat.x = (Window.width - boat.width) / 2
    rocks.each_with_index do |rock, i|
      rock.y = -i * 150
      rock.x = rand(0..Window.width)
    end
    message.each { |text| text.remove }
  end
end

update do
  if status == :playing
    rocks.each do |rock|
      rock.y += 4
      if rock.y > Window.height
        rock.y = 0
        rock.x = rand(0..Window.width)
      end
      if boat.contains?(rock.x, rock.y)
        status = :game_over
        message.each { |text| text.add }
      end
    end
  end
end

show

これは

shell
% ruby2d build --web main.rb

を打つだけですんなりコンパイルできます。

shell
 ruby2d_kawa (main #%)% ruby2d build --web main.rb

  ◆ Building web
    wrote build/web/app.html
    wrote build/web/app.js
    wrote build/web/app.wasm
    wrote build/web/app.data
    Run `ruby2d launch --web` to view

そうすると、起動コマンド打てと出てくるので、

shell
 ruby2d_kawa (main #%)% ruby2d launch --web
  Serving at http://localhost:8080/app.html
  Press Ctrl-C to stop.

これで自動でブラウザが開いて、localhost:8080で遊べます。

スクリーンショット 2026-08-11 23.41.31.png

どうやら、ローカルで build/web/app.html をそのまま file:// で開いても動かない(ブラウザのセキュリティで .wasm の読み込みがブロックされる)ので、ローカル確認だけ簡単な HTTP サーバーが必要という流れのようです。

早い話、S3とかgithub.ioとかに

この静的ファイル部分をあげてしまえば誰でも遊べちゃうという事ですね。
これはゲームじゃなくても色々なツールに応用効きそうで夢がありますね。

じゃあ複数ファイルに跨った場合は?

はい。今回お伝えしたいのはここなのですが、

テトリスに似た何かを作ったとします。

当然ちょっとしたファイル数になります。

ruby2d_tetraminos/
├── app/
│   ├── main.rb
│   └── models/
│       ├── blocks/
│       │   ├── block.rb
│       │   └── block_map.rb
│       ├── game/
│       │   └── game.rb
│       ├── stage/
│       │   └── stage.rb
│       └── tetrominos/
│           ├── tetromino_base.rb
│           ├── tetromino_i.rb
│           ├── tetromino_j.rb
│           ├── tetromino_l.rb
│           ├── tetromino_o.rb
│           ├── tetromino_s.rb
│           ├── tetromino_t.rb
│           └── tetromino_z.rb
├── config.rb
├── settings.rb
├── Gemfile
└── Rakefile

これらをmainに集約してコーディングするのはちょっと現実的でないので、一旦コンパイル前の1ファイルへの集約を含めてタスクを用意します。

config.rb の中にはシンプルに呼び出し順を考慮た読み込みを書いていましたので、

config.rb
require "ruby2d"

require "./settings"

require "./app/models/stage/stage"

require "./app/models/blocks/block"
require "./app/models/blocks/block_map"

require "./app/models/tetrominos/tetromino_base"
require "./app/models/tetrominos/tetromino_i"
require "./app/models/tetrominos/tetromino_j"
require "./app/models/tetrominos/tetromino_l"
require "./app/models/tetrominos/tetromino_o"
require "./app/models/tetrominos/tetromino_s"
require "./app/models/tetrominos/tetromino_t"
require "./app/models/tetrominos/tetromino_z"

require "./app/models/game/game"

これをRakefileに直読みさせるよう書きます。

Rakefile
task default: %w[game]

task :game do
  ruby "app/main.rb"
end

task :wasm do
  require "fileutils"
  FileUtils.mkdir_p ".bundle"

  files = [
    "settings.rb",
    "app/models/blocks/block.rb",
    "app/models/blocks/block_map.rb",
    "app/models/tetrominos/tetromino_base.rb",
    "app/models/tetrominos/tetromino_i.rb",
    "app/models/tetrominos/tetromino_j.rb",
    "app/models/tetrominos/tetromino_l.rb",
    "app/models/tetrominos/tetromino_o.rb",
    "app/models/tetrominos/tetromino_s.rb",
    "app/models/tetrominos/tetromino_t.rb",
    "app/models/tetrominos/tetromino_z.rb",
    "app/models/stage/stage.rb",
    "app/models/game/game.rb",
  ]

  bundle = %(require "ruby2d"\n\n)
  files.each { |f| bundle += File.read(f) + "\n" }

  main_body = File.read("app/main.rb").gsub(/^require.*\n/, "")
  bundle += main_body

  File.write(".bundle/bundle.rb", bundle)
  puts "Bundled → .bundle/bundle.rb"

  sh "ruby2d build --web .bundle/bundle.rb"

  FileUtils.cp "web/index.html", "build/web/app.html"
  puts "Template applied → build/web/app.html"
end

task :serve do
  puts "Serving at http://localhost:8080/app.html"
  sh "python3 -m http.server 8080 --directory build/web"
end

これで3つのコマンド使えます。

shell #アプリを動かす
% rake game
shell #wasm化
% rake wasm
shell #ブラウザでのプレビュー
% rake server

で、当然HTMLなので、

スクリーンショット 2026-08-11 21.37.24.png

こんな感じに整形する事も可能です。

ruby2dは教材としてすごくやりやすかったのですが、唯一発表の場のことだけは気になっていました。
ここまでできるようになると、是非とも子供達の作ったオリジナルとかを世に放ちたいですね。

ruby2dコミュニティや、コミッターの方々に感謝です。

2
0
0

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
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?