Ruby/SDL2 ことはじめ
はじめに
Ruby/SDL は更新されないものと諦めていたら Ruby/SDL2 が公開されていたので使ってみる。
セットアップ
とりあえず ~/src/ruby-sdl2-playground
に作る。
#!/bin/sh
cd ~/src
rm -fr ruby-sdl2-playground
mkdir -p ruby-sdl2-playground
cd ruby-sdl2-playground
bundle init
bundle add ruby-sdl2 --require sdl2 --optimistic
bundle exec ruby -r sdl2 -e "p SDL2"
cat <<'EOF' > main.rb
require "bundler/setup"
Bundler.require(:default)
SDL2.init(SDL2::INIT_EVERYTHING)
pos = SDL2::Window::POS_CENTERED
window = SDL2::Window.create("(title)", pos, pos, 640, 480, 0)
flags = 0
flags |= SDL2::Renderer::Flags::PRESENTVSYNC
renderer = window.create_renderer(-1, flags)
120.times do
SDL2::Event.poll
renderer.present
end
EOF
bundle exec ruby main.rb
-
SDL2::Renderer::Flags::PRESENTVSYNC
を指定すると垂直同期する
上のシェルスクリプトを流せば次のように Window を2秒間表示する。
本当に2秒?
Window は SDL2::Window.create
のタイミングで起動しているわけじゃないみたい。
まず SDL2::Event.poll
を呼んで溜まったイベントを捨て続けないといくら待っても起動しない。
で、次のイベントを順に受け流し、
SHOWN
EXPOSED
FOCUS_GAINED
FOCUS_GAINED
のタイミングでやっと表示される。
環境によって違うのかもしれないけど、私のところでは7ループ目に表示できた。
なので Window を2秒間表示するというなら次のように FOCUS_GAINED
を待ってからにしないといけない。
catch :break do
loop do
while ev = SDL2::Event.poll
case ev
when SDL2::Event::Window
case ev.event
when SDL2::Event::Window::FOCUS_GAINED
throw :break
end
end
end
end
end
sleep(2)
何か動かしてみる
require "bundler/setup"
Bundler.require(:default)
SDL2.init(SDL2::INIT_EVERYTHING)
pos = SDL2::Window::POS_CENTERED
window = SDL2::Window.create("(title)", pos, pos, 640, 480, 0)
flags = 0
flags |= SDL2::Renderer::Flags::ACCELERATED
flags |= SDL2::Renderer::Flags::PRESENTVSYNC
renderer = window.create_renderer(-1, flags)
include Math
frame_counter = 0
loop do
while ev = SDL2::Event.poll
case ev
when SDL2::Event::Quit
exit
when SDL2::Event::KeyDown
case ev.scancode
when SDL2::Key::Scan::ESCAPE
exit
when SDL2::Key::Scan::Q
exit
end
end
end
renderer.draw_blend_mode = SDL2::BlendMode::BLEND
renderer.draw_color = [0, 0, 64, 28]
renderer.fill_rect(SDL2::Rect.new(0, 0, *window.size))
renderer.draw_blend_mode = SDL2::BlendMode::NONE
renderer.draw_color = [255, 255, 255]
r = 64
w, h = window.size
x = w / 2 + cos(PI * frame_counter * 0.02 * 0.7) * w * 0.4
y = h / 2 + sin(PI * frame_counter * 0.02 * 0.8) * h * 0.4
renderer.fill_rect(SDL2::Rect.new(x - r, y - r, r * 2, r * 2))
renderer.present
frame_counter += 1
end
-
SDL2::Renderer::Flags::ACCELERATED
はなんとなく強そうなので入れてみた
GIFの仕様でカクカクしてるけど実際はぬるぬる
ミニフレームワーク化
リポジトリに含まれるサンプルは上のようにベタ書きが多い。
サンプルとしてはベタ書きの方が一直線に処理が追えてわかりやすいけど、そのままコードを書き足していくと何がなんだかわからなくなる。
それに初期化処理やメインループを特定のキーで抜ける処理を毎回書きたくない。
なのでテンプレートメソッドパターンで必要なところだけ書くようにする。
require "bundler/setup"
Bundler.require(:default)
class Base
include Math
class << self
def run(*args)
new(*args).run
end
end
def run
setup
loop do
event_loop
update
before_view
view
after_view
end
end
private
def setup
SDL2.init(SDL2::INIT_EVERYTHING)
end
def update
end
def view
end
def before_view
end
def after_view
end
def event_loop
while ev = SDL2::Event.poll
event_handle(ev)
end
end
def event_handle(ev)
case ev
when SDL2::Event::Quit
exit
when SDL2::Event::KeyDown
case ev.scancode
when SDL2::Key::Scan::ESCAPE
exit
when SDL2::Key::Scan::Q
exit
end
end
end
end
# Base.run
──として呼び出し順序と最低限必要な処理だけ書いとく。
抽象クラスだけど Window が出ないアプリとして実行できる。
Window の追加
module WindowMethods
attr_accessor :window
attr_accessor :renderer
attr_accessor :frame_counter
def setup
super
flags = 0
# flags |= SDL2::Window::Flags::FULLSCREEN
# flags |= SDL2::Window::Flags::FULLSCREEN_DESKTOP
pos = SDL2::Window::POS_CENTERED
@window = SDL2::Window.create("(Title)", pos, pos, 640, 480, flags)
flags = 0
flags |= SDL2::Renderer::Flags::ACCELERATED
flags |= SDL2::Renderer::Flags::PRESENTVSYNC
@renderer = @window.create_renderer(-1, flags)
@frame_counter = 0
end
def before_view
super
renderer.draw_blend_mode = SDL2::BlendMode::BLEND
renderer.draw_color = [0, 0, 64, 28]
renderer.fill_rect(SDL2::Rect.new(0, 0, *@window.size))
renderer.draw_blend_mode = SDL2::BlendMode::NONE
renderer.draw_color = [255, 255, 255]
end
def after_view
super
@frame_counter += 1
renderer.present
end
end
Base.prepend WindowMethods
# Base.run
実行して Window が出ればOK
マジックナンバーが目立つけどほっとく。
抽象化したり定数化したくなってくるけど逆にやらない我慢が大切で、やるメリットがめちゃくちゃある場合にやっとやるかやらないかぐらいでよい。
完成
class App < Base
def view
super
r = 64
w, h = window.size
x = w / 2 + cos(PI * frame_counter * 0.02 * 0.7) * w * 0.4
y = h / 2 + sin(PI * frame_counter * 0.02 * 0.8) * h * 0.4
renderer.fill_rect(SDL2::Rect.new(x - r, y - r, r * 2, r * 2))
end
run
end
ミニフレームワークでコードがとてもシンプルになった。
ここだけ書きたかったんですわ。
ミニフレームワークの拡張
秒間フレーム数を知りたい
module FpsMethods
attr_reader :fps
def setup
super
@fps = 60
@fps_counter = 0
@old_ticks = SDL2.get_ticks
end
def update
super
@fps_counter += 1
v = SDL2.get_ticks
t = v - @old_ticks
if t >= 1000
@fps = @fps_counter
@old_ticks = v
@fps_counter = 0
p fps
end
end
end
Base.prepend FpsMethods
# Base.run
クラス化して処理を分けたことで上のように元のコードを変更せずに機能を追加できた。
なので例えばジョイスティックの状態を読み取って扱いやすい形にしておくなどもこのようにモジュール化して追加すればよい。
ぱっと見、ミニフレームワーク内によくわからないインスタンス変数がばらまかれたのがいまいちなのでクラス化した方がよい気がするけどそれはいったん置いとく。
テキストを表示したい
上で求めた秒間フレーム数を画面上に表示させる。
require "pathname"
module FontMethods
def setup
super
font_file = "~/Library/Fonts/Ricty-Regular.ttf"
font_size = 32
SDL2::TTF.init
@font = SDL2::TTF.open(Pathname(font_file).expand_path.to_s, font_size)
@font.kerning = true
end
def after_view
system_line "#{frame_counter} #{fps}fps"
super
end
def system_line(text)
rect = SDL2::Rect.new(0, 0, *@font.size_text(text))
renderer.draw_blend_mode = SDL2::BlendMode::NONE
renderer.draw_color = [0, 0, 128]
renderer.fill_rect(rect)
font_color = [255, 255, 255]
texture = renderer.create_texture_from(@font.render_blended(text, font_color))
renderer.copy(texture, nil, rect)
end
end
Base.prepend FontMethods
# Base.run
テキスト表示するだけでこんなに書かんといけんのかって感じだけど使いやすいメソッドを作っておけばよさそう。
これで左上にカウンタと秒間フレーム数を表示できた。
super だらけなのが気になる
メソッドを Rails の before_action のようなブロックに置き換えると良いかもしれない。
require "active_support/callbacks"
require "active_support/core_ext/object/blank"
class Foo
include ActiveSupport::Callbacks
define_callbacks :view
def self.view(*args, &block)
set_callback(:view, *args, &block)
end
def run
run_callbacks :view
end
view do
p 1
end
end
class Bar < Foo
view do
p 2
end
view do
p 3
end
end
Bar.new.run
# >> 1
# >> 2
# >> 3
view の定義が追加になっているのがわかる。
ただし super を呼ばずにオーバーライドする選択の余地がなくなるので一長一短ある。
なのでミニフレームワークへの適用はいったん置いとく。
ベクトルライブラリを入れる
x y を個別に計算するのが面倒なのでベクトルライブラリを入れる。
とりあえず gem search vector
で出てきた vector2d を使ってみる。
bundle add vector2d
こんな感じで使えるようだ。
vec = Vector2d(3, 4) # => Vector2d(3,4)
vec.x # => 3
vec.y # => 4
vec.length # => 5.0
vec * 2 # => Vector2d(6,8)
vec * Vector2d(2, 2) # => Vector2d(6,8)
vec + Vector2d(1, 1) # => Vector2d(4,5)
vec.normalize.length # => 1.0
vec.to_a # => [3, 4]
が、ここで罠があった。
このライブラリめちゃくちゃ重い!
こんな数学系のライブラリなんかとくに速度を意識して実装してくれてると思うじゃん。
なんならネイティブ実装してくれててもおかしくないと思うじゃん。
だから、このあとで作るデモが 15 FPS になった原因がまさか Vector2d にあるとは信じられんかった。
Ruby の標準ライブラリだったけど、いつのまにか外部 gem になっていた matrix (に含まれるVectorライブラリ) と速度を比較してみると──
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "matrix"
gem "vector2d"
gem "benchmark-ips"
end
Benchmark.ips do |x|
x.report("Vector") { Vector[2, 3] + Vector[4, 5] }
x.report("Vector2d") { Vector2d(2, 3) + Vector2d(4, 5) }
x.compare!
end
# Warming up --------------------------------------
# Vector 49.777k i/100ms
# Vector2d 1.702k i/100ms
# Calculating -------------------------------------
# Vector 493.319k (± 1.8%) i/s - 2.489M in 5.046831s
# Vector2d 23.293k (±11.6%) i/s - 115.736k in 5.061193s
#
# Comparison:
# Vector: 493319.3 i/s
# Vector2d: 23293.3 i/s - 21.18x (± 0.00) slower
なんと 21.18 倍の遅さ。
何をやったらこんなに遅くなるのかわからないけどクリエイティブコーディングに向いてないことだけはわかる。
じゃあなんで matrix を最初から使わなかったかというと使いづらいところがあるから。
でもいいのがないので matrix のベクトルクラスを改良してみる。
さっきの vector2d は消して matrix を入れよう。
bundle remove vector2d
bundle add matrix
class Vector2d < Vector
def +(v)
if v.kind_of?(self.class)
super
else
super(self.class[v, v])
end
end
def -(v)
if v.kind_of?(self.class)
super
else
super(self.class[v, v])
end
end
def *(v)
if v.kind_of?(self.class)
map2(v) { |a, b| a * b }
else
super
end
end
def /(v)
if v.kind_of?(self.class)
map2(v) { |a, b| a / b }
else
super
end
end
end
def Vector2d(*args)
Vector2d[*args]
end
できた。上のようにオーバーライドしたので次のように書けるようになった。
Vector2d(2, 3) + 1 # => Vector[3, 4]
Vector2d(2, 3) - 1 # => Vector[1, 2]
Vector2d(2, 3) * Vector2d(2, 3) # => Vector[4, 9]
Vector2d(2, 3) / Vector2d(2, 3) # => Vector[1, 1]
これで使いやすくなったとはいえ matrix のベクトルライブラリは機能優先の多次元対応版なので、速度面でクリエイティブコーディングには向いてない。
今回は matrix 版を使うとしても今後は2D専用のシンプルなものに置き換えた方がよさそう。
仮に実装してみると──
require "matrix"
require "benchmark/ips"
class MyVector
attr_accessor :x, :y
def initialize(x, y)
@x, @y = x, y
end
def +(other)
self.class.new(@x + other.x, @y * other.y)
end
end
Benchmark.ips do |x|
x.report("Vector") { Vector[1, 2] + Vector[3, 4] }
x.report("MyVector") { MyVector.new(1, 2) + MyVector.new(3, 4) }
x.compare!
end
# Warming up --------------------------------------
# Vector 59.187k i/100ms
# MyVector 138.991k i/100ms
# Calculating -------------------------------------
# Vector 588.342k (±10.0%) i/s - 2.900M in 5.000701s
# MyVector 1.371M (± 4.3%) i/s - 6.950M in 5.078438s
#
# Comparison:
# MyVector: 1371388.1 i/s
# Vector: 588342.3 i/s - 2.33x (± 0.00) slower
2.33 倍速くなった。
要点
- vector2d は信じられないほど遅い
- matrix は vector2d より 21.18 倍速い
- 2D専用を自作すれば matrix より 2.33 倍速くなる
Tixy クローンのベースを作ってみる
Tixy (Creator: ) は短いコードでさまざまな模様の表現を試みるクールなサイトだ。
リスペクトの気持ちを持ってこれをまねてみる。
具体的には
- Time
- Index (セルの連番)
- X 座標
- Y 座標
を受けとって返した -1.0..1.0 の値に応じてセルの色と大きさが変わる仕組みになっている。
class TixyCloneApp < Base
CELL_N = 16
def setup
super
@window_rect = Vector2d(*window.size)
@cell_wh = @window_rect * 1.0 / CELL_N
@inner_top_left = @window_rect * 0.5 - @cell_wh * CELL_N * 0.5
end
def before_view
renderer.draw_color = [0, 0, 0]
renderer.clear
end
def view
super
time = SDL2.get_ticks.fdiv(1000)
index = 0
CELL_N.times do |y|
CELL_N.times do |x|
r = tixy_func(time, index, x, y)
if r.nonzero?
r = r.clamp(-1.0, 1.0)
center = @inner_top_left + @cell_wh * Vector2d(x, y) + @cell_wh * 0.5
radius = @cell_wh * 0.5 * r.abs * 0.95
top_left = center - radius
renderer.draw_color = tixy_color(r)
renderer.fill_rect(SDL2::Rect.new(*top_left, *(radius * 2)))
end
index += 1
end
end
end
def tixy_func(t, i, x, y)
sin(t - sqrt((x - 7.5)**2 + (y - 6)**2))
end
def tixy_color(v)
if v.positive?
v = 1.0
else
v = -1.0
end
c = v.abs * 255
if v.positive?
[c, c, c]
else
[c, 0, 0]
end
end
run
end
sin(t - sqrt((x - 7.5)**2 + (y - 6)**2))
-
tixy_func
関数の計算を工夫するだけでいろんな表現ができる - 16 * 16 = 256 個の四角形を描画しても60FPSでぬるぬる動いている
使ってみた所感
- Window モードでも垂直同期できるようになって嬉しい
- Ruby/SDL だとなぜかフルスクリーンのときしか垂直同期できなかった
- ruby コマンドで動くようになって嬉しい
- Ruby/SDL 時代は ruby のかわりに rsdl で起動しないと動かなかった
- 起動するだけありがたかったとはいえ、この罠にしょっちゅうはまった
- SGE がなくても描画できるようになって嬉しい
- Ruby/SDL 時代は SGE を別途入れないとろくに描画できなかった
- この SGE のインストールが激ムズ
- どこからダウンロードしてくればいいのかもわからない
- 現在も消息不明
- さらに謎のパッチを当てないといけない
- どこからダウンロードしてくればいいのかもわからない
- そのせいか Ruby/SDL 本体にこっそりバンドルされていた
- ビルドオプションをつけると一緒にインストールできた
- 使いやすくなっている気がする
- draw_color で色だけ指定できるところとか
- PRESENTVSYNC だけで垂直同期とか
- 速くなっている気がする
- Ruby/SDL で処理落ちする描画数でも60fpsを維持していた
- 日本語ドキュメントは(いまのところ)ないので
- リポジトリのサンプル を読む
- rubysdl2_init_video を読む
- SDL2のドキュメント を読む
- SDL2 でググリまくる
- ググり力重要
- 他のバインディングのドキュメントでもできることはだいたい同じ
- Nannou とは立ち位置が大きく異なる
- Nannou は
- フレームワーク
- 終了や全画面化のキーバインディング設定済み
- クリエイティブコーディングとやらをすぐに始められる
- ベクトルのライブラリがめちゃくちゃ作り込まれている
- 領域を扱うクラスも作り込まれている
- どこに何を書くかが厳密に決まっている
- ただ Rust がムズすぎる
- Ruby/SDL は
- ラッパー
- すべてユーザーまかせ
- Escape とか押しても終了しない
- メインループ自体自分で書く
- 面白半分に全画面にしたりするとバグったときPCを落とさないと復帰できなかったりする
- FPS 値をすぐに知るメソッドがない
- ベクトルライブラリがない
- 領域を扱う便利クラスがない
- Rect はあるけどほぼ x, y, w, h を保持する程度の役割り
- どこに何を書くかが決まってない
- Escape とか押しても終了しない
- それがいい
- Nannou は
参照
Author And Source
この問題について(Ruby/SDL2 ことはじめ), 我々は、より多くの情報をここで見つけました https://zenn.dev/megeton/articles/7170a5dc60c97b著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol