UnityでUIアプリ:Windows上がりのプログラマ:画面サイズとコンポーネントサイズの対応(Canvas Scaler)

Windowsプログラマ上がりの私は、スマホアプリを作るにあたり画面サイズとコンポーネントサイズの対応に悩みました。そのあたりを長くなりますが記載します。

Windows画面とスマホ画面の違い

Windowsでプログラムを作っていたものにとって、スマホ・タブレットの大きな違いは下記の2点ではないかと思います。

  • 画面サイズと解像度がいろいろある
    スマホ・タブレットの画面は大小さまざまで解像度もさまざまです。つまりその中におさまるよう画面設計する必要があります(またはスクロール画面にする)。確かにWindowsも画面サイズ・解像度はさまざまでですが、
    • スマホ・タブレットではアプリがフルスクリーンで実行されることが多く、画面サイズの違いがそのまま反映される。Windowsだとユーザが適当に調整してくれる(はず)。
    • 解像度はさまざまと言ってもスマホほどの違いはない。特に最近のスマホは超高精細で下手に設計すると機種によっては文字が小さすぎて読めなくなる。
  • 指で操作する(ことが多い)
    もちろん指以外で操作する機種もあるでしょうが指操作が主流と思います。コンポーネント(例えばチェックボックス)を押すのはマウスではなくて指です。マウスポインタはかなり精密に画面位置を指定できますが、指は大きいのであまり正確な操作には向いていません。

設計方針の案

ボタンやテキストフィールドなどの各コンポーネントの表示サイズの方針として下記2案が浮かぶと思います。

A. 画面サイズに対し同じ比率でコンポーネントを表示。
 つまり小さな画面では画面全体が小さくなり各コンポーネントも小さい。

B.どの画面でも同じ物理サイズでコンポーネントを表示
 大きな画面でも小さな画面でも各コンポーネントは同じ物理サイズで表示する。

いずれもUnityだと簡単にプログラミングできます(後述)。
ゲームアプリの場合はAの方針でよいかと思います。しかしUI中心のアプリだと超高解像度のスマホでは、ボタンが小さすぎて押せない、ラベルの文字が小さすぎて読めない、などの問題が発生しかねません。
Bはその心配はありませんが、大きなコンポーネントのまま表示するとはみ出してしまうため、コンポーネントの配置を工夫して列数や行数を変えたり、スクロールできる画面にしたりすることになります。
あとせっかくの超高解像度なのにそれが生かせてない(もったいない)感じもします。

Unityのコンポーネント: Canvas Scaler

前置きが長くなってしまいましたが、このようなこともUnityにはコンポーネントが用意されています。
コンポーネントは Canvas Scaler というもので、Game Object のCanvasに挿入して使います。(というか標準でついていると思います。)

基本のプロパティは UI Scale Mode というもので以下の3つから選択できます。

1. Constant Pixel Size:解像度にかかわらず同じピクセル数で表示。
 但し別途 Scale Factor というプロパティで拡大・縮小が可能。

2. Scale With Screen Size: 画面サイズに比例して表示。
 画面の解像度が高い場合、コントロールのピクセルサイズを大きくする。

3. Constant Physical Size: 同じ物理サイズで表示。
 画面のサイズ・解像度にかかわらず常に同じ大きさで表示。


ここで3パターンの端末機種を想定してみましょう。
① タブレット、物理的に大きな 10インチ画面。解像度は1920×1200ピクセル。
② スマホ、物理的に小さな 5インチ画面。高解像度で 1920×1200ピクセル。
③ スマホ、物理的に小さな 5インチ画面。低解像度で 960×600ピクセル。
それぞれの機種では下記のようになるかと思います。

すなわち、
2. Scale With Screen Size は画面の物理サイズに比例した大きさの画面コンポーネント → 前述のAの画面設計
3. Constant Physical Size は画面のサイズによらずコンポーネントの物理サイズが同じ → 前述のBの画面設計
ということになります。

1. Constant Pixel Size を採用するのはどういうケースか分かりかねるところですが、一般のWindowsアプリならこれが普通の動きです(100ピクセルで設計したコンポーネントなら当然100ピクセルで画面表示される)。
但し 1. Constant Pixel Size では Scale Factor というプロパティがあり、これを操作するとコンポーネントの拡大・縮小が可能です。つまり自前で操作が可能です。
例えば、Scale Factor を画面ピクセル数(Screen.width, Screen.height)に比例した値にセットすれば、2. Scale With Screen Size と同様の動きとなり、また画面解像度(Screen.dpi、1インチあたりのピクセル数)に比例してScale Factor をセットすれば、3. Constant Physical Size と同様の動きになると思います。

残念な話:私のとった方針

で私のとった方針を説明しますが、実は設計時このあたりの仕組みがよくわからず、かつスマホでアプリを作るのが初めてだったこともあり適当に方針を決めたので、あまり良い方針とは思えません。
一応、私がとった方法とそのコーディングサンプルを示します。

方針は、
・基本的には画面サイズに比例した動き(2. Scale With Screen Size の動き)
・但し解像度が高いスマホだと小さくなりすぎるので、さらに調整。
・こういう手動制御のため「1. Constant Pixel Size 」を採用し、Scale Factorで調整。
としました。具体的なコーディングは下記のとおりです。

Vector2 CDefaultRes = new Vector2(1920, 1080);
const float CDefaultDpi = 96f;

Vector2 resTarget = new Vector2(Screen.width, Screen.height);
float dpiTarget = Mathf.Max(Screen.dpi, CDefaultDpi);   // dpi を実装できてないシステムがあるらしいので
float resScale1 = Mathf.Min(resTarget.x, resTarget.y)
          / Mathf.Min(CDefaultRes.x, CDefaultRes.y);  // 短辺の比率取得
float resScale2 = Mathf.Max(resTarget.x, resTarget.y)
                   / Mathf.Max(CDefaultRes.x, CDefaultRes.y);  // 長辺の比率取得
float resScale = Mathf.Min(resScale1, resScale2);   
                   // 二つの比率の小さいほうを採用(特に理由はありません。)
float dpiScale = Mathf.Pow(dpiTarget / CDefaultDpi, 0.5f);
        // dpi の単純比例で拡大・縮小すると大きくなりすぎるので平方根にした。
        // dpi の影響をさらに少なくするには 0.5 より小さい値にする(但し0以上)。

CanvasScaler scaler = mainCanvas.GetComponent<CanvasScaler>();
if (Application.platform == RuntimePlatform.Android ||
    Application.platform == RuntimePlatform.IPhonePlayer) {
    scaler.scaleFactor = resScale * dpiScale;
} else {    // スマホ・タブレット以外は scaleFactor は1のまま
    scaler.scaleFactor = 1;
}

一応ポイントとしては、画面サイズと解像度の両方を加味しているのですが、単に解像度に比例させると大きくなりすぎる(解像度の違いによる影響が大きくなりすぎる)ことが考えられるので、累乗(pow関数)を使ってdpi比率の平方根を掛けて影響を弱くしている点です(powの値を変えることで影響度をさらに調整可能)。

Canvas Scaler, Scale Factor を使う場合の留意点

  • Transform の localScale との違い
    多分ほぼ同じ。但しメインCanvas の localScale は自前で操作できず、Scale Factorで操作するようです。
  • World座標との兼ね合い
    World座標は Scale Factor にかかわらず画面のピクセル値です。
    このためマウス座標(Input.mousePositionやPointerEventData.positionなど)のようにWorld座標をもつものは常にLocal座標に変換してから使う必要があります。
  • Windows などスマホ・タブレット以外の環境はどうする
    scaleFactor は 1 のままにしておくのが無難。
  • scaleFactorを変えるのはどのタイミングでするか。
    メインCanvas の Awake() でセットするのがよいようです。
    途中で Scale Factor を変えると、Handle() メソッド(Protected)を呼ぶ必要があるそうです。
    端末の解像度や大きさは途中で変わらないはずなので最初に1回だけセットするので良いかと思います。(Windowsのように画面サイズを変えられる場合はscaleFactor は 1 のままにする。)

結論

結局あまり役に立たない話でした。ただ、スマホ・タブレットのUI開発の上で画面サイズ対応はかなり面倒ですが、 Unity には Canvas Scaler という仕組みがあるのでいろいろ工夫することが可能です。という話でした。
皆さんはもっと上手に工夫してみてください。次に作るときは私ももう少しうまく工夫しようと思います。

以上