このガイドでは、さまざまな Android フォーム ファクタで動作するようにゲームを適応させる方法について説明します。
始める前に、大画面のサイズ変更をサポートするをご覧ください。Android の最新のゲームは、システムの互換性レイヤに依存することなく、すべての画面サイズに適応する必要があります。これにより、ゲームの表示方法を常に制御でき、XR などのフォーム ファクタや画面を回転できないデバイスでも、プレミアムなプレーヤー エクスペリエンスを提供できます。
これを行うには、次のタスクを完了します。
- システムのレターボックス表示を無効にして、独自のエンジン内レターボックス表示を実装します。
- レターボックス領域を使用可能にし、テーマに沿ったものにします。
- 21:9 から 1:1、9:21 までゲームをスケーリングできるようにするために必要な設計上の考慮事項に対処します。
テスト用コマンド
ゲームをビルドしてテストする際は、ゲーム自体がレターボックスをレンダリングすることを確認します。また、テレビや車載ディスプレイなどのデバイスは、デフォルトの横向きまたは縦向きから回転できないため、ゲームを縦向きと横向きに強制的に切り替える必要があります。
ゲームエンジンがシステム レターボックスを使用しているかどうかを確認するには、adb shell cmd window set-letterbox-style コマンドを使用します。たとえば、次のコマンドは、システムのレターボックスを明るい緑色にし、ゲームに角丸を追加します。これにより、ゲームがオペレーティング システムの互換モードにフォールバックしたことが明確になります。
adb shell "cmd window set-letterbox-style --backgroundType solid_color --backgroundColor '#00FF00' --cornerRadius 30 --isEducationEnabled false"
テスト用に画面の向きを強制するには、adb shell cmd window user-rotation lock を使用します。これにより、向きのアプリ オーバーライドを尊重しないデバイスでのゲームの動作をシミュレートしたり、デバイスの向きに関係なくウィンドウが縦向きまたは横向きで表示される XR デバイスや PC のようなデバイスでユーザーに表示される内容を把握したりできます。
まず、アプリの向きをオーバーライドできる状態にデバイスを設定します。
adb shell cmd window set-ignore-orientation-request true
adb shell cmd window fixed-to-user-rotation enabled
次に、ゲームを横向きに変更してテストします。
adb shell cmd window user-rotation lock 1
次に、ゲームを縦向きに変更してテストします。
adb shell cmd window user-rotation lock 0
再起動するか、次のコマンドを実行して、デバイスを OEM 構成にリセットします。
adb shell cmd window reset-letterbox-style
adb shell cmd window fixed-to-user-rotation default
adb shell cmd window user-rotation free
adb shell cmd window set-ignore-orientation-request reset
システム レターボックス表示を無効にする理由
システム レターボックス表示は常にシステムが制御します。画面サイズを検出し、表示領域を自分で管理することで、入力がゲーム領域から外れたときに発生する入力のドロップなどの問題を軽減できます。ゲーム コンテンツが中央に固定されている場合でも、ゲームプレイ以外の要素を画面の端まで拡大できます。システム互換性モードを使用すると、ゲームを利用可能にできますが、プレーヤーにとってゲームを優れたものにするかどうかは、ゲーム デベロッパー次第です。
レターボックスの実装例
ほとんどのゲームエンジンでは、カメラが画面上でレンダリングする場所を制御できます。独自のレターボックス表示を実装するには、レンダリング領域のアスペクト比を測定し、システムがゲームをレターボックス表示するのではなく、未使用領域のクリッピング長方形を調整します。次のサンプルでは、1 台のカメラを制限して、制限されたアスペクト比でレンダリングし、UI カメラまたはパスを論理画面サイズ全体に拡大できるようにしています。
Unity
[SerializeField] private float minAspectRatio = 4f / 3f; [SerializeField] private float maxAspectRatio = 21f / 9f; private Camera _camera; private void Awake() { _camera = GetComponent<Camera>(); } public void MaintainAspectRatio() { int viewWidth = Screen.width; int viewHeight = Screen.height; // 1. Calculate screen aspect ratio float scaleScreen = viewWidth / (float)viewHeight; Rect rect = _camera.pixelRect; // 2. Scale to the fullest of the lower between width or height if (scaleScreen > maxAspectRatio) { // use height, lock width to max at aspect scale rect.width = viewHeight * maxAspectRatio; rect.height = viewHeight; } else if (scaleScreen < minAspectRatio) { // use width, lock height to min at aspect scale rect.width = viewWidth; rect.height = viewWidth / minAspectRatio; } else { // within allowed aspect ratio range, use full screen rect.width = viewWidth; rect.height = viewHeight; } rect.x = Mathf.Floor((viewWidth - rect.width) * 0.5f); rect.y = Mathf.Floor((viewHeight - rect.height) * 0.5f); Debug.Log("camera view width = " + rect.width + " height = " + rect.height + " x = " + rect.x + " y = " + rect.y); // 3. Commit the new size to the camera so that it takes effect _camera.pixelRect = rect; }
C++(Vulkan)
/** * @brief Calculates a bounded-aspect-ratio gameplay viewport and scissor * rectangle and records them directly into a Vulkan command buffer. * * @note Requires `VK_DYNAMIC_STATE_VIEWPORT` and `VK_DYNAMIC_STATE_SCISSOR` to * be enabled on the bound graphics `VkPipeline`. * * @param command_buffer Active `VkCommandBuffer` in the recording state. * @param window_width Full physical window or swapchain width in pixels. * @param window_height Full physical window or swapchain height in pixels. * @param min_aspect_ratio Minimum allowed aspect ratio (`width / height`). * @param max_aspect_ratio Maximum allowed aspect ratio (`width / height`). */ void MaintainAspectRatio(VkCommandBuffer command_buffer, uint32_t window_width, uint32_t window_height, float min_aspect_ratio, float max_aspect_ratio) { if (command_buffer == VK_NULL_HANDLE || window_width == 0 || window_height == 0 || min_aspect_ratio <= 0.0f || max_aspect_ratio < min_aspect_ratio) { return; } // 1. Calculate screen aspect ratio. const float view_width = static_cast<float>(window_width); const float view_height = static_cast<float>(window_height); const float scale_screen = view_width / view_height; // 2. Scale to the fullest of the lower between width or height. float rect_width = view_width; float rect_height = view_height; if (scale_screen > max_aspect_ratio) { // Use height, lock width to max at aspect scale (pillarbox). rect_width = std::floor(view_height * max_aspect_ratio); rect_height = view_height; } else if (scale_screen < min_aspect_ratio) { // Use width, lock height to min at aspect scale (letterbox). rect_width = view_width; rect_height = std::floor(view_width / min_aspect_ratio); } else { // Within allowed aspect ratio range, use full screen. rect_width = view_width; rect_height = view_height; } const float rect_x = std::floor((view_width - rect_width) * 0.5f); const float rect_y = std::floor((view_height - rect_height) * 0.5f); // 3. Commit the new viewport and scissor to the command buffer so that // subsequent gameplay draw calls are transformed and clipped to this region. const VkViewport viewport = { .x = rect_x, .y = rect_y, .width = rect_width, .height = rect_height, .minDepth = 0.0f, .maxDepth = 1.0f, }; const VkRect2D scissor = { .offset = { .x = static_cast<int32_t>(rect_x), .y = static_cast<int32_t>(rect_y), }, .extent = { .width = static_cast<uint32_t>(rect_width), .height = static_cast<uint32_t>(rect_height), }, }; vkCmdSetViewport(command_buffer, 0, 1, &viewport); vkCmdSetScissor(command_buffer, 0, 1, &scissor); }
Unity での詳しい手順については、大画面や折りたたみ式デバイス向けに Unity ゲームを最適化する Codelab をご覧ください。
大画面サイズと小画面サイズを処理する
スマートフォンや折りたたみ式デバイスの外側ディスプレイなどの小さな画面では、縦向きまたは横向きに固定されたゲームが最適なユーザー エクスペリエンスとなる場合があります。タブレット、Chromebook、車載ディスプレイ、折りたたみ式デバイスのインナー ディスプレイなど、画面が大きくなるにつれて、デバイスを回転させることは望ましくなくなり、場合によっては不可能になります。一方、物理ディスプレイが大きくなると、軸外のアスペクト比でもゲームをプレイできるようになります。
プレーヤーに最適なエクスペリエンスを提供するには、実行時に画面サイズを検出し、必要に応じて回転を制限します。画面サイズが大きくなるほど、プレーヤーの柔軟性が高まります。Android では、理想的なカットオフは sw600dp、つまり最小幅 600 密度非依存ピクセル(600 dp)であると見なされます。これは WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND に関連しています。computeMaximumWindowMetrics を使用して、マルチディスプレイ構成で使用可能な画面サイズを検証します。
ゲームでこれをサポートするには、マニフェストですべての画面の向きをサポートし、onCreate() と onConfigurationChanged() の両方で最小幅を評価して、画面の向きのロックを適切に設定します。
マニフェストで静的な向きのロックを削除し、構成変更の処理を宣言します。
<activity android:name=".MainActivity" android:resizeableActivity="true" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|density|keyboard|keyboardHidden" />必要なインポートを追加します。
import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Bundle import androidx.activity.enableEdgeToEdge import androidx.window.core.layout.WindowSizeClass import androidx.window.layout.WindowMetricsCalculator import kotlin.math.minonCreateとonConfigurationChangedの両方を処理します。override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) applyAdaptiveOrientationPolicy() // ... } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) applyAdaptiveOrientationPolicy() // ... }画面の最小幅に基づいてリクエストされた向きを更新し、コンパクトな画面をゲームの優先デザインの向きにロックします。
private fun applyAdaptiveOrientationPolicy() { // compute the smallest screen bound in dp val maxMetrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this) val minDimensionPx = min(maxMetrics.bounds.width(), maxMetrics.bounds.height()) val displaySmallestWidthDp = minDimensionPx / maxMetrics.density val targetOrientation = if (displaySmallestWidthDp >= WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND) { // Large display (>= 600dp): allow free rotation in any orientation ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } else { // Compact display (< 600dp): lock to your game's preferred orientation // (use SCREEN_ORIENTATION_SENSOR_LANDSCAPE for landscape-first games) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } // Guard against redundant setter calls to avoid configuration change loops if (requestedOrientation != targetOrientation) { requestedOrientation = targetOrientation } }
一般的な問題に対処する設計
さまざまなアスペクト比と向きをサポートすることで、プレーヤーに最適なユーザー エクスペリエンスを提供できます。スマートフォンとは異なり、大型ディスプレイは横向きまたは縦向きに固定されていることが多く、場合によっては画面がほぼ正方形になります。このような大画面デバイスでは、プレーヤーはゲームをプレイしながらマルチタスクを行うことがよくあります。画面スペースを最大限に活用することで、より多くのプレーヤーをより頻繁に、より長いプレイ セッションに引き付けることができます。
以下のセクションでは、スマートフォン以外の画面に埋め込まれたスマートフォン ゲームではなく、フォーム ファクタ全体でプレミアムなエクスペリエンスを実現するためのソリューションについて説明します。
アスペクト対応カメラ
3D ゲームでは、画面が縦に長すぎると、プレーヤーは狭いスリットからゲームを見ているように感じることがあります。また、画面が横に長すぎると、プレーヤーは設計されたインタラクションを見逃す可能性があります。2D ゲームと同様に、「セーフエリア」を常に表示しておくことができます。次のコードは、アクティブなアスペクト比と基準アスペクト比(1920x1080)の比較に基づいて、カメラの水平画角と垂直画角を切り替えます。これにより、デザイナーは 1080p ディスプレイ用のゲームを構築し、その基準フレーム内で設計されたすべてのものが表示されたままになることを確認できます。
Unity
[SerializeField] private Camera _camera; [SerializeField] private float _referenceWidth = 1920f; [SerializeField] private float _referenceHeight = 1080f; [SerializeField] private float _referenceVerticalFov = 60f; public float ReferenceAspectRatio => _referenceWidth / Mathf.Max(0.0001f, _referenceHeight); private void Awake() { if (_camera == null) { _camera = GetComponent<Camera>(); } } // Built-in Render Pipeline callback. In URP, subscribe to // RenderPipelineManager.beginCameraRendering or call UpdateFov() on resize. private void OnPreCull() { UpdateFov(); } public void UpdateFov() { if (_camera == null || _camera.orthographic) { return; } float referenceAspect = ReferenceAspectRatio; float currentAspect = _camera.aspect; if (currentAspect <= 0.0001f) { return; } float targetVerticalFov; if (currentAspect >= referenceAspect) { // Viewport is wider than (or equal to) the reference safe area targetVerticalFov = _referenceVerticalFov; } else { // Viewport is skinnier than the reference safe area float referenceHorizontalFov = Camera.VerticalToHorizontalFieldOfView(_referenceVerticalFov, referenceAspect); targetVerticalFov = Camera.HorizontalToVerticalFieldOfView(referenceHorizontalFov, currentAspect); targetVerticalFov = Mathf.Clamp(targetVerticalFov, 1f, 179f); } if (!Mathf.Approximately(_camera.fieldOfView, targetVerticalFov)) { _camera.fieldOfView = targetVerticalFov; } }
C++(Vulkan)
/** * @brief Calculates an aspect-aware Vulkan perspective projection matrix using * GLM that keeps a reference safe area (e.g., 1920x1080) visible across * aspect ratios. * * @note Uses `glm::perspectiveZO` for Vulkan's `[0, 1]` depth range and * inverts `projection[1][1]` for Vulkan's top-left (`+Y` down) clip space. * * @param viewport_width Active gameplay viewport width in pixels. * @param viewport_height Active gameplay viewport height in pixels. * @param reference_width Authored safe-area reference width in pixels. * @param reference_height Authored safe-area reference height in pixels. * @param reference_vertical_fov Authored vertical FOV in degrees (e.g., 60.0f). * @param near_plane Near clipping plane distance (`zNear > 0.0f`). * @param far_plane Far clipping plane distance (`zFar > zNear`). * @return Aspect-aware `glm::mat4` perspective projection matrix for Vulkan. */ glm::mat4 CalculateAspectAwareProjectionMatrix( float viewport_width, float viewport_height, float reference_width = 1920.0f, float reference_height = 1080.0f, float reference_vertical_fov = 60.0f, float near_plane = 0.1f, float far_plane = 100.0f) { if (viewport_width <= 0.0001f || viewport_height <= 0.0001f) { return glm::mat4(1.0f); } // 1. Compare the active viewport aspect ratio against the reference safe // area. const float reference_aspect = reference_width / std::max(0.0001f, reference_height); const float current_aspect = viewport_width / viewport_height; float target_vertical_fov_rad = glm::radians(reference_vertical_fov); if (current_aspect >= reference_aspect) { // Viewport is wider than (or equal to) the reference safe area (Hor+). target_vertical_fov_rad = glm::radians(reference_vertical_fov); } else { // Viewport is skinnier than the reference safe area (Vert+): // Convert reference vertical FOV -> reference horizontal FOV -> new // vertical FOV at current_aspect so the safe area is never cropped. const float tan_half_horiz = std::tan(target_vertical_fov_rad * 0.5f) * reference_aspect; target_vertical_fov_rad = 2.0f * std::atan(tan_half_horiz / current_aspect); target_vertical_fov_rad = std::clamp( target_vertical_fov_rad, glm::radians(1.0f), glm::radians(179.0f)); } // 2. Build the Vulkan [0, 1] depth perspective matrix and flip +Y clip space. glm::mat4 projection = glm::perspectiveZO( target_vertical_fov_rad, current_aspect, near_plane, far_plane); projection[1][1] *= -1.0f; return projection; }
完全にインタラクティブなレターボックス
ゲームによっては、設計上、プレイエリアが固定されているものがあります。これは、ジャンルベースの設計上の考慮事項に一致することが多く、たとえば、縦スクロール シューティング ゲームの固定フィールドや、レトロな美学に使用されるフリップ画面スクロールなどがあります。
このようなゲームでは、ゲームプレイ エリアに合わせてゲームプレイ エリアをスケーリングし、ゲームの 1 つの軸が常に最大化されてゲームプレイ エリアを埋めるようにします。どの軸が最大化されるかは、ゲームの相対的なアスペクト比とディスプレイの境界によって変わる可能性があります。
HUD と UI 要素を画面のセーフエッジに固定します。カットアウトを避け、仮想コントロール、ヘルスバー、ミニマップなどのインタラクティブ要素を、ゲームプレイ エリアからできるだけ離れた場所に配置します。これにより、ゲームのインタラクティブな部分が画面全体を使用し、タブレットや折りたたみ式などのフォーム ファクタのプレーヤーは、指を過度に伸ばすことなくゲームを快適にプレイできます。
最後に、レターボックス領域を適切なテーマまたはダイエジェティックに設定します。画面上のパレットやタイルセットに一致する枠線、ボードゲームやレトロゲーム用の仮想テーブルや画面、拡張ミニマップや装飾的なビジュアルを検討してください。
次の例では、ゲームはほとんどのアスペクト比でフルサイズでレンダリングされます。アスペクト比が 4:3 より狭い場合は、テーマに沿った色のスカイボックスをレンダリングし、コントロールなどの UI 要素を画面の端に配置します。
|
|
戦場の霧
多くのゲーム デベロッパーは、極端な縦横比によってプレーヤーが不当な優位性を得られることを懸念しています。たとえば、マルチプレーヤー型オンライン バトルアリーナ(MOBA)などの対戦ゲームのプレーヤーは、非常に縦長の画面を使用して、対戦相手のレーンを深く見渡すことができます。
トップダウンの戦略ゲームやシミュレーション ゲームでは、戦場の霧のレンダリングを検討します。
- プレーヤーが表示できるようになるまで、ジオメトリをレンダリングしない。
- プレーヤー キャラクターがアクティブに表示できないジオメトリを、動的要素なしで暗くレンダリングします。
- プレーヤー キャラクターが能動的に見ることができる対戦相手、収集アイテム、その他の動的オブジェクトのみを、物語世界に沿ってレンダリングします。
この手法を完全インタラクティブなレターボックスと組み合わせ、UI を画面の端までレンダリングすると、レベル構造やチームバランスを再設計することなく、洗練された意図的なエクスペリエンスをプレーヤーに提供できます。
このゲームを拡大して、戦場の霧なしで大きな画面に表示すると、プレーヤーが見るべきではないマップの部分が表示されます。
|
|
戦場の霧をレンダリングし、敵やアイテムなどの動的要素を非表示にすることで、ゲームで使用可能なフレーム全体を使用しながら、ゲームの公平性を維持できます。
|
|
ジオメトリをフェードアウトする
多くのゲーム(特にターン制ストラテジー ゲーム)では、前のセクションで説明した戦場の霧に似た手法を使用して、プレーヤーが発見していないジオメトリや、特定のセットピース中にプレーヤーが操作できないジオメトリをレンダリングしません。
画角を広げると、プレーヤーに表示すべきでないセットやシネマティックの部分が表示される場合は、シーンの特定の部分をきれいにデザインされた方法でマスキングすることで、固定された静的な境界を認識することなく、レターボックスと同じ実用的な効果を実現できます。
次の例では、ゲームをより大きなアスペクト比に拡大すると、城壁の外にゾンビの大群が現れます。
|
|
代わりに、現在のプレイエリアの境界外にあるレベル ジオメトリのレンダリングを省略できます。
|
|
ダイエジェティックな境界
一人称または三人称のマルチプレーヤー ゲームで画角を広げると競争上の優位性が得られる場合や、固定セットベースのゲームで未作成の領域が明らかになる場合など、没入型の 3D ゲームでは、長方形のレターボックスに依存するのではなく、ゲームにダイエジェティック要素を組み込んで画角を制限することを検討してください。次に例を示します。
- 車や飛行機などの車両の内部コックピット
- ヘルメットの内側
- プレーヤーの視界の端を示すビネット
UI 要素と HUD 要素は、画面の端まで拡張するか、ダイエジェティックな境界に組み込む必要があります。また、動的にすることが理想的です。これらのダイエジェティック要素は、特に XR フォーム ファクタで乗り物酔いを経験するプレーヤーにも役立ちます。新しいアスペクト比の複雑さを軽減するだけでなく、ユーザー補助機能としても役立ちます。
インタラクティブでない静的なシーン内のサーフェスにゲームをレンダリングしないようにします。この方法は、レターボックスやピラーボックスに黒いバーをレンダリングするよりも優れていますが、プレーヤーは静的な枠線にすぐに気づきます。また、お気に入りのゲームがスマートフォン以外のフォーム ファクタでスマートフォン サイズの画面にレンダリングされると、不満を感じる可能性があります。
|
この例では、標準的なスマートフォンの画面向けに設計されたゲームが、スキューモーフィックなゲームパッドを備えたシミュレートされたテレビ内にレンダリングされ、折りたたみ式デバイスのインナー ディスプレイで一般的な、より正方形に近いアスペクト比に対応しています。
|
3D ゲームによっては、競技の公平性や設計上の考慮事項のために、画角を制限する必要がある場合があります。コックピット、ヘルメット、その他のダイエジェティックなメタファーを使用して画角を制限することは、黒いバーよりも好ましいです。
|
参考情報
- すべてのフォーム ファクタで Unity ゲームを最適化する
- すべてのフォーム ファクタにゲームを対応させる
- すべてのフォーム ファクタで自然な入力を可能にする
- Vulkan の事前回転でデバイスの向きを処理する