Archive for the ‘流動指向ゲームエンジン『Fooo』’ Category

スナップショットをとる

ここまでに紹介したTransit, UniversalTransit, Transformは
デーカーの見た目を変化させる命令であり、描画命令と呼ぶ。
描画命令は画像を表示する機能を持つデーカーに共通して使用できる。
イメージデーカー、テキストデーカー、バルーンデーカーなどだ。

しかしこれらの命令の効果は、その命令を受けたデーカーの見た目だけに限定される。
つまりこれらの命令を使って”複数のデーカーを合体させたもの”を
まとめてトランジッションしたりまとめて変形させるといったようなことはできない。

だがそのようなことがまったく不可能なわけではない。
別の機能と組み合わせることで実現できる。

Decor_Snapshot スナップショットデーカー

スナップショットデーカーはイメージデーカーの特殊版とも言えるデーカーで
他のデーカーの表示を取り込んで自身の画像とする機能を持つデーカーである。
このデーカーを使えば、”複数のデーカーを合体させた画像”を作ることができる。
そしてそのスナップショットデーカーに対してトランジッションを行えば、
“複数のデーカーを合体させたもの”をトランジッションさせるようなことができる。

スナップショットデーカーを作るにはCreateSnapshot命令を使う。


CreateSnapshot(name="スナップ");

そしてスナップショットを取りたい位置でRequest命令で@Snapshot()を指示する。


Request(to="スナップ", order=@Snapshot(target="画像1", w=1280, h=720));

target パラメータにはスナップショットをとりたい対象のデーカー名を指定する。
省略すると画面全体が対象となる。
w パラメータはスナップショット画像の横幅を指定する。
h パラメータはスナップショット画像の縦幅を指定する。
実際に組み込んでみよう。


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 Test(string $effect)
{
  Transform(to="スナップ", effect=$effect, time=1000);
  wait 1000;
  Transform(to="スナップ", effect=$effect, time=1000, begin=100%, end=0%);
  wait 1000;
}

method Main()
{
  CreateImage(name="画像2", image="画像2.png");
  CreateImage(name="画像1", image="画像1.png");
  CreateObject(name="画像1/煙", x=1280/2, y=720/2+150, class=@煙クラス());
  CreateSnapshot(name="スナップ");
  Enter(to="*");
  Enter(to="画像1/*");
  wait 3000;

  Request(to="スナップ", order=@Snapshot(target="画像1", w=1280, h=720));
  Exit(to="画像1");

  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

ずいぶん長いが重要なところは黄色で示されている部分だけだ。
Exit命令はEnter命令とは逆にデーカーを消す命令である。
スナップショットをとったら元の画像は必要ないのですぐ消すようにしている。

実行結果を見ると確かにスナップショットをとって
それを一つのイメージデーカーのように扱えているのがわかるだろう。
スナップショットデーカーは場面転換の演出を作る際などにうってつけである。


画像を変形させる

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

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

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


雪を降らす

ややこしい機能の説明はこのくらいにして
ここからは具体的にいろんな演出を作りながらFoooの表現力を紹介していこう。
まだまだ説明していないことはたくさんあるのだけど…

細かい説明はとりあえず省略する!

手初めに雪を降らしてみよう。
まず雪粒の素材を用意する。

雪粒.png
SnowBall

この雪粒をスクリプトで降らせる。


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

  int $number = 0;
  while(true)
  {
    string $name = "雪粒"+String($number++);
    int $x = rand_range(0, 1280);
    percent $scale = Percent(0.2+frand()*0.8);

    CreateImage(name=$name
      , x=$x, y=-100, ox="Center", oy="Middle"
      , sx=$scale, sy=$scale, image="雪粒.png");
    Enter(to=$name);

    Move(to=$name, time=rand_range(5000,20000)
      , x=$x+rand_range(-200,200), y=720+100);

    wait 50;
  }
}
YouTube Preview Image

たったこれだけで雪を降らせることができた!
どういう内容のスクリプトだかわかるだろうか。
雪粒を一定時間毎に画面上にランダムに作成して画面下方向に移動させているだけだ。
while文は繰り返しを意味する文で、プログラマにはおなじみだろう。

なんだかごちゃごちゃ計算してるのは、雪粒を個々にランダムに動かすためだ。
frand、rand_rangeは乱数を返す関数である。
ただし動き自体はシンプルな直線移動である。
よく雪粒の動きというのは蛇行させなくちゃいけないと思われがちだが
雪粒の移動方向を微妙にずらすと単に直線的に移動させるだけでも、

目の錯覚で蛇行してるっぽくなる。

ここからは上級編。
実はさっきのスクリプトにはちょっとマズいところがある。
画面下に行った雪粒を消していないので、無限に雪粒が増え続けてしまうのだ。
画面下に行った雪粒を削除するようにしてみよう。
これにはスレッドを使うと簡単だ。


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

  CreateImage(name=$name
    , x=$x, y=-100, ox="Center", oy="Middle"
    , sx=$scale, sy=$scale, image="雪粒.png");
  Enter(to=$name);

  Move(to=$name, time=rand_range(5000,20000)
    , x=$x+rand_range(-200,200), y=720+100);
  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++);
    ThreadCreate(call=@雪粒スレッド(name=$name));
    wait 50;
  }
}

雪粒ごとにスレッドを作成して、
スレッドが1つの雪粒の作成から削除までを管理するようにしている。
お手軽だ。

さらにクラス化して、独自の部品にしてみよう。


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

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

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

    CreateImage(name=$name
      , x=$x, y=-100, ox="Center", oy="Middle"
      , sx=$scale, sy=$scale, image="雪粒.png");
    Enter(to=$name);

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

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

  CreateObject(name="雪", class=@雪クラス());
  Enter(to="雪");
}

スレッドはクラス内でも使用できる。
この場合のスレッドはオブジェクトデーカーの管理下で動作する。

単に独自のデーカー化しただけだが、このようにしておくと
雪エフェクトの細かな動作を雪クラスの中に隠蔽でき、
演出素材として使いまわしやすくなる。