DXOpal はブラウザで実行できるため、SDL などのネイティブなライブラリが不要で、他の人に見てもらいやすいところが魅力的です。
しかし、実際開発してみるとブラウザで開いてから動き出すまでにそこそこ時間がかかったり、 デバッグまわりの勝手に慣れていなかったりでトライ&エラーの効率がなかなか上がりません。 なんとかできないかなと。
DXOpal は DXRuby に合わせて作られてますから、 開発中は DXRuby で作って、公開するときに DXOpal で動くように調整するというのはどうでしょうか(最終的には DXOpal で動かすのが目的です)。
……と一瞬考えましたが、ふだん使っているのが Ubuntu なので、DXRuby は使えない…… Wine で動かす試み ( DXRuby を Wine から使う - Qiita ) や dxruby-sdl などの互換ライブラリ ( DXRuby API互換のゲームライブラリを作るための頻出メソッド一覧 - Qiita )もあるようなのですが、いろいろあって今回は見送りました。
自分が今使いたい機能はといえば、 基本的な図形描画の機能(線、矩形、円 + 塗りつぶし)と、 マウス関連の機能(左ボタンの push イベントと x, y 座標取得)と、 後は効果音を出すくらいです。 アクションゲームやシューティングゲームを作っているわけではありませんから タイミングに関してシビアな要件はありません。
そこで思いついたのが、他のライブラリをバックエンドとして使いつつ API だけ DXOpal に合わせてお茶を濁す方法です。 やってみたら意外とすんなり動き、もうこれでいいや、となりました。
Ruby 2D に興味があったので最初は Ruby 2D を試しに使って書いてみて、 それでも十分動いたのでしばらく使っていました。 ただ、Ruby 2D は割と高水準よりの API になっていて ミスマッチな感じがしたのと、処理がちょっとだけもたつく感じだったので(これは Ruby 2D そのものの問題ではなく使い方が悪いせいです)、 Ruby/SDL で書き直しました。
できたものが下記の dxopal_sdl.rb
です。
上述のように、自分が今必要とする要件がゆるく、
開発中だけそれっぽく動いてくれればそれでいいという代物なので、
かなり適当です(p_
とか特に……)。
上記のるびまの記事では FPS の管理に fpstimer.rb
を使っていますが、そんなに高精度なものは必要なかったのでそこも適当。
※ この時点でのスナップショットということでベタッと貼りましたが、この後いくつか修正が入りました。 GitHub のリポジトリにあるものが最新版です。
(追記 2024-01-04)
事前コンパイル方式への変更に伴い kairo-gokko では dxopal_sdl.rb
は使わなくなりました。一応 old/ ディレクトリ に移動して残しています。
require "sdl" RUBY_ENGINE = "opal" def require_remote(path) path.sub!(/\.rb/, "") require "./#{path}" end def p_(*args) DXOpal.p_(*args) end module DXOpal C_BLACK = [255, 0, 0, 0] C_WHITE = [255, 255, 255, 255] M_LBUTTON = :m_lbutton @@p_count = 0 def self.p_(*args) args.each { |arg| p arg } if @@p_count < 10 @@p_count += 1 end module Window @@width = 640 @@height = 480 @@last_update = Time.now class << self def width=(w) @@width = w end def height=(h) @@height = h end def bgcolor=(color) @@bgcolor = color end def to_rgb_a(color) if color.size == 4 [color[1..3], color[0]] else [color, 255] end end def load_resources SDL.init(SDL::INIT_EVERYTHING) SDL::Mixer.open @@screen = SDL.set_video_mode( @@width, @@height, 16, SDL::SWSURFACE ) yield end def handle_event(event) case event when SDL::Event::Quit exit when SDL::Event::MouseMotion Input.mouse_x = event.x Input.mouse_y = event.y when SDL::Event::MouseButtonDown if event.button == SDL::Mouse::BUTTON_LEFT DXOpal::Input.mouse_pushed_map_set(M_LBUTTON, true) end when SDL::Event::MouseButtonUp if event.button == SDL::Mouse::BUTTON_LEFT DXOpal::Input.mouse_pushed_map_set(M_LBUTTON, false) end end end def fill_bg draw_box_fill(0, 0, @@width, @@height, @@bgcolor) end def loop fps = 10 interval_sec = 1 / fps.to_f while true while event = SDL::Event.poll handle_event(event) end if @@last_update + interval_sec < Time.now @@last_update = Time.now fill_bg yield @@screen.update_rect(0, 0, 0, 0) end end end def draw_line(x1, y1, x2, y2, color, z=0) rgb, alpha = to_rgb_a(color) antialias = false @@screen.draw_line(x1, y1, x2, y2, rgb, antialias, alpha) end def sdl_draw_box(x1, y1, x2, y2, color, fill) rgb, alpha = to_rgb_a(color) w = x2 - x1 h = y2 - y1 @@screen.draw_rect(x1, y1, w, h, rgb, fill, alpha) end def draw_box(x1, y1, x2, y2, color, z=0) sdl_draw_box(x1, y1, x2, y2, color, false) end def draw_box_fill(x1, y1, x2, y2, color, z=0) sdl_draw_box(x1, y1, x2, y2, color, true) end def sdl_draw_circle(x, y, r, color, fill) rgb, alpha = to_rgb_a(color) antialias = false @@screen.draw_circle(x, y, r, rgb, fill, antialias, alpha) end def draw_circle(x, y, r, color, z=0) sdl_draw_circle(x, y, r, color, false) end def draw_circle_fill(x, y, r, color, z=0) sdl_draw_circle(x, y, r, color, true) end end end class Sound @@map = {} class << self def register(name, *args, &block) path, _ = args @@map[name] = { path: path, sound: nil } end def [](name) sound = @@map[name][:sound] unless sound path = @@map[name][:path] wave = SDL::Mixer::Wave.load(path) sound = Sound.new(wave) @@map[name][:sound] = sound end sound end end def initialize(wave) @wave = wave end def play SDL::Mixer.play_channel(0, @wave, 0) end end module Input @@mouse_pushed_map = {} @@mouse_pushed_map[M_LBUTTON] = false @@mouse_x = 0 @@mouse_y = 0 class << self def mouse_pushed_map_set(code, val) @@mouse_pushed_map[code] = val end def mouse_x=(val) @@mouse_x = val end def mouse_y=(val) @@mouse_y = val end def mouse_x @@mouse_x end def mouse_y @@mouse_y end def mouse_push?(mouse_code) pushed = @@mouse_pushed_map[mouse_code] @@mouse_pushed_map[mouse_code] = nil pushed end end end end
この dxopal_sdl.rb
を使って開発を進め、
ある程度できたらブラウザ+DXOpal で動かしたいので、
次のように Kernel::Native
の存在でブラウザかどうか判別して切り替えるようにしてみました。
def browser? Kernel.const_defined?(:Native) end if browser? require "dxopal" else require "./dxopal_sdl" end
追記 2023-08-06
事前コンパイルする方法もおすすめです。