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

クラス化する

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

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

クラス化だ!

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

クラスはプログラミング用語では「オブジェクトの設計図」を意味する。
オブジェクトとは「機能的な構造を持つ部品」のようなものである。
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パラメータはちょっと奇妙なパラメータだが、
使いこなせればこのように特殊な演出も案外簡単に作ることができる。


窓に枠をつける

ウィンドウデーカーで作った「中身を動かせるコマ」には
枠がなかったことに気がついただろうか。
コマの枠はフレームデーカーの機能なのだから、当然といえば当然…
でもコマとして作っているのでやはり枠が欲しい…

Decor_Outline アウトラインデーカー

そんな時はアウトラインデーカーという特殊なデーカーを使う。
アウトラインデーカーは枠を表示するデーカーだ。
アウトラインデーカーを使えばフレームデーカーのものとまったく同じ枠を作ることができる。
アウトラインデーカーを作るにはCreateOutline命令を使う。


CreateOutline(name="枠"
  , w=405, h=720, shape="Fade", color=RGBA(0,0,0,64), thick=8);

さてさて、枠を作るのはこれだけで済むのだけれど
どーせなら窓と枠を1つの部品としてみなせるようにしておきたい。
そんな時は…そう、親子にすればいい。

ただウィンドウデーカーの子としてアウトラインデーカーを作ると、
アウトラインデーカーはウィンドウデーカーの表示制限機能によって見えなくなってしまう。
逆にアウトラインデーカーの子としてウィンドウデーカーを作ればよさそうではあるが
枠に窓がくっついているというのもイマイチ妙だ。
どちらかというと、窓と枠は兄弟なかんじがする。

Decor_Node ノードデーカー

そんな時に使うのがノードデーカーだ。
ノードデーカーは何も表示せず、何の機能も持たない最もシンプルなデーカーだ。
デーカーをひとまとめにしたい時に親として使う。
ノードデーカーを作る命令は…そう、CreateNodeだ。


CreateNode(name="コマ1"
  , x=440, y=0);

それでは組み込んでみよう。
全体としてコマとみなせるように名前も整理しておこう。


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=440, y=0);
  CreateWindow(name="コマ1/窓"
    , w=405, h=720);
  CreateImage(name="コマ1/窓/画像", image="コマ1.png"
    , x=222, 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);

  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;

  Move(to="コマ1/窓/画像", time=1000, x=182, 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

やりたいことは単純なのに、デーカーの親子関係にこだわったせいで
ちょっと面倒な作業になってしまった。
しかしこのようにノードデーカーを使って親子関係を整理しておくことが
演出を強化していく上で後々効いてくるのだ。


バイリニア補間する

前回窓の中で画像が動くようにしたが、その時もともとの画像を使いまわして
拡大してスライドさせていたので、画像がちょっと汚くなってしまっていた。

Sampling_Nearest

画像を拡大するとこのように汚くなるのは、
画像を拡大表示する際のプログラム的な仕組みによるものだ。

画像を拡大した時の見た目は”補間方法”によって変わってくる。

Adobe Photoshopを使ったことがある人なら画像の解像度を変える際に
『ニアレストネイバー』『バイリニア』などの言葉を目にしたことがあるだろう。
それらは”補間方法”の名前だ。

『ニアレストネイバー』は単純な近似をとる方法で「補間なし」とも言われる。
非常に高速である反面、とても汚く拡大される。
『バイリニア』は線形補間をとる方法で、やや低速だが綺麗に拡大される。

Foooではこの『ニアレストネイバー』と『バイリニア』に対応しており、
どちらを使うかはデーカーごとに sampling パラメータで指定することができる。
何も指定しない場合は、『ニアレストネイバー』が使われる。
sampling パラメータには”Nearest”, “BieLinear”のいずれかを指定できる。
“BieLinear”を指定すると次のようになる。

Sampling_BieLinear

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


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);

  CreateWindow(name="窓"
    , x=440, y=0, w=405, h=720);
  CreateImage(name="窓/コマ1", image="コマ1.png"
    , x=222, y=324, ox="Center", oy="Middle", sx=110%, sy=110%
    , sampling="BieLinear");
  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="窓");
  Enter(to="窓/コマ1");
  Enter(to="幕");
  wait 500;

  Move(to="窓/コマ1", time=1000, x=182, 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

動画が小さいので拡大してなんとか差がわかるレベルかも…
バイリニア補間は綺麗だが、決して軽い処理ではないのでうまく使い分けるのが肝心だ。


コマの中身を動かす

今度はコマの中身を動かしてみよう。
とはいえフレームデーカーは中身を動かすことができない。
中身を動かせるコマを作るには『ウィンドウデーカー』という特殊なデーカーを使う。

Decor_Window ウィンドウデーカー

ウィンドウデーカーは表示範囲を制限するデーカーだ。
ウィンドウデーカー自体は何も表示しないが、
子デーカーの表示を自己の領域内に制限するという働きをする。
そう、まさしく

Window(窓)のような機能を持っているのである。

ウィンドウデーカーで窓を作り、その中で画像を動かすことで
コマの中身が動いているかのような演出を実現できる…というわけだ。

ウィンドウデーカーを作成するにはCreateWindow命令を使う。


CreateWindow(name="窓"
  , x=440, y=0, w=405, h=720);
CreateImage(name="窓/コマ1", image="コマ1.png"
  , x=222, y=324, ox="Center", oy="Middle", sx=110%, sy=110%);

ただ単に親子にしているだけのように見えるが、これで表示範囲を制限できる。
実際に組み込んでみよう。


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);

  CreateWindow(name="窓"
    , x=440, y=0, w=405, h=720);
  CreateImage(name="窓/コマ1", image="コマ1.png"
    , x=222, y=324, ox="Center", oy="Middle", sx=110%, sy=110%);
  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="窓");
  Enter(to="窓/コマ1");
  Enter(to="幕");
  wait 500;

  Move(to="窓/コマ1", time=1000, x=182, 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

コマの中身が動いているかのようになった!


親子にする

十回近くにわたって手を入れたかいもあって「動く漫画」もずいぶんさまになってきた。
しかしよくよく演出を見てみると「クククク…」の吹き出しが出てくるのが
どーにもワンテンポ遅いかんじがする。

すると言ったらする!

どうやらコマの表示が終わるのをキッチリ最後まで待っているのが原因のようだ。
ここはひとつコマの表示が終わる前に吹き出しを表示し始めることにしよう。

けれども問題はそう簡単ではない。
コマは拡大しながら表示されるので、コマの表示が終わる前に吹き出しを表示させると
吹き出しの尾がコマの中のキャラクターの口もとから離れてしまうのだ。
それはちょっと具合が悪い。

コマの拡大に合わせて吹き出しも拡大させればいいと思うかもしれない。
しかしそうするとまた新たな問題にぶつかる。
コマの中のキャラクターの口もとの位置は、拡大によって移動していくので、
吹き出しの尾の位置を口もとの位置にきっちり合わせて動かすのが至難の技なのだ。
がんばればなんとかなることはなるが…なんとも泥臭い。
もっとスマートに実現したいところだ。

ではどうすればいいと言うのか。
ズバリ

吹きだしをコマにくっつけてしまえばいい。

吹きだしをコマにくっつけて、吹きだしとコマが一緒に動くようにすればいいのだ。
実はデーカーには他のデーカーをくっつけることができる機能がある。
この時、くっつけられる方を『親』、くっつける方を『子』と呼ぶ。
またこのようにデーカーをくっつけることを、デーカーを『親子にする』と言う。

親子とはプログラマにとっては馴染み深い言葉であるが、一般的にはわかりにくい言葉かも知れない。
ここで言う『親子』とは、人間の親・子とは意味がちょっと違う。
賭け事の親・子でもない。構造的な関係を表す比喩的な言葉だ。
それはこの関係構造を図で表すと木が枝を伸ばしたような図になり
それがある種の家系図のように見えるからだ。

──────ややこしい話はともかく
どうやってデーカーを親子にするのかサンプルを見てみよう。


method Main()
{
  CreateImage(name="親", image="親.png", x=100, y=100);
  CreateImage(name="親/子1", image="子.png", x=50, y=50);
  CreateImage(name="親/子2", image="子.png", x=50, y=50);
  CreateImage(name="親/子3", image="子.png", x=50, y=50);
  Enter(to="親");
  Enter(to="親/子1");
  Enter(to="親/子2");
  Enter(to="親/子3");
}

あるデーカーを子にしたい場合、nameパラメータに”親の名前/子の名前”という形で名前を指定する。
これだけでそのデーカーを子にすることができる。
このサンプルでは”親”に、”子1”、”子2”、”子3”の3つのデーカーを子として作成している。
toパラメータで子のデーカーを指定する場合も、同様に親の名前を書く必要があることに注意しよう。

“子1”、”子2”、”子3”の位置は”親”からの相対位置になる。
子は親が移動すれば一緒に移動し、親が拡大すれば一緒に拡大する。
完全に親の動きに追従するので、ある意味子は親の一部になっているとみなせる。

とまぁ…説明が長くなったけれど…やることは単純だ。
それでは組み込んでみよう。
子は親からの相対位置になるのでx, yパラメータの修正も忘れずに。


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);

  CreateFrame(name="コマ1", image="コマ1.png"
    , x=440, y=0
    , outline_shape="Fade", outline_color=RGBA(0,0,0,64), outline_thick=8);
  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="幕");
  wait 500;

  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

吹き出しがコマにくっついているかんじになった!
うん…ちょっと効果がわかりにくいカモシレナイ。