Ruby 4.0 / WebAssembly

Rubyで書く。
ブラウザで動く。

いつものRubyから、Webの世界へ。基本の文法を試し、ボタンやCanvasにつなぎます。小さな冒険を、一行ずつ育てましょう。

目次を開く(全20章)
Ruby 4.0 / ruby.wasm 2.10.1 — 実行時に読み込みます

本文はオフラインでも読めます。実行ボタンを押すと、CDNから実行環境を取得します(初回は約31 MB)。Rubyの実行環境は実行ごとに新しくなり、結果の出力は16 KBまでです。

01学習レベル:基礎

Rubyが、ブラウザの中で動く

Rubyで書いた計算やゲームの規則を、Webページでも使えます。ruby.wasmは、Rubyの実行環境をWebAssembly(WASM)向けにしたもの。ブラウザがWASMを読み込み、その中でRubyのコードを実行します。

Rubyのコード
hp -= damage
ruby.wasm
Rubyを実行する
ブラウザの画面
HTML・Canvas

Rubyの文法はターミナルで使うものと共通です。変わるのは、入力をどこから受け取るか、結果をどこへ表示するか。この編では、まずRubyの基本を試し、そのあとでHTMLとCanvasにつなぎます。

担当役割
HTML見出し、入力欄、ボタン、Canvasを用意する。
CSS色、大きさ、配置を整える。
Ruby計算、分岐、データ、ゲームの規則を書く。
JavaScript・WASMRubyを起動し、ブラウザの機能との橋渡しをする。
この編の読み方

Rubyが初めてなら順番に進みます。Rubyを知っている人は第3章の導入、第13章のHTML、第14章のCanvasへ進めます。HTMLやCanvas自体の基本は、ガイドの各編へ戻って確認できます。

02学習レベル:基礎

まず、一行動かしてみる

putsは、値を文字にして表示し、改行するメソッドです。下の「実行」を押すと、本物のRuby 4.0がブラウザで動きます。最初は実行環境を読み込むため、少し待ちます。

Ruby 4.0
# frozen_string_literal: true

puts "こんにちは、未來!"
puts 7 + 5
puts "Ruby #{RUBY_VERSION}"
puts RUBY_DESCRIPTION
「実行」を押すと、ここに結果が出ます。
「未來」を自分の名前に変えて実行しましょう。RUBY_VERSIONは実際に動いているRubyの言語版です。

pは、値の中身が分かる形で表示します。文字列なら引用符、配列なら角括弧も表示されます。計算した値を画面に出したいときは、puts 7 + 5のように、表示する指示も書きます。

実行欄の仕組み

このページのRuby実行欄では、puts・print・pの出力を「実行結果」に集めています。実行するたびに新しい環境を作るので、別の欄の変数や、前回の値は引き継ぎません。5秒を超える処理は停止します。

03学習レベル:基礎

自分のHTMLファイルで動かす

新しいフォルダーにindex.htmlを作り、次の内容を保存します。インターネットにつながった状態でブラウザで開くと、「こんにちは、Ruby …!」に変わります。

index.html — 最小の完成例
<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>はじめてのRuby WASM</title>
  <script src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.10.1/dist/browser.script.iife.js"></script>
</head>
<body>
  <h1>Rubyをブラウザで動かす</h1>
  <p id="message">Rubyを読み込んでいます…</p>
  <script type="text/ruby">
    require "js"
    document = JS.global[:document]
    message = document.querySelector("#message")
    message[:textContent] = "こんにちは、Ruby #{RUBY_VERSION}!"
    puts "この行は開発者ツールのコンソールに表示されます"
  </script>
</body>
</html>
  1. 実行環境を読み込む

    最初のscriptはCDNからruby.wasmを読み込みます。@ruby/4.0-wasm-wasiがRubyの言語版、@2.10.1が配布パッケージの版です。

  2. Rubyを書く場所を用意する

    <script type="text/ruby">の中がRubyです。ブラウザだけではこの形式を実行できないため、最初の実行環境が必要です。

  3. 表示先を見つける

    require "js"で橋渡しを読み込み、JS.global[:document]からHTMLを操作します。

自分のHTMLでは、putsは通常、開発者ツールのコンソールに出ます。ページ内へ表示するには、上の例のように要素のtextContentへ文字列を代入します。

ローカルサーバーを使うとき

ダブルクリックでうまく動かない場合や、外部の.rbファイル・保存・通信を試す場合は、フォルダーで次のコマンドを実行し、http://localhost:8000/を開きます。終了はターミナルでControl + Cです。

ターミナル
ruby -run -e httpd . -p 8000
版をそろえる

本編はRuby 4.0、配布パッケージは2.10.1で確認する構成です。latestで自動的に版を変えず、更新するときは実行スクリプトとWASM、動作例をまとめて確認します。

04学習レベル:基礎

変数・文字列・真偽値

変数は、値に名前を付けて使うためのものです。Rubyでは、最初に代入したときから使えます。文字列は引用符で囲み、ダブルクォート内の#{ }で値を埋め込めます。

Ruby 4.0
# frozen_string_literal: true

name = "未來"
hp = 420
equipment = "ルビーの剣"
puts "#{name}のHPは#{hp}です"
puts "装備:#{equipment}"

hp -= 50
puts "攻撃を受けたあとのHP:#{hp}"
p hp.positive?
p nil
「実行」を押すと、ここに結果が出ます。
数値や文字を変えて、もう一度「実行」を押してみましょう。
書き方意味
42 / 3.14整数 / 小数
"未來"文字列(String)
:hpシンボル。ハッシュのキーなどに使う名札。
true / false真 / 偽
nil値がないことを表す。
0も空文字列も「真」

Rubyで偽として扱う値はfalseとnilだけです。0、""、[]は真です。HPが0か調べるときはhp.zero?やhp == 0と書きます。

文字列を凍結する1行

# frozen_string_literal: trueは、コード中に直接書いた文字列を変更できなくする指定です。変数への再代入はできます。文字列を<<で組み立てるときはtext = +""で変更できる文字列を作ります。

05学習レベル:基礎

計算と型変換

整数どうしの割り算は、結果も整数です。割合を計算するときは、小数に変換するかfdivを使います。入力欄やgetsから受け取る値は文字列なので、計算の前に数へ変換します。

Ruby 4.0
# frozen_string_literal: true

puts 7 / 2
puts 7.fdiv(2)
puts 7 % 2
puts 2 ** 10

hp = 315
max_hp = 420
puts "HPの割合:#{(hp.fdiv(max_hp) * 100).round}%"

p "42abc".to_i
p Integer("42abc", exception: false)
p Integer("42", exception: false)
p Math.sqrt(16)
「実行」を押すと、ここに結果が出ます。
to_iは読めるところまで変換します。Integer(..., exception: false)は全体を整数として読めなければnilです。
書き方意味
+ - * / % **加算・減算・乗算・除算・剰余・累乗
n.clamp(0, 420)値を0〜420の範囲へ収める。
[hp - damage, 0].max小さいほうの限界を0にする。
format("%.2f", value)小数点以下2桁の文字列にする。
0.1r + 0.2r有理数のまま計算する。
小数の誤差

Floatは2進で近似した値なので、0.1 + 0.2はぴったり0.3になりません。金額なら円単位の整数にするなど、表す値に合う型を選びます。

06学習レベル:基礎

条件で行動を分ける

ifは、条件によって実行する処理を分けます。elsifで次の条件、elseでどれにも当てはまらない場合を書き、endで閉じます。

Ruby 4.0
# frozen_string_literal: true

hp = 80
mp = 25

if hp <= 100 && mp >= 20
  puts "叡智の祈りで回復する"
elsif mp >= 30
  puts "白光の一閃を使う"
else
  puts "ルビーの剣で攻撃する"
end

command = :guard
case command
when :attack then puts "攻撃!"
when :guard  then puts "守りを固める!"
else              puts "行動を選んでください"
end
「実行」を押すと、ここに結果が出ます。
hpを300、mpを40にすると、最初の分岐はどう変わるでしょう。
書き方意味
== != < > <= >=等しい / 等しくない / 大小の比較
&& / || / !かつ / または / ではない
puts "勝利!" if enemy_hp.zero?短い条件は行の後ろにも書ける。
return unless readyreadyが偽なら、そのメソッドから戻る。

07学習レベル:基礎

繰り返しとブロック

同じ処理を決まった回数行うならtimes、集まりの値を一つずつ使うならeachです。do ... endで囲む処理のかたまりをブロックと呼びます。

Ruby 4.0
# frozen_string_literal: true

3.times do |index|
  puts "#{index + 1}回目の攻撃"
end

%w[紅玉の鎧 叡智の冠 コードの書].each do |item|
  puts "#{item}を装備した"
end

enemy_hp = 90
while enemy_hp > 0
  enemy_hp = [enemy_hp - 30, 0].max
  puts "大魔王のHP:#{enemy_hp}"
end
「実行」を押すと、ここに結果が出ます。
数値や文字を変えて、もう一度「実行」を押してみましょう。
繰り返しから抜ける

breakは繰り返しを終え、nextは次の周回へ進みます。whileの条件が変わることを確認しましょう。このページの実行欄はWorkerで動かすので、「停止」ボタンで止められます。

Ruby 3.4以降のit

[1, 2, 3].map { it * 2 }のitは最初のブロック引数を表します。まずは|number|のように名前を付けると、値の役割を追いやすくなります。二つの引数が必要なら|value, index|と書きます。

08学習レベル:基礎

配列と2次元の地図

配列は、値を順番に並べたものです。番号は0から数えます。地図は「行の配列」をさらに配列へ入れると、map[row][column]で1マスを取り出せます。

Ruby 4.0
# frozen_string_literal: true

items = ["薬草", "ルビーの剣", "コードの書"]
puts items[0]
puts items[-1]
items << "紅玉の鎧"
p items.length

map = [
  ["草地", "森",   "城"],
  ["道",   "道",   "山"],
  ["村",   "草地", "海"]
]
puts "右上のマス:#{map[0][2]}"

board = Array.new(2) { Array.new(3, 0) }
board[0][0] = 1
p board
「実行」を押すと、ここに結果が出ます。
数値や文字を変えて、もう一度「実行」を押してみましょう。
書き方意味
a << value / a.pop末尾へ追加 / 末尾から取り出す。
a.each_with_index { |value, i| ... }値と番号を一緒に取り出す。
a.map { |x| x * 2 }各値を変換した新しい配列を作る。
a.select { |x| x > 0 }条件に合う値だけ集める。
a.sum / a.sample合計 / ランダムに一つ選ぶ。
行ごとに別の配列を作る

Array.new(2, Array.new(3, 0))では、2行が同じ配列を参照します。片方を変えるともう片方も変わるため、地図にはArray.new(2) { Array.new(3, 0) }と書きます。

09学習レベル:基礎

ハッシュ・集合・値のまとまり

ハッシュは、番号の代わりにキーで値を取り出します。勇者の名前やHPを、ひとまとまりにできます。Setは重複を持たない集まり、Dataは少数の値を名前付きでまとめる型です。

Ruby 4.0
# frozen_string_literal: true

hero = { name: "未來", hp: 420, mp: 100 }
hero[:hp] -= 50
puts "#{hero[:name]}のHP:#{hero.fetch(:hp)}"
puts hero.fetch(:weapon, "素手")

visited = Set["村", "森", "村"]
p visited.to_a
p visited.include?("城")

Point = Data.define(:x, :y)
position = Point.new(x: 2, y: 3)
moved = position.with(x: 4)
p position
p moved
「実行」を押すと、ここに結果が出ます。
Ruby 4.0のSetはrequireなしで使えます。Dataのwithは元の値を変えず、新しい値を返します。
ないキーの扱い

hero[:gold]は、キーがなければnilです。必ずあるべきキーはfetch(:gold)で取り出すと、抜けていることをエラーで発見できます。Hash.new([])も同じ配列を共有するため、キーごとの配列にはブロック付きのHash.new { |h, key| h[key] = [] }を使います。

10学習レベル:基礎

メソッドとラムダ

処理に名前を付けると、同じ規則を何度も使えます。メソッドはdefで定義し、最後に評価した式の値を返します。処理を値として渡したいときは、ラムダが使えます。

Ruby 4.0
# frozen_string_literal: true

def damage_for(attack:, defense:)
  [attack - defense, 1].max
end

def remaining_hp(hp, damage)
  [hp - damage, 0].max
end

damage = damage_for(attack: 125, defense: 30)
puts "ダメージ:#{damage}"
puts "残りHP:#{remaining_hp(80, damage)}"

double = ->(number) { number * 2 }
puts double.call(12)
p [1, 2, 3].map { |number| double.call(number) }
「実行」を押すと、ここに結果が出ます。
数値や文字を変えて、もう一度「実行」を押してみましょう。
書き方意味
def greet(name = "勇者")引数を省略したときの値を指定する。
def hit(power:, defense:)役割が分かるキーワード引数を受け取る。
def run(&block)ブロックを受け取り、block.callで呼ぶ。
def square(x) = x * x一つの式だけのメソッドを短く書く。
empty? / sort!?は問い、!は同名のメソッドより注意が必要な版という慣習。
イベントにも処理を渡す

後半で使うaddEventListenerは、クリックされたときに実行するブロックを受け取ります。「いま実行する処理」と「あとで呼ばれる処理」を分けて考えましょう。

11学習レベル:基礎

クラスに状態と操作をまとめる

勇者や敵がそれぞれHPを持つなら、クラスで共通の形を作れます。initializeはnewで作るときに呼ばれ、@hpはその個体が持つ値です。

Ruby 4.0
# frozen_string_literal: true

class Fighter
  attr_reader :name, :hp

  def initialize(name, hp)
    @name = name
    @hp = hp
  end

  def take_damage(amount)
    @hp = [@hp - amount, 0].max
  end

  def alive?
    @hp.positive?
  end
end

hero = Fighter.new("未來", 420)
boss = Fighter.new("大魔王", 960)
boss.take_damage(230)
puts "#{hero.name}のHP:#{hero.hp}"
puts "#{boss.name}のHP:#{boss.hp}"
p boss.alive?
「実行」を押すと、ここに結果が出ます。
数値や文字を変えて、もう一度「実行」を押してみましょう。
書き方意味
attr_reader :hp外から読むメソッドを作る。
attr_accessor :name読む・書くメソッドを作る。
private以降のメソッドを内部から使うものにする。
class Hero < Fighter共通の性質を継承する。
module Heal; ...; end / include Heal複数のクラスで使う機能をまとめ、取り込む。
変更の入口を用意する

HPを自由に書き換えられるようにするより、take_damageやhealへ変更を集めると、「HPを0未満にしない」という規則を守りやすくなります。

12学習レベル:基礎

入力を受け取り、エラーを確かめる

ターミナルのgetsは標準入力から読みます。このガイドの実行欄では、下の入力欄を一行ずつ読めるようにしています。自分で作るWebページの入力欄は、第13章のようにDOMから値を受け取ります。

Ruby 4.0
# frozen_string_literal: true

text = $stdin.gets.to_s.chomp
number = Integer(text, exception: false)

if number.nil?
  puts "整数を入力してください"
else
  puts "回復量:#{number.clamp(0, 140)}"
end

begin
  puts 10 / 0
rescue ZeroDivisionError => error
  puts "計算できません:#{error.message}"
end
「実行」を押すと、ここに結果が出ます。
入力を「80」「42abc」「-10」に変えて確かめましょう。下の入力欄は、このガイドが用意した標準入力です。
エラーを消す前に読む

SyntaxErrorなら文法、NameErrorなら名前、NoMethodErrorならその値に使えないメソッドを疑います。rescueは予想できる失敗を扱う場所です。原因の分からない失敗を全部隠すと、間違いを見つけにくくなります。

13学習レベル:実践

RubyからHTMLとボタンを操作する

require "js"で、Rubyからブラウザの機能を呼べます。querySelectorで要素を見つけ、[:value]で入力を読み、[:textContent]で文章を表示します。

index.html — 完成した作例
<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Rubyで勇者の名前を表示する</title>
  <style>
    body { margin: 0; padding: 24px; color: #14202b; background: #f6f5f2; font-family: sans-serif; }
    h1 { font-size: 20px; }
    input, button { max-width: 100%; padding: 10px; font: inherit; }
    button { margin: 8px 0; background: #1b4e77; color: white; border: 0; border-radius: 6px; }
    #message { line-height: 1.8; }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.10.1/dist/browser.script.iife.js"></script>
</head>
<body>
  <h1>冒険のはじまり</h1>
  <label>勇者の名前 <input id="name" value="未來" maxlength="30"></label>
  <button id="greet" type="button" disabled>冒険に出る</button>
  <p id="message" role="status">Rubyを読み込んでいます…</p>
  <script type="text/ruby">
    require "js"
    document = JS.global[:document]
    input = document.querySelector("#name")
    button = document.querySelector("#greet")
    message = document.querySelector("#message")

    button.addEventListener("click") do |_event|
      name = input[:value].to_s.strip
      message[:textContent] = if name.empty?
        "名前を入力してください。"
      else
        "#{name}はルビーの剣を手に、冒険に出た!"
      end
    end

    button[:disabled] = false
    message[:textContent] = "準備できました。名前を入力してボタンを押しましょう。"
  </script>
</body>
</html>

「作例を起動」でRubyを読み込みます。コードを変えるときはHTMLを保存して編集します。

addEventListener("click") do ... endの中は、ボタンが押されたときに実行されます。名前を読み込む処理もブロックの中にあるので、入力し直した名前が使われます。Rubyの準備ができるまでは、ボタンを無効にしています。

書き方意味
JS.global[:document]ブラウザのdocumentを取り出す。
document.querySelector("#name")idがnameの要素を見つける。
input[:value].to_sJavaScript側の文字列をRubyの文字列にする。
element[:textContent] = textHTMLとして解釈せず、文字として表示する。
button[:disabled] = falseJavaScript側のプロパティへRubyの値を渡す。
JavaScript側の値は変換して使う

ブラウザから返った値はJS::Objectです。文字列ならto_s、整数ならto_i、小数ならto_fへ変換します。JavaScriptのfalseをそのままRubyのifへ渡すと、オブジェクトなので真になります。真偽はvalue == JS::Trueで比較するか、対応する真偽変換メソッドを使います。

14学習レベル:実践

RubyからCanvasに描く

Canvasの描画命令も、Rubyから呼べます。描画先を見つけてgetContext("2d")を呼ぶところは、Canvas編と同じです。色などのプロパティには[:fillStyle] = ...の形で代入します。

index.html — 完成した作例
<!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>RubyでCanvasに描く</title>
  <style>
    body { margin: 0; padding: 20px; color: #14202b; background: #f6f5f2; font-family: sans-serif; }
    h1 { font-size: 20px; }
    canvas { display: block; width: 100%; max-width: 480px; height: auto; border-radius: 8px; }
    button { margin-top: 12px; padding: 10px 16px; color: white; background: #1b4e77; border: 0; border-radius: 6px; }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.10.1/dist/browser.script.iife.js"></script>
</head>
<body>
  <h1>ルビーの剣の白い光</h1>
  <canvas id="stage" width="480" height="230" aria-label="左に勇者の未來、右に大魔王。ボタンで白い光を描く戦闘の図。">未來と大魔王の戦闘シーン</canvas>
  <button id="attack" type="button" disabled>白光の一閃</button>
  <p id="status" role="status">Rubyを読み込んでいます…</p>
  <script type="text/ruby">
    require "js"
    document = JS.global[:document]
    canvas = document.querySelector("#stage")
    ctx = canvas.getContext("2d")
    button = document.querySelector("#attack")
    status = document.querySelector("#status")

    draw = ->(light) do
      ctx.clearRect(0, 0, 480, 230)
      ctx[:fillStyle] = "#182238"
      ctx.fillRect(0, 0, 480, 230)
      ctx[:fillStyle] = "#bd4765"
      ctx.fillRect(65, 90, 50, 80)
      ctx[:fillStyle] = "#dabd81"
      ctx.fillRect(70, 72, 40, 18)
      ctx[:fillStyle] = "#805397"
      ctx.fillRect(345, 65, 70, 105)
      ctx[:fillStyle] = "#ffffff"
      ctx[:font] = "16px sans-serif"
      ctx.fillText("未來", 70, 200)
      ctx.fillText("大魔王", 350, 200)
      if light
        ctx[:strokeStyle] = "#fff5d7"
        ctx[:lineWidth] = 12
        ctx.beginPath
        ctx.moveTo(120, 112)
        ctx.lineTo(365, 90)
        ctx.stroke
      end
    end

    draw.call(false)
    button.addEventListener("click") do |_event|
      draw.call(true)
      status[:textContent] = "ルビーの剣から、白い光が放たれた!"
    end
    button[:disabled] = false
    status[:textContent] = "準備できました。必殺技を使ってみましょう。"
  </script>
</body>
</html>

「作例を起動」でRubyを読み込みます。コードを変えるときはHTMLを保存して編集します。

座標の原点は左上です。右へ進むとxが増え、下へ進むとyが増えます。ここではキャラクターを四角形で表し、「背景→勇者と敵→文字→白い光」の順に描いています。あとから描いたものが手前に重なります。

書き方意味
ctx.fillRect(x, y, w, h)四角形を塗る。
ctx.beginPath新しい線を描き始める。
ctx.moveTo(x, y) / ctx.lineTo(x, y)始点へ移る / 次の点まで線をつなぐ。
ctx.stroke / ctx.fill線を引く / 囲んだ領域を塗る。
ctx.save / ctx.restore色や変形などの描画状態を保存 / 復元する。

詳しい描画はCanvas編の最初の3行、高精細ディスプレイへの対応を参照してください。JavaScriptで覚えた描画の順序や座標は、そのまま使えます。

15学習レベル:発展

アニメーションは、次の一コマを予約する

無限ループで画面を描き続けると、クリックや表示の更新が進みません。requestAnimationFrameへ処理を渡し、一コマを描いたらブラウザへ制御を返します。

前章のCanvasを用意したHTMLで、Rubyの描画処理を次のように置き換えます。ブラウザが渡す時刻はJavaScriptの数値なので、to_fで変換します。

Ruby — requestAnimationFrameで動かす
require "js"
canvas = JS.global[:document].querySelector("#stage")
ctx = canvas.getContext("2d")
x = 20.0
last = nil
frame = nil

frame = proc do |timestamp|
  now = timestamp.to_f
  dt = last ? [(now - last) / 1000.0, 0.05].min : 0.0
  last = now
  x = (x + 100 * dt) % 480

  ctx.clearRect(0, 0, 480, 230)
  ctx[:fillStyle] = "#bd4765"
  ctx.fillRect(x, 95, 30, 30)
  JS.global.requestAnimationFrame(frame)
end

JS.global.requestAnimationFrame(frame)
速さは1秒あたりで決める

x += 100 * dtは、1秒あたり100進むという意味です。画面の更新回数が違っても速さがそろいます。タブを戻した直後に飛ばないよう、dtを最大0.05秒に抑えています。停止する部品を作るなら、予約時に返る番号を保持してcancelAnimationFrameへ渡します。

考え方はCanvas編のアニメーションと共通です。Canvasは毎回描き直し、HPや位置などの状態はRubyの変数やオブジェクトへ保持します。

16学習レベル:実践

JSONとlocalStorageで冒険を保存する

ブラウザを閉じても残したい値は、保存する指示が必要です。小さなゲームの状態なら、JSONの文字列にしてlocalStorageへ入れる方法があります。公開するページと同じように、HTTPのURLで試しましょう。

Ruby 4.0
# frozen_string_literal: true

require "json"

hero = { name: "未來", hp: 315, mp: 70 }
json = JSON.generate(hero)
puts json

saved = JSON.parse(json)
puts "#{saved.fetch("name")}のHP:#{saved.fetch("hp")}"
「実行」を押すと、ここに結果が出ます。
JSONにすると、シンボルのキーは文字列になります。読み戻すときは saved["hp"] のように取り出します。
Ruby — 自分のHTMLで保存・読み込みする
require "js"
require "json"

storage = JS.global[:localStorage]
hero = { name: "未來", hp: 315, mp: 70 }
storage.setItem("mirai-adventure-v1", JSON.generate(hero))

raw = storage.getItem("mirai-adventure-v1")
unless raw == JS::Null
  begin
    saved = JSON.parse(raw.to_s)
    hp = saved.fetch("hp")
    raise "HPの形式が違います" unless hp.is_a?(Integer)
    hero[:hp] = hp.clamp(0, 420)
  rescue JSON::ParserError, KeyError, RuntimeError => error
    warn "保存データを読めません:#{error.message}"
  end
end
保存が使えない場合もある

容量の上限やブラウザの設定によって、localStorageの取得・書き込みが失敗することがあります。実際のゲームでは保存操作の失敗を受け止め、画面へ知らせます。file://での保存にはブラウザごとの差があるため、ローカルサーバーや公開先で確認します。このガイド内の隔離した作例には保存しません。

17学習レベル:発展

外部ファイルと非同期処理

RubyのFile.readが読むのは、WASM側へ用意されたファイルです。Webサーバー上のJSONやYAMLを読みたいときは、ブラウザのfetchを経由します。通信の終了を待つときは、非同期実行の指定が必要です。

実行環境を読み込んだHTMLに追加する
<script type="text/ruby" data-eval="async">
  require "js"
  require "json"

  response = JS.global.fetch("./hero.json").await
  raise "取得に失敗しました" unless response[:ok] == JS::True
  text = response.text.await.to_s
  hero = JSON.parse(text)

  JS.global[:document].querySelector("#message")[:textContent] =
    "#{hero.fetch("name")}のHP:#{hero.fetch("hp")}"
</script>
hero.json — index.htmlと同じフォルダー
{"name":"未來","hp":420,"mp":100}

awaitは通信が終わるまでそのRubyの続きを待ちます。data-eval="async"を付けることで、この待ち方を使えます。JavaScriptからVMを直接操作する場合は、vm.evalAsync(...)を使います。

YAMLも、文字列として取得してから読む

標準ライブラリ込みの環境でrequire "yaml"を使い、取得した文字列をYAML.safe_load(text, permitted_classes: [], permitted_symbols: [], aliases: false)へ渡します。敵のHPや装備名など、読み込んだ項目と型も確認します。

通信はHTTPで確認する

fetch("./hero.json")はfile://では制限されます。第3章のローカルサーバーで試してください。別のサイトから取得する場合は、相手側のCORSの許可も必要です。通信の例ではネットワーク切断やHTTPエラーを画面に伝える処理を加えて完成させます。

18学習レベル:実践

ターミナル版との違いを知る

Rubyの文法が同じでも、使えるOSの機能やライブラリは実行環境によって変わります。付録のコードを移すときは、「文法」と「外部とのやりとり」を分けて確認します。

ターミナルでの書き方ブラウザで確認すること
変数・分岐・配列・メソッド・クラス本編の基本例として使える。
puts / p標準では開発者ツールへ。このガイドは出力欄へ集める。
gets / io/consoleWebページの入力はDOM・イベントで受け取る。このガイドのgetsは用意した文字列を読む。
File.read / File.writeWASMへ渡した仮想ファイルの操作。利用者のMacのファイルを直接読むものではない。
require "json" / require "yaml"標準ライブラリを含む配布環境を選び、必要なものが入っているか確かめる。
Thread / Ractor通常のRubyと同じ並列処理を前提にしない。現行WASI版のThread関連APIには制限がある。
Net::HTTP / ネイティブ拡張gem通常のOS版のまま使えるとは限らない。通信はブラウザのfetch、gemはWASMへの対応を確認する。
system / シェルコマンド利用者のPCのコマンドを起動するものとして扱わない。
配布形態を確認する

ruby.wasmには標準拡張を省いたminimalと、含めたfullがあります。本編の実行欄はruby+stdlib.wasmを使います。ライブラリの有無を、Rubyの言語版だけで判断しないようにしましょう。

付録にあるスレッド、外部プロセス、バイナリファイルなどは、環境を確認してから使う発展項目です。まずはHP、条件分岐、配列、メソッドといった、外の機能に依存しない規則をRubyへまとめると移しやすくなります。

19学習レベル:基礎

Ruby文法の早見表

書き方を忘れたら、ここから戻れます。元の付録にある項目から、ブラウザの小さな作品に使いやすいものをまとめました。文法と標準クラスの詳しい説明はRuby 4.0の公式リファレンスへ進んでください。

用途書き方
変数・定数hp = 420 / MAX_HP = 420
文字列"#{name}のHP" / s.length / s.strip / s.split(",")
型変換to_s / to_i / Integer(text, exception: false)
条件if ... elsif ... else ... end / case ... when ... end
繰り返し3.times { |i| ... } / each do |x| ... end
配列[1, 2, 3] / a[0] / a << 4 / a.pop
ハッシュ{ hp: 420 } / h[:hp] / h.fetch(:hp)
集計map / select / find / sum / tally
メソッドdef hit(power:) ... end / def square(x) = x * x
ラムダ・ブロック->(x) { x * 2 } / call / yield
クラス・モジュールclass / initialize / attr_reader / include
値の型Data.define(:x, :y) / Struct.new(:x, :y)
パターンマッチcase value; in { hp: Integer => hp }; ...; end
正規表現/\A[0-9]+\z/.match?(text)
例外begin ... rescue ErrorClass => error ... end
調べるp value / value.class / value.respond_to?(:name)
乱数・範囲rand(10..20) / (1..5).each
ビット演算& / | / ^ / << / >>
時刻Time.now / Time.now.strftime("%H:%M")
Ruby→ブラウザrequire "js" / JS.global / object[:property]

20学習レベル:基礎

つまずいたときと、次の一歩

小さな成功を一つずつ確かめながら進めます。「Rubyが動く」「ボタンで値が変わる」「Canvasに描ける」を別々に確かめると、うまくいかない場所を絞れます。

困ったことまず確認すること
いつまでも読み込み中通信、CDNへの接続、開発者ツールのNetworkを確認する。初回は約31 MBの取得がある。
putsがページに出ない自分のHTMLでは開発者ツールのConsoleを確認する。ページへの表示はtextContentを使う。
名前の間違いで止まるエラーの行番号、綴り、変数を作った場所を確認する。
ボタンを押しても変わらないRubyが起動済みか、idとquerySelectorの指定が一致するかを確認する。
JavaScriptのfalseでifに入るJS::Objectを真偽値へ変換するか、JS::Trueとの比較を使う。
処理が終わらない実行欄の停止を押す。whileの条件や、状態の更新を確認する。
通信・保存だけ動かないfile://ではなくhttp://localhostで試し、通信や保存の例外を確認する。
Rubyを再実行すると値が消えるこのページは実行ごとに新しいVM。自分のアプリでは状態と保存の責任を決める。
小さなRPGへ育てる

第11章のFighterを使い、「攻撃ボタンで敵のHPを減らす」「0になったら勝利を表示する」「回復を加える」の順に作ってみましょう。規則をRuby、ステータスとボタンをHTML、戦闘の絵をCanvasへ分けると、役割が見えやすくなります。

公式資料

文法の構成は、書籍付録「Ruby 文法クイックリファレンス」を参考にしました。最終確認時の対象はRuby 4.0 / ruby.wasm 2.10.1です。

描画をもっと知りたいときCanvas編で、描く仕組みを学ぶ四角形から、線・画像・アニメーション・操作へ。Rubyから呼ぶ描画APIも共通です。