みつまめ杏仁

アンリアルエンジン(UE4)でGUIを作るためにゴニョゴニョしてます。UIデザイナーの皆様の助けになれば幸いです。

スコアを表示する方法について試してみた

久しぶりに筆をとります

早いもので周りはもう新緑の候。88夜も過ぎ、滲みよる梅雨の気配を感じる季節になってますが、忙しくなると意識しなくなるというか、あえて意識から外しているところもあったりして、無理に季節感のあるコトバを並べてみたけど、なんか空々しい感じだな。

あ~早く暇になりたい。というわけで、ふと思いついて気になった表現方法を試してみたら以外に時間がかかってしまった件、自分としては一応解決を見たと思うので、記事を書くことにしました。

 

きっかけはYoutube動画で スコア表示のカウントアップ演出を見たとき。

前置きとしてフォントの話から。

最近普通に手に入るフォントのほとんどがプロポーショナルフォントなので、画面に文字を表示するとき、アルファベットの ” I ”(アイ)や ” l ” (小文字の L)のような字幅(余白も含めた)の狭いグリフは左右の字間が狭くなるように設定されているフォントをよく見かけます。数字の 1 も同様に狭く設定されていることが多い印象。

似たアルファベットと区別しやすくするためにセリフが強調されていたり、桁数を伝えることが重要なぶん、意図的に余白を広めにとって極端に字幅を狭くしないようにしているフォントもたくさんあります。とはいえ表示する桁数が増えると、 字幅の狭い文字や広い文字が同時にいくつも出現すると11111 と 88888 のようにチリツモで全体の表示幅が大きく変わってきます。

ただ画面に表示されているだけなら、それほど気にならなかったりしますが、リザルト画面やステータス画面などで、上下に並んだ時、桁数がわかりにくくなったり、カウントアップやダウン時に高速で書き換わるとガタガタと震えるように見えたりします。

 

サンプルとして上の3書体をエンジンにインポートして試してみました。

パラメータ増加やスコアアップなど変動時の演出を想定して 1000~9999 の間のランダムな数字を約0.1秒で表示更新してみると

桁は常に4桁ですが、文字幅の差次第で桁数が変わっているかのように見えてしまいます。

 

なるべく等間隔になるようデザインされたフォントを選ぶと、高速でカウントアップやカウントダウンをしてもガクガクと動くのは減らせるけど、それだけの理由でゲームの世界観に合うようにせっかく選んだお気に入りのフォントを諦めるのは悔しいです。

そういった時には、ひと桁ずつばらして個別に数字を書くか、昔ながらの方法でテクスチャにラスタライズして切り出して表示すると確実です。

ただこの方法は地味に手数が多いので、余裕がないときは手を出しにくいところでもあります。

 

そこで思いついたのが、

数字のテクスチャをタイリングさせてうまいこと計算してズラせばいい感じにスコア表示に使えるんじゃないか?

というもの。

 

すんなりと行かなかったけど、結果的にはうまくいきました

 

以下、いろいろ試すことになったその奮闘記。

 

まず数字を並べたテクスチャを作って

(32x512px)

 

これを、シェーダーでタイリングして

 

桁ごとに、縦方向にUVをずらせば・・・

 

さっそくワクワクしながら試してみた。

 

タイリングはUVを1以上の値になるよう乗算すればいい。

上の図だと U に4を掛けると以下のようにできる。

 

Uの値をFloorで切り捨てれば、Integer(整数)ぽく扱えるはず。

その 0~ の値と各桁の数字を紐づけられれば・・・・

はて?配列なんかあったっけ?どうする?

 

今の自分の知識では配列的に扱うのが難しいと判断してひとまず方針を変更。

テクスチャのタイリングをやめて、シェーダー内で重ねる方法を試しました。

見た目にいい感じの結果にはなりました。

 

透明とはいえ、表示面積を桁ぶん計算して重ねているので桁が増えるほど1ピクセルあたりの計算量が増えるので、正直使えないですね。

この方法を作ってみて感じた最大のメリットは、斜めになったフォントをいい感じに詰められるところ。

 

全体図

各桁をシェーダーで分解して、UVに補正値入れる仕組み。

ノードがいっぱいですが、桁分解以外は同じパラメータをコピペしてつないでいます。

UV補正はマテリアルファンクションを用意しています

 

Widgetに置いたときに、1マテリアルで済むので、Widgetブループリントはすっきりできます。

ただ、最終的な仕様が決まらない以上は、この手法は非効率です。

決まったとしても、8桁とか桁数が多くなると計算量が無駄に増えて負荷が心配になります。フレームレートに余裕があってGPUよりCPUが忙しいゲームであれば、逆に使えるかもしれませんが、そんなゲームあるのかな?

 

 

と、諦めの気持ちを引きずりながら ぼちぼち記事を書いてて、突然 マテリアルパラメータコレクションを使うことを思いつきました。まさに天啓。

マテリアルパラメータコレクションは、ブループリントから、マテリアルにパラメータを間接的に渡す仕組みです。

見た目に配列風なので、ひょっとしてと思い実験してみました。

 

マテリアルパラメータコレクションから値を取り出す専用ノードが用意されていますが、Name型で指定する仕様。

これだと配列的にアクセスするのは無理。

 

ここまでか~

 

とまた諦めかけたのですが、コードならひょっとしたら・・・

とひらめいたので最後の悪あがきを実行。

 

マテリアルパラメータコレクションの表示が配列っぽいのが気になっていたので、何かしらやり方があるはずと、簡単なマテリアルを作って、HLSLのコード化してみた。

 

Window > Shader Code > HLSL Code

を実行するとこんなコードが別ウィンドウで出力されるので、左上の Copy ボタンでテキストエディタにコピペ

パラメータの名前を検索してみるけど出てこず。

むむ・・・

 

そこで、マテリアルのノードを少し変更してコードを再出力。

先のコードと合わせて2つのコードをテキストファイルにして、WinMergeを使って差分を検出。

 

 

なるほど、こうやってパラメータを取得してるのか。

 

MaterialFloat4 Local1 = MaterialCollection0.Vectors[0];

 

ノードをいじりつつ、何度か出力を繰り返してわかってきた。

Vectors[0]は Float4なので、 Floatについては Swizzle演算子 を使って取り出している。

マテリアルパラメータコレクションに設定した Scalar型のパラメータについては、

Index[0] = Vectors[0].r

Index[1] = Vectors[0].g

Index[2] = Vectors[0].b

Index[3] = Vectors[0].a

Index[4] = Vectors[1].r

Index[5] = Vectors[1].g

Index[6] = Vectors[1].b

Index[7] = Vectors[1].a

という形で取り出せることになる。

 

5つ目以降については、4つごとに添字が1増える。

ということで、書き方を変えても

Index[0] = Vectors[0][0]

Index[1] = Vectors[0][1]

Index[2] = Vectors[0][2]

Index[3] = Vectors[0][3]

Index[4] = Vectors[1][0]

Index[5] = Vectors[1][1]

Index[6] = Vectors[1][2]

Index[7] = Vectors[1][3]

問題ないことが分かったのでCustomノードを用意。

このようなコードを書いた。

float num = MaterialCollection0.Vectors[int(V)][int(W)];
return num;

このカスタムノードでUVを整形。

数字は 8桁を想定。パラメータの Digits は桁数、Height は一文字ぶんの高さ。

右端の続きはサンプラーにつないでいるだけ

 

ちなみに グラフ内に

パラメータコレクションノードを一つでもつながないとエラーが出る。

ノードを単体で置くだけというのもダメ。

マテリアルパラメータコレクションをコード内でバインドする方法は調べ方がヘタなせいか見つけられなかった。

 

今回配列の順番がずれるのを嫌ったために、下図のような無駄な接続を作ってしまったけど、他に必要なパラメータがあれば、それをコレクションに置くことでスリムにできそうです。

 

これでよし。諦めなくてよかった!

 

さっそくWidgetで組み込んでみる。

ついでにHorizontalBox版も動かしてみる。

HorizontalBoxに入れたImageからDynamicMaterialInstanceを作って配列に入れる。

ForLoopが便利。

HorizontalBoxから取り出した子オブジェクトは一度 Image型にキャストが必要だけど、この処理でマテリアルを配列に入れておくことで、あとからマテリアルにアクセスするときはこの配列にアクセスするだけでOK。

HorizontalBoxのImageにセットしているマテリアルは以下。

シンプルに受け取った数に応じてUVをずらすだけ。

 

数字を各桁に分解して、マテリアルに反映するカスタムイベント。

ゼロサプレスしてます。

テクスチャのタイリング設定で、Y方向は Clamp にしておくと 「9」 の次に何もないので透明になります。X方向のみ Wrap にするのがミソ。

 

ゼロフィル(ゼロ埋め)の場合はBranchノードが不要になります。

 

値 % 10  というのは 10 の剰余を求めていて、例えば 123456789 という数だと、9 という数字だけにできます。一の位だけを取り出せます。

そのあと、元の数を10 で割ることで、一の位を捨てることができます。これは整数の割り算の特性で、小数以下は切り捨てられるのを利用しています。例えば 123456789÷10で 本当なら 12345678.9 になるところが .9 が捨てられるので、それを元の変数に戻してやることで、12345678 になり次に処理する時は一桁減った状態になっています。

受け取った数をいじりたくない場合はPower(累乗)を使用することになるかと思います。

Powerは整数では扱えないようなので、Float型にキャスト(型変換)が必要なのがちょっとアレな感じです。

ゼロサプレスしないのであれば、分岐はなくせます。

 

ForLoopの Indexを 7から引き算しているのは、HorizontalBoxの 子オブジェクトが 左詰めで、そのまま素直に配列にしてしまったのが原因。

あとあと全体に地味に効いてくるので、設計を吟味するのは大事ですね。

(次の画像↓ にあるSelectノードの左にあるピンの順番も、左から右の順です)

 

マテリアルに値を書き込むのは専用の関数を用意。(上図の中央付近)

この関数は両方のタイプに向けて処理してるけど、実際にはどちらか一方だけにする。

Option 0~7 でセットしているName型の名前はマテリアルパラメータコレクションで定義した名前になります。↓ 適当に追加ボタンを押した例

 

 

マテリアルに値を書き込むのは

HorizontalBox向けなら、SetScalarParameterValueノードで。

マテリアルパラメータコレクション向けなら、 SetScalarParameterValueで。

おっと、同じ名前ですね。

グラフ上で、"Scalar" と検索すると後者が。マテリアルノードからドラッグして検索すると、前者が取り出せます。

検索するとき Contex tSensitive のチェックを外すと 2つ同時に出てきます。

マウスオーバーでTipsが出るのでそこで判断するといいと思います。

 

あとは、用意した数字を桁分解してマテリアルに反映するカスタムイベントを呼び出すだけ。

テストとして、0.5秒おきに繰り返しランダムな数字を表示するカスタムイベント。

これで完成。

さっそくテスト

よかったよかった。ちゃんと動いた。

8桁だと尺が長かったのと、尺を縮めると数字が飛びすぎて面白くなかったので 0~123456 までのカウントアップです。

 

ちなみにカウントアップは Lerp(線形補間)を使ってます

カウントする時間を制御する変数をひとつ使いますが、変化量に関係なく一定時間で終わるようになります。

変化量が大きいときに便利ですが、逆に変化量が少なすぎるとのんびりした動きになってしまうので適宜補正が必要。

 

 

残念な反省文で終わるかと思った今回の記事ですがなんとか、それなりに形にできたので満足。

 

UIって意外なところで地味に手数がかかることが多いので、せめて作りはじめのスタートアップ時にさくっといい感じにできないものかと日々考えていますが、なかなか難しいですね。

 

今回、特段手数が減っているわけではないので、良い手法というわけではないですが、マテリアルと、マテリアルパラメータコレクション数字のテクスチャは、汎用アセットとして持ち歩けば結構便利なのでは?と思っています。

桁数を増やすのも簡単だし、何よりもレイアウトする時に、Image を一つ置くだけなのでレイアウト作業の能率が上がる気はします。

ただ懸念としては、マテリアルパラメータコレクションの扱い方です。

同時に表示する数字があると、マテリアルパラメータコレクションひとつだけでは足りないので、複数用意するかしないかになると思います。増やさない場合は中の Scalarを増やすことになります。

マテリアルパラメータコレクションを複数用意するとなると、マテリアルからの参照を切り替える必要がでてきます。Scalarの数を増やすと値の取り出しと書き込みの位置を慎重に管理することになります。

今のところ HorizontalBoxを使うのが一番無難でシンプルかもしれません。作業者のスキルと分業具合によっては効果があったりしないかなとか思いつつ、また何か思いついたら試していこうと思います。

 

ではでは今回はこの辺で

ステキなカウント表示ライフを!

 

 

補足というかおまけ

テクスチャアトラスを使わずにHorizontalBoxTextBlockを入れて使う場合の設定を載せておきます。

設定次第で、ガタガタしないようにできます。

 

下段は普通に追加しただけの設定

すべての桁が同じ設定なので、ヒエラルキーで複数選択すると一気に設定できます。

こちはら字間はPaddingで調整します。

 

上段は少し設定をいじっています

Auto を Fill に変えて、Horizontal Alignment を Center に変えるだけ。

こちらの場合、字間は Padding ではなくHorizontalBoxのサイズで調整できます。

 

テクスチャアトラス作るのダルいとかTextBlockのままで問題ない場合などに、HorizontanBoxはいいですね。

ただ、左詰めしか選べないのは残念。

Paddingにマイナスの値を入れれば右詰ぽくできましたが・・・・

 

以上おまけでした