En esta guía, aprenderás a adaptar tu juego para que funcione en diversos factores de forma de Android.
Antes de comenzar, lee Cómo admitir el cambio de tamaño en pantallas grandes. Los juegos modernos en Android deben adaptarse a todos los tamaños de pantalla sin depender de las capas de compatibilidad del sistema. De esta manera, siempre tendrás control sobre cómo se muestra tu juego y este ofrecerá una experiencia de juego premium, incluso en factores de forma como la RE o dispositivos en los que no se puede rotar la pantalla.
Para ello, completa las siguientes tareas:
- Inhabilita el formato letterbox del sistema e implementa tu propio formato letterbox en el motor.
- Haz que el área de formato letterbox sea útil y temáticamente apropiada.
- Aborda las consideraciones de diseño necesarias para permitir que tu juego se ajuste de 21:9 a 1:1 y a 9:21.
Comandos para realizar pruebas
Cuando compiles y pruebes tu juego, verifica que renderice los formatos letterbox por sí mismo. También debes forzar el juego a las orientaciones vertical y horizontal, ya que los dispositivos como las TVs y las pantallas de los vehículos no pueden rotar desde su orientación horizontal o vertical predeterminada.
Para verificar si tu motor de juego usa el formato letterbox del sistema, usa el comando adb shell cmd window set-letterbox-style. Por ejemplo, el siguiente comando hace que los formatos letterbox del sistema sean de color verde brillante y agrega un radio de esquina a tu juego, lo que hace que sea obvio si tu juego recurre al modo de compatibilidad del sistema operativo:
adb shell "cmd window set-letterbox-style --backgroundType solid_color --backgroundColor '#00FF00' --cornerRadius 30 --isEducationEnabled false"
Para forzar una orientación para las pruebas, usa
adb shell cmd window user-rotation lock. Esto te ayuda a simular cómo se comporta tu juego en un dispositivo que no respeta las anulaciones de la app para la orientación o a comprender lo que ven los usuarios en un dispositivo de XR o similar a una PC en el que las ventanas pueden aparecer en modo vertical u horizontal, independientemente de la orientación del dispositivo.
Primero, pon tu dispositivo en un estado en el que puedas anular la orientación de la app:
adb shell cmd window set-ignore-orientation-request true
adb shell cmd window fixed-to-user-rotation enabled
A continuación, prueba cambiar el juego al modo horizontal:
adb shell cmd window user-rotation lock 1
Luego, prueba cambiar el juego al modo vertical:
adb shell cmd window user-rotation lock 0
Restablece el dispositivo a la configuración de OEM reiniciándolo o ejecutando los siguientes comandos:
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
¿Por qué inhabilitar el formato letterbox del sistema?
El sistema siempre controla el formato letterbox del sistema. Si detectas el tamaño de la pantalla y administras el área de visualización por tu cuenta, puedes mitigar problemas, como las entradas descartadas, que se producen cuando las entradas salen del área del juego. También puedes expandir los elementos que no son del juego hasta el borde de la pantalla, incluso si el contenido del juego permanece fijo en el medio. Los modos de compatibilidad del sistema pueden hacer que un juego esté disponible, pero depende de ti, como desarrollador, hacer que tu juego sea excelente para los jugadores.
Implementación de ejemplo de formato letterbox
La mayoría de los motores de juegos te permiten controlar dónde se renderiza una cámara en la pantalla. Para implementar tu propio formato letterbox, mide la relación de aspecto del área renderizada y ajusta el rectángulo de recorte para el área sin usar, en lugar de permitir que el sistema aplique el formato letterbox a tu juego. En el siguiente ejemplo, se restringe una cámara para que renderice en una relación de aspecto limitada, mientras que se permite que las cámaras o los pases de la IU se expandan al tamaño de pantalla lógico completo:
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); }
Para obtener una explicación paso a paso en Unity, consulta el codelab Optimiza juegos de Unity para pantallas grandes y dispositivos plegables.
Controla tamaños de pantalla grandes y pequeños
En pantallas más pequeñas, como las de teléfonos y las pantallas externas de dispositivos plegables, la mejor experiencia del usuario podría ser un juego bloqueado en orientación vertical u horizontal. A medida que las pantallas se vuelven más grandes, como las de las tablets, las Chromebooks, las pantallas de los vehículos o las pantallas internas de los dispositivos plegables, se vuelve menos deseable o incluso imposible rotar el dispositivo, mientras que la pantalla física más grande mantiene el juego en relaciones de aspecto fuera del eje.
Para que los jugadores disfruten de la mejor experiencia, detecta el tamaño de la pantalla durante el tiempo de ejecución y restringe la rotación cuando sea necesario, a la vez que les brindas más flexibilidad a medida que aumenta el tamaño de la pantalla. Android considera que el límite ideal es sw600dp, un ancho mínimo de 600 píxeles independientes de la densidad (600 dp).
Esto se correlaciona con WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND. Usa computeMaximumWindowMetrics para validar el tamaño de pantalla disponible en configuraciones de varias pantallas.
Para admitir esto en tu juego, admite todas las orientaciones en tu manifiesto y,
luego, en onCreate() y onConfigurationChanged(), evalúa el ancho
más pequeño y establece el bloqueo de orientación de forma adecuada:
Quita los bloqueos de orientación estáticos en tu manifiesto y declara el control de cambios de configuración:
<activity android:name=".MainActivity" android:resizeableActivity="true" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|density|keyboard|keyboardHidden" />Agrega las importaciones requeridas:
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.minControla
onCreateyonConfigurationChanged:override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) applyAdaptiveOrientationPolicy() // ... } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) applyAdaptiveOrientationPolicy() // ... }Actualiza la orientación solicitada según el ancho de pantalla más pequeño y bloquea las pantallas compactas en la orientación de diseño preferida de tu juego:
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 } }
Diseña teniendo en cuenta los problemas habituales
La compatibilidad con una variedad de relaciones de aspecto y orientaciones les brinda a los jugadores la mejor experiencia del usuario posible. A diferencia de los teléfonos, las pantallas más grandes suelen tener una orientación fija, ya sea horizontal o vertical, y, en algunos casos, la pantalla es casi cuadrada. En estos dispositivos más grandes, los jugadores suelen realizar varias tareas a la vez mientras juegan. Si maximizas el uso del espacio de la pantalla, puedes atraer a más jugadores con más frecuencia y durante sesiones de juego más largas.
En las siguientes secciones, se proporcionan soluciones que hacen que tu juego se sienta como una experiencia premium en todos los factores de forma, en lugar de un juego para teléfonos incorporado en una pantalla que no es de teléfono.
Cámaras que reconocen la relación de aspecto
En los juegos en 3D, si una pantalla es demasiado alta, los jugadores pueden sentir que están viendo el juego a través de una ranura estrecha, y si es demasiado ancha, es posible que se pierdan las interacciones diseñadas. Al igual que en los juegos en 2D, puedes mantener un "área segura" siempre visible. El siguiente código cambia una cámara entre un campo visual horizontal y uno vertical según cómo se compara la relación de aspecto activa con una relación de aspecto de referencia de 1920 x 1080. De esta manera, un diseñador puede crear un juego para una pantalla de 1080p y saber que todo lo que se diseñe dentro de ese marco de referencia permanecerá visible:
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; }
Buzones completamente interactivos
Algunos juegos tienen un área de juego fija por diseño, que suele coincidir con consideraciones de diseño basadas en el género, como el campo fijo de un juego de disparos vertical o el desplazamiento de pantalla giratoria que se usa para una estética retro.
En estos juegos, escala el área de juego para que se ajuste de modo que un eje del juego siempre se maximice para llenar el área de juego. El eje que se maximiza puede cambiar según las relaciones de aspecto relativas de tu juego y los límites de la pantalla.
Ancla los elementos de HUD y de la IU hasta el borde seguro de la pantalla: evita los cortes, pero, de lo contrario, coloca los elementos interactivos, como los controles virtuales, las barras de salud y los minimapas, lo más lejos posible del área de juego. De esta manera, la parte interactiva del juego usa toda la pantalla, y los jugadores que usan factores de forma como tablets y plegables pueden jugar cómodamente sin estirar los dedos de forma excesiva.
Por último, asegúrate de que el área de formato letterbox tenga un tema adecuado o sea diegética. Considera bordes que coincidan con la paleta o el conjunto de mosaicos en pantalla, una mesa o pantalla virtual para juegos de mesa o retro, o minimapas extendidos y elementos visuales decorativos.
En el siguiente ejemplo, el juego se renderiza en tamaño completo para la mayoría de las relaciones de aspecto. Cuando la relación de aspecto es más estrecha que 4:3, renderiza un skybox con colores temáticos y, al mismo tiempo, sigue colocando elementos de la IU, como controles, en el borde de la pantalla:
|
|
Niebla de guerra
A muchos desarrolladores de juegos les preocupa que los jugadores puedan obtener una ventaja desleal con relaciones de aspecto verticales u horizontales extremas. Por ejemplo, un jugador de un juego competitivo, como una arena de combate multijugador en línea (MOBA), podría usar una pantalla muy vertical para obtener una vista profunda del carril de un oponente.
En el caso de los juegos de estrategia y simulación de arriba hacia abajo, considera renderizar una niebla de guerra:
- No renderices la geometría hasta que el jugador pueda verla.
- Renderiza la geometría que el personaje del jugador no puede ver de forma activa como atenuada, sin elementos dinámicos.
- Renderiza solo los oponentes, los objetos coleccionables y otros objetos dinámicos que el personaje del jugador puede ver de forma activa y diegética.
Si combinas esta técnica con pantallas con formato de pantalla ancha completamente interactivas y renderizas tu IU en el borde de la pantalla, el jugador obtiene una experiencia que se siente refinada y deliberada sin que tengas que rediseñar la estructura de niveles ni el equilibrio del equipo.
Expandir este juego para llenar una pantalla más grande sin una niebla de guerra revela partes del mapa que el jugador no debería ver:
|
|
Si renderizas una niebla de guerra y ocultas elementos dinámicos, como enemigos y elementos, mantienes la equidad en el juego mientras usas el fotograma completo disponible para el juego:
|
|
Atenuar la geometría
Muchos juegos, en especial en el espacio de estrategia por turnos, usan una técnica similar a la niebla de guerra descrita en la sección anterior y no renderizan la geometría que el jugador no descubrió o con la que no puede interactuar durante una determinada jugada de pizarrón.
Si un campo visual expandido revela partes de tu set o cinemática que el jugador no debería ver, enmascarar ciertas partes de la escena de una manera bien diseñada logra el mismo efecto práctico que un formato letterbox sin la percepción de un borde estático fijo.
En el siguiente ejemplo, expandir el juego a una relación de aspecto más grande revela una horda de zombies fuera de las paredes del castillo:
|
|
En cambio, puedes omitir la renderización de la geometría de nivel más allá de los límites del área de juego actual:
|
|
Límites diegéticos
En los juegos en 3D envolventes en los que un campo visual expandido puede generar una ventaja competitiva en los juegos multijugador en primera o tercera persona, o revelar áreas no creadas de juegos basados en conjuntos fijos, considera compilar elementos diegéticos en el juego para limitar el campo visual en lugar de depender de un formato de pantalla ancha rectangular. Estos son algunos ejemplos:
- La cabina interior de un vehículo, como un automóvil o un avión
- El interior de un casco
- Una viñeta que indica el borde de la visión de un jugador
Los elementos de la IU y el HUD deben extenderse hasta el borde de la pantalla o incorporarse en los límites diegéticos, y lo ideal es que sean dinámicos. Estos elementos diegéticos también pueden ayudar a los jugadores que experimentan cinetosis, en especial en los factores de forma de XR, lo que proporciona una ayuda de accesibilidad, además de ayudar a mitigar la complejidad de las nuevas relaciones de aspecto.
Evita renderizar el juego en una superficie dentro de una escena estática no interactiva. Aunque ese enfoque es mejor que renderizar barras negras para el formato letterbox y pillarbox, los jugadores notan rápidamente los bordes estáticos y pueden frustrarse si sus juegos favoritos se renderizan en un parche de pantalla del tamaño de un teléfono en factores de forma que no son de teléfono.
|
En este ejemplo, un juego diseñado para la pantalla de un teléfono estándar se renderiza dentro de una televisión simulada con un control de juegos esqueuomórfico para adaptarse a la relación de aspecto más cuadrada que suele haber en las pantallas internas de los dispositivos plegables:
|
Es posible que algunos juegos en 3D deban restringir el campo visual por cuestiones de equidad competitiva o por consideraciones de diseño. Es preferible usar una cabina, un casco o alguna otra metáfora diegética para restringir el campo visual en lugar de barras negras:
|
Recursos adicionales
- Haz que tu juego de Unity sea excelente en todos los factores de forma
- Haz que tu juego sea compatible con todos los factores de forma
- Habilita la entrada natural en todos los factores de forma
- Cómo controlar la orientación del dispositivo con la rotación previa de Vulkan