Ads by Google

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

Lua によるメッセージプロシージャとタスクマネージャの実装

Flash (の開発者の方々) が羨ましい。全世界で広く使われていて、用途も娯楽から実用まで様々 (「遊びに使えるだけじゃない」っていうのがいい)。Flash 開発陣の仲間に入れさせてください。憧

今日はそれとは関係なくて、Lua のコルーチンやらを使って汎用的なタスクマネージャとかを作るお話。長くなるので詳細は↓へ。

ここ数日間は Action Script 3.0 のリファレンスを見つつ Lua スクリプトを書くという、ちょっとよく分からない生活をしていまして。

それで出来たのがタイトルにもあるメッセージプロシージャ (a.k.a. イベントハンドラ) とタスクマネージャの2つ。どちらも50行足らずの実装だけど、それなりに使えそうなのでスクリプトと解説を書いてみようと思います。

まずは簡単なメッセージプロシージャの方から。
sys = {}

function sys.proc()
  local inst = {}

  -- fields
  inst.user = {} -- registered user procedures into here


  -- methods
  -- register message procedure
  --   msg : any   -- message id
  --   func: function -- msg procedure

  function inst:reg(msg, func)
    self.user[msg] = func
  end


  -- execute message procedure
  --   msg : any -- message id
  --   param: any -- msg parameter(s)

  function inst:exe(msg, param)
    return self.user[msg](param)
  end


  return inst
end

sys.proc 関数では、ユーザーが自由にプロシージャを定義できるフィールド (inst.user) を確保しておいて、それを操作するメソッドも一緒に格納して呼び出し元に返すというもの。

利用するときはこんな感じ。
-- generate new procedure
proc = sys.proc()

-- register "add" message procedure
proc:reg("add",
function(param)
  return param.a + param.b
end)

-- execute "add" message procedure
result = proc:exe("add", { a = 1, b = 2 })

-- show result
io.write("result: " .. tostring(result) .. "\n")

実行結果:
  3

↑使うことのメリットが見えてこないサンプルの例

元々は C/C++ とのインターフェイスを共通化する目的で作ったものなので、Lua 単体で利点を簡単に説明するのはちと難しいかも。

タスクマネージャの実装は↓
sys = {}

function sys.task()
  local inst = {}

  -- fields
  inst.user = {} -- registered user tasks into here

  -- methods
  -- register task
  --   name : string  -- task name
  --   func : function -- task function
  --   param: any   -- func parameter(s)

  function inst:reg(name, func, param)
    table.insert(self.user, { name = name,
                 func = coroutine.create(func),
                 param = param })
  end


  -- execute tasks
  function inst:exe()
    local index = 1

    while index <= table.getn(self.user) do
      local value = self.user[index]

      local alive, result = coroutine.resume(value.func, value.param)

      if alive == false then
        table.remove(self.user, index) -- kill task
        index = index - 1
-- --[[
      else
        io.write("[" .. value.name .. "] " .. tostring(result) .. "\n")
-- --]]
      end

      index = index + 1
    end
  end


  return inst
end

こんな感じ。reg で登録するのは sys.proc と同じだけど、今度の exe では登録されたタスク群が並列実行される点に注意。

↓例えば2つのタスクを走らせてみるとこんな感じ
task = sys.task()


-- simple task function
function tfunc(param)
  for i = param.min, param.max - 1 do
    coroutine.yield(i)
  end

  return "dead"
end


-- register two tasks with different parameters
task:reg("limits 2", tfunc, { min = 1, max = 2 })
task:reg("limits 6", tfunc, { min = 1, max = 6 })


-- test loop
for i = 1, 8 do
  io.write("\n* " .. tostring(i) .. " *\n")

  task:exe()
end

実行結果:
  * 1 *
  [limits 2] 1
  [limits 6] 1

  * 2 *
  [limits 2] dead ← タスクが死んだ
  [limits 6] 2

  * 3 *
  [limits 6] 3 ← タスクテーブルから削除されている

  * 4 *
  [limits 6] 4

  * 5 *
  [limits 6] 5

  * 6 *
  [limits 6] dead

  * 7 * ← そして誰もいなくなった

  * 8 *

実行結果が切ないけどコルーチン便利すぎ。

例えば音を 5.0 秒後にフェードアウトさせるような処理も sound:fadeout(5.0) といったメソッドを一度呼ぶだけで、後のボリューム調整はタスクに任せておけば OK みたいな事が手軽に出来ちゃう。

指定したポイントまでスプライトを連続移動させたりとか、proc と task の合わせ技でボタン (GUI) とかも作れるよね。

ちなみに、sys.proc も sys.task も複数のプロシージャやタスクを生成して使えるようになっておりますゆえ、
  taskA = sys.task()
  taskB = sys.task()

で別々にタスク (プロシージャ) を登録/実行することも可能でゴザ。

なんかもう Lua 主体のエンジン設計にしたくなってきたですよ。

C/C++ 側で読込/描画/計算etc. の重い処理をして、Lua 側ではそれらのハンドルIDを受け取って、エンジンに処理のオーダーを出すだけにすれば大丈夫かな。こういうのってどこまで細かく制御できるようにするか悩むよね。
sys.task.exe のタスク実行ループを見て「イテレータ使って for で回せばもっとスッキリするのになぁ」と思った人へ。

最初はその方針でいこうと思っていたんだけど、色々と問題が浮き彫りになってきたため今の形に落ち着きましたとさ。

その"問題"っていうのは3つあって、

「user テーブルからタスクを削除したら要素が切り詰められるので、その分だけ実行位置を巻き戻さなければならない」

と、

「table.remove は第二引数にインデックス値を必要とするので、どちらにしろ先頭からのオフセットを取得する必要がある」

と、

「Lua における for 文の3つの制御式は、ループが始まる前に一度だけしか評価されない (*) ので、table.remove によってテーブルの要素が減ったときにアクセス違反を起こす」
* リファレンスマニュアルの「2.4.5 For Statement」にもその記述がある

という理由で table.getn と while を使った実行ループになっております。
あーもうひとつ書き忘れた!

「sys.proc は user のキーに文字列を使うことを許可しているけど、sys.task の user に使われるキーはなんで table.insert が割り当てる整数値だけなの?」と思った人がいるかもしれないので。

table.remove は第二引数に pos という整数値を渡しますが、実はコレ…文字列をキーとするテーブルには効果が無い (!) のですね。

最初はタスク名をキーにして実行しようとしてたんだけど (そうするとタスクの上書きが簡単に出来る)、何故か要素が削除されないという事態に陥りまして。

* 先頭からのオフセットを実行時計算して table.remove に渡してみたけど駄目だった

まあタスクの上書きとかは今の構造でも実装コストは低いので大丈夫かな。
むしろ「知らない間にタスクが上書きされてた」とかのほうが怖いので、こっちのほうがいいと思いやす。
すご〜。
なるへそ、こういう風にLuaを使えば、C/C++で書くと「うわ〜ん」ってなりがちなタスク処理とかも楽しくかけますね。
LuaJITとか使えば速度的にも問題ないだろうし。
toge さんコメントどうもです!お久しぶりですね。

ジャンプテーブルを使ったエセ並列処理は昔やったことがあるのですが、こういったちょっと本格的なものは初体験なので自分でも感動しました。

現段階の構想としては、(本文の最後にも書いてありますが) ローカルなクライアントサーバシステムで行きたいと思ってます。

エンジンをサーバと見立て、クライアントであるスクリプトからの要求 (スプライトの描画、音の再生, etc...) を処理していく雰囲気ですな。

その方針でいくと、もしかしたら Mist Engine の Lua フロントエンドを公開、なんてこともできるようになるかもしれませんね。

他には、システム側にレンダリング用、ミキシング用、ネットワーク用etc. のように種類別に分けられた予約済みタスクを用意して、一括してそこからエンジンにオーダーを送るようにすればプロファイラに統一したインターフェイスを提供できるかもなぁ、とか考えています。

ユーザが記述するスクリプトには個別のプロシージャ、タスクを用意させて、そこからスクリプト単位でプロファイリングしたりも楽しそうですね。

ってコレ、次に書こうとしてたネタだった Σ
(もしかしたら整理してから次のエントリに新しく書くかもです)
このコメントは管理者の承認待ちです
このコメントは管理者の承認待ちです
このコメントは管理者の承認待ちです
このコメントは管理者の承認待ちです
このコメントは管理者の承認待ちです
このコメントは管理者の承認待ちです
このコメントは管理者の承認待ちです
このコメントは管理者の承認待ちです
このコメントは管理者の承認待ちです
このコメントは管理者の承認待ちです
このコメントは管理者の承認待ちです
このコメントは管理者の承認待ちです













管理人にのみ表示を許可する


ホーム


最近は Reason で作曲をしたり、
noughts で絵を描いたりもする。

現在 17 歳。普通科高校 3 年生。
C/C++, Lua, Squirrel, GLSL
を喋るヒトっぽい。

Categories

Archives

Comments