ぷちコンのゲームを作っていてちょっとはまってしまった部分があるので、推察ではありますが解説と回避方法をご紹介します。
皆さん、Sequenceノードは使用していますか?
複数の処理を1つの連続した処理として実行してくれるノードです。
Blueprintは連続した処理をExecピンを繋げることで簡単に実行することが出来ますが、あまりに長い処理になってくると横方向にノードが伸びていって見難くなってしまいます。
文字列を改行するかのように折り返す方法では右から左への長いラインが出来てしまいますし、関数化やマクロ化は関連性の薄い処理をまとめるのに躊躇します。
そんな時に使いやすいのがSequenceノードです。
例えばこんな処理を考えてみます。
"Hello" "World"と2行に文字列を表示するだけです。
これをSequenceノードを使って実装するとこうなります。
結果は変わらず、2行に表示されます。
つまり、この2つの処理は等価ということになります。正確ではありませんが、少なくとも結果は等価です。
では、ここでちょっと改造してこういう状態にしてみます。
2秒のDelayを間に挟んでみました。
Delayは次の処理までの時間を遅らせるノードですので、"Hello"と表示された2秒後に"World"と表示されます。
これをSequenceノードで実装してみます。
この結果は上と同じ…になるかと思いきや、そんなことはありません。
結果は"Hello"の後にすぐに"World"が表示されます。Delayノードの2秒は完全に無視されてしまいます。
この結果から、Execピンを連続して繋ぐ処理とSequenceノードを使う処理が等価ではないことがわかると思いますが、もう少し動作を確認するために以下の2つの処理を試してみましょう。
最初の実装では"Hello"と表示された2秒後に"My", "World"と連続して表示されます。
次のSequenceを使った実装では"Hello", "World"と連続して表示された2秒後に"My"と表示されます。
Sequenceと言っておきながら、まるで並列に動作しているかのような印象を受けますが、これこそがSequenceノードの動作なのです。
では、続きからなぜそうなるのかの考察と、LoopにおけるDelayの問題点を考えていこうと思います。
注意。
今回の記事はソースコードを読んで完全に理解した結果ではありません。
あくまでも動作と普通の実装から推察したことに過ぎません。
間違っている場合もありますので、間違ってたら教えていただけると嬉しいです。
さて、Sequenceを使うとまるで並列動作しているかのような挙動になってしまうことはわかりました。
実はSequenceを使わなくても同じ挙動を実現する方法があります。
このようにカスタムイベントとして処理を呼び出すとSequenceを使ったのと同じ結果が得られます。
つまり、Delayが無視される状態になるわけです。
推察ではありますが、Sequenceノードは暗黙的にこのようなカスタムイベントを生成しているものと思われます。
では、直列につないでいる処理とカスタムイベントを介して直列につないでいる処理はどう違うのでしょう?
これを考えるために緑の小人さんを召喚したいと思います。
緑の小人さんは”イベント”という種族です。
まず、最初に直列につないでいる処理を小人さんに実行してもらいましょう。
BeginPlayという名前の小人さんは”Hello”と言ってから2秒待って”World”と言います。
この処理はBeginPlayさんが一人で行うので、前の仕事が終わる前に次の仕事はできないのは自明ですね。
では、カスタムイベントを使っている場合を見てみましょう。
BeginPlayが発行された最初のフレームはこのようになります。
緑の小人さんであるBeginPlayには二人の舎弟がいました。
CustomEvent_0さんとCustomEvent_1さんです。
彼は仕事が面倒だったのでこの二人にそれぞれ仕事を投げました。
仕事を投げるともうやることがないので、そのまま帰宅しています。
最初に仕事を受けたCustomEvent_0さんは”Hello”と言った後に2秒待てと言われました。
なので、2秒待つことにしました。
同じ日に仕事を受けたCustomEvent_1さんはCustomEvent_0さんが”Hello”と言った後に”World”と言いました。
そして仕事がなくなったので帰りました。
2秒後、CustomEvent_0さんは次の仕事があると思っていたのですが、実は仕事がない事に気づきました。
しかももう二人とも帰ってしまっていました。
一人寂しく家路につくCustomEvent_0さんでした。
と、こんな感じになるわけです。
もうちょっと詳しく説明すると、イベントの呼び出しというのはあくまでも呼び出すだけの行動なのです。
呼び出したイベントが何を行っているかは呼び出し元は感知しません。
その呼び出したイベントによって世界が滅ぼうがゾンビの群れが湧こうが知ったこっちゃないわけです。
ただし、呼び出されたイベントの、”同一フレーム内での”処理順番だけは維持されます。
つまり、大元のイベントがCustomEvent_0の後にCustomEvent_1が呼び出されれば、CustomEvent_0の方が先に処理されます。
前述の例の場合、BeginPlayが呼び出したフレーム内においてはCustomEvent_0が先に処理され、CustomEvent_1が後で処理されます。
しかし、次のフレームではCustomEvent_1はすでに処理を終えているので何も行いませんが、CustomEvent_0は2秒待てと言われているので2秒経過するまで処理を先に進めることが出来ないというわけです。
このような問題はDelayやTimelineといった時間に関する処理を行った場合に限られます。
DelayやTimelineが関係していないのであれば直列につないだものもカスタムイベントを用いたものもSequenceノードを用いたものも同一の結果を生み出すものと思われます。
もしもDelayやTimelineを間違っても入れてほしくない場合はカスタムイベントではなく関数を用いましょう。
関数は呼び出した直後に実行され、時間に関するノードを受け付けないことが確定しています。
マクロの場合は時間に関するノードを挟むことが出来ますが、これは仕事を1つの形にまとめただけで処理はそのイベントが行うことになります。
ここで注意が必要なのは、マクロはDelayやTimeline、Sequenceノードを用いることが出来ます。
特にSequenceノードが使われているとそれが隠蔽されてしまい、予想外の挙動を示すことがあります。
わかりやすい例はループ関係の処理です。
ForLoopやWhileLoopノードは内部でSequenceを用いています。
例えばForLoopで10回のループを回すことを考えましょう。
1回めの処理の最後でDelayを2秒で置くと1回めのループ処理が終わって2秒後に2回めのループ処理が走る…という形になりません。
そのような処理をしたい場合はループノードを使用せずに自前のループ処理を作成しましょう。
例として、複数フレームにまたがるカウントアップ処理を考えましょう。
ゲームではボーナススコアをゲットした場合に一気にトータルスコアに加算されるのではなく1フレームに100ポイントずつ加算されていく、という処理がありますよね。
これを実装するには以下のようにします。
ここではスコア0から0.02秒毎に1ずつカウントアップしていって100になったら止まります。
Delayの後にブランチに直接戻していますが、WhileLoopを使った場合には内部でSequenceが使われているので、1フレームで100個のカスタムイベントが暗黙的に作成、実行されてしまいます。
結果、一瞬にして100までカウントアップされるでしょう。
今回の話はDelayやTimelineのような時間に関する処理とSequenceやカスタムイベントの関係についてでしたが、処理をサブルーチン化する場合に関数にするかマクロにするかカスタムイベントにするかの判断材料にもなると思います。
それぞれには特徴があるので、適切な処理を選ぶといいでしょう。