emitter3d: How Simulator works?
#emitter3d において、各パーティクルがどの位置にあり、どのように動いているか等は全てシミュレータ上で計算される。 Behavior DSL
パーティクルの動作パターン(ビヘイビア)をランダム生成・記述するにあたって、BulletMLのようなDSLを設けることにした。以下のような特徴を持つ: 内部的には S 式で、 ASTの構造は Number, Symbol, List だけ 以下、構文はS式なのでコードブロックが lisp にとしている
3 つの括弧 {} [] <> による糖衣構文がある
このうち {} で囲われた部分には特殊な構文が用いられ、特に改行が意味を持つ
まずは糖衣構文を含まない S 式でビヘイビアを書いてみる。
以下のコードは、Live Demoの左下の + からコードエディタに貼り付けることで動作を確認できる。 code:block-serial.lisp
(block
((speed 1.5)
(30 nop)
(30 speed 0)
(20 opacity 0)))
ここで、
(speed 1.5) で速度を設定している
命令は (op arg1 arg2 ...) の形をとる
(30 nop) (30 speed 0) のように数値を前置して、その命令に何ステップかけるかを設定している
nop は何もしない
speed は速度を変化させる
opacity は不透明度を変化させる
これらの命令列を (block (...)) で直列に実行している
block は複数の命令列を引数に取ることができ、これらは並列に実行される。
code:block-parallel.lisp
(block
((speed 2)
// 速度の変化と回転を同時に行う
(block
((30 speed 0) (30 speed 2))
((30 rotate 0 -90 0) (30 rotate 0 90 0)))))
子パーティクルの生成は emit 命令によって行う。 emit の 4 番目の引数に与えられた命令列が子の動作パターンになる。
code:emit.lisp
(block
((speed 1)
(40 nop)
(emit 8 1 1 (60 nop))))
40 nop のあと、 emit 命令によって子のパーティクルが 8 個生成され、それぞれが 60 nop して消滅する。
しかしながらこれではパーティクルが重なって生成され、同じように動作するだけなので、「それぞれのパーティクルを放つ方向を変える」ということを表現したい。 emit 命令に「どのような方向に向けて」といった引数を加えることも可能なように思われるが、emitter3d では「生成後のパーティクルがそれぞれ別の方向を向く」という形で表現している。
code:$each-range.lisp
(block
((speed 1)
(40 nop)
(emit 8 1 1 (block
((rotate 0 ($each-range -45 45) 0)
(60 nop))))))
それぞれのパーティクルは自身が emit 命令において何番目に生成されたかを知っており、その情報に基づいてパーティクルごとに異なるパラメータによる動作をさせることができる。 ($each-range -45 45) は、 emit 命令において何番目に生成されたかに基づいて -45 から 45 に均等に割り振られた値を採用する。これで弾幕シューティングにおける n-way 弾が実装できる。
同様の命令に $each-choice $each-angle がある。
code:$each-choice,$each-angle.lisp
(emit 24 1 1 (block
((speed 1)
(rotate 0 $each-angle 0)
(30 nop)
(30 rotate 0 ($each-choice -40 40) 0)
(60 nop))))
($each-choice -40 40) は 0 番目のパーティクルは -40, 1 番目のパーティクルは 40, 2 番目のパーティクルは -40, ...のように順番に値を採用する。 $each-angle は ($each-range 0 360) に似ているが、終端を含まないように数値が割り振られるので、これによって全方位向きのパーティクルが重なって生成されないようにするなどができる。
パーティクルごとにランダムな値を採用する $random-range $random-choice $random-angle もあるが、見栄えの良さには統率された動きが強く影響するのであまり利用頻度は多くなかった。
code:$random.lisp
(100 emit 1 100 1 (block
((speed ($random-range 3 4))
(hue ($random-choice 200 240 270))
(rotate $random-angle $random-angle $random-angle)
(60 ease-out speed 0)
(30 opacity 0))))
DSL の糖衣構文
前述のとおり {} [] <> には糖衣構文が割り当てられている。DSL かつベースが S 式ということで、頻出のパターンにこのような字句要素を惜しみなく与えることができる。
[] <> は簡単で、それぞれ $each-xxx $random-xxx への糖衣構文となる。
code:syntax-sugar1.lisp
30 -30 // ($each-choice 30 -30) [] // $each-angle
<-45 .. 45> // ($random-range -45 45)
<30 -30> // ($random-choice 30 -30)
<> // $random-angle
{} は block だ。 ; と | で区切って直列、並列な動作を記述する。
code:block.lisp
{
(speed 2);
{
(30 speed 0);
(30 speed 2)
|
(30 rotate 0 -90 0);
(30 rotate 0 90 0)
}
}
このセミコロンの存在は不思議に思われるかもしれない。それぞれの S 式の終端は明白なので不必要な区切りのはずだ。
実際には、 {} 内の命令列は従来の S 式の列とは構文解釈が異なる。具体的には {} 内では、
セミコロンは「文」の区切りとして解釈される。
それぞれの文について、S 式が 2 つ以上含まれる文なら、両端に括弧を補って一つの式として解釈される。
したがって上の定義は以下のように括弧を省略して記述できる。セミコロンが意味のある区切りとなった。
code:block2.lisp
{
speed 2;
{
30 speed 0;
30 speed 2
|
30 rotate 0 -90 0;
30 rotate 0 90 0
}
}
さらに {} の中では改行も単なるスペースではなく、セミコロンと同様に文の区切りとして扱われるため、今回の例はそのままセミコロンを省略できる。
code:block3.lisp
{
speed 2
{
30 speed 0
30 speed 2
|
30 rotate 0 -90 0
30 rotate 0 90 0
}
}
最後に、トップレベルも {} 中と同様のパースが行われるため、トップレベルの {} を取り除くことができる。
code:block4.lisp
speed 2
{
30 speed 0
30 speed 2
|
30 rotate 0 -90 0
30 rotate 0 90 0
}
emit の例も。全体として表面上は S 式っぽさがない記述になる。
code:emit-with-block.lisp
emit 24 1 1 {
speed 1
rotate 0 [] 0
30 nop
60 nop
}
Behavior DSL は基本的にはこれだけである。その他、左下コードエディタの Open + からサンプルコードや命令セットの一覧が確認できるほか、ランダムなパターン生成 (auto-generate にチェックを入れることで行われる) のたびコードエディタのコードが目まぐるしく変わっていくのでその辺を合わせて眺めてもらいたい。