実践Flutter

Flutterでスマホを中心にアプリ制作していきます。

Flame06:Flame公式ドキュメントを読み下していく6

はじめに

Flame公式ドキュメントを読み下していく第6弾の今回はエフェクトです。

エフェクトというのは、フィールド上にアイテムが出現して、それが一定時間で点滅しだして、消えてしまう、といったような時間とともに色や形、大きさや透明度などが変わるものをワンタッチで扱えるようにする仕組みです。

これによってかなり放っておいてもキャラクタたちが生き生き動くようになってくるというわけです。非常に便利ですよね。

主人公キャラについても、燃えたり、凍ったり、毒の状態になったり、瀕死になったりで色を変えてみせるとプレイヤーにその状態がうまく伝わりますよね。このときにいちいち別の絵を用意しなくても、エンジン側でうまく変形・変色して表示できるのはありがたい機能です。

今回はその前半戦ということでエフェクトの種類、次回はエフェクトコントローラーの種類を見ていきます。







Effects

エフェクトは、別のコンポーネントの外観や性質に変更を加えるために使われる特別なコンポーネントです。

例えばあなたがパワーアップアイテムを集めるようなゲームを作っているとして、そのパワーアップアイテムがマップ上にランダムにあり、しばらく時間がたつと消えていくとします。すぐにわかるように、パワーアップはSpriteComponentで作れますよね。そしてマップ上に置けばいいのですが、じつはもっとずっとうまくやることができます。

パワーアップが最初に表示されたときにアイテムを0から100%のサイズに膨らませるために「ScaleEffect」を追加しましょう。 そしてアイテムをわずかに上下に動かすために、無限に繰り返される交互の「MoveEffect」を追加します。 次に、アイテムを3回「点滅」させる「OpacityEffect」を追加します。この効果には、30秒の遅延を組込みます。あるいはパワーアップをそのまま表示させておいてもよいですね。 最後に、「RemoveEffect」を追加します。これにより、指定した時間が経過するとゲームツリーからアイテムが自動的に削除されます(「OpacityEffect」の終了直後にタイミングを設定することもできます)。

ここでわかる通り、いくつかの単純な効果で、単純で動きのないスプライトをはるかに興味深いアイテムに変えました。 さらに重要なことは、コードの複雑さが増すことがないことです。エフェクトは、追加されると自動的に機能し、終了するとゲームツリーから自動的に削除されます。

https://docs.flame-engine.org/1.0.0/effects.html

ここへきてFlameのドキュメント史上はじめて、新しく出てきた概念がなになのかをそこそこ説明してくれていますね。

Effectはスプライトを動かしたり消したりといった動きをつける演出を着脱可能な部品として提供してくれます。

フレームワークとして煩雑になりがちな部品を提供してくれているというのは非常にありがたいところです。


概要

「効果」の機能は、一部のコンポーネントのプロパティを時間の経過とともに変化させることです。 これを実現するには、「効果」でプロパティの初期値、最終値、および時間の経過とともにどのように進行するかを知る必要があります。 通常、初期値はエフェクトによって自動的に決定され、最終値はユーザーによって明示的に提供され、時間の経過に伴う進行は `EffectController`によって処理されます。

Flameには様々なのエフェクトがあり、独自のエフェクトを作成することもできます。例えば次のようなエフェクトです:

  • ColorEffect
  • MoveEffect.by
  • MoveEffect.to
  • MoveAlongPathEffect
  • RotateEffect.by
  • RotateEffect.to
  • ScaleEffect.by
  • ScaleEffect.to
  • SizeEffect.by
  • SizeEffect.to
  • OpacityEffect
  • RemoveEffect

EffectControllerはエフェクトがどのように時間の経過に従って展開していくかを決めるものです。最初のエフェクトの進捗が0%で、最後が100%だとして、EffectControllerはその中での変化に物理的な時間を対応させます。つまり実際の秒単位の時間を論理的な0から1の時間に対応させます。

EffectControllerも様々なものが用意されています:

  • EffectController
  • LinearEffectController
  • ReverseLinearEffectController
  • CurvedEffectController
  • ReverseCurvedEffectController
  • PauseEffectController
  • RepeatedEffectController
  • InfiniteEffectController
  • SequenceEffectController
  • DelayedEffectController


https://docs.flame-engine.org/1.0.0/effects.html

今回紹介していくエフェクトとエフェクトコントローラー一覧です。一つ一つの内容をこれから見ていきます。


ビルトインのエフェクト

Effect

基盤となるEffectクラスは、それ自体は抽象クラスなので、直接使うものではありません。いくつかの共通する機能の枠組みを提供します:

  • メソッドeffect.pause()とeffect.resume()でそのエフェクトを一時停止・再開させる。effect.isPausedで今現在そのエフェクトが一時停止中かを確認できます。
  • メソッドeffect.reverse()によって時間の向きを逆転させる。effect.isReversedで逆転しているかどうかを確認できます。
  • 属性値removeOnFinish(デフォルトtrue)によって、エフェクトのコンポーネントが、そのエフェクトの終了時にゲームツリーから削除されるかどうかを指定できます。そのエフェクトを再利用するつもりであればfalseにします。
  • メソッドreset()はエフェクトをその初期状態に戻し、再び実行する準備をします。

https://docs.flame-engine.org/1.0.0/effects.html

すべてのエフェクト基盤のフレームワークを提供するのが抽象クラスのEffectです。基本的にビルトインの(予め組み込まれた)エフェクトを使う分には意識する必要がありませんが、オリジナルのエフェクトを作るときには各メソッドの制約をチェックしておくとよいですね。

MoveEffect.by

このエフェクトはPositionComponentに適用され、あらかじめ指定されたoffsetの量だけ動かします。このoffsetは現在の位置からターゲットへの相対量です:

final effect = MoveEffect.by(Vector2(0, -10), EffectController(duration: 0.5));

もしコンポーネントがVector2(250, 200)にあるとしたとき、このエフェクトの終了地点はVector2(250, 190)になります。

複数の移動エフェクトを一つのコンポーネントに同時に作用させることができ、結果はすべての個々のエフェクトを複合させたものになります。

https://docs.flame-engine.org/1.0.0/effects.html

動きを決めるもっとも単純なエフェクトです。複数のエフェクトを同時に作用させて複雑な動きを実現できるというのもポイントですね。


MoveEffect.to

このエフェクトはPositionComponentを現在の位置から特定の目的地となる点までまっすぐ移動させるものです。

final effect = MoveEffect.to(Vector2(100, 500), EffectController(duration: 3));

推奨されていませんが、同時に複数のこのエフェクトを同じコンポ―ネントに適用することは可能です。

https://docs.flame-engine.org/1.0.0/effects.html

MoveEffect.byはオフセット(差分)なので、現在の位置から相対的に移動量を与えていました。

それに対してこちらのMoveEffect.toは絶対的なゴール地点を示し、そこに向かって一直線に進むエフェクトです。

エフェクトの複合は種類によって非推奨だったりすることもある、というのがわかりますね。これはチェックしておいたほうが良さそうです。


MoveAlongPathEffect

このエフェクトはPositionComponentを特定のパスに沿って動かすものです。パスは非線形のセグメントを持つことができ、それらは一つにつながっている必要があります。パスはVector2.zero()から始まることが推奨されています。これはコンポーネントのポジションが突然ジャンプすることを避けるためです。

final effect = MoveAlongPathEffect(
  Path() ..quadraticBezierTo(100, 0, 50, -50),
  EffectController(duration: 1.5),
);

オプションのフラグ「absolute」はtrueであればパスの定義が絶対的であることを意味します。つまりパスのスタート地点にジャンプして移動して、そのパスに従って動く形になります。

もう一つのフラグとして「oriented」があります。これをtrueにすると、エフェクトのターゲットはパスに従って動くだけではなく、そのカーブに合わせて回転します。このフラグはmove-とrotate-のエフェクトを同時に適用していることになります。

https://docs.flame-engine.org/1.0.0/effects.html

「パスが非線形のセグメントを持つことができる」というのは、サンプルにもありますが例えばベジェ曲線で表される節が途中に入っていても良いということです:


ja.wikipedia.org

「線形」というのが1次関数で表されるような直線。それ以外の曲がっている線が非線形です。

絶対パスと相対パス、というように絶対と相対がよくでてきます。絶対的な位置は画面上の(x, y)の座標が決められたらまさにその絶対的位置のことを表し、相対的な位置はあるキャラクタがいたときに、その現在位置から上にx, 横にy移動させた位置、ということを表します。

「oriented」というのは「向きを調整されている」ということを意味するものですが、移動に従って回転してくれるというのは便利ですね。


RotateEffect.by

エフェクトのターゲットを時計回りに指定した角度だけ回転させる。角度はラジアンで指定。次の例は90°(=tau/4ラジアン)を時計回りに回転させる:

final effect = RotateEffect.by(tau/4, EffectController(2));

https://docs.flame-engine.org/1.0.0/effects.html

tau(タウ)というのは自分はあまり馴染みがないのですが、tau=2πです。


dic.nicovideo.jp

日本の教育課程ではπを使うのが普通ですよね。でも確かに1周=τ(タウ)ラジアンというのもわかりやすいかもしれません。

余談ですがDartのMathパッケージに定数tauを追加すべきだという議論が残っていますね:


github.com

なるほど、2*piと同じ値であるものの、この2*piという演算が重いからtauを追加したほうが良いというわけですね。piはおなじみの3.141592...というdoubleの値で、たしかにこれに2を掛ける演算を省けるならば、スピードが求められるゲームの中での演算では重要な違いが出てきます。

RotateEffect.to

特定の指定された角度まで、ターゲットを回転させます。例えば次のコードではターゲットを東の向き(0°が北だとして90°が東、180°が南、270°が西)に回転させます:

final effect = RotateEffect.to(tau/4, EffectController(2));

https://docs.flame-engine.org/1.0.0/effects.html

RotateEffect.byが相対的な回転、RotateEffect.toが絶対的なゴールを設定した回転、というわけですね。これはMoveEffectと同じ関係性です。


ScaleEffect.by

このエフェクトは指定された量の拡縮を行います。次の例だと50%拡大することになります:

final effect = ScaleEffect.by(Vector2.all(1.5), EffectController(0.3));

https://docs.flame-engine.org/1.0.0/effects.html

Vector2指定ということで横軸、縦軸それぞれで何倍にするかを指定できるということですね。サンプルのコードでは縦にも横にも1.5倍指定です。


ScaleEffect.to

このエフェクトはScaleEffect.byとにていますが、拡縮の絶対量を指定します。

final effect = ScaleEffect.to(Vector2.zero(), EffectController(0.5));

https://docs.flame-engine.org/1.0.0/effects.html

これもRotateEffect.byとRotateEffect.toと同じ関係ですね。サンプルはVector2.zero()指定ということでゼロに向かって拡縮する、つまり消してしまうということですね。


SizeEffect.by

このエフェクトはターゲットのコンポーネントのサイズを変えます。例えばもしターゲットのサイズがVector2(100, 100)だった場合、つぎのエフェクトを適用すると新しいサイズはVector2(120, 50)になります:

final effect = SizeEffect.by(Vector2(20, -50), EffectController(1));

PositionComponentのサイズは負の数になりえません。もしこのエフェクトが適用された結果のサイズが負になる場合、ゼロで変化が止まります。

このエフェクトが機能するためには、ターゲットのコンポーネントはレンダリングのときにそのサイズを考慮に入れたレンダリングをしている必要があります。加えてサイズの変更は子コンポーネントに反映されていくわけではないことに注意してください。SideEffectではなくScaleEffectを使う場合は、その拡縮は子コンポーネントにも同時に作用します。

https://docs.flame-engine.org/1.0.0/effects.html

拡大縮小ということではなく、与えられた任意のサイズに関する差異を反映するように大きさを変更するということですね。拡縮は倍率であるのに対し、こちらは何ピクセル伸ばすか、縮めるかという指定ができます。

注意書きにある通り、子コンポーネントにその拡縮が伝搬していきませんので、そうしたい場合はScaleEffectを使います。ピクセル数から倍率を計算すれば特に問題なくSideEffectを使いたい場合でも、ScaleEffectを代わりに使えるという場面は多いかと思います。


SizeEffect.to

指定されたサイズにターゲットの大きさを変更する。ターゲットサイズは負の数を指定できません:

final effect = SizeEffect.to(Vector2(120, 120), EffectController(1));

https://docs.flame-engine.org/1.0.0/effects.html

こちらは絶対的に、サンプルコードでは120、120のサイズに大きさを合わせていきます。


OpacityEffect

コンポーネントの透明度を指定されたα値に向けて変えていくエフェクトです。現段階では、このエフェクトはHasPaintミックスインを持っているコンポーネントにのみ適用可能になっています。もしターゲットのコンポーネントが複数のペイントを持っている場合、このエフェクトはそれぞれのペイントIDのパラメタに対して適用することができます:

final effect = OpacityEffect.to(0.5, EffectController(0.75));

透明度の値が0というのは、完全に透明なコンポーネントということになります。そして透明度の値が1だと完全に不透明(つまり元画像と同じ)です。便利コンストラクタとしてOpacityEffect.fadeOut()とOpacityEffect.fadeIn()というのがあって、それぞれ完全に消失させるエフェクト、完全に可視化していくエフェクトを実現します。

https://docs.flame-engine.org/1.0.0/effects.html

α値というのが透明度を表す値として一般に使われています。0~1の値で0に近いほど消えていって、0だと完全に消失します。fadeOut()はだんだんこのα値を増やしていって消失させるというエフェクトですね。


RemoveEffect

このエフェクトはゲームツリーからもコンポーネントを削除するものです。そして画面からも消えます。

final effect = RemoveEffect(delay: 10.0);


https://docs.flame-engine.org/1.0.0/effects.html

コンポーネントを削除するエフェクトです。これは単純ですね。アイテムが一定時間で自動的に削除される場合、その時間制限を設定しておくと消えてしまいます。

ColorEffect

このエフェクトでは、ベースカラーのペイントから、与えられた範囲で色合いを変えていきます。

myComponent.add(
  ColorEffect(
    const Color(0xFF00FF00),
    const Offset(0.0, 0.8),
    EffectController(duration: 1.5),
  ),
);

オフセット値はどれだけの色がコンポーネントに反映されるかを表します。この例では0%からスタートして80%にまで色合いの反映割合が引き上げられます。

注意:このクラスの実装とFlutterのColorFilterクラスの機能のため、このエフェクトは他のColorEffectとミックスすることができません。もし複数のColorEffectが一つのコンポーネントに加えられた場合、最後のエフェクトのみ有効になります。

https://docs.flame-engine.org/1.0.0/effects.html

キャラクタが燃えるとか、凍るとか、電撃だとかで色合いを変えたいときが頻繁にあります。こういうときはこのColorEffectが便利です。

注意書きにあるとおり、複合のColorEffectは無効になりますので、この点は注意したいところです。


新しいエフェクトを作る

Flameは様々なエフェクトを提供しますが、それでも十分ではないと感じるときが来ると思います。幸運にも、新しいエフェクトを作るのは簡単です。

全てのエフェクトはEffectクラスを継承しています。あるいはもう少し機能が追加されたComponentEffectあるいはTransform2DEffectを使うとよいかもしれません。

EffectクラスのコンストラクタはEffectControllerのインスタンスを要求します。殆どの場合、あなたがつくるコンストラクタでもコントローラを使うでしょう。このとき、エフェクトコントローラーはその複雑性がよくカプセル化されていて、その機能を再構築する必要なく活用することができます。

最後に、apply(double progress)というメソッドを実装します。これはupdateのたびに呼び出されるものです。このメソッドのなかであなたのエフェクトに従ってターゲットを変化させていきます。

加えて、エフェクトの開始と終わりになにか処理を加えたい場合は、onStart()とonFinish()のコールバックメソッドを実装すればOKです。

applyメソッドを実装する際は、相対的なupdateのみを実装することをおすすめします。すなわちターゲットの属性値についてダイレクトに固定値を設定するよりも、現在の値から段階的に増減させる形です。これによって複数のエフェクトが共存して変化を与えられるようになります。

https://docs.flame-engine.org/1.0.0/effects.html

自分独自のエフェクトを作るときもルールは簡単で、基本的にはベースとなる抽象クラスのEffect(あるいはそこに機能が加えられたクラス)を継承し、applyというメソッドを実装するということです。

このapplyはupdateのたびに呼び出されて、コンポーネントの状態に変化を加えるというものです。

この理屈がわかってさえいれば、あとはここに紹介したいくつかのエフェクトの実装をソースコードで見てみることで、独自のエフェクトを作ることは容易に可能かと思います。複数のエフェクトが混ざるときにうまく邪魔しないようにするというのは確かにポイントになります。








おわりに

Flameで使えるエフェクトの種類と、自作するためのガイドラインをざっとみてきました。

主人公キャラ、敵キャラ、アイテム、障害物などでそれぞれ様々なエフェクトが活用できそうですよね。

このエフェクトをさらにうまく動かしていくべく、次回はエフェクトコントローラーを見ていきいます。

f:id:linkedsort:20211218230039j:plain