[ruby]RubyのFiberを使ってマイクロスレッドでタスクを管理する
@fum1h1ro の開発した社内スクリプト言語ではコルーチンが簡単に書ける。 この言語を使ってゲームを開発するようになって、ようやくこのコルーチンの便利さが理解できた。
RiteVMが出ることだし、RubyのFiberについて調べてみるとまさにこれ!ということがわかり、 ちょっと触ってみたのでメモしておく。
そもそもFiberがどんな時に便利なのかピンと来ない人も多いかもしれない。 オレ自身ごく最近までその便利さが理解できなかった。 だが、もはやこれがないと(ゲームの)プログラムが書けないと言っていいくらい必須なものとなりそうなのだ。
Fiber(コルーチン)が使えるのはこんなケースだ。
1回ボタンが押されると3発のミサイルが発射される仕様のゲームがあったとする。
- 1発目のミサイルはボタンを押した瞬間に発射される
- 2発目のミサイルはボタンを押した1秒後に発射される
- 3発目のミサイルはボタンを押した2秒後に発射される
- ボタンが押されてから3秒間はミサイルを発射することができない
今までならカウンタ変数を設けて、カウンタが一定値に達したことを条件としてミサイルの発射処理を実行していた。
ここでFiberを用いると上記のような処理を直接的かつシンプルに表現できる。
以下にFiberを使ったコードを具体的に示す。TaskクラスはFiberを継承したクラスでその小さな実装はあとで紹介する。
maintask = Task.new do
is_ready = true
is_over = false
Task.create { Task.sleep 5; is_over = true }
until is_over
if is_ready #and pad_pushed?(:a)
is_ready = false
puts "fire1!"
Task.create do
Task.sleep 1
puts "fire2!"
Task.sleep 1
puts "fire3!"
Task.sleep 1
is_ready = true
end
end
Task.yield
end
Task.join
Task.yield true
end
until maintask.resume
sleep 0.1
puts "."
end
タスクは親子関係を持っていて、タスクの中ではTask.createを用いていくつでも子タスクを作ることができる。 Task.yieldが呼ばれるとそこでいったん処理が中断し、 Task.sleepは与えられた秒数だけ処理を中断する。
パッドのボタンが押されたとき、ミサイルの発射処理を行うのは以下の部分だ。
is_ready = false
puts "fire1!"
Task.create do
Task.sleep 1
puts "fire2!"
Task.sleep 1
puts "fire3!"
Task.sleep 1
is_ready = true
end
簡単に説明すると、まず最初のミサイルを発射した後、新しいタスク(fiber) 作成している。 タスクの中では1秒待って次のミサイルを発射し、もう1秒待ってミサイルを発射し、もう1秒待ってミサイル発射準備を整えているわけだ。is_readyがtrueになるまではここに来ないので次のミサイルを発射することはできない。
ここで作られたタスクは裏で動くことになるので、Task.createメソッド自体はすぐに処理が終了して帰ってくる。
is_overがtrueになるとループが終了するが、 メインタスクが起動してから5秒後にis_overをtrueにするという処理も冒頭でタスク化している。
ちなみに、ミサイルの発射部分のタスクは次のようにそれぞれ別のタスクとして書くこともできる。
Task.create { Task.sleep 1; puts "fire2!" }
Task.create { Task.sleep 2; puts "fire3!" }
Task.create { Task.sleep 3; is_ready = true }
このすれば、最初にパッドが押されたタイミングを基準として発射時間を管理することができる。
maintaskの最後に次の2行がある。
Task.join Task.yield true
joinはタスクの終了を待つメソッドだ。引数を省略すると子スレッドすべての終了を待つ。 最後でTask.yieldにtrueを渡しているのはmaintask.resumeの戻り値をプログラムの終了条件としているためだ。
Fiberを使って処理を書いていくといろんなものがタスク化出来ることに気づく。 きっと発射されたミサイルもそれぞれが独立したタスクとして管理されることだろう。
このように少なくともゲームにおけるキャラクター操作、そしてエフェクト表示やサウンド再生のタイミングなどは、Fiberを用いることでとてもシンプルに管理できるようになるのだ。
RiteVMがでればゲームにRubyを組み込むことはこれまでとは比較にならないほど容易になるだろう。 Rubyで本格的なゲームを作れる日は近いはず。
最後に上記コードを走らせるのに必要なTaskクラスの実装を載せておく。 joinとjoinに関連した部分で長くなっているが、それでも50行ほどの小さなコードだ。
class Task < Fiber
@@current_task = nil
attr_accessor :tasks, :status
def initialize(*args)
super
@tasks = []
@status = :run
end
def alive?
@status
end
def resume(*args)
@@current_task = self
r = super(self)
@@current_task = nil
dead_tasks = []
@tasks.each do |t|
begin
t.resume(t)
rescue FiberError
t.status = nil
dead_tasks << t
end
end
@tasks -= dead_tasks
r
end
def create_task(*args, &block)
@tasks << Task.new(*args, &block)
end
def join(*args)
args = tasks if args.empty?
Task.yield while args.flatten.any?(&:alive?)
end
def self.create(*args, &block)
@@current_task.create_task(*args, &block)
end
def self.join(*args)
@@current_task.join(*args)
end
def self.sleep(sec)
t = Time.now
self.yield while Time.now - t < sec
end
end
ツッコミは @dan5ya まで。