Archive for 2月, 2013

Dictionaryは遅い?

Posted 2013.02.28 in 吉里吉里

吉里吉里のDictionaryの作成がやたらめったら遅いという話を
方々で見かけたので、こちらのサイトを参考に原因を探ってみた。
※有用な情報を公開されている先人の方々に感謝いたします。

吉里吉里/KAG小技集 > 配列と辞書、どちらが早い?

掲載されているテストスクリプトを実行してみると…


配列作成中 ...完了。
ary作成時間は93msです。
hash作成時間は5562msです。
ary検索時間は2msです。
hash検索時間は63msです。

確かに書かれているような結果になった。
しかし吉里吉里のソースコードを見ながら原因を探っていくうちに
どうもこのテストスクリプトにはいくつか複雑な問題があることがわかった。

Dictionaryの仕組み

Dictionaryクラスは連想配列(辞書配列)の一種で『ハッシュテーブル』という仕組みで実装されている。
通常の配列は整数値をインデックスとして使いその値の位置にデータを格納するが、
連想配列は文字列などの任意の値をキーとして使いデータを格納する。
Dictionaryクラスは文字列をキーとするハッシュテーブルである。

ハッシュテーブルの構造は基本的にはただの配列だ。
ハッシュテーブルではキーからハッシュ値を計算し、
そのハッシュ値をインデックスとして配列に値を格納する。

通常の配列に比べて余計な手間がかかっているように思うかもしれないが
このようにすることでキーに整数以外の値を使うことができるし、
要素の挿入・削除・検索を通常の配列よりもずっと高速に行うことができる。

ハッシュテーブルの落とし穴

しかしハッシュテーブルには2つの大きな落とし穴がある。

1つ目の落とし穴は異なるキーであってもハッシュ値が同じ値になる場合があるということである。
これはすなわちテーブルの同じ位置に2つ以上の要素が入る可能性があることを意味する。
Dictionaryではテーブルに格納される要素を連結リストにして複数の値を格納できるようにしている。

2つ目の落とし穴は、テーブルとなる配列を十分な大きさで確保しておかなければならないという点だ。
キーから計算されたハッシュ値はテーブルのサイズに合わせて切り取って使われる。
しかしテーブルのサイズが小さすぎると、切り取られたハッシュ値が高い頻度で重複するようになる。
ハッシュ値が頻繁に重複すると、テーブルの各要素のリストがどんどん伸びていくような形になる。
最悪の場合、ただのリストとほとんど変わらなくなり、連想配列である恩恵が失われてしまう。

かと言って、使うかもわからないの巨大なテーブルを常に用意しておくのもそれはそれで無駄だ。
キャッシュ効率が悪くなり逆に遅くなってしまう場合もある。
すなわちハッシュテーブルを効果的に使うには、適切なテーブルのサイズをいかに決めるか。
そこが肝心なのである。

Array.findが速すぎる

ハッシュテーブルについて理解したところで、さっきのテストスクリプトの結果を見てみよう。
1つ明らかに不可解な点がある。
配列の検索に比べて辞書の検索が遅すぎるのだ。
辞書の検索は配列に比べて高速になるはずである。
というかよくよく考えてみると辞書の検索が遅いわけではなく、むしろ…

配列の検索が爆速すぎる!

いくらなんでも速過ぎである。
吉里吉里のソースコードを見てみると原因は明らかだった。

tjsArray.cpp
findの実装は877行目あたり(2013/2/28現在)

Array.findは、戻り値を返す先がないと何も処理しないような実装になってる!
つまりそもそも検索処理は一切行われていない。
爆速なのは当たり前だ。

正しく検索されるようにするには


var temp = ary.find("hash_number_is_" + "%06d".sprintf(chknums[i]));

のようにして、返り値の受け取り先を用意する必要がある。
このようにして実行しなおすと…


配列作成中 ...完了。
ary作成時間は94msです。
hash作成時間は5520msです。
ary検索時間は5350msです。
hash検索時間は37msです。

期待通り(?)激遅になった!

辞書検索が速くなっているが、これはおそらくキャッシュ効率の影響だろう。

辞書作成が重い原因

そしてもう1つ不可解な点がある。
それは辞書作成の遅さに対して辞書検索が速すぎる点だ。
数値が小さいのは試行回数が少ないからだが、それにしてもちょっと速い。
不可解じゃない?いやいや不可解なのである。
なぜ不可解なのか説明するには、
そもそも辞書作成がなぜこんなに遅くなってるのかについてまず言及する必要があるだろう。

吉里吉里のDictionaryのソースコードはこちら。
tjsDictionary.cpp

辞書作成が遅い原因をずばり言おう。
辞書作成が遅いのは、辞書に追加する要素数に対して

ハッシュテーブルが小さすぎるせいである。

ハッシュテーブルが小さいとどういうことが起こるのか。
それはさっき述べた通りだ。
ハッシュ値の重複が頻繁に起き、ハッシュテーブル内の同じ位置に要素がどんどん追加されていく。
同ハッシュ値の要素はリストとして繋がれていく。
ハッシュテーブルが小さく、追加する要素数がものすごく多い場合、
このリストはものすごい長さになってしまう。

ただ…要素をリストに繋ぐ処理の負荷は軽微なもので、それ自体はさほどたいした負荷ではない。
重さの原因になっているのは、リストに新しく要素を繋ぐ際に、
リスト内に同じキーの要素がないか検索する処理の部分だ。
特に前出のサンプルスクリプトのようなケースの場合、
リスト内に同じキーの要素があるはずがないので
処理上最悪のケース…すなわちリストの頭から末尾まで毎回毎回検索が行われる。
この処理による負荷は膨大なものになる。

すなわちハッシュテーブルが小さいことによって、
辞書検索処理が極めて重くなってしまっているというのが、
辞書作成が異常に重くなる真相である。

辞書検索が速すぎる

これを理解すると、さっき辞書検索が速すぎる!と言った意味がわかるだろう。
むちゃくちゃ速いというわけではないが、なんだか想像より速い。
辞書検索処理はハッシュテーブルが小さいことによってかなり重くなってるはずなのだ。
配列の時のように検索処理自体が行われなくなっているわけでもない。
検索はキッチリ行われている!
どういうことなのか。

これはソースコードを注意深く見ると理由がわかる。
辞書検索を速くしているのは、RebuildHashという仕組みだ。
1.5秒ごとに発生するイベントによって辞書にRebuildHashフラグが立てられ、
このフラグが立った辞書を参照するとハッシュテーブルの再構築が行われるという
トリッキーな仕組みである。

ハッシュテーブルの再構築とは、辞書に格納されている要素数に応じて
ハッシュテーブルのサイズを調整するという処理だ。
この処理によってハッシュテーブルが拡張され同一要素のリストが十分に短くなる。
同一要素のリストが十分に短くなれば、リスト検索の負荷もほとんどなくなる。
ゆえに速くなるのである。

ハッシュテーブルの再構築の効果

ここでまたテストスクリプトを振り返ってみよう。
このテストスクリプトの2つめの問題は
辞書を作成した後に「完了。」と表示を行っている部分にある。
それこそが辞書検索が速くなっている鍵なのだ!

吉里吉里はイベントシステムであり、溜まったイベントはシステムに処理が返った時に実行される。
つまり『1.5秒ごとに発生するイベント』は、システムに処理を返した時に初めて実行される。
そう、「完了。」と表示している部分、そこでシステムに処理が返りイベントが実行されているのである。
これにより次に辞書を参照した時、最初に辞書の再構築が実施される。

この動作を確認するために、試しに辞書検索速度を計測する前に
1回だけ辞書検索する処理を入れて計測してみよう。


配列作成中 ...完了。
ary作成時間は94msです。
hash作成時間は5861msです。
ary検索時間は5497msです。
hash検索時間は1msです。

辞書検索が爆速!

実は30ms以上かかっていたのはハッシュテーブルの再構築の処理であり、
辞書検索自体はこれだけ高速なのである。
ただしハッシュテーブルが適切な大きさならば、だ。

次に完了と表示している部分を削除して実行してみよう。
これで、ハッシュテーブルが再構築されなかった場合の辞書検索の速度がわかる。


配列作成中 ...
ary検索時間は5354msです。
hash検索時間は90msです。

再構築処理分を加味した値よりちょっと重い。
再構築された場合と比べると…尋常じゃなく重い!
これがハッシュテーブルのサイズが適切でない場合の辞書検索の重さである。
辞書作成時にはこの負荷が大きくのしかかっているのだ。

ということで辞書作成がなぜ重いのか、原因が明確になった。
この問題を解決する方法についてはまた次回~


吉里吉里

Posted 2013.02.27 in 吉里吉里

最近仕事で吉里吉里をいじっている。

吉里吉里とは W.Dee 氏が開発したパソコンゲームで広く使われている
オープンソースのゲームエンジン(マルチメディアエンジン)だ。
多くのコントリビューター(協賛者)によって日々拡張が行われている。

吉里吉里 ダウンロードページ
http://kikyou.info/tvp/

吉里吉里に関しては本当に勉強を始めたばかりで、
まだまだぜんぜん全体像を把握しきれていないのだけれど…
内部構造などごちゃごちゃ研究していたりする。

その最中で気がついたことなどいろいろあるので
共有できそうな情報は書いていこうと思う。
ただ…いかんせん思い込みや勘違いが結構あったりするので
何かトンチンカンなこと言ってるようならズバっとご指摘いただきたく(汗)


画像を変形させる

ユニバーサルトランジッションやクロスフェードトランジッションでは
結局のところ画像をフェードさせることしかできない。
画像を変形させるような演出にはまた別の機能を使用する。

Transform命令

Transform命令はその名のとおり画像を変形させる命令である。


Transform(to="画像1", effect="LinearLeft", time=1000);

effectパラメータには、変形効果の種類を指定する。
かなりの種類があるのだが、いくつか紹介しておこう。

Transform_LinearLeft Transform_BlindLeft Transform_CurtainLeft Transform_ShaveLeft Transform_SlashH
LinearLeft BlindLeft CurtainLeft ShaveLeft SlashH
Transform_BoxCenter Transform_HoleCenter Transform_FanCenter Transform_TensileLeft Transform_Mosaic
BoxCenter HoleCenter FanCenter TensileLeft Mosaic

ユニバーサルトランジッションを使えば実現できるものも多いが
わざわざ画像を用意する必要がない分、場合によってはこっちの方が簡単だ。
さっそく使ってみよう。


method Main()
{
  CreateImage(name="画像2", image="画像2.png");
  CreateImage(name="画像1", image="画像1.png");
  Enter(to="画像1");
  wait 1000;

  Transform(to="画像1", effect="LinearLeft", time=1000);
}
YouTube Preview Image

他の変形も試してみよう。
メソッド化して簡潔に記述する。


method Test(string $effect)
{
  Transform(to="画像1", effect=$effect, time=1000);
  wait 1000;
  Transform(to="画像1", effect=$effect, time=1000, begin=100%, end=0%);
  wait 1000;
}

method Main()
{
  CreateImage(name="画像2", image="画像2.png");
  CreateImage(name="画像1", image="画像1.png");
  Enter(to="*");
  wait 1000;

  call @Test(effect="LinearLeft");
  call @Test(effect="BlindLeft");
  call @Test(effect="CurtainLeft");
  call @Test(effect="ShaveLeft");
  call @Test(effect="SlashH");
  call @Test(effect="BoxCenter");
  call @Test(effect="HoleCenter");
  call @Test(effect="FanCenter");
  call @Test(effect="TensileLeft");
  call @Test(effect="Mosaic");
}

YouTube Preview Image
※動画がなんだかガクついてるのは動画圧縮の問題?

トランスフォームは単に画像を変形させるだけで、
厳密には画像を入れ替える演出ではないが、主にトランジッション効果として使用する。


ユニバーサルトランジッション

ユニバーサルトランジッションは、クロスフェードトランジッションの
パワーアップ版とでも言うべき演出効果である。
ユニバーサルとは「普遍的、万能」という意味だ。
そう、すなわちユニバーサルトランジッションとは…

万能なトランジッションなのである。

なんとも大げさな名前だが、この効果の一般的な呼び名である。
いったい何が万能なのか。

ユニバーサルトランジッションは具体的には、ルール画像を使って変則的なトランジッションを行う。
ルール画像を使うという点以外は、仕組み的にはクロスフェードトランジッションと一緒である。
ルール画像は白黒の画像だ。

UniversalTransit_Rule1 UniversalTransit_Rule2 UniversalTransit_Rule3
ルール1.png ルール2.png ルール3.png

ルール画像の色の黒いところから先にクロスフェードトランジッションが行われる。
ルール画像は単なる画像にすぎないので、自在に描きかえることができる。
これはルール画像次第でクロスフェードの仕方を自由に変えられるということを意味する。
それゆえ「万能」なのである。

UniversalTransit命令

ユニバーサルトランジッションを行うにはUniversalTransit命令を使う。


UniversalTransit(to="画像1", image="画像2.png"
  , rule="ルール.png", fade=20, time=1000);

rule パラメータにはルール画像を指定する。
fade パラメータにはフェード階調数を指定する。
フェード階調数が少ないほどシャープなフェードに、多いほどなだらかなフェードになる。

それでは実際に組み込んでみよう。
前回の立ち絵だとちょっと効果がわかりづらいので、今回の素材はコレで。

UniversalTransit_Image1 UniversalTransit_Image2
画像1.png 画像2.png

method Main()
{
  CreateImage(name="画像1", image="画像1.png");
  Enter(to="*");
  wait 1000;

  UniversalTransit(to="画像1", image="画像2.png"
    , rule="ルール1.png", fade=20, time=1000);
}
YouTube Preview Image

せっかくなので他のルールも試すようにしてみよう。


method Main()
{
  CreateImage(name="画像1", image="画像1.png");
  Enter(to="*");
  wait 1000;

  UniversalTransit(to="画像1", image="画像2.png"
    , rule="ルール1.png", fade=20, time=1000);
  wait 1000;
  UniversalTransit(to="画像1", image="画像2.png"
    , rule="ルール1.png", fade=20, time=1000, begin=100%, end=0%);
  wait 1000;

  UniversalTransit(to="画像1", image="画像2.png"
    , rule="ルール2.png", fade=20, time=1000);
  wait 1000;
  UniversalTransit(to="画像1", image="画像2.png"
    , rule="ルール2.png", fade=20, time=1000, begin=100%, end=0%);
  wait 1000;

  UniversalTransit(to="画像1", image="画像2.png"
    , rule="ルール3.png", fade=20, time=1000);
  wait 1000;
  UniversalTransit(to="画像1", image="画像2.png"
    , rule="ルール3.png", fade=20, time=1000, begin=100%, end=0%);
  wait 1000;
}
YouTube Preview Image

begin, end パラメータは開始時のトランジッション度合いと
終了時のトランジッション度合いを指定するパラメータだ。
本来このパラメータはbegin=0%, end=100%なのだが、
逆にbegin=100%, end=0%とすることで、逆動作をさせている。


表情を変える

パーティクルエフェクトはこのくらいにしておいて
今度は一般的なアドベンチャーゲームでよく見かける
立ち絵の表情を変える演出を作ってみよう。

立ち絵は単なる画像にすぎないので、立ち絵の表情を変えるには
立ち絵の画像を入れ替えるという操作をする。
画像は瞬間的に入れ替えるのではなく、
時間をかけてクロスフェードさせるのがよくある演出だ。

それではFoooスクリプトで
「立ち絵をクロスフェードで入れ替えるスクリプト」を書いてみよう。
Opaque命令を使えば簡単にできそうだ。

立ち絵1.png
TransitImage1

立ち絵2.png
TransitImage2


method Main()
{
  CreateImage(name="立ち絵1", image="立ち絵1.png");
  CreateImage(name="立ち絵2", image="立ち絵2.png", alpha=0%);
  Enter(to="*");
  wait 1000;

  Opaque(to="立ち絵1", time=1000, alpha=0%);
  Opaque(to="立ち絵2", time=1000, alpha=100%);
}
YouTube Preview Image

これでうまくいった………
かと思いきや、よく見るとうまくいっていない。
立ち絵が一瞬なんだか白っぽくなってしまっている。
背景が透けてしまっているのだ。

これは画像が描画される順番をよく考えれば当たり前である。
まず画面に『背景』が描かれ、次に『立ち絵1』が半透明で描かれる。
この段階で画面は『背景』と『立ち絵1』が混ざったものになる。
さにらにその上に『立ち絵2』を半透明で描く。
すると…画面は、『背景』と『立ち絵1』と『立ち絵2』が混ざったものになる。

今やりたいことは、『立ち絵1』と『立ち絵2』だけが混ざったものを描くことである。
しかし実のところそのようなことは単純に不透明度をいじるだけでは実現不可能だ。
これを行うにはTransit命令を使う。

Transit命令

Transit命令はデーカーをクロスフェードトランジッションする命令である。
トランジッションとは「変化、遷移」という意味で、CG用語では一般的に場面転換を意味する。
ここではデーカーの変化といったニアンスで使われている。
Transit命令を使うとデーカーのクロスフェードを簡単に実現できる。


method Main()
{
  CreateImage(name="立ち絵1", image="立ち絵1.png");
  Enter(to="*");
  wait 1000;

  Transit(to="立ち絵1", image="立ち絵2.png", time=1000);
}
YouTube Preview Image

透けなくなった!


花火を打ち上げる

花火は代表的な「パーティクルエフェクト」だが、
その見た目のシンプルさに対して案外実装は難しい。
なぜなら粒をランダムにではなく、規則的に放たなければいないからだ。
そうしないと花火っぽく見えないのだ。

ちなみに花火の粒は「星」と呼ぶので、ここでもそう呼ぶことにする。

星.png
Star


class 星クラス
{
  method 星クラス()
  {
    CreateImage(name="Image"
      , ox="Center", oy="Middle", image="星.png", blend="Add");
  }
  method OnEnter()
  {
    Enter(to="Image");

    // ゆっくり落ちていく
    Move(to="Image", y=rand_range(50,300), time=3000, step="Acc5");
  }
}

class 花火クラス
{
  method 花火クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;

    // 星を用意する
    command $star = @星クラス();

    // 多重の円の形になるように星を飛ばす
    int $ring = rand_range(6,10); // 円の数
    float $max_r = Float(rand_range(100,200)); // 最大半径
    for(int $j=0; $j<$ring; $j++)
    {
      int $divide = $j*4; // 円を構成する星の数
      float $r = $max_r*sin(pi()/2.0*Float($j)/Float($ring)); // 飛距離
      int $offset = mod($j,3)==0 ? 0 : 360/$divide/2; // たまにずらす
      for(int $i=0; $i<$divide; $i++)
      {
        string $name = "星"+String($number++);
        ThreadCreate(call=@星スレッド(name=$name, star=$star
          , move=$r, angle=360*$i/$divide+$offset+rand_range(-2,2)));
      }
    }

    WaitDecor();
    Delete();
  }

  method 星スレッド(string $name, command $star, float $move, int $angle)
  {
    CreateObject(name=$name, class=$star);
    Enter(to=$name);

    float $x = $move*cos(radian(Float($angle)));
    float $y = -$move*sin(radian(Float($angle)));
	int   $time = rand_range(1900,2100);

    Move(to=$name, time=$time, x=$x, y=$y, step="Dec5");
    Opaque(to=$name, time=rand_range(1500,2500), alpha=0%, step="Acc5");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Main()
{
  CreateColor(name="背景", w=1280, h=720, color=black);
  Enter(to="背景");

  int $number = 0;
  while(true)
  {
    string $name = "花火"+String($number++);
    CreateObject(name=$name
      , x=rand_range(100,1280-100), y=rand_range(260,460)
      , class=@花火クラス());
    Enter(to=$name);
    wait rand_range(200,1000);
  }
}
YouTube Preview Image

このスクリプトのポイントはfor文を使っているところだ。
for文はwhile文と同じように繰り返しを意味する文だが、特定回数繰り返す場合に主に使う。

最初に言ったように星をランダムに放っても花火っぽくはならない。
星は規則的に放つ必要がある。
理由は実物の花火の構造を考えれば自明だ。

花火の玉には星が円形に綺麗に並べて詰められて、その真ん中に爆薬が詰められている。
花火が爆発すると、綺麗に並んでいた星が一斉に放たれる。
この時星の放たれ方は決してランダムではない。
星が詰められていた位置によって決められた方向へと飛んでいくのだ。

さらに実際花火が炸裂した時の姿は円形ではなく球形である。
そのあたりも注意が必要で、飛距離でsin計算をしているのはそのためだ。

うーん、説明しててもややこしい。
とにかく大変なのである。

それではもうちょっと手を入れてみよう。
星が単色で地味なので、カラフルにしてみよう。


class 星クラス
{
  color $m_color2;

  method 星クラス(color $color1, color $color2)
  {
    operate $m_color2 = $color2;

    CreateImage(name="Image"
      , ox="Center", oy="Middle", image="星.png", blend="Add"
      , dynamic_color=$color1, dynamic_blend="Color");
  }
  method OnEnter()
  {
    Enter(to="Image");

    // ゆっくり落ちていく
    Move(to="Image", y=rand_range(50,300), time=3000, step="Acc5");
    Color(to="Image", color=$m_color2, time=1500, step="AccSin");
  }
}

class 花火クラス
{
  method 花火クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;

    // 星を用意する
    color $color1 = RGB(rand_range(0,255),rand_range(0,255),rand_range(0,255));
    color $color2 = RGB(rand_range(0,255),rand_range(0,255),rand_range(0,255));
    command $star = @星クラス(color1=$color1, color2=$color2);

    // 多重の円の形になるように星を飛ばす
    int $ring = rand_range(6,10); // 円の数
    float $max_r = Float(rand_range(100,200)); // 最大半径
    for(int $j=0; $j<$ring; $j++)
    {
      int $divide = $j*4; // 円を構成する星の数
      float $r = $max_r*sin(pi()/2.0*Float($j)/Float($ring)); // 飛距離
      int $offset = mod($j,3)==0 ? 0 : 360/$divide/2; // たまにずらす
      for(int $i=0; $i<$divide; $i++)
      {
        string $name = "星"+String($number++);
        ThreadCreate(call=@星スレッド(name=$name, star=$star
          , move=$r, angle=360*$i/$divide+$offset+rand_range(-2,2)));
      }
    }

    WaitDecor();
    Delete();
  }

  method 星スレッド(string $name, command $star, float $move, int $angle)
  {
    CreateObject(name=$name, class=$star);
    Enter(to=$name);

    float $x = $move*cos(radian(Float($angle)));
    float $y = -$move*sin(radian(Float($angle)));
    int   $time = rand_range(1900,2100);

    Move(to=$name, time=$time, x=$x, y=$y, step="Dec5");
    Opaque(to=$name, time=rand_range(1500,2500), alpha=0%, step="Acc5");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Main()
{
  CreateColor(name="背景", w=1280, h=720, color=black);
  Enter(to="背景");

  int $number = 0;
  while(true)
  {
    string $name = "花火"+String($number++);
    CreateObject(name=$name
      , x=rand_range(100,1280-100), y=rand_range(260,460)
      , class=@花火クラス());
    Enter(to=$name);
    wait rand_range(200,1000);
  }
}
YouTube Preview Image

星をカラフルにすると言ったが、新たな素材は何も使っていない。
CreateImage命令のdynamic_color, dynamic_blendというパラメータを使っている。
このパラメータは画像に動的に色を合成することを指定するパラメータで
これを使って星の色をランダムに変えているのだ。

さらにColor命令は指定色を後から変える命令で、
星の色がだんだんと変化するような演出を加えている。


火をともす

火は煙と同じ要領でできる。

火粒.png
Fire


class 火クラス
{
  method 火クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "火粒"+String($number++);
      ThreadCreate(call=@火粒スレッド(name=$name));
      wait 50;
    }
  }

  method 火粒スレッド(string $name)
  {
    CreateImage(name=$name
      , sx=100%, sy=100%, ox="Center", oy="Middle", alpha=0%
      , angle=rand_range(0,359), image="火粒.png", blend="Add");
    Enter(to=$name);

    int   $range = 5;
    int   $angle = 90+rand_range(-$range,$range);
    float $rad = radian(Float($angle));
    float $radius = 500.0;
    float $x = $radius*cos($rad);
    float $y = -$radius*sin($rad);
    int   $time = 1500;

    Move(to=$name, time=$time, x=$x, y=$y, step="AccSin");
    Zoom(to=$name, time=$time, sx=50%, sy=50%);
    Opaque(to=$name, time=100, alpha=50%, step="DecSin");
    wait 100;
    Opaque(to=$name, time=$time-100, alpha=0%, step="DecSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Main()
{
  CreateColor(name="背景", w=1280, h=720, color=black);
  Enter(to="背景");

  CreateObject(name="火", x=1280/2, y=720/2+150, class=@火クラス());
  Enter(to="火");
}
YouTube Preview Image

煙がだんだん減速しながら広がっていくのに対して
火はだんだん加速しながら狭まっていく感じにする。

火粒が融合して艶かしい不思議なかんじになっているが
これは合成モードを”Add”(加算)にしている効果だ。
加算合成の画像同士を近づけるとひっついたかんじになるのが加算合成の特徴である。

さて、やはりこれだけでは単純すぎるのでまた工夫してみる。
火元を動かせるようにする。


class 火クラス
{
  method 火クラス()
  {
    CreateNode(name="火元");
  }

  method OnEnter()
  {
    int $number = 0;
    float $old_x;
    float $old_y;
    while(true)
    {
      // 火元が前回の位置から動いていたらハードモードに設定
      decor $target = GetDecor("火元");
      float $tx = $target.GetPosX();
      float $ty = $target.GetPosY();
      bool $hard = ($old_x != $tx || $old_y != $ty);
      operate $old_x = $tx;
      operate $old_y = $ty;

      string $name = "火粒"+String($number++);
      ThreadCreate(call=@火粒スレッド(name=$name, tx=$tx, ty=$ty, hard=$hard));

      // ハードモードなら発生間隔を詰める
      wait $hard ? 10 : 50;
    }
  }

  method 火粒スレッド(string $name, float $tx, float $ty, bool $hard)
  {
    CreateImage(name=$name
      , x=$tx, y=$ty
      , sx=100%, sy=100%, ox="Center", oy="Middle", alpha=0%
      , angle=rand_range(0,359), image="火粒.png", blend="Add");
    Enter(to=$name);

    int   $range = $hard ? 30 : 5;
    int   $angle = 90+rand_range(-$range,$range);
    float $rad = radian(Float($angle));
    float $radius = 500.0;
    float $x = $tx+$radius*cos($rad);
    float $y = $ty-$radius*sin($rad);
    int   $time = 1500;

    Move(to=$name, time=$time, x=$x, y=$y, step="AccSin");
    Zoom(to=$name, time=$time, sx=50%, sy=50%);
    Opaque(to=$name, time=100, alpha=50%, step="DecSin");
    wait 100;
    Opaque(to=$name, time=$time-100, alpha=0%, step="DecSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Main()
{
  CreateColor(name="背景", w=1280, h=720, color=black);
  Enter(to="背景");

  CreateObject(name="火", x=1280/2, y=720/2+150, class=@火クラス());
  Enter(to="火");
  wait 2000;
  Move(to="火/火元", x=-500, time=1000, step="AccSig");
  wait 1000;
  Move(to="火/火元", x=500, time=2000, step="AccSig");
  wait 2000;
  Move(to="火/火元", x=0, y=-200, time=1000, step="AccSig");
  wait 1000;
  Move(to="火/火元", x=-500, y=0, time=1000, step="AccSig");
  wait 1000;
  Move(to="火/火元", x=0, y=0, time=1000, step="AccSig");
  wait 1000;
}
YouTube Preview Image

まず”火元”という名のノードデーカーを作成している。
そしてそのノードデーカーの位置を基準に火粒を発生させるようにしている。
するとノードデーカーを動かすことで、火粒の発生位置を動かすことができるようになる。
…といった寸法だ。

GetDecor関数はデーカーへの参照を得る関数で、
この参照を通してデーカーの様々な状態値を得ることができる。

ハードモードなる判定をしているが、
これは火が移動した時に火の見た目がおかしくなるのを防ぐ細工だ。
この火のエフェクトは火粒がある程度密集していることで火のように見せかけているが
火元がすばやく移動すると、火粒が密集せず散らばってまばらになってしまい、
個々の火粒がはっきりと見え、火のように見えなくなってしまう。

そこで火元が移動する時だけ火粒の発生間隔をつめるようにしている。
これは奇しくも現実の火をすばやく動かすと酸素を多く取り込んで強く燃え上がるのに似ている。
またさらに動いた時の風によるぶれを表現するために火粒の散り具合を上げるなどしている。

火のエフェクトというのは原理は簡単でも調整がすごく難しい…


煙を焚く

今度は煙を焚いてみよう。
これもやることは雪を降らすのとあまり変わらない。
まず煙の小さな塊を画像として用意する。

煙粒.png
Smoke


class 煙クラス
{
  method 煙クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "煙粒"+String($number++);
      ThreadCreate(call=@煙粒スレッド(name=$name));
      wait 100;
    }
  }

  method 煙粒スレッド(string $name)
  {
    CreateImage(name=$name
      , x=0, y=0, sx=20%, sy=20%, ox="Center", oy="Middle"
      , angle=rand_range(0,359), image="煙粒.png");
    Enter(to=$name);

    int $time = 2000;
    int $angle = 90+rand_range(-10,10);
    float $rad = radian(Float($angle));
    float $move = 300.0;
    float $x = $move*cos($rad);
    float $y = -$move*sin($rad);

    Move(to=$name, time=$time, x=$x, y=$y, step="DecSin");
    Zoom(to=$name, time=$time, sx=200%, sy=200%);
    Opaque(to=$name, time=$time, alpha=0%, step="AccSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Main()
{
  CreateColor(name="背景", w=1280, h=720, color=black);
  Enter(to="背景");

  CreateObject(name="煙", x=1280/2, y=720/2+150, class=@煙クラス());
  Enter(to="煙");
}
YouTube Preview Image

今までと大きく違うのは1点から粒を放出するというところだ。
煙の塊をまばらに飛ばして拡大しながらだんだん透明にしていくと
このように煙が噴出してるかのような見た目になる。

「粒子の画像」と「粒子の動き方」をちょっと変えるだけで
同じような枠組みで様々な効果を作ることができることがわかるだろう。
このような粒子を用いた演出テクニックを一般的に

『パーティクルエフェクト』と言う。

これだけではあまりに簡単すぎてつまらないので
もうちょっと工夫してみよう。


class 煙クラス
{
  int $m_angle = 0;

  method 煙クラス()
  {
  }

  method OnEnter()
  {
    ThreadCreate(call=@煙発生スレッド());
  }

  method 煙発生スレッド()
  {
    int $number = 0;
    while(true)
    {
      string $name = "煙粒"+String($number++);
      ThreadCreate(call=@煙粒スレッド(name=$name));
      wait 100;
    }
  }

  method 煙粒スレッド(string $name)
  {
    CreateImage(name=$name
      , x=0, y=0, sx=20%, sy=20%, ox="Center", oy="Middle"
      , angle=rand_range(0,359), image="煙粒.png");
    Enter(to=$name);

    int $time = 2000;
    int $angle = 90+$m_angle+rand_range(-10,10);
    float $rad = radian(Float($angle));
    float $move = 300.0;
    float $x = $move*cos($rad);
    float $y = -$move*sin($rad);

    Move(to=$name, time=$time, x=$x, y=$y, step="DecSin");
    Zoom(to=$name, time=$time, sx=200%, sy=200%);
    Opaque(to=$name, time=$time, alpha=0%, step="AccSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }

  method 向き設定(int $angle)
  {
    operate $m_angle = $angle;
  }
}

method Main()
{
  CreateColor(name="背景", w=1280, h=720, color=black);
  Enter(to="背景");

  CreateObject(name="煙", x=1280/2, y=720/2+150, class=@煙クラス());
  Enter(to="煙");
  wait 2000;

  Request(to="煙", order=@向き設定(angle=45));
  wait 2000;

  Request(to="煙", order=@向き設定(angle=-45));
  wait 2000;

  Request(to="煙", order=@向き設定(angle=0));
  wait 2000;
}
YouTube Preview Image

煙を放つ向きを、Request命令で設定できるようにしてみた。
煙発生をOnEnterメソッドではなく、煙発生スレッドで行うようにしているが、
これはオブジェクトデーカーがメソッドの実行中にリクエストを受けると
実行中のメソッドを中断してしまうからだ。
スレッドがさらにスレッドを呼び出すという面白い構造になっているが
こういうことも可能なのである。


花を降らす

次は花を降らせてみよう。
花のつぼみがくるくる回転しながら落ちていくような演出にする。
これまた雪を降らすスクリプトとほとんど一緒だ。

花粒.png
Flower


class 花粒クラス
{
  method 花粒クラス()
  {
    CreateImage(name="花"
      , ox="Center", oy="Middle", image="花粒.png", blend="Add");
  }

  method OnEnter()
  {
    // 回転し続ける
    Enter(to="*");
    while(true)
    {
      Rotate(angle=0);
      Rotate(time=3000, angle=360);
      wait 3000;
    }
  }
}

class 花クラス
{
  method 花クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "花粒"+String($number++);
      ThreadCreate(call=@花粒スレッド(name=$name));
      wait 75;
    }
  }

  method 花粒スレッド(string $name)
  {
    int $x = rand_range(0, 1280);
    percent $scale = Percent(0.2+frand()*0.8);

    CreateObject(name=$name
      , x=$x, y=-100, sx=$scale, sy=$scale, class=@花粒クラス());
    Enter(to=$name);

    int $time = rand_range(5000,20000);
    Move(to=$name, time=$time
      , x=$x, y=720+100);
    wait $time;
    Delete(to=$name);
  }
}

method Main()
{
  CreateColor(name="背景", w=1280, h=720, color=black);
  Enter(to="背景");

  CreateObject(name="花", class=@花クラス());
  Enter(to="花");
}
YouTube Preview Image

動きが激しすぎるせいか、動画重っ!

粒をイメージデーカーではなく、オブジェクトデーカーにしているのが特徴だ。
回転しつづける動きを粒自身にさせることで、
粒を降らせる部分のスクリプトをほとんどいじらずに済んでいる。

花粒クラスのRotate命令でtoを省略しているが、この場合”自身”を指す意味になる。


雨を降らす

今度は雨を降らせてみよう。
基本的には雪を降らすのとあまり変わらない。

まず雨粒の画像を用意する。
と言っても画像の内容は粒ではなく、雨粒が降った時の軌跡のようなもの。

雨粒.png
RainLine


class 雨クラス
{
  method 雨クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "雨粒"+String($number++);
      ThreadCreate(call=@雨粒スレッド(name=$name));
      wait 10;
    }
  }

  method 雨粒スレッド(string $name)
  {
    float $angle = 250.0;
    float $move = 2720.0;
    int $x = 450+rand_range(-100, 1280+100);
    int $y = -1000;
    float $rad = radian($angle);
    float $tx = Float($x)+$move*cos($rad);
    float $ty = Float($y)-$move*sin($rad);
    float $scale = 0.2+frand()*0.8;
    int $time = Int(5000.0*(1.0-$scale));

    CreateImage(name=$name
      , x=$x, y=$y, ox="Center", oy="Middle"
      , sx=Percent($scale*2.0), sy=Percent($scale)
      , angle=$angle, image="雨粒.png");
    Enter(to=$name);

    Move(to=$name, time=$time, x=$tx, y=$ty);
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Main()
{
  CreateColor(name="背景", w=1280, h=720, color=black);
  Enter(to="背景");

  CreateObject(name="雨", class=@雨クラス());
  Enter(to="雨");
}
YouTube Preview Image

特徴的なのは角度の計算をしている部分だ。
雨粒の落ちてゆく角度と、雨粒の画像の角度を合わせている。
角度をちゃんと合わせないと雨が直進しているように見えなくなってしまうので注意が必要だ。