Bu kılavuzda, oyununuzu farklı Android form faktörlerinde çalışacak şekilde nasıl uyarlayacağınızı öğreneceksiniz.
Başlamadan önce Büyük ekranlarda yeniden boyutlandırmayı destekleme başlıklı makaleyi okuyun. Android'deki modern oyunlar, sistem uyumluluk katmanlarına ihtiyaç duymadan tüm ekran boyutlarına uyum sağlamalıdır. Bu sayede, oyununuzun nasıl görüntüleneceğini her zaman kontrol edebilir ve oyununuzun XR gibi form faktörlerinde veya ekranın döndürülemediği cihazlarda bile üstün bir oyuncu deneyimi sunmasını sağlayabilirsiniz.
Bunun için aşağıdaki görevleri tamamlayın:
- Sistem sinemaskopunu devre dışı bırakın ve kendi motor içi sinemaskopunuzu uygulayın.
- Sinemaskop alanını kullanılabilir ve tematik olarak uygun hale getirin.
- Oyununuzun 21:9'dan 1:1'e ve 9:21'e ölçeklenebilmesi için gereken tasarım konularını ele alın.
Test için komutlar
Oyununuzu oluşturup test ederken oyununuzun tüm sinemaskopları kendisinin oluşturduğunu doğrulayın. TV'ler ve araç içi ekranlar gibi cihazlar varsayılan yatay veya dikey yönlerinden döndürülemediğinden oyununuzu dikey ve yatay yönlere zorlamanız da gerekir.
Oyun motorunuzun sistem sinemaskopunu kullanıp kullanmadığını kontrol etmek için adb shell cmd window set-letterbox-style komutunu kullanın. Örneğin, aşağıdaki komut sistem sinemaskoplarını parlak yeşil yapar ve oyununuza köşe yarıçapı ekler. Böylece oyununuzun işletim sisteminin uyumluluk moduna geri dönüp dönmediği anlaşılır:
adb shell "cmd window set-letterbox-style --backgroundType solid_color --backgroundColor '#00FF00' --cornerRadius 30 --isEducationEnabled false"
Test için bir yönü zorunlu kılmak istiyorsanız adb shell cmd window user-rotation lock kullanın. Bu sayede, oyununuzun yön için uygulama geçersiz kılma işlemlerini dikkate almayan bir cihazda nasıl davrandığını simüle edebilir veya kullanıcıların, pencerelerin cihaz yönünden bağımsız olarak dikey ya da yatay görünebildiği XR veya PC benzeri bir cihazda ne gördüğünü anlayabilirsiniz.
Öncelikle cihazınızı, uygulama yönünü geçersiz kılabileceğiniz bir duruma getirin:
adb shell cmd window set-ignore-orientation-request true
adb shell cmd window fixed-to-user-rotation enabled
Ardından, oyununuzu yatay konuma getirerek test edin:
adb shell cmd window user-rotation lock 1
Ardından, oyununuzu dikey konuma getirerek test edin:
adb shell cmd window user-rotation lock 0
Cihazınızı yeniden başlatarak veya aşağıdaki komutları çalıştırarak OEM yapılandırmasına sıfırlayın:
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
Sistem sinemaskopunu neden devre dışı bırakmalısınız?
Sistem, her zaman sistem sinemaskopunu kontrol eder. Ekran boyutunu algılayıp görüntüleme alanını kendiniz yöneterek, girişler oyun alanınızdan çıktığında meydana gelen girişlerin bırakılması gibi sorunları azaltabilirsiniz. Oyun içeriğiniz ortada sabit kalsa bile oyun dışı öğeleri ekranın kenarına kadar genişletebilirsiniz. Sistem uyumluluk modları bir oyunu kullanılabilir hale getirebilir ancak oyununuzu oyuncularınız için mükemmel hale getirmek oyun geliştiricisi olarak size bağlıdır.
Letterbox örnek uygulaması
Çoğu oyun motoru, kameranın ekranda nerede oluşturulacağını kontrol etmenize olanak tanır. Kendi sinemaskopunuzu uygulamak için oluşturulan alanın en boy oranını ölçün ve sistemin oyununuzu sinemaskop yapmasına izin vermek yerine kırpma dikdörtgeninizi kullanılmayan alan için ayarlayın. Aşağıdaki örnekte, bir kameranın sınırlı bir en boy oranında oluşturma yapması kısıtlanırken kullanıcı arayüzü kameralarının veya geçişlerinin tam mantıksal ekran boyutuna genişlemesine izin verilmektedir:
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'de adım adım açıklamalı bir kılavuz için Unity Oyunlarını Büyük Ekranlar ve Katlanabilir Cihazlar İçin Optimize Etme codelab'ine bakın.
Büyük ve küçük ekran boyutlarını yönetme
Telefonlar ve katlanabilir cihazların dış ekranları gibi daha küçük ekranlarda en iyi kullanıcı deneyimi, dikey veya yatay olarak kilitlenmiş bir oyun olabilir. Tabletler, Chromebook'lar, araç içi ekranlar veya katlanabilir cihazların iç ekranları gibi ekranlar büyüdükçe cihazı döndürmek daha az tercih edilir, hatta imkansız hale gelir. Ancak daha büyük fiziksel ekran, oyunun eksen dışı en boy oranlarında oynanabilir olmasını sağlar.
Oyunculara en iyi deneyimi sunmak için çalışma zamanında ekran boyutunu algılayın ve gerektiğinde döndürmeyi kısıtlayın. Ekran boyutu arttıkça oyunculara daha fazla esneklik sağlayın. Android, ideal sınırın sw600dp, yani en küçük genişliğin 600 yoğunluktan bağımsız piksel (600 dp) olduğunu kabul eder.
Bu, WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND ile ilişkilidir. Çoklu ekran yapılandırmalarında kullanılabilir ekran boyutunu doğrulamak için computeMaximumWindowMetrics kullanın.
Oyununuzda bunu desteklemek için manifest dosyanızda tüm yönleri destekleyin ve ardından hem onCreate() hem de onConfigurationChanged() içinde en küçük genişliği değerlendirip yön kilidini uygun şekilde ayarlayın:
Manifest dosyanızdaki statik yön kilitlerini kaldırın ve yapılandırma değişikliği işlemeyi beyan edin:
<activity android:name=".MainActivity" android:resizeableActivity="true" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|density|keyboard|keyboardHidden" />Gerekli içe aktarma işlemlerini ekleyin:
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.minHem
onCreatehem deonConfigurationChangedile ilgilenin:override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) applyAdaptiveOrientationPolicy() // ... } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) applyAdaptiveOrientationPolicy() // ... }İstenen yönü en küçük ekran genişliğine göre güncelleyerek kompakt ekranları oyununuzun tercih edilen tasarım yönüne kilitleyin:
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 } }
Yaygın sorunları göz önünde bulundurarak tasarım yapma
Çeşitli en-boy oranlarını ve yönleri desteklemek, oyuncularınıza mümkün olan en iyi kullanıcı deneyimini sunar. Telefonların aksine, daha büyük ekranlar genellikle yatay veya dikey yönde sabitlenir ve bazı durumlarda ekran neredeyse karedir. Bu daha büyük cihazlarda oyuncular genellikle oyununuzu oynarken aynı anda başka işler de yapar. Ekran alanını en verimli şekilde kullanarak daha fazla oyuncuyla daha sık ve daha uzun oyun oturumları boyunca etkileşim kurabilirsiniz.
Aşağıdaki bölümlerde, oyununuzun telefon dışı bir ekrana yerleştirilmiş telefon oyunu gibi değil, form faktörleri genelinde premium bir deneyim gibi hissettirmesini sağlayan çözümler sunulmaktadır.
En boy oranına duyarlı kameralar
3D oyunlarda ekran çok uzunsa oyuncular oyunu dar bir yarıktan izliyormuş gibi hissedebilir, çok genişse de tasarlanmış etkileşimleri kaçırabilirler. 2D oyunlarda olduğu gibi, "güvenli alan"ın her zaman görünür olmasını sağlayabilirsiniz. Aşağıdaki kod, etkin en boy oranının 1920x1080 referans en boy oranıyla karşılaştırılmasına bağlı olarak kamerayı yatay görüş alanı ile dikey görüş alanı arasında değiştirir. Bu sayede tasarımcı, 1080p ekran için oyun tasarlayabilir ve bu referans çerçevesinde tasarlanan her şeyin görünür kalacağını bilir:
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; }
Tamamen etkileşimli mektup kutuları
Bazı oyunların oyun alanı, tasarımları gereği sabittir. Bu durum genellikle türe dayalı tasarım konularıyla (ör. dikey ateş etme oyununun sabit alanı veya retro estetik için kullanılan çevirmeli ekran kaydırma) ilgilidir.
Bu oyunlarda, oyun alanını sığacak şekilde ölçeklendirin. Böylece oyunun bir ekseni her zaman oyun alanını dolduracak şekilde büyütülür. Hangi eksenin büyütüleceği, oyununuzun ve ekran sınırlarının göreceli en boy oranlarına göre değişebilir.
HUD ve kullanıcı arayüzü öğelerini ekranın güvenli kenarına kadar sabitleyin: Çentiklerden kaçının ancak sanal kontroller, sağlık çubukları ve mini haritalar gibi etkileşimli öğeleri oyun alanının mümkün olduğunca dışına yerleştirin. Bu sayede oyunun etkileşimli kısmı tüm ekranı kullanır ve tabletler ile katlanabilir cihazlar gibi form faktörlerini kullanan oyuncular, parmaklarını aşırı derecede uzatmak zorunda kalmadan oyununuzu rahatça oynayabilir.
Son olarak, sinemaskop alanını uygun şekilde temalı veya diegetik hale getirin. Ekranda gösterilen palet veya karo setiyle uyumlu kenarlıklar, masa oyunları ya da retro oyunlar için sanal bir masa veya ekran ya da genişletilmiş mini haritalar ve dekoratif görseller kullanabilirsiniz.
Aşağıdaki örnekte oyun, çoğu en boy oranı için tam boyutta oluşturulur. En-boy oranı 4:3'ten dar olduğunda, tematik olarak renklendirilmiş bir gökyüzü kutusu oluşturur ve kontroller gibi kullanıcı arayüzü öğelerini ekranın kenarına yerleştirmeye devam eder:
|
|
Sis Rengi
Birçok oyun geliştirici, oyuncuların aşırı dikey veya yatay en boy oranlarından haksız avantaj elde edebileceğinden endişe duyuyor. Örneğin, çevrimiçi çok oyunculu savaş arenası (MOBA) gibi rekabetçi bir oyundaki oyuncu, rakibin koridorunu derinlemesine görmek için dikey bir ekran kullanabilir.
Yukarıdan aşağıya strateji ve simülasyon oyunlarında savaş sisi oluşturmayı düşünebilirsiniz:
- Oyuncu görene kadar geometriyi hiç oluşturmayın.
- Oyuncu karakterinin aktif olarak göremediği geometrik şekilleri, dinamik öğeler olmadan soluk olarak oluşturun.
- Yalnızca oyuncu karakterinin aktif olarak görebileceği rakipleri, koleksiyon öğelerini ve diğer dinamik nesneleri oluşturun.
Bu tekniği tamamen etkileşimli sinemaskoplarla birleştirir ve kullanıcı arayüzünüzü ekranın kenarına kadar oluşturursanız oyuncu, seviye yapısını veya takım dengesini yeniden tasarlamanıza gerek kalmadan hem şık hem de amaca yönelik bir deneyim yaşar.
Bu oyunu, savaş sisi olmadan daha büyük bir ekranı dolduracak şekilde genişletmek, oyuncunun görmemesi gereken harita bölümlerini gösterir:
|
|
Savaş sisi oluşturup düşmanlar ve öğeler gibi dinamik öğeleri gizleyerek oyunun kullanılabilir tam karesini kullanırken oyunda adaleti korursunuz:
|
|
Geometriyi yavaşça azaltma
Özellikle sıra tabanlı strateji oyunlarında, önceki bölümde açıklanan savaş sisi tekniğine benzer bir teknik kullanılır. Oyuncunun keşfetmediği veya belirli bir sahne sırasında etkileşimde bulunamayacağı geometriler oluşturulmaz.
Genişletilmiş görüş alanı, oyuncunun görmemesi gereken set veya sinematik bölümlerini ortaya çıkarıyorsa sahnenin belirli bölümlerini düzgün bir şekilde maskelemek, sabit statik kenarlık algısı olmadan sinemaskop ile aynı pratik etkiyi sağlar.
Aşağıdaki örnekte, oyun daha büyük bir en boy oranına genişletildiğinde kale duvarlarının dışında bir zombi sürüsü gösteriliyor:
|
|
Bunun yerine, mevcut oyun alanının sınırları dışındaki seviye geometrisini oluşturmayı atlayabilirsiniz:
|
|
Diegetik sınırlar
Birinci veya üçüncü şahıs çok oyunculu oyunlarda genişletilmiş görüş alanının rekabet avantajı sağlayabileceği ya da sabit set tabanlı oyunların yazılmamış alanlarını ortaya çıkarabileceği sürükleyici 3D oyunlarda, görüş alanını sınırlamak için dikdörtgen bir sinemaskop kullanmak yerine oyuna diegetik öğeler eklemeyi düşünün. Örnekler:
- Araba veya uçak gibi bir aracın kokpitinin içi
- Kaskın içi
- Oyuncunun görüş alanının sınırını gösteren bir vinyet
Kullanıcı arayüzü ve HUD öğeleri ekranın kenarına kadar uzanmalı veya diegetik sınırlara dahil edilmelidir. Bunları dinamik hale getirmek idealdir. Bu diegetik öğeler, özellikle XR form faktörlerinde hareket hastalığı yaşayan oyunculara da yardımcı olabilir. Yeni en boy oranlarının karmaşıklığını azaltmanın yanı sıra erişilebilirlik desteği de sağlar.
Oyunu, etkileşimli olmayan statik bir sahnedeki yüzeyde oluşturmaktan kaçının. Bu yaklaşım, sinemaskop ve dikey ekranlar için siyah çubuklar oluşturmaktan daha iyi olsa da oyuncular statik kenarlıkları hemen fark eder ve en sevdikleri oyunlar telefon dışı form faktörlerinde telefon boyutunda bir ekranda gösterilirse hayal kırıklığına uğrayabilir.
|
Bu örnekte, standart bir telefon ekranı için tasarlanmış bir oyun, katlanabilir cihazların iç ekranlarında yaygın olan daha kare en boy oranına uyum sağlamak için skeomorfik bir oyun kumandasıyla simüle edilmiş bir televizyonun içinde oluşturulur:
|
Bazı 3D oyunlarda, rekabetçi adalet veya tasarım konuları nedeniyle görüş alanının kısıtlanması gerekebilir. Görüş alanını kısıtlamak için kokpit, kask veya başka bir diegetik metafor kullanmak, siyah çubuklara tercih edilir:
|
Ek kaynaklar
- Unity oyununuzu tüm form faktörlerinde mükemmel hale getirme
- Oyununuzu tüm form faktörleriyle uyumlu hale getirme
- Tüm form faktörlerinde doğal girişi etkinleştirme
- Vulkan ön döndürme ile cihaz yönünü yönetme