DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

【RubyKaigi 2023】スポンサーに当選したので弾幕シューティングゲームを作ってみた

こんにちは。DIGGLEエンジニアリングマネージャーのzakkyです。発売以来スプラトゥーン3のフェスに全敗しているので「zakkyと逆張りすれば勝てる説」が立証されつつあります。

はじめに

早速本題です。

弊社では、今年RubyKaigi 2023Platinum Sponsorsに初めて応募しまして、そして無事に当選することができました。

弊社以外のRubyKaigi 2023スポンサーには、本当に有名な企業が並んでおりまして、そんな有名企業に挟まれる初参加企業ということで、「何かかまさなきゃ! 何か目をひくものを作らなくちゃ!」という事で、弾幕シューティングゲームを作りましたのでご紹介させていただきます。

ちなみに、ruby.wasmに関する調査~実装完了までを週末の空き時間(10hくらい)で済ませましたので、結構ハードルは低いのではないかと思います。

なんで弾幕シューティング?

RubyKaigiへの出展ということで、「Rubyに関する出し物を作ろう!」ということで、色々とメンバーと考えていたのですが、その中で、Ruby 3.2からWASIベースのWebAssemblyサポート(以降ruby.wasm)が行われたので、ruby.wasmを使って何か作れないか。という話が出ました。

作成物の候補としては・・・

  • ゲーム
    • シンプルに目をひきそう
  • アニメーション動画
    • CMっぽいものをアニメーションとして作る

上記が挙がったのですが、「実際触れるものが良いだろう」ということでゲームを作ることになりました。

・・・で、弾幕シューティングに決めた理由ですが、「ノリで実装してたらいつのまにか完成してた ruby.wasmで実装した場合、実際どれくらいのパフォーマンスで動くのかを視覚的に見やすく・面白く表現する方法として考えました」となります。

ruby.wasmってなに?

「そもそもruby.wasmって何?」という方に向けた説明なのですが、一言で言うと、「ブラウザ上でRubyを動かす仕組み」となります。

今までJavaScriptで書いていた部分をRubyを使って書けるようになったので、Railsでerbやhamlを使って開発しつつ、インタラクションなどのDOM操作周りをruby.wasmで実装すれば、「JavaScriptを一切使わずに一気通貫な実装が可能になった」とも言えます。

どうやって実装するの?

ruby.wasmで検索すると色々と出てきますので、詳しくはそちらを参考にしていただくとして、ruby-head-wasm-wasiを読込むだけでOKです。

 <script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/browser.script.iife.js"></script>

rbファイルとして切り出し

そこそこ大きいコードを書こうとすると、ファイルの分割が必要になってきますが、JavaScriptファイルの読込みのように、scriptタグで書いてあげればOKです。(type="text/ruby"という書き方、新鮮ですね!

<script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/browser.script.iife.js"></script>
<script type="text/ruby" src="ruby/game/point.rb"></script>
<script type="text/ruby" src="ruby/game/box.rb"></script>

rbファイルの読込みは上記で完了しているので当たり前ではありますが、ファイル間(=クラス間)のアクセスに関して、requireなどの記述は不要となります。

※ただし、コードの読込むrbファイルの順番には気を付ける必要があります。

class Box < Point # requireをすることなくBoxクラスからPointクラスを参照できる
  class Area
    attr_reader :top, :right, :bottom, :left
    def initialize(x, y, size_x, size_y)
      @left = x
      @top = y
      @right = x + size_x
      @bottom = y + size_y
    end

    # NOTE: シンプルに現在位置同士で衝突を判定し、移動経路上での衝突は見ない
    def hit?(area)
      left <= area.right && right >= area.left && top <= area.bottom && bottom >= area.top
    end
  end

  attr_writer :size
  attr_reader :size
  def initialize(x, y, size_x, size_y)
    super(x, y)
    @size = Point.new(size_x, size_y)
  end

  (省略)
end

初期セットアップ周りの覚書

ローカルで検証をする際に何も考えずにrbファイルを切り出すとCORSエラーになる

ruby.wasmに限らず遭遇する問題ですが、ローカルファイルをそのまま開くとrbファイルがCORSエラーとなって読込めない問題が発生しました。

何かしらの方法で http://localhost/でアクセスできるようにさえしてあげれば良いので、今回はnpmのreloadを使ってローカルでの動作検証を行いました。(他にもgemのhttpdやnpmのhttp-serverなども試してみましたが、どこかでキャッシュするらしく、うまく動かないので不採用にしました)

修正したrbファイルが更新されない

上記のreloadを使っても、まだキャッシュされてしまうようで、rbファイルを修正&ブラウザをリロードしても上手く更新されない問題がありました。 結論としては、reloadを使用したうえで、更にmetaタグの指定が必要でした。

  <head>
    <title>DIGGLE rubykaigi 2023</title>
    <link rel="icon" href="favicon.ico" />
    <meta charset="utf-8" />
    <meta http-equiv="Pragma" content="no-cache" /> <!-- ここの記述が必要 -->
    <meta http-equiv="Cache-Control" content="no-cache" /> <!-- ここの記述が必要 -->
  </head>

実装まわりの覚書

JavaScriptに絡むパラメータや関数の呼び出し方法

Rubyには、他言語と違いメソッド呼び出しの末尾の()が不要となる仕様があります。そのため、パラメータを参照したい場合、[:parameter]として呼び出す必要があります。

    @canvas = JS.global[:document].querySelector('#canvas')
    @context = canvas.getContext("2d")

    body = JS.global[:document].querySelector('body')
    @display_size = Point.new(body[:clientWidth].to_i, body[:clientHeight].to_i)

上記ですと、documentはパラメータなので、[:document]として取得し、querySelectorは関数なので、JS.global[:document].querySelector('#canvas')という記述になります。ここは、普段JavaScriptで書くお作法とは結構違うので最初は戸惑いました。

型変換が面倒

JavaScriptから取得したオブジェクトは、そのまま文字列や数字として使用することができません。

例えば、下記のようなコードを書くとエラーが発生します。

button[:textContent] = button[:textContent] + "_suffix"

型変換をしない場合にはエラーが発生

そのため、下記のようにto_sやto_iなどで変換する必要があります。

button[:textContent] = button[:textContent].to_s + "_suffix"

同じく、JavaScript側へ想定外の型で渡そうとするとエラーになります。

button[:width] = 100.0

想定外の型でJavaScriptへ渡すとエラー

このエラーに関してはエラー内容からは原因が類推できないため、かなり悩まされました。今回のゲームの特性上、floatで弾の移動ベクトルを持たせていたのですが、どこか1か所でもto_iを書き漏れていると↑のエラーが発生してしまい、泣きそうになりました。

また、nullやundefinedがnilとは違う扱いになっており、ruby.wasmから別途定義&提供されているJS::NullやJS::Undefinedと比較する必要がある点も注意が必要です。*1*2

if tbody[:firstChild]) == JS::Null
  puts 'null'
end

ActiveSupportが使えない

当たり前ではありますが、ActiveSupportは使えません。普段Railsを使っていてActiveSupportのことを意識せず恩恵を受けている私は「delegate_missing_toってActiveSupportに入ってたのか~」などなど、改めてActiveSupportのありがたさを嚙み締めながら実装することになりました。

実際のゲーム画面

ということで、スプラトゥーンで負けが込んで気分転換をしているうちにこんな感じのゲームになりました。実際に触りたい方は是非ブースまでお越しください!

実際のゲーム画面

まとめ

ruby.wasmを使ってみましたが、結構簡単に実装できるので、Railsを使ってお手軽に小さくサービスを作る際には重宝するかもしれません。ただ、性能はそこまで良くないので、注意が必要となります。(画面左上にFPSを出しているのですが、画面に表示している弾数が多くなるとFPSがガクッと下がります)

宣伝

最後に宣伝なのですが、RubyKaigi 2023ではスポンサーブースを出しておりますので、ご来場予定の方がいらっしゃいましたら、是非お立ち寄りください。(もちろん私も参加します)

ブースでは、今回ご紹介する弾幕シューティングゲームのURL配布や、撃破タイムのランキング掲示などを行おうと思っておりますので、RubyKaigi 2023に参加する予定の方は、一度お立ち寄りいただけますと幸いです。

追記

ソースコードの公開はRubyKaigi終了後を予定しておりましたが、想像以上に希望される方がいらっしゃいましたので、当初予定よりかなり早いですが公開します。

github.com

We're hiring!

こんな私と一緒に開発してくれるメンバーを募集しています。少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

herp.careers