大画面のサイズ変更のサポート

スマートフォンからさまざまな大画面フォーム ファクタに拡張する場合、ゲームでのウィンドウ管理の処理方法に関する考慮事項が必要になります。ChromeOSPC 版 Google Play Games では、メインのデスクトップ インターフェースからウィンドウ モードでゲームを実行できます。Android 12L(API レベル 32)以降を搭載し、画面幅が 600 dp を超える新しい Android タブレットや折りたたみ式デバイスでは、ゲームを他のアプリと分割画面モードで並べて実行したり、サイズ変更を行ったり、折りたたみ式デバイスで内側と外側のディスプレイ間を移動することで、ウィンドウ サイズや一部のデバイスでは画面の向きの構成が変更されます。

Unity ゲームでのサイズ変更可能性

大画面の基本構成

ゲームがサイズ変更に対応できるかどうかを宣言します。

<android:resizeableActivity="true" or "false" />

サイズ変更をサポートできない場合は、サポートする最小アスペクト比と最大アスペクト比をゲーム マニフェストで明示的に定義するようにしてください。

<!-- Render full screen between 3:2 and 21:9 aspect ratio -->
<!-- Let the platform letterbox otherwise -->
<activity android:minAspectRatio="1.5">
<activity android:maxAspectRatio="2.33">

PC 版 Google Play Games

PC 版 Google Play Games の場合、プラットフォームは指定されたアスペクト比を考慮しながらウィンドウのサイズ変更を処理します。ウィンドウ サイズは最適なサイズに自動的にロックされます。メインの向きが横向きの場合は 16:9 以上、ゲームが縦向きの場合は 9:16 以上のアスペクト比をサポートする必要があります。最適なエクスペリエンスを得るには、横向きゲームの場合は 21:9、16:10、3:2 のアスペクト比を明示的にサポートしてください。ここではウィンドウのサイズ変更は必須ではありませんが、他のフォーム ファクタの互換性を確保するために必要です。

詳細とベスト プラクティスについては、PC 版 Google Play Games のグラフィックを設定するをご覧ください。

ChromeOS と Android の大画面

ChromeOS と大画面の Android デバイスでゲームの表示領域を最大化するには、全画面没入モードをサポートし、decorView、システム UI の表示、または WindowInsetsCompat API を使用してシステムバーを非表示にします。また、回転やサイズ変更の設定イベントを適切に処理するか、ChromeOS デバイスでそのようなイベントが発生しないようにする必要があります。

大画面の Android デバイスでは、まだ処理していない構成でゲームを実行できます。ゲームが一部のウィンドウ サイズと向きの構成をサポートしていない場合、プラットフォームはゲームを互換モードでレターボックス表示し、必要に応じて、サポートされていない構成に変更する前にプレーヤーにメッセージを表示します。

図 1. 構成の互換性ダイアログ。

一部のデバイスでは、プレーヤーがサポートされていない設定に移動すると、ゲームを再読み込みして新しいウィンドウ レイアウトに最適なアクティビティを再作成するためのオプションが表示され、それによってプレイ エクスペリエンスが中断される場合があります。さまざまなマルチウィンドウ モード構成(2/3、1/2、1/3 ウィンドウ サイズ)でゲームをテストし、ゲームプレイや UI 要素が中断されたりアクセス不能になったりしないことを確認します。さらに、折りたたみ式デバイスで内側と外側の画面間を移動する際に、折りたたみ式デバイスの連続性にゲームがどのように反応するかをテストします。問題が発生した場合は、これらの設定イベントを明示的に処理し、大画面のサイズ変更に関する高度なサポートを追加してください。

高度な大画面サイズ変更

図 2.デスクトップの UI とテーブルトップ形状の折りたたみ式デバイスの UI。

互換モードを終了し、アクティビティの再作成を回避するには、次のようにします。

  1. メイン アクティビティをサイズ変更可能として宣言します。

    <android:resizeableActivity="true" />
    
  2. すべての大画面構成イベントを受け取るために、ゲーム マニフェストの <activity> 要素の android:configChanges 属性で「orientation」、「screenSize」、「smallestScreenSize」、「screenLayout」、「密度」の明示的なサポートを宣言します。

    <android:configChanges="screenSize | smallestScreenSize | screenLayout | orientation | keyboard |
                            keyboardHidden | density" />
    
  3. onConfigurationChanged() をオーバーライドして、現在の向き、ウィンドウ サイズ、幅、高さなどの設定イベントを処理します。

    Kotlin

    override fun onConfigurationChanged(newConfig: Configuration) {
       super.onConfigurationChanged(newConfig)
       val density: Float = resources.displayMetrics.density
       val newScreenWidthPixels =
    (newConfig.screenWidthDp * density).toInt()
       val newScreenHeightPixels =
    (newConfig.screenHeightDp * density).toInt()
    
       // Configuration.ORIENTATION_PORTRAIT or ORIENTATION_LANDSCAPE
       val newScreenOrientation: Int = newConfig.orientation
    
       // ROTATION_0, ROTATION_90, ROTATION_180, or ROTATION_270
       val newScreenRotation: Int =
    windowManager.defaultDisplay.rotation
    }
    

    Java

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
       super.onConfigurationChanged(newConfig);
       float density = getResources().getDisplayMetrics().density;
       int newScreenWidthPixels = (int) (newConfig.screenWidthDp * density);
       int newScreenHeightPixels = (int) (newConfig.screenHeightDp * density);
    
       // Configuration.ORIENTATION_PORTRAIT or ORIENTATION_LANDSCAPE
       int newScreenOrientation = newConfig.orientation;
    
       // ROTATION_0, ROTATION_90, ROTATION_180, or ROTATION_270
       int newScreenRotation = getWindowManager().getDefaultDisplay()
               .getRotation();
    }
    

WindowManager をクエリして、現在のデバイスの回転を確認することもできます。このメタデータを使用して、新しいウィンドウのサイズを確認し、フルウィンドウ サイズにレンダリングします。アスペクト比の違いにより、この方法がうまくいかない場合もあります。その場合は、ゲームの UI を新しいウィンドウ サイズに固定し、コア ゲームプレイ コンテンツをレターボックス表示します。技術的または設計上の制約があり、いずれの方法も使用できない場合は、独自のエンジン内レターボックスを使用してアスペクト比を維持し、resizeableActivity = false を宣言して構成モードを回避しながら、可能な限り最適なサイズにスケーリングします。

どのようなアプローチを採用するにしても、さまざまな構成(折りたたみと展開、異なる回転の変更、分割画面モード)でゲームをテストし、ゲーム内 UI 要素が切り取られたり重複したりしないか、タップ ターゲットのユーザー補助の問題、アスペクト比の問題でゲームが引き伸ばされたり、押しつぶされたり、歪んだりしないようにします。

また、画面が大きいほどピクセル数が大きくなるのが一般的です。領域が広ければピクセルの数が同じになるためです。これにより、レンダリング バッファのサイズが小さくなったり、アセットが低解像度になったりすると、ピクセル化が発生する可能性があります。大画面のデバイスでは最高品質のアセットを使用し、ゲームのパフォーマンス プロファイルを使用して問題がないことを確認します。ゲームで複数の品質レベルをサポートする場合は、大画面デバイスを考慮する必要があります。

マルチ ウィンドウ モード

マルチ ウィンドウ モードは、複数のアプリで同じ画面を同時に共有できる機能です。マルチウィンドウ モードはアクティビティのライフサイクルを変更しませんが、複数のウィンドウでのアプリの再開状態は Android のバージョンによって異なります(マルチウィンドウのサポートマルチウィンドウ モードでのアクティビティのライフサイクルをご覧ください)。

プレーヤーがアプリやゲームをマルチウィンドウ モードにすると、大画面のサイズ変更の詳細設定セクションで指定されているように、システムはアクティビティに対して構成の変更を通知します。構成の変更は、プレーヤーがゲームのサイズを変更するか、ゲームを全画面モードに戻した場合にも発生します。

マルチ ウィンドウ モードになったアプリがフォーカスを取り戻す保証はありません。そのため、アプリの状態イベントのいずれかを使用してゲームを一時停止する場合は、フォーカス取得イベント(フォーカス値が true の onWindowFocusChanged())を使用してゲームを再開しないでください。代わりに、onConfigurationChanged()onResume() などの他のイベント ハンドラや状態変更ハンドラを使用してください。なお、isInMultiWindowMode() メソッドを使用して、現在のアクティビティがマルチウィンドウ モードで実行されているかどうかを検出できます。

ChromeOS のマルチウィンドウ モードでは、初期ウィンドウの寸法が重要な考慮事項となります。ゲームは全画面表示である必要はなく、そのためのウィンドウ サイズを宣言する必要があります。これに取り組むおすすめの方法は 2 つあります。

1 つ目の方法では、Android マニフェストの <layout> タグの特定の属性を使用します。defaultHeight 属性と defaultWidth 属性は、初期ディメンションを制御します。minHeight 属性と minWidth 属性についても、プレーヤーがサポートしていないサイズにゲーム ウィンドウのサイズを変更しないように注意してください。最後は gravity 属性です。この属性は、起動時にウィンドウを画面上のどこに表示するかを決定します。これらの属性を使用したレイアウト タグの例を次に示します。

<layout android:defaultHeight="500dp"
        android:defaultWidth="600dp"
        android:gravity="top|end"
        android:minHeight="450dp"
        android:minWidth="300dp" />

ウィンドウ サイズを設定する 2 つ目のオプションは、動的な起動境界を使用することで機能します。setLaunchBounds(Rect)⁠⁠ を使用すると、開始ウィンドウの寸法を定義できます。空の長方形を指定すると、アクティビティは最大化された状態で開始されます。

また、Unity または Unreal のゲームエンジンを使用している場合は、マルチウィンドウ モードを適切にサポートしている最新バージョン(Unity 2019.4.40 および Unreal 5.3 以降)を使用していることを確認してください。

折りたたみ式デバイスの形状のサポート

Jetpack の WindowManager レイアウト ライブラリを使用して、テーブルトップなどの折りたたみ形状をサポートし、プレーヤーの没入感とエンゲージメントを高めます。

図 3. ディスプレイの垂直部分にメインビュー、水平部分にコントロールがあるテーブルトップ形状のゲーム。

Kotlin

fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}

Java

boolean isTableTopPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}