今回のぷちコンで作ったUIの中から、スクロールするリストについて書こうと思います。
ゲーム内でグレイマンがタイルを踏むたびに、画面下部のリストに文字が追加されていきます。画面いっぱいになると、文字が追加されるたびに、一文字ぶん左にスクロールアウトするという仕様です。
実は結構ハマった部分なので、ちょっと難しい説明が出てきたりしますが、ひとまず中身について書いていきます。
この仕様のためのWidgetは2つ。文字を指示通りに切り替えるだけのWidgetと並べてスクロールさせる表示用のWidgetです。
制限時間のあるゲームとはいえ、追加し続けるとメモリを圧迫するのと、HorizontalBoxのサイズが大きくなりすぎて不具合が出たりしても怖いので、スクロールさせつつ端から文字を消していってます。こうすることでいくらでも足していけます。
今回難しかったのは、この消しつつスクロールする処理の 『タイミング』 です。
流れを図にするとこんな感じ。
さっそく一つ目のWidgetから見ていきます。
キャンバスには文字と▼を配置。
今回文字については、ステージに並べたタイルで使っているテクスチャ(1024x1024)を使い回してるので、赤い × は別のテクスチャにしました。×はこのWidget専用でそんなに解像度も必要ないので、あらかじめキャンバスに置いておいて、文字とスイッチすることにしました。
テクスチャはこんな感じ。アルファチャンネルを合成表示してるので、ピンクのところは抜きになります。
マテリアルでトリミングして差し替えるようにしています。
WIdgetブループリントのキャンバスの配置は以上で、 EventGraph はというと、Event Construct のみ。
Event Construct はカスタムイベントと違って(ビルトインというやつになるのかな)引数を設定することができません。なので、Int型の変数を一つ用意してExpose on Spawnの設定にします。このときInstance Editable も一緒にチェック付けないとあとで注意されます。
この変数が文字の種類を数字で受け取ります。
数字と文字の対応はこんな風。
-1= ×
0 = ぷ
1 = ぶ
2 = ち
3 = さ
0以下(ようするにマイナス)かどうかで、表示を切り替えています。
これでこのWidgetは完成です。
次は並べる方のWidget。
まずはキャンバスから。
HorizontalBox を配置するのですが、クリッピング(見える範囲を限定する)のためにCanvasPanel の子供にしています。
この CanvasPanel の Clipping 設定を、Clip to Bounds に変更します。
HorizontalBox の方は Inherit のままでOK.
これで CanvasPanel の外には何も表示されなくなります。
HorizontalBox の設定はこんな感じ。アンカーは左上。
この HorizontalBox のアニメーションを用意しています。
0.25秒で、-90移動させています。これは一文字ぶんの長さから。
再生するとこうなります。
CanvasPanel の外にはみ出させています。
キャンバスはこれで完成。次はブループリントの編集。
外から追加のリクエストをもらう形なので、カスタムイベントからつないでいきます。
Create Widget ノードに、先に用意しておいた 文字のWidgetをセットしています。
その戻り値(Return Value)を、HorizontalBox に Add Child すると自動的に横方向に並べてくれます。
EventTickで スクロール処理中(isScrollingフラグ)かどうかをチェックして、スクロール処理していなければ、HorizontalBox が抱えている子供の数をチェックして一定数(今回は20個)を越えていればスクロールの処理を行います。
上図の続き
スクロールしたら、Remove Child して、ポジションリセット
するだけで いけるかと思ったんだけど、タイミング的な問題で アニメーションの再生を止める処理を追加しました。ここが今回てこずった部分です。
最初原因が判らずいろいろ調べていたのですが、どうやらアニメーションの終了時間を取得してタイマーを動かしているのですが、その時点でまだアニメーションが終了しきっていないことがあるのです。アニメーションが終了しないまま、ポジションリセットを行って、アニメーションで上書きされて・・・ということが起こっていました。正常にタイマーと同期がとれるのは10回のうち1回あるかないかです。
アニメーションとブループリントとで同じプロパティをいじっているから、このようなバッティングが発生しやすいのです。といっても数フレームでのタッチの差みたいなものなので、強制的にアニメーションを止めても、見た目にはわからない程度だというのもあり、上のような処理で落ち着きました。
とりあえず以上で完成です。
説明用のサンプルを動かしてみたのがこれ。
ちなみに、EventTrackで
スクロール終了イベントを呼ぶようにしてみたけど、アニメーションの終了よりも、EventTrackの方が先に動くようなので、バッティングは避けられないようです。
ここからちょっと難しい話。
アニメーションが完全に終了してから、次が呼ばれるのが理想ですが、プレイヤーのアクションによって動作する部分なので予測が難しいです。イベントドリブンな設計では、いつどのようなタイミングで呼ばれても問題なく動くようにしておくのが大事だなと改めて思いました。
当たり前ですが、アニメーションには『時間』があります。想定の時間より短い間隔で呼びだされると、アニメーションが衝突することになります。
UMGの Play Animation ノードは、実行すると必ずタイムラインの先頭から再生します。また、Set Timer by Event ノードも 再実行すると、残り時間が再セットされます。
なので、呼び出されたイベントで、
文字追加 → アニメーション再生 → 終わったら1文字削除とポジションリセット
としていると、短い間に何度も実行した場合に、呼び出された回数と一致しなくなるのです。
文字追加 は確実に処理されてるのに、 アニメーション再生 は再セットされてタイマー延長、という状態に陥るのです。
例えば3回立て続けにイベントが呼ばれると、追加処理は3回、3文字追加されますが、アニメーションは1回です。
演出が終わっていない間に受け付けたものはどう処理するかをよく検討する必要があります。と言っても選択肢はあまりないですが、
- 無視する(先のやつ優先で後のやつ以降はなかったことにする)
- 先のやつを止めて、改めて後からのやつを処理(後のやつ優先)
- 一時的にスタックしておいて順次処理してゆく
- 追加分として一緒にまとめる
くらいでしょうか。
UI的に見せ方や重要度によって選択することになります。今回は3つ目のスタックするやつを実装しています。これは、文字を追加するリクエスト処理と、スクロールさせる処理を別々にすることで可能になりました。
追加するだけさせておいて、EventTickでいつ追加されるかのタイミングに関係なく溢れが解消するまで、監視しつつスクロールを処理します。ゲーム的にある程度のリアルタイム性は維持したいので、アニメーションの尺を短かくしています。
今回の台風はなかなかでした。結局7時間ほどで復電しましたが、記事を書いている途中でも何度か瞬間的な停電があって、ノートPCさまさまと思ってたのですが、ルータが落ちたら記事の保存ができなくなるので、結局おとなしく嵐が過ぎるのを待つことに。夏場の長時間停電で暑いわ暗いわ冷蔵庫の中身は心配だわ、で結構精神的に削られました。電気って素晴らしいですね!
ではでは
今回はこの辺で。
ステキなUMGアニメーションライフを!