煙を焚く

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

煙粒.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="雪");
}

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

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


クラス化する

今回の話は正直かなりややこしい…

前回は同時実行されるフェードインアウト演出をスレッド化によって実現した。
今回はスレッド化とは別のアプローチで、同じような動作を実現してみよう。
それが今回のお題の…

クラス化だ!

クラスとは等級、階級、科目、分類といった意味で、
プログラマーにはオブジェクト指向でお馴染みだろう。
まさしくその『クラス』だ。

クラスはプログラミング用語では「オブジェクトの設計図」を意味する。
オブジェクトとは「機能的な構造を持つ部品」のようなものである。
Foooにおいて部品はデーカーだから…
Foooにおけるクラスとは

デーカーの設計図

であると言える。
Foooには独自に設計したデーカーをクラスとして定義する仕組みがある。
と言ってもなんでもかんでも自由に作れるというわけではない。
既存のデーカーを組み合わせて複雑な動作をするデーカーを定義できるのである。

クラスの定義

それではフェードインアウトをクラスとして定義してみよう。


class フェードインアウト
{
  method フェードインアウト(color $色=white, string $合成="Normal")
  {
    CreateColor(name="幕", color=$色, w=1280, h=720, blend=$合成);
  }

  method OnEnter()
  {
    Enter(to="幕");
  }

  method 開始(int $フェード=1000, int $待ち=0)
  {
    Opaque(to="幕", time=$フェード, alpha=0%, step="DecSin");
    wait $フェード;
  
    wait $待ち;

    Opaque(to="幕", time=$フェード, alpha=100%, step="AccSin");
    wait $フェード;
  }
}

なんだかちょっと不思議な表記に感じるかもしれない。
とにもかくにもこれでフェードインアウトクラスの定義ができる。

クラスの使用

定義したクラスを使ってデーカーを作るにはCreateObject命令を使う。


CreateObject(name="フェード"
  , class=@フェードインアウト(色=white, 合成="Add"));
Enter(to="フェード");

CreateObject命令で作られるデーカーはオブジェクトデーカーと呼ぶ。
オブジェクトデーカーはclassパラメータに指定されたクラスの定義に従って動作する。

1つめのフェードインアウトメソッドは、オブジェクトデーカーが作成された時に自動的に呼び出される。
このようなクラス名と同じ名前のメソッドを「コンストラクタ」と呼ぶ。
コンストラクタでは構成部品となるデーカーの作成だけをするのがお約束だ。
なのでフェードインアウトメソッドでは”幕”の作成だけしている。
ちなみにこの”幕”はオブジェクトデーカーの子として作成される。

2つめのOnEnterメソッドは、オブジェクトデーカーがEnter命令で表示された時に自動的に呼び出される。
OnEnterメソッドにはデーカーが表示される際の動作を記述する。
ここでは子として作った幕をEnterしている。

3つめの開始メソッドは、自動的に呼ばれるメソッドではない。
まったくの独自のメソッドである。
任意に呼び出して使用する。
外からオブジェクトデーカーのメソッドを呼ぶにはRequest命令を使う。


Request(to="フェード", order=@開始(待ち=5000));

これらオブジェクトデーカーの持つメソッドは、オブジェクトデーカーの管理下にあり、
スレッドと同じように呼び出し元と同時進行で実行される。
これはすなわち

デーカーがメソッドを実行してくれる

ということである!

デーカーがメソッドを
実行してくれる

ということである!!!
大事なことなので(ry

──────ばばーっと説明してきたので、
このような仕組みにいったいどんな意味があるのかわかりにくかったかもしれない。
とりあえず実際に組み込んでみよう。


style 普通 { face="MS ゴシック", size=32 }
style 小 { size=24 }
style つぶやき { interval=100, speed=250, effect="Rise" }

#base_param CreateBalloon(style="普通")
#base_param CreateFrame(outline_shape="Vivide"
  , outline_color=white, outline_thick=8)

class フェードインアウト
{
  method フェードインアウト(color $色=white, string $合成="Normal")
  {
    CreateColor(name="幕", color=$色, w=1280, h=720, blend=$合成);
  }

  method OnEnter()
  {
    Enter(to="幕");
  }

  method 開始(int $フェード=1000, int $待ち=0)
  {
    Opaque(to="幕", time=$フェード, alpha=0%, step="DecSin");
    wait $フェード;
  
    wait $待ち;

    Opaque(to="幕", time=$フェード, alpha=100%, step="AccSin");
    wait $フェード;
  }
}

method Main()
{
  CreateImage(name="背景", image="背景.png"
    , x=0, y=0);

  CreateNode(name="コマ1"
    , x=642, y=0, sx=0%);
  CreateWindow(name="コマ1/窓"
    , w=405, h=720, ox="Center", propagation="CancelParentZoom");
  CreateImage(name="コマ1/窓/画像", image="コマ1.png"
    , x=20, y=324, ox="Center", oy="Middle", sx=110%, sy=110%
    , sampling="BieLinear");
  CreateOutline(name="コマ1/枠"
    , w=405, h=720, shape="Fade", color=RGBA(0,0,0,64), thick=8
    , ox="Center", adaptive=true);

  CreateFrame(name="コマ2", image="コマ2.png"
    , x="OutRight", y=65);
  CreateFrame(name="コマ3", image="コマ3.png"
    , x=407, y=505, ox="Center", oy="Middle", sx=0%, sy=0%
    , outline_color=black);

  CreateBalloon(name="台詞1"
    , text="それにしても...<BR>腹(はら)が減(へ)った"
    , x=585, y=65, shape="Dumpling", w=300, h=185, tail=-30);
  CreateBalloon(name="コマ3/台詞2"
    , style="小", text="<FONT style='つぶやき'>クククク…"
    , x=63, y=15, shape="Rock", w=220, h=125, tail=160);

  CreateObject(name="フェード"
    , class=@フェードインアウト(色=white, 合成="Add"), silent=true);

  Enter(to="背景");
  Enter(to="コマ1");
  Enter(to="コマ1/窓");
  Enter(to="コマ1/窓/画像");
  Enter(to="コマ1/枠");
  Enter(to="フェード");
  wait 500;

  Request(to="フェード", order=@開始(待ち=5000));
  Zoom(to="コマ1", time=1000, sx=100%, step="DecSin");
  Move(to="コマ1/窓/画像", time=1000, x=-20, step="DecSin");
  WaitDecor();

  Enter(to="コマ2");
  Move(to="コマ2", time=800, x=780, step="Dec3");
  WaitDecor();

  Enter(to="台詞1", effect="Bound");
  WaitDecor();

  Enter(to="コマ3");
  Zoom(to="コマ3", time=1000, sx=100%, sy=100%, step="AccSig");
  wait 600;

  Enter(to="コマ3/台詞2", effect="Expand");
  WaitDecor();
}

何気にsilent=trueという指定が加わっているが、
これはWaitDecor命令でこのデーカーを待たないことを意味する指定だ。

こうして組み込んでみると、クラスとして定義したデーカーが
オリジナルのデーカーとして扱えるようになっている様子がよくわかるだろう。
若干定義は面倒くさいものの、慣れればスレッドよりもずっと直感的だ。

そして何よりも「デーカーがメソッドを実行してくれる」という事実がもたらす
利便性と自由度の高さは計り知れない。


スレッド化する

前回はフェードインメソッドを作ったが、
今度はフェードインした後一定時間経ったらフェードアウトするメソッドを作ってみよう。
フェードインメソッドをちょちょいと細工して…


method フェードインアウト(color $色=white, string $合成="Normal"
  , int $フェード=1000, int $待ち=0)
{
  CreateColor(name="幕", color=$色, w=1280, h=720, blend=$合成);
  Enter(to="幕");
  wait 500;

  Opaque(to="幕", time=$フェード, alpha=0%, step="DecSin");
  wait $フェード;

  wait $待ち;

  Opaque(to="幕", time=$フェード, alpha=100%, step="AccSin");
  wait $フェード;
}

こんなかんじでよさそうだ。
幕を作って500ms経過してからフェードインしはじめ、
フェードインが終わったら待ち時間分だけ待ってフェードアウトする…という流れだ。
それでは組み込んでみよう。


style 普通 { face="MS ゴシック", size=32 }
style 小 { size=24 }
style つぶやき { interval=100, speed=250, effect="Rise" }

#base_param CreateBalloon(style="普通")
#base_param CreateFrame(outline_shape="Vivide"
  , outline_color=white, outline_thick=8)

method フェードインアウト(color $色=white, string $合成="Normal"
  , int $フェード=1000, int $待ち=0)
{
  CreateColor(name="幕", color=$色, w=1280, h=720, blend=$合成);
  Enter(to="幕");
  wait 500;

  Opaque(to="幕", time=$フェード, alpha=0%, step="DecSin");
  wait $フェード;

  wait $待ち;

  Opaque(to="幕", time=$フェード, alpha=100%, step="AccSin");
  wait $フェード;
}

method Main()
{
  CreateImage(name="背景", image="背景.png"
    , x=0, y=0);

  CreateNode(name="コマ1"
    , x=642, y=0, sx=0%);
  CreateWindow(name="コマ1/窓"
    , w=405, h=720, ox="Center", propagation="CancelParentZoom");
  CreateImage(name="コマ1/窓/画像", image="コマ1.png"
    , x=20, y=324, ox="Center", oy="Middle", sx=110%, sy=110%
    , sampling="BieLinear");
  CreateOutline(name="コマ1/枠"
    , w=405, h=720, shape="Fade", color=RGBA(0,0,0,64), thick=8
    , ox="Center", adaptive=true);

  CreateFrame(name="コマ2", image="コマ2.png"
    , x="OutRight", y=65);
  CreateFrame(name="コマ3", image="コマ3.png"
    , x=407, y=505, ox="Center", oy="Middle", sx=0%, sy=0%
    , outline_color=black);

  CreateBalloon(name="台詞1"
    , text="それにしても...<BR>腹(はら)が減(へ)った"
    , x=585, y=65, shape="Dumpling", w=300, h=185, tail=-30);
  CreateBalloon(name="コマ3/台詞2"
    , style="小", text="<FONT style='つぶやき'>クククク…"
    , x=63, y=15, shape="Rock", w=220, h=125, tail=160);

  Enter(to="背景");
  Enter(to="コマ1");
  Enter(to="コマ1/窓");
  Enter(to="コマ1/窓/画像");
  Enter(to="コマ1/枠");

  call @フェードインアウト(色=white, 合成="Add", 待ち=5000);

  Zoom(to="コマ1", time=1000, sx=100%, step="DecSin");
  Move(to="コマ1/窓/画像", time=1000, x=-20, step="DecSin");
  WaitDecor();

  Enter(to="コマ2");
  Move(to="コマ2", time=800, x=780, step="Dec3");
  WaitDecor();

  Enter(to="台詞1", effect="Bound");
  WaitDecor();

  Enter(to="コマ3");
  Zoom(to="コマ3", time=1000, sx=100%, sy=100%, step="AccSig");
  wait 600;

  Enter(to="コマ3/台詞2", effect="Expand");
  WaitDecor();
}
YouTube Preview Image

………止まっちゃってるし!

何がいけなかったのか。
というか………こうなるのは至極当たり前である。
フェードインアウトメソッドを呼び出すと処理はフェードインアウトメソッドに移り、
その実行が終わるまでMainメソッドには戻ってこないからだ。

これはそもそもフェードインとフェードアウトを
1つのメソッドにしてしまうことに無理がありそうだ。
しかしその無理を通す方法が、実はある。
それが『スレッド化』だ。

スレッドとは筋、糸という意味だが、
プログラミング用語では同時進行する処理の1つの流れのことを意味する。
メソッドは通常の呼び出し方とは別に、スレッドとして呼び出すことができる。
メソッドをスレッドとして呼び出すと、呼び出したメソッドは呼び出し元と

同時進行で実行される。

これはメソッドをある種の命令のようにみなせるということでもある。
命令の実行完了を待つ必要がないのと同じように、
スレッドも実行完了を待つ必要が無いのだ。

ThreadCreate命令

メソッドをスレッドとして呼び出すにはThreadCreate命令を使う。


ThreadCreate(call=@フェードインアウト(色="white", 合成="Add", 待ち=5000));

call文とさほど表記は変わらない。
フェードインアウトメソッドをスレッドとして呼び出すとどうなるのか…
とにもかくにも組み込んでみよう。
違いはごくわずかだ。


style 普通 { face="MS ゴシック", size=32 }
style 小 { size=24 }
style つぶやき { interval=100, speed=250, effect="Rise" }

#base_param CreateBalloon(style="普通")
#base_param CreateFrame(outline_shape="Vivide"
  , outline_color=white, outline_thick=8)

method フェードインアウト(color $色=white, string $合成="Normal"
  , int $フェード=1000, int $待ち=0)
{
  CreateColor(name="幕", color=$色, w=1280, h=720, blend=$合成);
  Enter(to="幕");
  wait 500;

  Opaque(to="幕", time=$フェード, alpha=0%, step="DecSin");
  wait $フェード;

  wait $待ち;

  Opaque(to="幕", time=$フェード, alpha=100%, step="AccSin");
  wait $フェード;
}

method Main()
{
  CreateImage(name="背景", image="背景.png"
    , x=0, y=0);

  CreateNode(name="コマ1"
    , x=642, y=0, sx=0%);
  CreateWindow(name="コマ1/窓"
    , w=405, h=720, ox="Center", propagation="CancelParentZoom");
  CreateImage(name="コマ1/窓/画像", image="コマ1.png"
    , x=20, y=324, ox="Center", oy="Middle", sx=110%, sy=110%
    , sampling="BieLinear");
  CreateOutline(name="コマ1/枠"
    , w=405, h=720, shape="Fade", color=RGBA(0,0,0,64), thick=8
    , ox="Center", adaptive=true);

  CreateFrame(name="コマ2", image="コマ2.png"
    , x="OutRight", y=65);
  CreateFrame(name="コマ3", image="コマ3.png"
    , x=407, y=505, ox="Center", oy="Middle", sx=0%, sy=0%
    , outline_color=black);

  CreateBalloon(name="台詞1"
    , text="それにしても...<BR>腹(はら)が減(へ)った"
    , x=585, y=65, shape="Dumpling", w=300, h=185, tail=-30);
  CreateBalloon(name="コマ3/台詞2"
    , style="小", text="<FONT style='つぶやき'>クククク…"
    , x=63, y=15, shape="Rock", w=220, h=125, tail=160);

  Enter(to="背景");
  Enter(to="コマ1");
  Enter(to="コマ1/窓");
  Enter(to="コマ1/窓/画像");
  Enter(to="コマ1/枠");

  ThreadCreate(call=@フェードインアウト(色=white, 合成="Add", 待ち=5000));
  wait 500;

  Zoom(to="コマ1", time=1000, sx=100%, step="DecSin");
  Move(to="コマ1/窓/画像", time=1000, x=-20, step="DecSin");
  WaitDecor();

  Enter(to="コマ2");
  Move(to="コマ2", time=800, x=780, step="Dec3");
  WaitDecor();

  Enter(to="台詞1", effect="Bound");
  WaitDecor();

  Enter(to="コマ3");
  Zoom(to="コマ3", time=1000, sx=100%, sy=100%, step="AccSig");
  wait 600;

  Enter(to="コマ3/台詞2", effect="Expand");
  WaitDecor();
}
YouTube Preview Image

フェードインアウトが同時進行している様子がわかるだろう。
スレッドはちょっととっつきにくい印象があると思うが、
演出が複雑に入り組む場合非常に便利な機能である。


メソッド化する

演出にはフェードインのように非常によく使うものがある。
そのような処理を何度も何度も書くのは面倒であるばかりか
スクリプトを著しく読みにくくし、後からの修正もしにくくする。

メソッドの定義

そのような時はよく使う処理を『メソッド』という形で予め定義しておき、
必要な箇所でそれを使いまわすようする。
メソッドとは方法、手法という意味だが、プログラミングにおいては
ある特定の処理手続きを指すようなニアンスで使われる。
それでは例としてフェードインをするメソッドを定義してみよう。


method フェードイン()
{
  CreateColor(name="幕", color=white, w=1280, h=720, blend="Add");
  Enter(to="幕");
  wait 500;
  Opaque(to="幕", time=1000, alpha=0%, step="DecSin");
}

──────何だか見覚えがあるだろう。
そう今まで散々書いてきた method Main(){ ~ } にとても良く似ている。
それもそのはず method Main(){ ~ } は”Main”という名のメソッドの定義なのだ。
Mainメソッド はFoooエンジンが必ず最初に実行するメソッドである。
上の例は”Main”とは別に”フェードイン”という名前のメソッドを定義している。

メソッドの呼び出し

定義したメソッドを利用するにはcall文を使う。
call文の書き方は若干特殊だ。


  call @フェードイン();

callという名が示す通り、メソッドを利用することを一般的に「メソッドを呼び出す」と言う。
メソッドを呼び出すと、処理はいったん呼び出し先のメソッドに移り、
そのメソッド命令を最後まで実行し終えると呼び出し元に戻る。

メソッドのパラメータ定義

よく使う演出とは言っても、演出が完全に決まり決まっていることは少ない。
フェードインであっても白からのフェードイン、黒からのフェードインなど
多数のバリエーションが考えられる。
そのようなケース毎にメソッドを作っていたら、途端にメソッドの数は膨大なものになってしまう。

メソッドにパラメータを定義すればこの問題に対処できる。
詳しい説明をすると長くなるので省くが、次のようにするとフェードインメソッドが
色と、合成モードと、所要時間をパラメータにとることができるようになる。


method フェードイン(color $色=white, string $合成="Normal", int $時間=1000)
{
  CreateColor(name="幕", color=$色, w=1280, h=720, blend=$合成);
  Enter(to="幕");
  wait 500;
  Opaque(to="幕", time=$時間, alpha=0%, step="DecSin");
}

呼び出しは次のようになる。


  call @フェードイン(色=white, 合成="Add");

ちょっとこのあたりはプログラミングの知識がないと理解が難しいかもしれない…
でも使い方を見れば、どういう意味なのかなんとなく想像がつくだろう。たぶん。
それでは組み込んでみよう。


style 普通 { face="MS ゴシック", size=32 }
style 小 { size=24 }
style つぶやき { interval=100, speed=250, effect="Rise" }

#base_param CreateBalloon(style="普通")
#base_param CreateFrame(outline_shape="Vivide"
  , outline_color=white, outline_thick=8)

method フェードイン(color $色=white, string $合成="Normal", int $時間=1000)
{
  CreateColor(name="幕", color=$色, w=1280, h=720, blend=$合成);
  Enter(to="幕");
  wait 500;
  Opaque(to="幕", time=$時間, alpha=0%, step="DecSin");
}

method Main()
{
  CreateImage(name="背景", image="背景.png"
    , x=0, y=0);

  CreateNode(name="コマ1"
    , x=642, y=0, sx=0%);
  CreateWindow(name="コマ1/窓"
    , w=405, h=720, ox="Center", propagation="CancelParentZoom");
  CreateImage(name="コマ1/窓/画像", image="コマ1.png"
    , x=20, y=324, ox="Center", oy="Middle", sx=110%, sy=110%
    , sampling="BieLinear");
  CreateOutline(name="コマ1/枠"
    , w=405, h=720, shape="Fade", color=RGBA(0,0,0,64), thick=8
    , ox="Center", adaptive=true);

  CreateFrame(name="コマ2", image="コマ2.png"
    , x="OutRight", y=65);
  CreateFrame(name="コマ3", image="コマ3.png"
    , x=407, y=505, ox="Center", oy="Middle", sx=0%, sy=0%
    , outline_color=black);

  CreateBalloon(name="台詞1"
    , text="それにしても...<BR>腹(はら)が減(へ)った"
    , x=585, y=65, shape="Dumpling", w=300, h=185, tail=-30);
  CreateBalloon(name="コマ3/台詞2"
    , style="小", text="<FONT style='つぶやき'>クククク…"
    , x=63, y=15, shape="Rock", w=220, h=125, tail=160);

  Enter(to="背景");
  Enter(to="コマ1");
  Enter(to="コマ1/窓");
  Enter(to="コマ1/窓/画像");
  Enter(to="コマ1/枠");

  call @フェードイン(色=white, 合成="Add");

  Zoom(to="コマ1", time=1000, sx=100%, step="DecSin");
  Move(to="コマ1/窓/画像", time=1000, x=-20, step="DecSin");
  WaitDecor();

  Enter(to="コマ2");
  Move(to="コマ2", time=800, x=780, step="Dec3");
  WaitDecor();

  Enter(to="台詞1", effect="Bound");
  WaitDecor();

  Enter(to="コマ3");
  Zoom(to="コマ3", time=1000, sx=100%, sy=100%, step="AccSig");
  wait 600;

  Enter(to="コマ3/台詞2", effect="Expand");
  WaitDecor();
}

フェードインをメソッド化したことでスクリプトが少し簡潔になり
何をしているのかよりわかりやすくなった。


デフォルト値を変える

Foooでは命令のパラメータの記述を任意に省くことができる。
パラメータの指定を省いた時には仕様上決められた値が自動的に使われるが、
この値のことを一般に『デフォルト値』と言う。既定値、初期値などとも言われる。
ちかごろ巷で

「もち、デフォ買いっスよ!」

などという言い回しを聞くことがあると思うが、
そのデフォとはこのデフォルトのことだ。

デフォルト値は便利なのだが、仕様上のデフォルト値は
あらゆるケースを想定しているわけではなく、とりあえずの値にすぎないので
デフォルト値がまったく期待にそぐわない値である場合がある。
「テキストデーカーのデフォルトの文字サイズが小さすぎる!」といったケースだ。
デフォルト値が期待しているものと異なると、
結局そのパラメータを毎回指定せねばならずものすごい手間が発生する。

#base_param文

そこでFoooにはデフォルト値を自由に変更するための機能がある。
それが#base_param文だ。
使い方は非常に簡単で、命令文のように記述できる。


#base_param CreateBalloon(style="普通")

CreateBalloon命令のstyleパラメータのデフォルト値は仕様上は “” だが、
この記述でデフォルト値が “普通” に変わる。
それでは組み込んでみよう。


style 普通 { face="MS ゴシック", size=32 }
style 小 { size=24 }
style つぶやき { interval=100, speed=250, effect="Rise" }

#base_param CreateBalloon(style="普通")
#base_param CreateFrame(outline_shape="Vivide"
  , outline_color=white, outline_thick=8)

method Main()
{
  CreateImage(name="背景", image="背景.png"
    , x=0, y=0);

  CreateNode(name="コマ1"
    , x=642, y=0, sx=0%);
  CreateWindow(name="コマ1/窓"
    , w=405, h=720, ox="Center", propagation="CancelParentZoom");
  CreateImage(name="コマ1/窓/画像", image="コマ1.png"
    , x=20, y=324, ox="Center", oy="Middle", sx=110%, sy=110%
    , sampling="BieLinear");
  CreateOutline(name="コマ1/枠"
    , w=405, h=720, shape="Fade", color=RGBA(0,0,0,64), thick=8
    , ox="Center", adaptive=true);

  CreateFrame(name="コマ2", image="コマ2.png"
    , x="OutRight", y=65);
  CreateFrame(name="コマ3", image="コマ3.png"
    , x=407, y=505, ox="Center", oy="Middle", sx=0%, sy=0%
    , outline_color=black);

  CreateBalloon(name="台詞1"
    , text="それにしても...<BR>腹(はら)が減(へ)った"
    , x=585, y=65, shape="Dumpling", w=300, h=185, tail=-30);
  CreateBalloon(name="コマ3/台詞2"
    , style="小", text="<FONT style='つぶやき'>クククク…"
    , x=63, y=15, shape="Rock", w=220, h=125, tail=160);

  CreateColor(name="幕", color=white, w=1280, h=720, blend="Add");

  Enter(to="背景");
  Enter(to="コマ1");
  Enter(to="コマ1/窓");
  Enter(to="コマ1/窓/画像");
  Enter(to="コマ1/枠");
  Enter(to="幕");
  wait 500;

  Zoom(to="コマ1", time=1000, sx=100%, step="DecSin");
  Move(to="コマ1/窓/画像", time=1000, x=-20, step="DecSin");
  Opaque(to="幕", time=1000, alpha=0%, step="DecSin");
  WaitDecor();

  Enter(to="コマ2");
  Move(to="コマ2", time=800, x=780, step="Dec3");
  WaitDecor();

  Enter(to="台詞1", effect="Bound");
  WaitDecor();

  Enter(to="コマ3");
  Zoom(to="コマ3", time=1000, sx=100%, sy=100%, step="AccSig");
  wait 600;

  Enter(to="コマ3/台詞2", effect="Expand");
  WaitDecor();
}

今回の例ではほんの少し記述がすっりした程度だが、
もっと長いスクリプトの場合、デフォルト値の指定は非常に重要になってくる。


ここまでのまとめ

ここまで毎回ちょこちょこと小さな変更を加えながら演出を強化してきたが
ほんとに小さな変更を繰り返してきたので、
それらの変更がどれほどの効果があるのかいまいちよくわからなかったかもしれない。

でも振り返って最初の段階のものと現段階のものとを見比べて見ると、
Foooの様々な機能がどれほどの演出効果をもたらしているかわかるだろう。

YouTube Preview Image YouTube Preview Image

ただ単に『動く漫画』のようなものを作ることは、技術的にはたいして難しいことではない。
『動く漫画』という新しいジャンルの持つ可能性を引き出し、
漫画ともアニメーションとも異なる独自の表現をより高いレベルで昇華させ、
実現することこそがFoooの目指したところである。

しかし演出は随分と強化されたが、スクリプトもかなりごちゃごちゃしたかんじになってきた。
「これだけの演出を実現するのにこんなに長いスクリプトが必要なのか」
と思うかもしれないが、プログラムをかじったことのある人はむしろ
「これだけの演出をたったこれだけで記述できるのか」と感じるだろう。

しかもこのような演出が「データ」ではなく、
「スクリプト」として実現されているというところがポイントだ。
次からはそのあたりの意義と、Foooのプログラマブルな機能について説明して行こうと思う。


窓を変形させる

今度は窓が閉じた状態から、だんだん開いていくような演出を作ってみよう。

窓が開くような動きはZoom命令で横方向の倍率を0%から100%に変化させてやればよい。
ただし窓だけでなく枠も一緒に拡大してやる必要があることに注意が必要だ。
一緒に拡大しないと枠がはずれたようなかんじになってしまう。

けれども前回ノードデーカーを使って窓と枠を兄弟にしておいたので、
これはまったく大した問題ではない。
親のノードデーカーを拡大すれば、窓と枠を一緒に拡大できるのだ。

ちゃちゃっ作ってみよう。
真ん中から拡大させるために窓と枠の原点を中央に移動させて
ノードデーカーにZoom命令を発行するだけだ。


style 普通 { face="MS ゴシック", size=32 }
style 小 { size=24 }
style つぶやき { interval=100, speed=250, effect="Rise" }

method Main()
{
  CreateImage(name="背景", image="背景.png"
    , x=0, y=0);

  CreateNode(name="コマ1"
    , x=642, y=0, sx=0%);
  CreateWindow(name="コマ1/窓"
    , w=405, h=720, ox="Center");
  CreateImage(name="コマ1/窓/画像", image="コマ1.png"
    , x=20, y=324, ox="Center", oy="Middle", sx=110%, sy=110%
    , sampling="BieLinear");
  CreateOutline(name="コマ1/枠"
    , w=405, h=720, shape="Fade", color=RGBA(0,0,0,64), thick=8
    , ox="Center");

  CreateFrame(name="コマ2", image="コマ2.png"
    , x="OutRight", y=65
    , outline_shape="Vivide", outline_color=white, outline_thick=8);
  CreateFrame(name="コマ3", image="コマ3.png"
    , x=407, y=505, ox="Center", oy="Middle", sx=0%, sy=0%
    , outline_shape="Vivide", outline_color=black, outline_thick=8);

  CreateBalloon(name="台詞1"
    , style="普通", text="それにしても...<BR>腹(はら)が減(へ)った"
    , x=585, y=65, shape="Dumpling", w=300, h=185, tail=-30);
  CreateBalloon(name="コマ3/台詞2"
    , style="小", text="<FONT style='つぶやき'>クククク…"
    , x=63, y=15, shape="Rock", w=220, h=125, tail=160);

  CreateColor(name="幕", color=white, w=1280, h=720, blend="Add");

  Enter(to="背景");
  Enter(to="コマ1");
  Enter(to="コマ1/窓");
  Enter(to="コマ1/窓/画像");
  Enter(to="コマ1/枠");
  Enter(to="幕");
  wait 500;

  Zoom(to="コマ1", time=1000, sx=100%, step="DecSin");
  Move(to="コマ1/窓/画像", time=1000, x=-20, step="DecSin");
  Opaque(to="幕", time=1000, alpha=0%, step="DecSin");
  WaitDecor();

  Enter(to="コマ2");
  Move(to="コマ2", time=800, x=780, step="Dec3");
  WaitDecor();

  Enter(to="台詞1", effect="Bound");
  WaitDecor();

  Enter(to="コマ3");
  Zoom(to="コマ3", time=1000, sx=100%, sy=100%, step="AccSig");
  wait 600;

  Enter(to="コマ3/台詞2", effect="Expand");
  WaitDecor();
}
YouTube Preview Image

………窓が開くような動きじゃない!

窓の中身まで一緒に変形してしまっている。
しかしまぁ…子は親にくっついていて1つの部品とみなせるわけだから
親を拡大縮小したら子も拡大縮小するのは当然の話だ。
でもこれは…期待していたものとはちょっと違う。
窓は変形させたいが、中身は変形して欲しくないのだ!

propagationパラメータ

こんな時はCreateWindow命令にpropagationパラメータを指定する。
propagationとは随分長ったらしい名前だが”伝播(でんぱ)”という意味だ。
親や自分の姿勢の状態が子デーカーにどのように影響するか指定できる。
と言われても、よくわからないかもしれないが…
今回はとりあえず”CancelParentZoom”(親の倍率を伝えない)を指定する。

adaptiveパラメータ

さらによくよく見ると枠の太さも変化してしまっている。
拡大縮小した際に枠の太さが変化しないようにするには
CreateOutline命令にadaptiveパラメータを指定する。
adaptiveとは”順応性”という意味だ。
adaptive=trueとすると、囲う領域が変化しても枠の太さが変化しないようになる。

それでは組み込んでみよう。


style 普通 { face="MS ゴシック", size=32 }
style 小 { size=24 }
style つぶやき { interval=100, speed=250, effect="Rise" }

method Main()
{
  CreateImage(name="背景", image="背景.png"
    , x=0, y=0);

  CreateNode(name="コマ1"
    , x=642, y=0, sx=0%);
  CreateWindow(name="コマ1/窓"
    , w=405, h=720, ox="Center", propagation="CancelParentZoom");
  CreateImage(name="コマ1/窓/画像", image="コマ1.png"
    , x=20, y=324, ox="Center", oy="Middle", sx=110%, sy=110%
    , sampling="BieLinear");
  CreateOutline(name="コマ1/枠"
    , w=405, h=720, shape="Fade", color=RGBA(0,0,0,64), thick=8
    , ox="Center", adaptive=true);

  CreateFrame(name="コマ2", image="コマ2.png"
    , x="OutRight", y=65
    , outline_shape="Vivide", outline_color=white, outline_thick=8);
  CreateFrame(name="コマ3", image="コマ3.png"
    , x=407, y=505, ox="Center", oy="Middle", sx=0%, sy=0%
    , outline_shape="Vivide", outline_color=black, outline_thick=8);

  CreateBalloon(name="台詞1"
    , style="普通", text="それにしても...<BR>腹(はら)が減(へ)った"
    , x=585, y=65, shape="Dumpling", w=300, h=185, tail=-30);
  CreateBalloon(name="コマ3/台詞2"
    , style="小", text="<FONT style='つぶやき'>クククク…"
    , x=63, y=15, shape="Rock", w=220, h=125, tail=160);

  CreateColor(name="幕", color=white, w=1280, h=720, blend="Add");

  Enter(to="背景");
  Enter(to="コマ1");
  Enter(to="コマ1/窓");
  Enter(to="コマ1/窓/画像");
  Enter(to="コマ1/枠");
  Enter(to="幕");
  wait 500;

  Zoom(to="コマ1", time=1000, sx=100%, step="DecSin");
  Move(to="コマ1/窓/画像", time=1000, x=-20, step="DecSin");
  Opaque(to="幕", time=1000, alpha=0%, step="DecSin");
  WaitDecor();

  Enter(to="コマ2");
  Move(to="コマ2", time=800, x=780, step="Dec3");
  WaitDecor();

  Enter(to="台詞1", effect="Bound");
  WaitDecor();

  Enter(to="コマ3");
  Zoom(to="コマ3", time=1000, sx=100%, sy=100%, step="AccSig");
  wait 600;

  Enter(to="コマ3/台詞2", effect="Expand");
  WaitDecor();
}
YouTube Preview Image

窓の中が変形しなくなった!
propagationパラメータはちょっと奇妙なパラメータだが、
使いこなせればこのように特殊な演出も案外簡単に作ることができる。