チャットUIを作る
11
再生してみると、テキストを何度か入力してうちにズレてくる。手で改行を入れる一つ前の入力タイプでは、自然とこの問題を回避できていたのだ。表示バグというやつだ。
入力方法の違う2タイプを作ったことで、結果的にモックの段階で発見できたのは良かったが、さて、どうやって解決したものか・・・
フキダシWidgetの高さを返すところで、UIDをセットしている部分をちょこっといじる。
適当な文字列を入力して確認してみる。
どうやら自動改行が入った場合、何かしらの条件で正しく行数が計測されないということがわかった。間の半角スペースの数を変えたり、文字を変えたり、いろいろ試してみたけど、今一つこれだ、という法則のようなものにたどり着けない。入力のタイミング的なものかもしれないと思い、クリップボードに入れておいた文字列を適当なタイミングで出力してみると、これは必ず同じ値になる。ということはやはり、改行処理にかかる時間か何かが関係している気がする。Get Desired Size のタイミングなのだろうか。
改行のルールも今一つピンとこない。適当な文字列を入力して試しているときに気づいたのがこれ。
2つ目の入力で aaa a aaa と入力すると短いセンテンスなのに改行された。真ん中の a が冠詞として判断されたのかと思い、アルファベットb~zに変えて入力してみたら、 i j l のアイ、ジェイ、エル については改行されなかった。文字の幅?と思ってたら、m と w でも改行されなかった。まだまだ自分の知識の及ばない世界のラテン文字文化圏のルールか何かがあるのかもしれない。こんな短いセンテンスでも改行されるうえに、サイズ取得がうまくいかないということで、より謎が深まってしまった。
首をひねっていると、周りで人の動く気配がする。時計を見ると12時だった。もうこんな時間か。結構集中していたようで伸びをして固くなった背中と首をほぐす。よし気分転換しよう。
外に出てみると、気持ちよく晴れていた。澄んだ空の色がさっきのモヤモヤした気分をいっとき忘れさせてくれたけど、すぐに思い出してしまったので、いつもの書店へは向かわず、コンビニに行くことにした。このまま歩いていると会社に戻るのを忘れてしまいそうで、できれば永遠に忘れてしまいたかったけど、早くこのモヤモヤを解消したいという気持ちが、わずかな責任感という協力を得て勝利を収めた。
コンビニで適当な炭水化物と適当なお茶を入手。適当なルートを歩いていたらいつの間にか自分の椅子に腰掛けていた。対策を考えながらぼんやりと歩いていたので、まったく気分転換できていない気がする。適当に選んだおにぎりを食べながらノードをいじる。
Auto Wrapping(自動改行)に頼る以上は、この処理が終ってから高さを調べればいいのだけど、それはタイミング的にいつなのか分からない。描画処理が少しでも進めば良さそうだけど、Tick使ってみるか。
テキストを流し込んだ後の部分で接続を切る。
Force Layout Prepass ノードとTick をつなぐんだけど、1回でいいので、Do Once ノード入れておこう。
これで再生してみる。
ダメか。
もう少し進めたらうまくいくのかな。ものは試しで、delayノードを入れてみよう。
まずは Duration を 0.0 で。
これでどうだ?
お、端数が出てきた。なんか精度が上がった? でもうまくいってるっぽい。
ちょっとスマートじゃないけどなんとかなったかな。
最初から改行が入っていると問題はないので、このTick処理が負担になるようだったら、入力フォームをあらかじめ3行にしてしまうのが無難だろう。とにかくモックを仕上げて入力のタイプを決めないと。
ちょうど誰かがセットした目覚ましのアラームが昼休みの終了を告げる。
UIDの表示を戻して、谷山田を呼びに行く。ゲーム系情報サイトを眺めていた彼は声をかけるとすぐに来てくれた。入力タイプの違う2つを順番に再生して触らせる。
「うん、うん、いいっスね。」
「ほうほう」
ひとしきり触ってから、
「Dに見てもらいましょう。ちょこっと予定聞いてきますね。」
と言って立ち上がると、鳥囃子ディレクターのところに向かった。そんなに広いフロアではないし、ほとんど人がうろうろしない職場なので、パーテーションより背が高く動く物体は目で追いやすい。
どうやら席にいないようだ。そのまま彼は自分の席でPCに向かって操作を始めた。恐らく予定管理ソフトで予定を確認しているのだろう。しばらくして再びこちらに戻ってきた。
「今ミーティングみたいッスね。予定入れておくんで、後でまたお願いします。」
「分かった。」
ということで一時解散することになり、谷山田は自席に戻って行った。コーヒーを飲みたくなったので、いつもの自動販売機コーナーへと向かう。エレベーターから降りると人影があった。
「あぁ、なんとバッタリ!」
目が会った。立木坂だった。
「お、おう、おつかれ。」
いきなりな言葉で咄嗟に切り返すことができず、動揺してしまった。疲れるから妙なタイミングでこちらの意表を突くのをやめていただきたい。
「おつかれさまです~」
手に何も持ってないところを見るとまだ買っていないようだ。すると、
「あ、どうぞどうぞ。」
少し下がって自販機の前を空けてくれた。ホットかアイスかも決めていなかったので、急に譲られても戸惑ってしまうじゃないか。
「いやいやまだ決めてないし、お先にどうぞ。」
と、こちらも譲り返して手振りで促すが、
「そんなそんな、わたしもまだ決められないんで大丈夫です。」
と、一度譲ったものは譲らないという固い意思を感じたので、さすがに折れることにする。
「じゃお言葉に甘えて。」
と言ったものの、さっきからのやり取りで、何が飲みたかったのかを一瞬忘れてしまい、いくつもある商品ボタンを一通り眺めてから、ようやくブラックしか飲まないのを思い出した。なんだか妙な汗が出てる気がしてアイスコーヒーのボタンを押す。
カップが出てくるまで時間がかかるやつである。30という数字が点灯。なんとなく気詰まり感を覚えたので、気になってたことを訊いてみる。
「そういえば、スタンプの話ってうちのプロデューサーから直々に?」
「あ、そうです。ふらっとやってきて突然、絵描けるよね?って訊かれて、えぇ、まぁ・・・って答えたら、OKって一言を残して去って行ったんですよ。不吉な予感がするなぁって思ってたら、そのあとスグにチームメンバー全員ミーティングルームに召集されて、プロジェクト見直しの話が出て、あぁって。アイツそれを先に知ってて確認しに来たんですよ?間違ってないけどなんかみんなより先に動けるのってズルイ気がしません?」
一気に吐き出すように返してきた。
「アイツって・・・まぁ直前のミーティングで決定したその足で声をかけてきただけだろう?チームのディレクターもその場にいただろうし。」
その手の話をプロデューサーが後から知ることはないだろう。なにしろプロデューサーが決めるんだから。とツッコミたくなったが、そこまで言うのはさすがにくどい気がしたので飲み込む。
自販機のカウントが0になり、カップを匿っていた扉がぎこちなく開く。奥の紙コップをそっと取り出す。
「まぁ、とりあえずイラストよろしく。」
自販機の前を空けると、
「了解です。頑張ります。」
と明るい声で答えた。何気なく顔を見て、初めて見る笑顔に、思わずドキリとした。
「で、決まった?」
動揺を隠しながら、自販機を指さすと、
「いえ、いいんです、本当はちょっとネタに詰まって、誰かと話したかっただけなんで。顔が見れてよかったです。戻りますか。」
「え、あ、うん。」
なぜか一緒に戻ることになり、エレベーターの矢印ボタンを押してカゴが降りてくるのを待つ。
「わたしもアンリアルエンジン触ってみようと思うんですけど、いろいろ教えてもらってもいいですか?」
「いいよ。」
「家で勉強できたらいいなぁ。家のパソコンで動くかなぁ。」
「いつ頃買ったか覚えてる?」
買った年が分かれば、スペックについてはおおよその見当を付けることができる。
「いつだったかなぁ?うーん。」
そこで到着音が鳴ってタイムアップ。エレベータの扉が開くと、先に何人か乗っていて、見知った顔もあった。
フロアに戻ると、予定管理ソフトから通知が来ていた。予約者が谷山田で、出席依頼に自分と、鳥囃子、南河原となっていた。場所は空欄になっている。ミーティングルームじゃないということは、おそらくチャット画面のお披露目会で間違いないだろう。集合時間まで、まだ時間があるな。だいぶ氷の溶けたコーヒーを飲む。ときどき唇に当たる氷の粒を感じながら、さっきの立木坂との会話を思い出す。いつものコーヒーと違う味がした。
つづく
チャットUIを作る
10
翌日は晴れて澄んだ青空が広がっていた。抱えた屈託が全て溶けてクリアになるようなそんな色。こんな青空を見ていると、どこか遠くに行きたくなる病が発症しそうになる。なんとかチャット画面のモックがそれとなく出来上がってきたのを思い出し、続きを作らないといけない使命感をサルベージしながら移動。ようやく会社にたどり着いた。
今日は少し早く着いたのでいつもよりフロアは静かだった。PCを起動しUE4のエディタが開くまでの間に昨日帰ってから頭の中で考えていた構造を思い出す。
テキスト入力を左右に分離させたバージョンを作るにあたって、用意するWidgetはいくつか考えてみた。
最初、左右反転した見た目になるのであれば、フキダシWidgetのように、片側だけ作って2個置けば?というアイデアが浮かんだ。ただし片方でテキスト入力中、もう一方は触ることができない状態にする必要がある。2つが同時にアクティブになることはない。左右の入力Widget間で相互にやり取りするのはとても面倒だ。かといって親のレイアウトWidget経由も手間が増えるだけでまどろっこしい印象。
ここは一つのWidgetにキャンバスパネルによって左右2つの入力部分を管理するのがよさそうだと思い至った。さらに親のレイアウトWidgetで受け取る際にバインドしている処理も変えなくても済む。
まずは今のテキスト入力Widgetを複製して、これを改造していこう。コンテンツブラウザからテキスト入力Widgetを複製する。
とりあえず名前を wb_WriteBox にしておこう。
使えそうなパーツは流用する。入力用の下敷きは、いまのフキダシと同じテクスチャにして調整。ボタンは一つだけでいいので片方を消す。テキスト入力は1行入力だからMultiLineじゃないやつに差し替える。
大きさはフキダシの許容文字数分横に伸ばしておいてっと・・・こんなもんかな。顔アイコンは適当なやつをセットしておこう。
入力開始ボタンのテクスチャはこんな感じでいいかな。
しばらくパーツの調整に手間取ったが、なんとか整った。このテキスト入力で必要な「状態」は3つ。
● テキスト入力中
ユーザーがコメントを入力中はこの状態。
● 待機中 [DEFAULT]
入力する前の状態。左右どちらも同じ。この状態のみ入力を開始するための +ボタンが表示される。
● 非アクティブ [N/A]
反対側で入力を行っている間の状態。見た目に存在感を弱く。CanvasPanelの透明度を変えるとラクチン。
ヒエラルキーはこんな状態。
反対側も同じ構成になるので、キャンバスパネルを一つ追加してある。
このCanvasPanel_Left をまるっと複製して 名前を CanvasPanel_Right に変更。
見た目にシンメトリーになるように調整。
ヒエラルキーはこうなった。
入力中、待機中、非アクティブと、見た目にこの3つの状態を遷移させるためにアニメーションを使って切り替える。いきなり切り替わると味気ないので、入力開始と終了時くらいは短いながらもアニメーションを作ってみよう。状態遷移のイメージはこんな感じになると思う。
この遷移イメージを元に、左と右それぞれにアニメーションを用意する。待機と非アクティブについては、状態の記憶として利用するので、尺は不要。開始 0.0 の位置にバシっと確実に状態を切り替えるためのキーを打つだけでいい。
これをブループリントの方で組んでいこう。
+ボタンは、前のWidgetから残したパーツで、Buttonコンポーネントだから OnClickイベントが有効だ。そこへアニメーションの再生ノードをつなぐ。まずは左側のクリック。
右側のクリックも同じ要領で、再生するアニメーションが変わる。
Widget の Buttonコンポーネントは、画面に表示されている間はクリックを受け付けてくれる。なので、クリックした直後にアニメーションで Visibility を Collapse にするキーを打っておくと必要なクリックを防ぐことができて安全。
Play Animationノードは、再生のトリガーでしかないので、再生を開始したらすぐに次のノードに処理が流れてくれる。ただ基本的に終わりを待ってくれないので、終わってから何かする場合だけ、特別な処理が必要になる。
次は投稿の処理だけど、送信ボタンが無いので何をトリガーにしようか。確かEnterキーを判定するイベントがあったはず。
あった。たぶんこれだ。
Commit Method は ENumのピンだから、判定用の Equalノードが使えそう。
あった!
ということは、さっそくプルダウンを見てみると・・・
On Enter !
これをブランチノードで判定すればよさそうな気がする。True の時に、用意してあった、イベントディスパッチャーにテキストのピンをつなげれば入力した内容が送信できるはず。
左側用だから、isLeft を True にしておく。
右側のも同じように作ってから、テストしてみよう。
テストするためには、レイアウトWidgetに配置してやらなければいけない。
と、その前にコイツをひとまずどかす。
Visibility を Collapse にしてしまえばいいのだけど、ここは Event Pre Construct を使おう。
これで、コンパイルした瞬間にキャンバスから消えてくれる。
ヒエラルキーの可視オプションを見えないようにすればいい。存在を戻すときはノードの接続を切るだけなので安全。
これで、改めて新しい入力Widget を配置できる。
テキストをバインドしてイベントディスパッチャーから受け取っているところがあったから、そこをつなぎ変えないと。
今こうなってる。
wb_WriteBoxCenter のバインドを一時的に外して、今回追加した wb_WriteBox からバインドノードを取り出す。
最終的にどちらの入力方法で行くか決まれば、片方は消滅することになる。
よし、再生してみるか。
よしよし、うまくいってる。
でも、フキダシの開始位置が低くて重なってしまっているので、VerticalBox の位置を上にあげよう。
あとは、OnEnterで送信した後の処理だな。元の待機状態に戻さないと。レイアウトWidgetはコンパイルして編集完了。続いてテキスト入力Widgetの方。
この続きに、閉じて元に戻す処理をつないでいく。
左と右、順番に気を付けながら元に戻すためのアニメーションを再生していく。
イベント全体はこんな感じ。
再生してみる。
特に問題はなさそうだ。あとは顔アイコンのセットと入力の途中キャンセル、改行ができたら見せられるかな。
とりあえず顔アイコンを渡すところを作ってしまおう。テキスト入力Widgetに受け取り用の関数を用意すればいいだろう。
今はこの関数をに対してレイアウトWidgetから顔アイコンのテクスチャ渡すようにすればいい。
このあたりは画面の初期化処理としてイベントにしておいた方がいいかもしれない。Event Constructで処理している場合、Widget がViewportに追加された段階でデータが渡されていないといけないからだ。データの受け渡しのタイミングはネットワークが絡むと思うようにはコントロールできないことが多い。後でプログラマと相談しよう。
UI表示を作る場合、Viewport に追加された瞬間に動き出すやつと、そうでないやつがある。追加された瞬間動きだすのはワンショット的に役目を果たすと消滅するタイプ。主にエフェクトとか演出系の表示。そうでない方は、基本的に常駐することになる。もう少し細かく分けると、常時表示されていて更新されていくタイプと、適宜必要に応じて呼び出されて表示するタイプに分けられる。これらのタイプを用途に合わせて設計する。プログラマに相談する際にも、この辺の分類を念頭に入れて話すとスムーズに仕様を決めることができる。と考えているのだけれど、なかなかこういった話ができるUIデザイナーが周りにいないのはさみしい。
さて、わかりやすくコメントをつけておいて、あとで改造しやすくしたところで、次はキャンセル処理か。
入力用Widgetの外をクリックしたら入力をキャンセルして閉じるようにしてみよう。感触が良くなかったら、キャンセルボタンを置くことになるかな。
テキスト入力エリア以外のクリック判定なので レイアウトWidgetに判定させるのがよさそう。
OnMouseButtonDown 関数をオーバーライドしてと、
ここに判定用のパーツを持ってくるわけだけど、Widgetのクリック検出は、Visible状態じゃないと判定してくれないので、キャンバスに何も置いていないとこの関数が呼び出されることはない。というわけで、キャンバスにテキスト入力Widgetを避けて、CanvasPanelを配置する。
このCanbasPanel の Visibility の設定をVisibleに 変更しておく。
名前を CanvasPanel_CancelArea としておこう。
これをさっきオーバーライドした関数の中に置く。
レイアウトWidgetのキャンバスに置かれているすべてのパーツが検出対象で、何かあるところでマウスボタンを押すと、この関数が呼び出される。特定のパーツのみを対象にしたい。ここで判定してはじくために isHoverd ノードを使う。hover は WebページのデザインでCSSを触ったことがあるならピンとくるはず。このノードはカーソルが乗っているかどうかをブーリアンの値で返してくれるノード。テストするためにPrintStringをつないでみる。
コンパイルするとWarningが出た。
そうだ、ReturnValue が空いてるとダメなのだった。Handled ノードをつないで祈る。鎮まり給へ!
鎮まった。
テストしてみよう。
よし、ちゃんと反応してる。
今度は閉じるイベント作って、それを呼び出せばいい。テキスト入力Widgetを編集しよう。
ただ呼び出すだけでいいのか?勝手にいろんなタイミングでクリックされるから、都合の悪い時がある。あと、入力中のやつはキャンセルするけど、非アクティブの方はすでに閉じてる。となると、フラグが要るな。ブーリアン型の変数を2つ用意しよう。
まずこの値をセットするはここだ。+ のButtonをクリック処理するところ。
このフラグを元にカスタムイベントで閉じることが可能かチェックする。
このあとに閉じるアニメーションをつなぐんだけど、ちょうどいい場所があった。
すっぽり!
送信した直後だから、ただ閉じる処理しかしていない。で、この処理の一番最後尾でフラグを元に戻せばいい。
これで、テキスト入力Widget側の準備はできた。レイアウトWidgetの OnMouseButtonDown 関数に戻る。テストでPrintStringをつないでいたところを差し替える。さっき準備したカスタムイベントを呼び出す。
これでOK。
テストしてみよう。
よし、これで良さそうだ。あとはフキダシの改行表示かな。うまくいくといいけど。
しばらくキャンバスのパーツを触っていると、SizeBoxにそれっぽいパラメータを発見。あと、TextBlock の改行設定を入れておく必要があった。
SizeBoxの子供の TextBlock_Body に改行無しのダミーテキストを入れて、Wrappingの設定を変更する。
で、SiizeBoxの方も設定を変える。
まず SizeBox の Size To Content を一時的に無効にする。
いい感じのサイズになるようにSizeXとYを調整。
そして、Child Layout カテゴリの Max Desired Width と Max Desired Height を有効にして Size X と Size Y を移植する。Size To Content のチェックを戻せばOK。
これで大丈夫。
テストしてみよう。
よかった。なんとかできた。
あとはプレゼンに向けてちょっと細工を。レイアウトWidgetの送信処理のところを、2種類のテキスト入力Widgetを両方有効にしておく。
Event Pre Construct のところで、切り替えられるようにする。
コンパイルが必要だけど、ブランチノードのCondition を切り替えてコンパイルするだけで切り替えができる。確認しておこう。
?!・・・あれ?なんかスクロールがおかしいな。
つづく
チャットUIを作る
9
メッセージは立木坂から届いたものだった。一時的にチームから捨てられたという表現をしていたが、まぁアイツのデフォルトの物言いなので無視しつつ、やり取りしていると、どうもスタンプの絵を描く仕事を請けたらしい。プロデューサーの手の速さに若干の不気味さを感じる。良いものにしたいという気持ちで動く分にはプロジェクトチームのメンバーとして当然だ。なぜかプロデューサーという立ち位置が個人的にあまり好きになれない。プロデューサーの職分というより、たまたまそこにいる人間との相性な気もするが。
ただ、仕様決定の場に不在なことが多いのもあって、どうしても現場からの距離を感じざるを得ないし、外から中をつついてかき混ぜてくるような印象しか持てないのは、個人的すぎる感傷が由来なのは分かっているつもりだ。今のところ、この心象を変えてくれるほどの出会いは無い。単にプロデューサーと仲良くなればいいだけの話だと言われてしまったら、言い返せるほど経験値が高くないし、まぁ人見知りだから仕方がない。ということにしておこう。
で、プロデューサーのことはさておき、立木坂はスタンプの仕様を教えろと言ってきた。仕様もなにも自分もさっきスタンプ機能追加の件を聞いたばかりだと返したら、
(立N)じゃあ表示サイズと、種類を教えてください
・・・コイツ人の話聞かないやつだな・・・
(自分)まだ決めてない
(立N)何にも決まってないのに何を描いたらいいんですか?
(自分)訊かれても困る
(立N)うーん、とりあえず猫でも描いて癒されます
(自分)じゃそれ
(立N)はーい
という流れになった。なんかアヤフヤなままで不安しかないが、出来に関してはこちらから依頼したわけではないので、いったんモックの完成を目指そう。やることは増えたが素材を用意する手間は省けたのはありがたい。絵の内容はまた別途すり合わせの時間をとればいいだけのことだ。後でスケジュールの確認もしなきゃな。
さて、ここまでできたけど、なんか忘れてるものがないか、
ラフはどんな感じだったかな。
そうだ、左右の振り分けが簡単にできたせいで、この < > を置くの忘れてた。
自分のコメントに色を付けるのは簡単だ。ストラクチャから値を引き出せるようにしたから、すでに要領は得ている。サブメニュー表示はちょっと重めなのと、レイアウトが安定してからにしたい。ここは、 < > を作っていこうか。
とはいえ、これは少々作り替えが必要だな。
現状、フキダシWidgetは必要最小限のサイズでしか表示していない。そして、横幅が画面サイズと同じ Vertical Box に対して AddChild している。 さらに、isLeft というブーリアンの値でもって、setHorizontalAlignment ノードを使って左右にレイアウトしている。
ここに < > を表示しようとすると、①重ねて出すか、②VerticalBox を2つ左右に分割するか、③フキダシの方に追加するか。今思いつくのはこの3つ。
①の重ねて出す方法は、新しく< >の専用Widgetを作り、フキダシ追加に合わせて一緒に追加。Y方向にマイナスマージンの値を入れれば実現できそうだ。当然フキダシの高さが可変なので、値を調整してやる必要がある。
②は左右が独立することになるため、同じ高さを維持できさえすれば、シンプルなつくりが期待できるが、2つのVerticalBoxにアクセスすることになるので、その切り替えがちょっと面倒な印象。
③は、フキダシWidgetのレイアウトをいじるだけで解決できそうなので、今のしくみから大きく変わることはない。ただし、左右の振り分けではなく左右反転になる。
あとは、スクロールアウトした際の処理だけど、①と②、どちらも別々のWidgetが追加されているので、画面から消すときに常に2つづつ消すことになる。③は今まで通り。
これはチャットのUIだ。時系列で過去から現在にかけてリニアに並んでいる。余計な要素は入れない方が、サーバーとの差分が無い分シンプルに管理できるはず。発言順がそのまま表示順になっていた方がチェックがラクだし、なにより表示する側の都合でしかないのに、システム側に表示側の都合に合わせろというのは、後々のことを考えると経験から言ってリスクである可能性が高い。
ここは③のフキダシWidgetに手を加える方法で進めていこうと思う。
やることは今のレイアウトにパーツを追加することと、フキダシのサイズ可変に同調できることがゴールだ。左右反転については、フキダシWidgetの中でやってるので、むしろレイアウトWidgetでやってる左右振り分けが不要になるはずだ。
縦方向の可変イメージはこんな感じ。
さっそく < のテクスチャを作ってインポートする。
とりあえず、SideArrow と名付ける。
これをキャンバスに配置するわけだけど、まぁ試しながらやってみるか。
Size to Content の仕様と、アンカーのストレッチに悩まされつつ、1時間ほど試行錯誤を繰り返してようやくなんとかなった。と思う。
フキダシの下敷き画像は、親のキャンバス(CanvasPanel_Container)にサイズ依存していたので、SizeBox以下とフキダシの下敷き画像を、新しく追加したキャンバスパネル(CanvasPanel_fukidashiBody)でくるむことで、階層的に一段下げた。そしてその CanvasPanel_fukidashiBody と同階層に SideArrow を追加するのだが、親キャンバス(CanvasPanel_Container)のSize to Contetnt が利いているせいで、サイズの変化するフキダシに対して、右端をアンカーとした SideArrow が配置できない。そこで、SideArrow も サイズ固定した CanvasPanel でくるむことで位置を確保。
危なかった。一瞬あきらめかけたけどなんとかなった。
あとは、レイアウトWidgetの方を編集。フキダシWidgetを追加する関数のところで左右振り分けの処理をバッサリ。
こうやってノードが減ってスッキリすると気持ちがいい。
これでいけるだろう。
何度も再生して、そのたびに落胆してきたので、感動するより安堵の溜息が出る。ようやく想像通りになった。この辺はもっといろんなUIを作って経験を積むしかないなと改めて思う。
あとは、自分のコメントに色を付ける処理か。これは、判定するのは表示側ではないのと、作るとなっても大してコストがかからないので、後回しにしよう。それより真ん中のテキスト入力部分が見た目に重いので、ホバー処理を入れてみたい。普段アルファ値で薄くなってて、カーソルが乗ったらハイライトする。
テキスト入力Widget は レイアウトWidget で管理してるから、そこでホバーの判定を取ってもいいけど、すでにEventTickでスクロールの制御をしてるから、今はまだあまり複雑にはしたくない。やっぱりテキスト入力Widgetの方でやるか。外すときも簡単だ。
パーツを抱えている親のキャンバスに対して、Is Hovered ノードで判定を取る。on Hovered みたいなちょうどいいマウスイベントはなさそうなので、 EventTickで毎フレームチェックする形になる。処理負荷が心配だけど、後でプログラマに相談しよう。
まずはこんなところか。
Render Opacity は、キャンバスに使うと、配下の子供たちにもフェードが掛けられるとても便利なパラメーターだ。
こちらは詰まることなく予想通りにできた。
ふと辺りで人が動いているのに気付いた。自分の位置からはフロアの扉は見えないが、ばたばたと立て続けに出入りする音でわかる。もうこんな時間か。時計を見たら急に疲れが出てきたような気がする。キリがいいから、今日はこの辺にしておこうか。ここまでくれば、テキスト入力を左右個別に配置するタイプを作ってもいいと思う。
よし、帰ろう。帰り支度をしてフロアを出る。
「お先に失礼しま~す。」
外に出ると雨は止んでいた。薄く雲が残っていて、ぼんやりと残照を拡散していてまだ明るい。急に腹が空いてきた。晩飯について考えながら駅に向かって歩き出した。
つづく
チャットUIを作る
8
近くに窓が無いので外の様子が分からないが湿度が高い。帰るまでに雨がやんでるといいんだけど。エレベータを待つ間、ホットにしようかアイスにしようかな迷ったけど、それほど喉が渇いているわけではなかった。少し体が冷えた感じがするので軽くストレッチをして戻ることにする。
画面丸ごととなると表示するものが多いから結構かかるよな。今日どこまでいけるかな。フキダシの見た目だけでももうちょっと何とかしておきたい。そんなことを考えながら伸びをしていると、カゴが到着する音が鳴って慌てて体勢を整える。ボタンを押してたのを思い出した。幸いドアが開いたけど誰も乗ってなかったので、無駄に乗らなくて済んだ。
さて、まずはフキダシにシッポを付けないとな。Photoshopをアクティブにするとフキダシの作業用ドキュメントが開いたままになっていた。左側のフキダシがデフォだから、画像サイズを右寄せで拡張して、88x64pxくらいかな。
シッポを付けたいけど、ちょっと角削りすぎたか。調整して・・・
サイズが可変にできるよう Boxで指定しているので、分割位置を小数で割り切れるように調整するのが面倒だったけど、どうにかできた。
これをそのまま上書きして、Reimportする。
Reimportしたら、フキダシWidgetを開いて、パラメータを書き換えないといけない。
分割の指定はMarginのところで行う。まず、左上から Left と Top が 0.625 なので、残りは どちらも 0.375だ。
やっぱり少ない桁で割り切れると気持ちいいな。職業柄 UVを扱って長いので小数点耐性には自信がある。UI制作では小数点で指定することが多かったからだ。そういえばカラーも最近は 0.0~1.0で扱う機会が増えている。
テキストの位置がずれたので、SizeBox にある Paddingで調整する。
ここに、顔アイコンも入れておくか。サイズは 80x80 くらいでいいかな。
その分、フキダシとSizeBox を右に移動。
フキダシはOffsetX、SizeBoxはPositionXに80を入力する。
ひとまずこの辺でいいだろう。編集モードをグラフに切り替えて、新しく変数を追加する。顔アイコンを受け取るためだ。Textrue2D 型で、Expose on Spawn にチェックを付ける。
コンパイル時に警告が出るので、 Instance Editable にもチェックを付けておく。
キャンバスに追加した顔アイコンのパーツをGetで配置して、EventConstruct の最後でテクスチャをセットする。
Set Brush from Texture ノードに、さっき用意した変数もつなぐ。
これで受け取り準備完了。
いったんコンパイルして保存しておく。
レイアウトWidgetに戻って、フキダシWidgetを追加するイベントを確認。Create Widgetノードの上で、Refresh Nodes を行うと、ピンが増える。
ここに顔アイコンのテクスチャをストラクチャから取り出して渡せばいい。
コメントのテキスト入力ピンが New Var 0 のままだな。ついでに変えておけばよかった。
この辺で様子を見てみようかな。
しまった! 横着してたの忘れてた。テキスト入力用の下敷きを専用で作ってやらないと。
・・・
気を取り直して再生。適当に入力して試してみる。
右側の時だけ、並びを逆にしたいな。さてやり方はどうしよう。
思いつくのは2つ。今フキダシWidgetを「左専用」にして、新たにフキダシWidget「右専用」を作るか、Widgetは増やさずに今のフキダシWidgetの中でレイアウトをタイムリーに「スイッチ」する処理を追加するか。
ひとまずスイッチするのを考えてみよう。フキダシWidgetを開いて編集モードをグラフにする。isLeft のフラグを受け取ってから左右だけ入れ替えるマクロでも作って入れよう。その前に、フラグを受け取る変数を用意しておかないと。これもExpose on Spawn だな。変数名は、アセットが違うので同じで大丈夫。
まずこれでコンパイルして保存。あ、ついでに New Var 0 だった変数名も変えておこう。テキスト型の変数を、commentBody に変更して、改めてコンパイルして保存する。
レイアウトWidget を開いて、CreateWidgetノードを確認してみると、New Var 0 のピンが赤くなってる。
右クリックして Refresh Nodes すると色が戻った。接続が切れるのでつなぎなおして、増えたピンにストラクチャの isLeft をつなぐ。
これで、左か右かのフラグがフキダシWidgetに渡されるようになった。レイアウトWidgetもコンパイルして保存。
再びフキダシWidgetを編集。スイッチ用のマクロを作ろう。名前は switchSide でいいか。
Inputs と Outputs に Exec ピンを一つずつ追加。
これを Event Construct のところに差し込む。
さて肝心の中身だけど、どうしようかな。ひとまず左右反転してみるか?アンカーいじるのややこしそうだし。Render Transform でひっくり返してみよう。中のテキストはもう一回ひっくり返せば元の見た目に戻るはず。裏の裏は表だ。ひっくり返すのはこいつら。
左右どちらかというのは、 ブーリアンの変数 isLeft が受け取っているはずだから、そこから select ノード使えばいけそう。2つの状態に応じて値を振り分けるのはSelectノードが本当に便利だ。 isLeft が false の時だけひっくり返せばいいから、
こんなとこかな。フキダシWidgetの編集は完了。
さっそく試してみよう。
おお!いいねいいね。
あ、そうか。顔アイコン白いままだから気にならなかったけど、顔アイコンとかもひっくり返さないといけなくなったな。ひっくり返すものが増えてくると、罪悪感みたいなものが膨らんでくる感じがする。SizeBoxのアンカーを右にしたときにうまくいくか試してみないとわからないな。フキダシの大きさを可変にしたから難易度が上がってしまった。アンカーの設定から見直す必要がありそうだ。この辺は追々詰めるとして、今は先に進もう。
顔アイコンのダミーでも用意してみるか。なんかフリーで使えそうなのないかな。
しばらくネットを検索結果を行ったり来たりして、まぁ結局いつものとこで調達。ほんとにいつもお世話になってます。手を合わせて心の中でいただきますと呟く。
透過のPNGをPhotoshopでリサイズ。たくさんあるアセットの設定変更は、Asset Action > Bulk Edit via Property Matrix... が便利。一度Importしてから、コンテンツブラウザ上でまとめて選択して右クリックメニューから選ぶ。画像を用意するのに時間がかかったけど、16個のインポートはサクッと終わった。
このアイコンたちを、レイアウトWidgetからフキダシWidgetに渡せばいいだろう。今は左右の矢印ボタンをクリックすると、テキスト入力Widgetとバインドしてイベントディスパッチャーで受け取ってるところがある。そこからフキダシの追加を実行している。
まだテスト用だから、コメント入れておこう。イベントの名前も CustomEvent_0 のままだと役割が分かりにくいので、recieveComment に変えておく。
ここのMakeノード(ストラクチャ用)にテクスチャ2D型のピンがあるからここにつなぐ。そのためにまずはテクスチャを配列にでも入れようか。新しく変数を追加して配列にする。一度コンパイルしてから、16個分の+ボタンをクリックしてエレメントを増やす。テクスチャはプルダウンから選んでもいいし、コンテンツブラウザからドラッグするか、矢印ボタンを使ってもいい。
数が少なければ検索するけど、多いのでドラッグしよう。
配列が用意で来たらグラフに取り出して、GetノードとつないでMakeノードにつなぐ。このままだと 0番のテクスチャしか使われないので、雰囲気を見るためにランダムノードを使う。
さっそくテストしてみよう。
かなり雰囲気出てきた。どの顔が出るかわからないながらテキストを打ってみたけど、男の子?以外はキレイに左右に分かれてしまった。うん、確かに顔の向きが反転されていない。
マクロに顔アイコンのパーツも追加するか。
これでどうかな?同じ顔が出るようにランダムノードいったん外して見てみよう。
OK。じゃこの流れでUIDも追加しよう。
テキストブロックをフキダシに追加して、パーツの位置を調整。文字の色はこんな感じかな。小文字が使えたはずだから、ディセンダに注意しないといけない。gyjを追加してみる。
ちょっと被るけど、これくらいならいいかな。ベースラインから下に伸びる文字はいくつかあるけど、意図的に組まない限り出現頻度はそれほど多くない。そこを心配してベースラインがフキダシから離れる方が塊り感が弱くなるのでよろしくない。離れていても、アウトラインや下敷きでグルーピングする方法も採れるけど、チャットなので、縦方向に積み上がっていくことを考えると、あまり上下に無駄な空間は作りたくはない。まぁ画面が採用されて本実装となったら、このまま微調整するか、デザインを見直してもいい。
TextBlockは Size to Content にチェックを付けておく。これは、サイズを固定すると、余白ができるし、反転した時見た目のズレを、修正する処理が必要になるからだ。
キャンバスはこれでいいとして、UIDを受け取る変数を追加しないと。
これも、Expose on Spawn と Instance Editable にチェックを付けておく。
次はグラフに、これを書き換える処理を追加しよう。場所は EventConstructの最後に追加でいいかな。
UID文字列の内容によってフキダシの高さは変化しないから、一番後ろで大丈夫だろう。で、反転対策を入れれば出来上がり。switchSideマクロを編集する。
フキダシWidgetの編集は一旦完了。受け取る方の準備ができたら、つぎは値を渡す方。レイアウトWidget の CreateWidgerノードをリフレッシュする。
だいぶ混線してきたな。そろそろ、受け取るフキダシWidgetの変数もストラクチャにした方がスッキリしていいかもしれない。よし一気にやってしまおう。
フキダシWidgetスッキリ受け取り改造作戦開始。
まずフキダシWidget側に変数を追加。もちろん Expose on Spawn と Instance Editable にチェック。
つぎにバラバラと個別に追加した変数たちを、グラフから取り除きつつ、このストラクチャから取り出してつないでやればいい。まずはマクロのブーリアン型 isLeft。マクロの中ではなく、外から入力ピンとして受け取れるようにする。
ストラクチャの内容を取り出してつないでいくんだけど、どうしようかな。
ここから、あちこちにラインが伸びていくのが想像できる。そうだ、これを個別の関数にしてしまおうか。いったんこれを Collapse to Function する。
関数名は getSideFlag としておこうか。Return ノードを取り出して、そこへストラクチャの isLeft だけを刺してやる。
関数の仕事結果を返す Returnノードは、ピリオドで検索できるので便利。この関数をイベントグラフのマクロのところにつないでやる。
同じように、他のコメントや顔アイコンのテクスチャとかを関数化しよう。関数って複製できたかな。どうやらできるみたい。
ばばっと複製して、Outputs のピンを差し替えればよさそうだ。Duplicateすると関数のグラフに切り替わって、関数名の変更を要求してくる。このあたり、良く設計されているなといつも感心させられる。エディタ作業においてのヒューマンエラーが時間のロスであることを心得ているかのようだ。実際にプロジェクトで使われて叩き上げられてきた結果だろう。
コメントとUIDについては表示するときにText型にするので、この関数内でキャスト(型変換)しておこう。
一度String型のまま Returnノードに刺して、出力ピンを作ったあと、接続を切って、Detailタブから変数の型をText型に変える。そして改めてストラクチャのBreakノードからつなぐとキャストノードを入れてくれる。こうするとReturnノードの出力ピン名が分かりやすくなる。まぁReturn Value のままでも不都合があるわけはないので好みの問題か。
イベントグラフに戻って関数をつなぐ。
この調子で差し替えていこう。
スッキリ。
全体を眺めてみる。
これで、使わなくなった変数を消してフキダシWidgetの編集終了だ。
さて、次はレイアウトWidgetを開く。
予想はしてたけど、ちょっと禍々しい感じ。ここは Refresh Nodesの出番だな。
Breakeノードは isLeft を別のところで使っているので、ノードをなくすことはできないけど、▲ボタンで少しだけ縮めることができる。
かなりスッキリできた。やっとテストできる。
よしよし。今のところいい感じに進んでる。にやけそうになりながら、適当なコメントを入力して一人チャットを楽しんでいると、後ろから声をかけられた。
「どうすか?」
谷山田か。「おう」と軽く返事をしつつ手を止めて振り返る。
「メッチャできてるじゃないすか?僕もちょこっとだけ触ってみていいすか?」
座ったまま少し横に移動し、キーボードの前を開けてやると彼は軽快にキーを叩きだした。
「いいすね。メッチャそれっぽい!」
後は、と言いかけた時、
「鳥囃子さんに見てもらいましょうか?」
「いや、まだできてないとこあるし。もうちょっと」
まだ見せられるとこまでできている実感がなかったところへ、突然見せようと言われると躊躇してしまう。確かにある程度のチャット感は出てきたと思う。谷山田が声をかけてきたのは、それが顔に出てたのかもしれないな。
「試してみたいことあるし、形になったらまた声かけるよ。」
こういった返事は、こちらがいいって言うまで見るんじゃねぇよ、という意味合いを滲ませてしまうので、違う言い方にすればよかったと、口に出してから気づく。
「ですよね、突然ですみません。」
なんとなく察したか。
「楽しみにしてます。それはそうとして、相談があるんですけど。」
「?」
「スタンプを送れるようにできたらなぁと思うんスけど。いけます?」
なるほど、これが声をかけてきた目的か。とりあえず即答は避ける。
「うーん。そうだなぁ。」
UMGのテキストは途中に絵文字やアイコンを混ぜるのは難しい。どちらか一方のみの送信なら。
「コメントかスタンプか、モード切り替えみたいにしていいならそんなにかからないと思う。」
谷山田の顔がぱっと明るくなった。
「まじっスか?じゃぁまたどんな感じになるかできたら、ちょこっと見せてもらっていいスか?」
「わかった。ところで、そのスタンプ誰が描くの?」
今のプロジェクトチームにUI担当は自分しかいない。イラストを描くようなデザイナーは今キャラ作成に追われて余裕がないはずだ。 なんとなく自分にお鉢が回ってきそうな気がしたので確認してみる。
「実は鳥囃子さんから聞いた話ですけど、チャットやるならスタンプが欲しい、とプロデューサーが言ってきたそうなんス。ご褒美アイテムとか限定プレゼントとかで使えそうじゃん。て言われたとか。」
ま、ありそうな話だ。まだ肝心な答えを聞いてないので、だまって次を促す。
「で、もちろん人くれるなら、って鳥囃子さんが条件を付けたそうなんですけど、わかったと。」
「トリさんもやる前提かよ。」
思わずニックネームが出てしまったが、突っ込みを入れたくなったのですかさず返す。
「チームポテイトがいったん企画から練り直しになったんで、デザイナーを一時的に開放するらしいんス。」
あ、チームポテイトといえば立木坂のいるとこじゃなかったか?まぁエンジンの選定に悩んでるような状態って聞いてたし、あまり健康的にビジョンがまとまってなかったんだろう。
「そこから引っ張ってくるそうです。」
「わかった、こころの準備しとく。まずは今のやつを整えてからで。」
「じゃぁ、よろしくお願いしま~す。」
そう言って自席に戻って行った。
やることが増えた。まだモックを作り始めて1日も終わってないのに。本実装の前だからまだいくらでもやりようはあるし、その分柔軟な仕様拡張への準備だってできる。「できた!」って言ってから「実は・・・」と後出しするよりはいい。と考えると、若干もやもやするが、今でよかったと納得することにしよう。
作業を再開しようとモニターに目を向けると、メッセージが届いたというポップアップが画面の隅に現れた。誰だろう? 立木坂?
つづく
チャットUIを作る
7
調べてみたら、テキスト入力を扱った記事が見つかった。思ってたよりむずかしくなさそうだ。よしさっそく参考にしながら作ってみよう。そういえば、入力のスタイルを2種類作ってみる、などと谷山田との会話で豪語していたのを思い出した。仕方がない、まずは中央のボックスバージョンからつくろう。
画面のラフデザインはこれだ。
新しくWidgetを作成する。名前は wb_WriteBoxCenter にしておこうか。下敷きの見た目はフキダシ用のテクスチャを代用する。あと左右の矢印ボタンを用意すれば作れそうだ。
Photoshopで右向きの矢印を作る。べき乗が大好きなので大きさは 64x128px。左向きは反転すればいい。背景と距離感を出すためにフチドリを付けておこう。
デザインはテクスチャサイズいっぱいまで詰めない。何事もゆとりは大事。
テクスチャをインポートしたら、新しく作った Widget を開く。まっさらなキャンバスに向かい合いレイアウトのイメージを頭に思い出す。画面の下にそれほど占有しないので、フルスクリーンのキャンバスは必要ないだろう。エディタ右上の設定を変更する。画面のラフデザインから大体のサイズを測ってキャンバスサイズとして設定。
これでUMGに最初から置かれているデフォルトの CanvasPanel のサイズが変更できる。
ここに下敷きと、左右の矢印、テキストボックスを配置していこう。
まずは 下敷きを中央に。Image を置いて、テクスチャにフキダシで使っているやつをセットする。自分専用なのでカラーを変えておこう。配置はストレッチにするけど、左右にボタンをおくから余白を開けておく。
この設定にしておけば、あとからキャンバスのサイズを変更しても追随するから安心。
次は左右の矢印。矢印はクリックを受け付けるので、Imageではなく Buttonで配置する。テクスチャと同じくサイズを64x128にしてレイアウトしておこう。
アンカーを右端中央にして、Pivotを(1.0, 0.5)にして、Positionを (0, 0)。あとは、Buttonに画像をセットする。ふるまいに合わせて画像を変えられるけど、今回は同じ画像にしてカラー変更で対応しておこう。通常時の Normal、マウスカーソルが乗った時の Hovered、そしてクリックしたときのPressedの3箇所に矢印のテクスチャ画像をセットする。
右側ができたから、このButtonを複製して左側に配置する。今度はアンカーを左端中央にして、Pivotを(0.0, 0.5)にして、これもPositionを (0, 0) にする。
左側の RenderTransform の Size X を -1.0 にして左右を反転すると、
よしできた。
あとは、この上にテキストボックスを載せるだけ。えっとどれだっけ?
EditableText (Multi-Line) こっちだな。
これも下敷きと同じようにアンカーはストレッチでいこう。フォントとMarginを設定して、
よし。こんなもんでいいかな。ヒエラルキーはこんな感じになった。
いったん保存して、レイアウトしてみよう。
レイアウト用のWidgetを開いて、User Created のカテゴリから、作ったばかりのテキスト入力Widgetを探すとすぐに見つかった。早速キャンバスに配置する。
あれ?
Size To Content にチェックを付けるとつぶれてしまった。あのエディタ右上のキャンバスサイズはあくまでも作業のための設定ということか。仕方がないので手入力でサイズをセットする。 アンカーは中央下端。
試しに変なサイズにしても、ちゃんと問題なく合わせてくれる。
再生してみると、特に問題なくレイアウトが確認できた。
さて、ボタンをクリックしたときにメッセージをサーバーに飛ばすことになるんだけど、そういえば、左右の振り分けとテキストの送信については相談してなかったな。
席を立ち、プログラマの南河原さんのところに向かう。プロジェクトがまだ小規模なので、そんなに席が離れていない。背後からおつかれさまですと声をかける。
「今、大丈夫ですか?」
彼はコードを吟味していた手を止め「はいはい。」と振り向いた。
そこで、
「例のチャット表示を作ってるんですけど、」と切り出し、ユーザーの発言毎に、画面の左右に表示位置を振り分けたいということと、そのユーザーが任意で右か左かを選べるということを再度確認の意味を込めて話した後、
「テキストと一緒に左か右かの情報をくっつけて送ることになると思うんですよね。もう準備とか始められてたりします?」
「いや、まだなんも手ぇ付けてない。来週あたりサーバー担当と話しよかなって。」
うーん、と言ってつかのま考えてから、
「そうやなぁ、他にも ユーザーID とかアイコン情報とか諸々決めなアカンねんけど、すぐ決めた方がいい?」
そう訊かれたので、
「いえ、大丈夫です。プロトタイプなんで適当にやっときます。」
と答えると、
「ゴメンな。決まったらまた共有するし。」
「はい、お願いします。」
ということで、自席に戻って考えてみる。おそらくサーバーへ送信するための関数を用意してもらえると思うので、それをイメージしたテスト用の関数をこちらで用意しよう。あとは受け取り用のフキダシ追加イベントのパラメータを拡張していけばいいだろう。
今は実装方法を詰めたいわけではなく、とにかくプロトタイプを触って操作感や挙動について検証し、最終的なゴールをプロジェクト内で共有するのが目的だ。そこで見つかった問題を解決し、「余分」と「余白」について検討する。「余分」は「余白」を最大化するための伸びしろだと捉えるようにしている。一方の「余白」は将来の「余裕」と見るか、「不足」と見るかは判断が難しいところだけど、仕様変更や調整の波をかぶりやすいUIにとっては「保険」という意味合いも含ませていいと個人的には考えている。他人には「物足りない」という印象を与えてしまうリスクがあるけれど、「あ、まだ仮なんで」という決めゼリフがあるので問題ない。ちなみにこの決めゼリフを本実装のあとで吐くことになると辛い。声が震えるのを抑えながら言うことになる。
さてと、フキダシとしてサーバーから送られてくるコメントを表示するにあたって、フキダシWidgetが欲しい情報で思いつくのは以下の 5つ。
①ユーザーID
②コメント本文
③左か右
④顔アイコン
⑤自分かどうか
ローカライズはしないのと、加工や整形ができるので ①と②はString型でいいと思う。③と⑤は選択肢としては2つしかないのでBoolean型でいけそう。④はTexture型で。ひとまずこれでストラクチャを使ってみよう。
ストラクチャはコンテンツブラウザで右クリック、Blueprints の中にある。
chatComment と命名。あとでちゃんとしたやつが来るので、それまでの暫定ネーム。
ダブルクリックして中身を編集。
保存して、レイアウト用Widgetに戻る。
まずセットするのは、フキダシを追加するイベントのところ。
イベントノードをフォーカスして、Detailsタブの項目 Inputs からピンを追加する。型は、さっき作ったストラクチャの名前で検索するとリストから見つかるのでそれを選択。
適当に名前を付けると、カスタムイベントのノードに濃い青色のピンが追加された。
ストラクチャは、複数のデータの寄せ集めな状態なので、一つのピンから情報を個別に取り出す必要がある。そこで Breakノードの出番。
Breakノードで分解されたピンから、ストラクチャで自分が追加した CommentBody があるので、選んでダミーテキストをつないでいたところに刺す。
String型とText型は型が違う。このように違う型同士をつなぐ場合、キャスト(型変換)してからつなぐのが儀式なんだけど、UE4のブループリントエディタは直接つなぐことができて、さらに自動的にキャストノードを間に入れてくれるので、ラクチンだ。
よし、このままフキダシの左右レイアウト対応もやっておこうか。
まずはダミーコメントの表示で使ってたカウンター処理を外す。
この右端の外したところに、水平方向のレイアウトで、右寄せ、左寄せを決める set Horizontal Alignment ノードをつなぐんだけど、えっとどこからだっけ? VerticalBoxの設定になるから、Add Child to Vertical Box ノードにある、Return Valueから探せば、あ、あったあった。
なんだか、Vertical(垂直)、Horizontal(水平)と混ざり合ってややこしいけど、カテゴリが、 Layout > Vertical Box Slot になってるからここで間違いない。
出てきたノードをつないで、ラインを整える。
で、この濃い緑のピンにつなぐのは、ストラクチャの isLeft というBoolean型のピンだけど、こんなときは Select ノードの出番。この濃い緑のピンからドラッグして、Select で検索する。
いい感じにノードが見つかって取り出すとき、思わずドラえもんの声真似をしてしまう。大丈夫、声には出てない。このSelectノードの 一番下にあるピンとストラクチャに設定した isLeft ピンとつなぐ。
どう見ても、Integer型のピンだし、Index って書いてあるけど、ここも気にせずつなげることができる。先にプルダウンを開けてBooleanに変えておいてもいい。
つなぐと、Selectノードの 左にある入力ピンのラベルが変更される。isLeft の内容が True なら 左。Falseならその逆の右 になるので、それぞれのプルダウンを変更する。
これで、コメントのフキダシが左右に振られるようになったはず。
ここで、現状のコメントが表示されている流れを確認しておこう。
スペースキーが押されたかどうかの検出を、レベルブループリントで行っている。
キー入力系のイベントは、Widget内に置けないからだ。
レベルブループリントから、レイアウトWidgetにあるフキダシ追加の関数を呼び出され実行している。コメントはダミーで、レイアウトWidgetが内部で持っている。
ここまではコメントの表示方法を検証してきたのでこれでも問題なかったけど、コメントを作成して送る表示ができたので、この仕組みを変える必要が出てきた。
受信時はレイアウトWidget経由で、コメントが渡されることになるのは今の状態とそれほど変わらない。一方の送信時はテキスト入力Widgetがトリガーとなって最初に動くことになる。だから機能として追加しないといけないのが、テキスト入力Widgetからの送信を受け付ける流れ。
フキダシWidgetから親であるレイアウトWidgetに通知するには、イベントディスパッチャーで行う。コメントを入力し終わって、左右のボタンをクリックしたら、入力した内容と左右どちらのボタンを押したかの情報を共に通知する。
さっそく、イベントディスパッチャーを追加する。
通知するパラメータに Boolean型の isLeft 、String型の commentBody 2つを追加する。
このイベントディスパッチャーを呼び出すのは、左右ボタンをクリックした時だから、OnClicked のイベントを用意しよう。Buttonで作っておいたので、イベントの追加は簡単だ。
緑のボタンをクリックすると、イベントノードをグラフに置いてくれる。
ここにさっき追加したイベントディスパッチャーを ”Call” のカタチで呼び出してつなげる。このイベントはすでに右か左かハッキリしてるので、isLeft のところもあらかじめチェックを付けておくことができる。
あとはにコメントの内容をゲットしてつなげれば出来上がりだ。
コメント自身は、MultiLineEditableText が持っている。そこからGetしよう。
これ最低限の処理はできたけど、判定とかも加えたいので、さらにこれをマクロにする。マクロの方が関数より分岐がやりやすい。GetText~ノードとキャストノードをフォーカスしておいて、右クリック > Collapse to Macroだ。
できたマクロを編集する。
まずは、Inputs に Execute ピン を追加する。同じくInputs にある Self ピンの名前を変えておく。 EditableText あたりでいいだろう。
ブランチノードを追加して、Execピンとつなぐ。
続けてブランチノードの右側のピンを、マクロのOutputsにドラッグ&ドロップする。
ピンの順番はここで変更できる。
今ここで判定したいのは、コメントが空だった場合。
内容が空っぽかどうかを調べるノードとして Text is Empty ノードが用意されているので、これを使う。
これでマクロはひとまずできあがり。マクロの名前は getSubmitText にしておこう。
イベントグラフに戻ってつなぎなおす。
コメントが空の場合、Text is Empty が True(真) になるので、コメントが入っていると、False(偽)になる。理解していてもイベントグラフだけを見るとなんか気持ち悪い。Outputsのピン名を変えよう。もう一度マクロを編集する。
True を Failur(失敗)、FalseをSuccess(成功)にしよう。
うん。解りやすくなった。
送信した後は、空にしておかないと。
これでテキスト入力Widgetは編集完了。次はレイアウトWidgetでバインドしてコメントを受け取れるようしよう。
レイアウトWidgetのキャンバスに置いた、テキスト入力Widget を グラフに取り出して、ed_submitComment にバインドする。そしてバインドノードの Eventピンからカスタムイベントノードを取り出す。
ここにフキダシ追加イベントをつないでみよう。ひとまず表示テストはできるはず。
フキダシ追加イベント addFukidashi は、コメントをストラクチャで受け取るようにしたばかりなので、渡せるようにMakeノードを使う。
ストラクチャへの受け渡しは、まんま同じ型ならそのままつながるけど、単品で値を書き換えたり読み出したりする場合は、 BreakノードとMakeノードを駆使する。ここでは一部分だけ渡したいのでMakeノードが便利。
早速テストしてみよう。
左のボタンをクリックすると、
お、いい感じ。適当に飛ばしてみる。
うん大丈夫そうだ。ちゃんと左右に割り振れてる。できてきた感!
あとは、入力中のテキストに文字数制限処理入れたり、入力中かどうかのハイライト処理とかもあった方がいいかな。その前にいい加減アイコン出せるようにしようか。UIDも出さないとな。フキダシの尻尾もちゃんと作らないといけないし。とりあえず休憩しよう。席を立ち空調の効いたフロアを出る。
つづく
テキスト入力を試してみる
最近帰りが遅くて、駅から家までの経路に田んぼがあるんですが、毎晩カエルの大合唱を聞きながら歩いて帰宅しています。 月の映る水面をチラ見しながら歩きつつ、ダンジョンメーカーに勤しんでる今日この頃です。はい、歩きスマホはダメですよ~。
さてさて、訳あってUMGのテキスト入力を試すことにしました。公式のWebドキュメントを見ていると、何やらいくつか種類がある様子。とりあえず最近のエンジンのバージョンで見てみるとこんな感じ。特に変わった様子はない。
公式ではPrimitiveの中にカテゴライズされた画像が貼ってある。いつのVerだろうか。
見た目
とりあえず、Text で Box なやつをピックアップして並べてみた。
一見すると背景有り無しと、改行ありの複数行タイプか、改行なしの1行のみのタイプ。名前的に EditableText と TextBox この2タイプに分けられている理由が分かれば話が早いんだけど、公式ではこれといって言及されてないっぽい。公式以外で触れている情報が少ない。まぁだいたい理由は想像つくけど。
実際に表示してみて試してみると、ほとんど差が分からない。大きな差があるのは、Style という設定項目を持っている TextBox(Multi-Line) 。こいつはスクロールバーも持っている。それ以外の3つは、いつもお世話になっているTextBlock と同じような Appearance を備える程度。
機能要件の差とコンポーネントとして作られ実装された時期によるものだと思うけど、さすがにこれはちょっと躊躇う。TextBox(Multi-Line)だけは、Appearance の項目スッキリしていて、見た目の調整は Style という設定カテゴリにまとめてある。
自分の作りたい見た目が実現可能かどうか、実際に試してみながら判断するしかなさそう。
ちなみに、
キャンバスに置くと、is Variable に最初からチェックが付いてて、どちらもブループリントで触れるようになるんだけど、デフォルトの名前とアイコンがこれ↓
ほとんど同じ。Box 付きは 実線で、 Box無しは 破線のアイコン。
イベント
用意されているバインドできるイベントは4種類とも OnTextChanged と OnTextCommitted の2つ。
全部置いてみたらこうなった↓
OnTextChanged~ は内容が変更されるたびに呼ばれるイベント。
OnTextCommitted~ は内容が確定(編集終了という意味合いだと思う)した際に呼ばれるイベント。このボタンからイベントを作って試してみたら、このWidgetのフォーカスが外れた際に、確定したと認識されるらしい。
変数化したオブジェクトから、 イベントを検索してみても専用のやつは上の2種類だけみたい。
Widget Event というカテゴリにある。
ところがTextBox だけは違ってて、Text Box というカテゴリが用意されてる。さすがCommonカテゴリに分類されるだけのことはあるとうことか・・・
ブループリントからも見た目をいじることができて、Get Style ノードが用意されているけれど、EditableText (Multi-Line) だけは なぜか、 Get Widget Style という名前のノードで別のカテゴリに分類されている。グラフに取り出してみるとこの通り。
こうやって比較してみると、ややこしい事この上ない。まさに 混ぜるなキケン!
というやつか。
ここまできて、今更ながら思ってたより深い沼だったことが判ってしまった。そっ閉じしてあとはEpicの猫のひとかhistoria様のブログに期待を寄せる方がいいかもしれない。などと弱音を吐いてみる。
とりあえず、このまま比較していくと、全て検証を終えた時には夏休みも過ぎ2学期が始まってしまっているかもしれないので、どれかに決めて次に進もう。
改行処理を試したいので複数行が扱える EditableText(Multi-Line) に決める。
いじる
背景があった方が大きさが分かりやすいので、テクスチャを用意。
この64x64のテクスチャを Image にセットして、Box で描画する。
とりあえず必要そうなパーツを置いてそれっぽくする。
ヒエラルキーは ↓のような状態。
今回、EditableText(Multi-Line) が主人公なのでサイズが固定。これに合わせて他のパーツを調整することになるので、背景(Image)のサイズをEditableTextと一緒にしておき、マージン設定で内側に余白をつくり、見た目の枠と距離をとるようにしています。
EditableTextのサイズが変わっても、サイズをコピペするだけなので、頭使わなくていいというメリットがあります。この辺は好みの分かれるところかもしれないですね。
改行(Wrapping)の設定もチェック。
Default Wrapping だと、単語と単語の区切りで改行してくれるけど、日本語のように半角のスペースを入れない文字列だと、意図的に改行しない限り、ず~と右にはみ出してしまう。なので、 Allow Per Character Wrapping に変更します。
ボタンをクリックしたときに実行されるOnClickedイベントをバインド。PrintString につないで確認してみる。
改行文字を置換できるか実験。Get Text ノードから PrintStringノードまでを関数にする。
文字列をゴニョゴニョするには、Text型を 一旦 String型にしてやる必要がある。関数にすると、ローカル変数が使えるから一時的にStringの処理をするにはうってつけ。
関数の中身はこんな感じ。TempStr というのがローカル変数。
Replace ノードは From に置換対象の文字列を入れて、To に 入れ替える文字列を入れて使う。Shift キーを押しながら、Enter キーを押すと、改行文字が入力できるので、From に改行文字を、 To に <> を入れて試してみる。
無事置換できた。
次は、行数に制限をかける方法を考えたい。
それっぽいノードや設定項目はなさそう。
とりあえずWidget Reflector で見てみると、
Desired Size が入力した文字によって変動しているのが判った。
内容が変更されると呼ばれるイベントでGet Desired Size ノードで調べて表示させてみる。
1行・・・52.590
2行・・・89.181 (+36.591)
3行・・・125.771 (+36.59)
4行・・・162.362 (+36.591)
5行・・・198.952 (+36.59)
6行・・・235.543 (+36.591)
いったん一つ前の内容をキャッシュしておいて、オーバーしたら、強制的にキャッシュで上書きするというのを試してみる。
どうやらDesired Size の値が遅れていることが判明。内容が変化したタイミングでは、Desired Size が更新されていない様子。Reflectorではきっちり取れてたっぽいのだけど。
ということで Force Layout Prepass を入れてみたらうまくいった。
改行文字を置換する関数を少し整えて、結果をReturnValueにして外に出すようにする。ついでに、キャッシュ用の変数も更新。関数名をFormatTextToStringに変更。
ちょっとした演出を作ってみます。
まずは、文字を削る関数を用意します。
ここでもEditableTextの内容を一旦ローカル変数に入れて、 Right Chop というノードで一文字削って また元に戻しています。
この関数を動かすために、カスタムイベントを新しく作ります。
結果を見て、引き続き実行するかしないかを分岐します。文字の数だけこのカスタムイベントが実行されます。
あとは、送るボタンを押した時に、このカスタムイベントを呼べばいいだけ。
Playしてみると。
送ってる感が出てる気がする。
ひとまずこんなとこかな。
おまけ
ビヘイビアの設定をちょっとだけ確認してみた。
■ Select All Text when Focused 初期値:false
これを有効にすると、フォーカスした際に、テキストが全選択された状態になる。
コピペさせたい時に使えそう。
■ Clear Text Selection on Focus Loss 初期値:true
選択中の文字列がハイライトされた状態を維持するかどうか。デフォルトだと、フォーカスが他に移った時点でハイライト(Selection)がクリアされる。
■ Revert Text on Escape 初期値:false
入力中のテキストを一つ前の状態に戻すかどうか。フォーカスアウトした際にテキストの内容が確定してどこかしらに保存されるようで、内容を書き換えて ESCキーを押すと、フォーカスアウトした時点に戻る。書きかけでも安心。
■ Clear Keyboard Focus on Commite 初期値:true
キーボードでのフォーカス遷移に関係してそうだけど、うまく検証できなかった
■ Allow Context Menu 初期値:true
いわゆる右クリックメニュー。有効にするかしないか。
そういえば、 Appearance の設定で気になるのがあって、まだ使い方が分かってないんだけど、あれは リッチテキスト用かな?また機会があれば触ってみたいと思います。
最後に
今回の文字入力用Widgetについてあまり記事が上がっていない件で、思いつく範囲で少し触れてみたいと思います。
使い方はそれほど難しいものでもない。
というのが、あえて記事に取り上げる意欲が湧かない要因だと思いますが、ゲームでユーザーのテキスト入力を扱うということは、とてもたくさんのリスクを作り手側が負うことになるので、慎重にならざるを得ないのが実情で、これによって扱わない開発者も多いのでは、というのも結構大きな要因かと思います。
まずこのリスクをいくつか挙げておきます。
アジア圏の文字種の多さ。これは多言語対応する場合はフォントの選択肢が現状ほぼありません。表示できない文字が出てくる。
フォントの契約によっては、ユーザーが自由に文字組を編集できるということがライセンス許諾外になることが多く、その場合追加のライセンス料が発生することがある。
差別や性的、侮辱的、ドラッグ絡みなどのNGワードの置き換え処理が大変。オンラインだと多言語での対応になるので。日々ネットスラングが生まれる現状でセンシティブな内容にどこまでシステムがフォローできるかは頭の痛い問題。置き換えをあきらめた場合は、ダイアログウィンドウで事前に告知したり、不快な文章を発信しているユーザーを報告したりキックできる仕組みが必須になります。
オンラインの場合、UGC(UserGeneratedContents)になりうるので、これもゲームの場合、CEROやESRBなど、レーティングが上がることになります。
ちょっと前に流行ったのは自由文ではなく、定型文の組みあわせによる文字列生成。
これはフォントの問題は解決できるし、事前に翻訳できるので多言語対応しやすい。
最近ではスタンプもよく見かけます。
ユーザーが文字入力できるというのはそれなりに覚悟がいるので、実装経験が少ないということだろうと、勝手に推測しています。
ではでは
今回はこの辺で
ステキな文字入力ライフを!
チャットUIを作る
6
ListViewはまた今度時間作っていじってみようと心に決め、VerticalBoxでスクロールアウトした部分の処理について考えることにする。できれば削除せずににひたすらスクロールさせれることができればいいのだけど。短いとはいえ2分程度の観戦時間でもかなりの量のフキダシを抱えることになるだろうから、適度に軽くしてやらないといけない。プログラマにお願いしたいところだけど、見た目の調整も含め設計をやっている以上こちらである程度カタチにしてからの方が、問題点を見つけやすいし改善点についても相談しやすくなる。デザイナーが何をしようとしているのか、どういう部分の見た目にこだわっているのか、といったようなことが口頭だと伝わりにくいので、それなりに動くプロトタイプは結果的に無駄な連携コストを下げることができると思う。実をいうと単に作ってみたいだけだったりする。
まずは確実に画面外に出たかどうかが分かれば、その要素を削除して、間が詰まったことを悟らせないようにする。まず思いつくのは移動した分を戻す方法。
要素が削除されると当然次の子要素が上がってくる。上に詰められた高さぶんを瞬間的に移動させて、何事もなかったかのようにしないといけない。
配列の削除と位置移動を瞬間的に行うことになるので処理落ちした時に大丈夫か心配にはなるけど、その辺の危険性はこのプロトタイプができてからプログラマに訊いてみよう。もっと素敵な方法があればあとから構造を変えればいい。あらかじめ一番上にスペーサーを入れておいて、フキダシを消してからサイズを変更するとか、Paddingの設定でスキマを調整するのもありかもしれない。それはそれで、投稿数が多くなれば危険か。VerticalBoxを使わないというのもありかもしれない。おっといけない。プロトタイプは作ってから検証しないとな。どんどん作ろう。
画面から出たかどうかの判定はどうするか。フキダシ毎に高さが変わるので、フキダシの高さを保存しておく変数が必要だ。画面にいくつも並ぶからそのぶんの変数が要るとなると配列だな。
これは、イベントディスパッチャーから返ってきた値を受け取るイベントがあったから、そこで積もう。
保存した高さは、画面外に出た量と比較するときに使って、また位置を戻すときの量にもなる。設定したMarginの分も入れとかないと。
次は削除イベント作ろうかな。まずはカスタムイベントを置いて、と。
VerticalBoxから一番上に積んであるフキダシ、つまり子要素の削除だから、child で検索すると・・・あった。Remove Child Atノード。
Index番号は常に先頭を削除するから 0のままでOKだ。
削除したぶんの高さを移動するから、Set Render Translation ノードをつないで。
VerticalBoxの座標は、positionCurrent 変数が持ってるから、そこから差し引けばいいかな。VerticalBoxの子要素削除と移動は処理に間を空けたくないから、事前に計算しておこう。
あとは、高さの配列も先頭のやつを削除しておかないとズレてしまう。
現在地の座標を下げたら、目的地の座標も下げないと。
よし、ひとまずこれで必要そうな処理はできたはず。このイベントを呼ぶためにチェックするのは座標を更新してるTickで。
さてどう比較するかだけど、VerticalBox は画面の下から上に向かって伸びていく。Translation の値は0から増えていくから、はみ出すころには画面の高さを越えてるはず。今回解像度は1080p想定だから、これをスクロール量から引けばいいのか。
こうかな。
よし試してみよう。
ってあれ?
なぜ戻る?目的地と現在地を一緒に戻してるからズレないはずだけど・・・
あ、ここか!
高さをキャッシュしている配列の先頭がいなくなってから目的地を戻してる。それはズレるな。悔しい。順番を逆にしないと。
ふう、これで大丈夫。再生してみても問題なし。
今は処理に余裕があるから問題ないように見えているだけかもしれないけど。
そうだ、フキダシの追加と削除は随時いろんなタイミングで行われるから、 Tick処理の停止を入れておこう。
Booleanでフラグを作って。
まずはここで止めねば。
そして初期値が false だからこのWidgetがViewortに置かれた時点ではTickの処理はここで止まり、先へは走らないようになった。となると、このTickを動くようにする蛇口に相当するトリガーのようなものが必要になる。それは、フキダシを追加した後だな。
バインドしたイベントディスパッチャーの受け取りイベントの最後で、フラグを true にする。ただしここはフキダシが追加される度に来るから、DoOnce ノード入れておこう。
これで、初めてフキダシが追加されて、高さの値が返ってきたらTick開始となる。
あとは、フラグを false にする部分だけど、フキダシの削除が決定したBrachのところが最適だろう。Tickは常時ものすごく短い間隔で処理されるから少しでもタイムラグをなくしたい。
この辺で一旦流れを確認してみよう。
1.このWidgetが Viewportに 追加されるとき isAnimEnable は false だからTickは動かない
2.フキダシ「追加」のイベントが呼ばれる まだ isAnimEnable は false
3.バインドしたフキダシWidgetから値が返ってきた ここで isAnimEnable を true
※初回の1回だけ
4.Tick が動きだして、VerticalBox が移動を始め、はみだしチェック常時行う
5.はみだし量が先頭のフキダシの高さを超えたので isAnimEnable を false にして Tick止める
6.フキダシ「削除」のイベントしてVerticalBoxの位置を戻す
7.フキダシは随時追加されるたびにスクロールの目的地は増え続けている
8.Tick再開・・・
おっと、 isAnimEnable を 再び true にするの忘れてた。
フキダシ削除イベントの最後でいいかな。
よし、これで動かしてみて問題なさそうだったら次に進める。
大丈夫そう。ちょっとグラフを眺めてみるか。基本の処理はスクロール担当の EventTick と、フキダシの追加と削除の 3つだ。
・EventTick
・addFukidashi
・removeFukidashi
あとは、ダミーテキスト用の関数。
フキダシの表示は大体できたも同然だな。左右のレイアウト切り替えと顔アイコン、フキダシのしっぽは、パーツを追加すればいい程度だしそんなに心配するような難しさじゃないと思いたい。やっぱりいよいよ入力フォームかな。なんだか緊張する。
硬くなった筋をほぐすように首と肩を動かす。プログラマたちの談笑が聞こえる。そういえばずっと聞こえていたような気がするが、それだけ集中できていたということだろう。
さた、フォームはやったことないから、調べないとな。
つづく