土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

#unity 小ネタ:RectTransform.sizeDeltaの初期値について

 ここ1週間くらいcanvasの挙動に嵌まっていて、ようやく解決出来たのでメモ。結果的には自分の単純な見落としで反省しました。

 趣味でテキストレイヤフレームワークを作っています(そんな趣味ある?*1)。canvas上で動作する物で、1文字ずつオブジェクトを生成して並べて行を構成し、複数の行を並べてテキストウィンドウを構成します(なんでTextなりTextMeshProを使わないのかという話はまたいずれ)。

 プロポーショナルフォントを使っているため文字毎に幅が異なり、整列にはAutoLayoutを使っています。この仕組みはそれなりに動いているんですが、ある挙動(以下フローA)を実行した時だけ、表示される文字群が途中から100ピクセル単位で等間隔に並んでしまう現象が起きました。これの原因が分からない。

 このフレームワークでは疑似的な非同期で文字単位アニメーションが可能で、その為、毎フレーム複雑な処理を実行しています。文字群の途中から描画がおかしくなるのは恐らくそのせいなのでしょう。そのためコードにバグが無いか調べて続けたもののどうにも分からない。最後の方では「これcanvasのバグなんじゃ?」とまで思ってました(冤罪でしたごめんなさい)。

 そもそも、なんで不具合発生時に100ピクセル単位で配置されるんでしょうか? その「100」って数字はどこから出て来たんだ? 自分のソースコード内にはそんなマジックナンバーないぞ? ……と、ここで気づければ良かったんですが、その後も色々試しては撃墜されていました。

 最終的に、この「100」という数字が、RectTransform.sizeDeltaの初期値だったことが判明しました。

 土屋のコードでは以下の様なロジックになっています。

1・文字オブジェクトを生成してCanvasに追加する(このオブジェクトはRectTransformを含む)
2・文字オブジェクトの中身を初期化する(ここで文字テクスチャが生成され、RectTransform.sizeDeltaに文字サイズが設定される)
3・AutoLayoutが実行され、文字サイズに応じて整列する

 ご存知の通りMonoBehavior継承オブジェクトはコンストラクタを持てないため、生成と初期化は同時には行えません。通常フローでは1(生成)と2(初期化)は連続して行われます。しかし、フローA実行時のみ、処理タイミングがずれて1と2の間に1フレーム発生していました。
 結果、フローAの際は以下の様なロジックになっていました。

1・文字オブジェクトを生成してCanvasに追加する
3・AutoLayoutが実行され、文字サイズに応じて整列する
2・文字オブジェクトの中身を初期化する(ここで文字テクスチャが生成され、文字オブジェクト自体のサイズが決定される)

 AutoLayoutは、あるフレームにおいて、他のオブジェクトが処理を終えた後で実行されます。その為、1と2の間でフレームが発生する場合、文字オブジェクトのサイズを決めるように前にAutoLayoutが実行されてしまいます。この時RectTransform.sizeDeltaには初期値(100,100)が設定されているため、当然文字オブジェクトは100ピクセル単位で整列されます。
 そして次のフレームでようやく文字オブジェクトのサイズが決定し、RectTransform.sizeDeltaが更新されるのですが、その時には既に100ピクセル単位で整列してしまっているわけです。

 というわけでした。今回のケースは、土屋がcanvasのルールに対する理解の浅さに起因した物で、本来は避けられた物でした。ただ、RectTransform.sizeDeltaの初期値が(100.0f,100.0f)だと知っていれば、不具合特定にこんなに時間をかけずに済んだとも言えるので、そのことを忘れないように記事に残しておく事にしました。

 RectTransform.sizeDeltaの初期値は(100.0f,100.0f)だ! 忘れるな!(ただしドキュメントには書いてないので本当に100.0fなのかは知らない)

*1:あるんです。