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

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

大画面の基本構成

ゲームがサイズ変更可能性を処理できるかどうかを宣言します。

<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 が表示される。

互換モードを終了し、アクティビティの再作成を回避する手順は次のとおりです。

  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);
}