Mobil Oyun Ölçeklendirme Optimizasyonu: 1M+ Eş Zamanlı Oyuncu İçin Şehir Mimarisi Tasarımı
Özet olarak
Mobil oyunlarda milyonlarca eş zamanlı kullanıcıyı desteklemek için gereken sunucu ve istemci tarafı optimizasyon tekniklerini ele alan bu rehber, Spatial Hashing ve Network Interest Management gibi kritik stratejilere odaklanmaktadır. Makalede, mobil donanım kısıtlamalarını aşmak için HLOD, asenkron asset streaming ve etkili bellek yönetimi yöntemleri teknik örneklerle açıklanmaktadır. Ayrıca, horizOn gibi Backend platformlarının karmaşık altyapı ve Server Sharding süreçlerini nasıl otomatize ederek geliştiricilere zaman kazandırdığı vurgulanmaktadır.
Her Multiplayer geliştiricisi, mobil oyun mimarisinin çatladığı o anı çok iyi bilir. Geniş ve büyüleyici bir kentsel ortam tasarlarsınız. Yerel ortamda 10 simüle edilmiş istemci ile test edersiniz ve build kusursuz bir şekilde 60 FPS'de çalışır. Ardından, 1.000 eş zamanlı oyuncunun merkezi meydana doluştuğu canlı bir ortama geçiş yaparsınız. Saniyeler içinde, düşük segment Android cihazlar Out-Of-Memory (OOM) hataları nedeniyle çöker, iOS Jetsam uygulamanızı agresif bir şekilde sonlandırır ve Dedicated Server CPU'nuz, binlerce çakışan entity için network replication hesaplamaya çalışırken %100'e fırlar.
Milyonlarca aktif kullanıcıyı desteklemek üzere tasarlanmış bir mobil MMO veya büyük ölçekli bir açık dünya inşa ederken, oyun motorunun varsayılan ayarlarına güvenemezsiniz. Mobil donanımlar katı termal kısıtlamalara ve sert bellek limitlerine sahiptir (orta segment cihazlarda oyununuz genellikle 2 GB'dan daha az kullanılabilir RAM ile sınırlanır). Aynı zamanda, sunucunuzun yoğun oyuncu kümelerini sarsılmadan yönetmesi gerekir.
Gerçek bir mobil oyun ölçeklendirme optimizasyonu (Mobile Game Scaling Optimization) sağlamak üç sütunlu bir yaklaşım gerektirir: sunucuda agresif Spatial Partitioning, istemcide acımasız bellek yönetimi ve devasa bağlantı hacmini yönetmek için dağıtık Backend mimarisi. Bu adım adım eğitimde, mobil platformlar için büyük ölçekli şehirlerin tam olarak nasıl mimari edileceğini inceleyeceğiz.
Adım 1: Sunucu Tarafı Spatial Partitioning
Devasa Multiplayer oyunlarda sunucu performansının temel düşmanı O(N²) problemidir. Eğer sunucunuz, ağ güncellemelerine kimin ihtiyacı olduğunu belirlemek için her oyuncunun diğer her oyuncuya olan mesafesini kontrol eden bir döngü çalıştırırsa, matematiksel yük felaket bir şekilde ölçeklenir. 100 oyuncu, her tick başına 10.000 mesafe kontrolü gerektirir. 1.000 oyuncu için bu sayı 1.000.000'dur. 30Hz sunucu tick rate değerinde bu, saniyede 30 milyon kontrol demektir.
Bunu çözmek için Spatial Hashing (veya Grid/Quadtree sistemi) uygulamalıyız. Şehri mantıksal bir ızgaraya (grid) bölerek, oyuncular yalnızca mevcut hücrelerindeki ve hemen çevrelerindeki hücrelerdeki entity'lerin ağ uygunluğunu kontrol ederler. Bu, O(N²) kabusumuzu bir O(1) grid aramasına ve ardından ağır şekilde kısıtlanmış bir yerel kontrole indirger.
Spatial Hash Grid Uygulaması (C# Örneği)
İşte Unity, Godot (C# üzerinden) veya tüm dünya durumunu döngüye sokmadan entity yakınlığını yönetmek için özel bir Backend sunucusuna uyarlayabileceğiniz, C# ile yazılmış yüksek verimli bir 2D Spatial Hash Grid uygulaması.
using System.Collections.Generic;
using UnityEngine;
public class SpatialHashGrid
{
private readonly float _cellSize;
private readonly Dictionary<Vector2Int, HashSet<uint>> _grid;
public SpatialHashGrid(float cellSize = 50f)
{
_cellSize = cellSize;
_grid = new Dictionary<Vector2Int, HashSet<uint>>();
}
// Convert a world position to a grid coordinate
private Vector2Int GetCellCoordinate(Vector3 position)
{
return new Vector2Int(
Mathf.FloorToInt(position.x / _cellSize),
Mathf.FloorToInt(position.z / _cellSize)
);
}
// Add or update a player's position in the grid
public void UpdateEntityPosition(uint entityId, Vector3 oldPosition, Vector3 newPosition)
{
Vector2Int oldCell = GetCellCoordinate(oldPosition);
Vector2Int newCell = GetCellCoordinate(newPosition);
if (oldCell != newCell)
{
if (_grid.ContainsKey(oldCell))
{
_grid[oldCell].Remove(entityId);
}
if (!_grid.ContainsKey(newCell))
{
_grid[newCell] = new HashSet<uint>();
}
_grid[newCell].Add(entityId);
}
}
// Retrieve all entities in the immediate vicinity (9 cells)
public List<uint> GetEntitiesInProximity(Vector3 position)
{
List<uint> nearbyEntities = new List<uint>();
Vector2Int centerCell = GetCellCoordinate(position);
// Loop through the 3x3 grid around the player
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
Vector2Int cellToCheck = new Vector2Int(centerCell.x + x, centerCell.y + y);
if (_grid.TryGetValue(cellToCheck, out HashSet<uint> entitiesInCell))
{
nearbyEntities.AddRange(entitiesInCell);
}
}
}
return nearbyEntities;
}
}
Network replication mantığınızı GetEntitiesInProximity üzerinden yönlendirerek, sunucunuz yalnızca birbirine aktif olarak yakın olan birkaç düzine oyuncu için kesin mesafeleri hesaplar; bu da CPU yükünü büyük ölçüde azaltır ve sunucunuzun aynı instance içinde binlerce eş zamanlı oyuncuyu rahatça yönetmesini sağlar.
Adım 2: Network Interest Management
Spatial Hashing sunucunun CPU darboğazını çözse bile, hala bir bant genişliği probleminiz vardır. Mobil ağlar (4G/5G) doğası gereği kararsızdır, yüksek jitter'a yatkındır ve katı bant genişliği sınırlamalarına sahiptir. Her tick başında yakındaki 50 oyuncu için veri göndermek, mobil istemcinin socket buffer'ını dolduracak ve aşırı desync sorunlarına yol açacaktır.
Interest Management (veya Network Relevancy), ağ üzerinden ne gönderileceğine öncelik verme uygulamasıdır. 2 metre ötede bir çatışmaya giren bir oyuncu saniyede 30 güncelleme gerektirir. 40 metre ötede farklı bir sokakta yürüyen bir oyuncu ise saniyede sadece 2 güncellemeye ihtiyaç duyar.
Network Relevancy'yi Geçersiz Kılma (Unreal Engine C++ Örneği)
Unreal Engine içerisinde, IsNetRelevantFor fonksiyonunu override ederek bunun kontrolünü ele alabilirsiniz. Bu, görüş hattı (line-of-sight) ve mesafe katmanlarına göre ağ trafiğini agresif bir şekilde ayıklamanıza olanacak tanır.
bool ACityPlayerCharacter::IsNetRelevantFor(const AActor* RealViewer, const AActor* ViewTarget, const FVector& SrcLocation) const
{
// 1. Always relevant to ourselves
if (RealViewer == this || ViewTarget == this)
{
return true;
}
// 2. Calculate squared distance (faster than exact distance)
const float DistanceSquared = FVector::DistSquared(SrcLocation, GetActorLocation());
// 3. Absolute Cull Distance (e.g., 10,000 units = 100 meters)
const float MaxRelevancyDistSq = 100000000.0f;
if (DistanceSquared > MaxRelevancyDistSq)
{
return false;
}
// 4. Dynamic Network Update Frequency based on distance
// If they are far away, we lower how often we send data
if (DistanceSquared > 25000000.0f) // 50 meters
{
NetUpdateFrequency = 2.0f; // 2 updates a second
}
else
{
NetUpdateFrequency = 30.0f; // 30 updates a second
}
return Super::IsNetRelevantFor(RealViewer, ViewTarget, SrcLocation);
}
NetUpdateFrequency değerinizi mesafeye göre dinamik olarak ölçeklendirerek, sunucu giden bant genişliğini %70'in üzerinde azaltabilir, oyuncunun mobil veri planını koruyabilir ve gecikme (latency) sıçramalarını önleyebilirsiniz.
Adım 3: İstemci Tarafı Bellek Limitleri ve Asset Streaming
Sunucuların bol miktarda RAM'i vardır; cep telefonlarının ise yoktur. Bir iPhone 13, 4 GB birleşik belleğe sahiptir. iOS işletim sistemi bunun genellikle 1.5 GB ile 2 GB arasını kendine ayırır. Oyununuz, kalan 2 GB'lık alana tamamen sığmalıdır. Eğer büyük ölçekli bir şehrin tamamını belleğe bir kerede yüklerseniz, işletim sistemi uygulamayı anında sonlandıracaktır.
Bu ortamda hayatta kalmak için şehriniz parçalara (chunk) bölünmeli ve asenkron olarak stream edilmelidir.
- Hierarchical Level of Detail (HLODs): Uzaktaki bir şehir bloğunda 50 ayrı binayı render etmek yerine (bu yaklaşık 3.000 draw call demektir), tüm o şehir bloğunu birleşik bir texture atlas ile tek bir static mesh haline getirmelisiniz (bake işlemi). Bu, uzak geometri için draw call sayısını binlerceden tam olarak bire indirir.
- Addressable Asset Systems: Temel veri asset'lerinizde asla sert referanslar (hard references) kullanmayın. Bir oyuncu A Bölgesi'nde spawn olduğunda, istemci sadece A Bölgesi için gerekli olan texture ve mesh'leri indirmek veya yüklemek için asenkron yükleme (örneğin Unity Addressables veya Unreal PrimaryAssetLabels) kullanmalıdır. B Bölgesi RAM'den titizlikle temizlenmelidir.
- Texture Compression: Mobil için özel olarak ASTC (Adaptive Scalable Texture Compression) formatına güvenin. Bu format, oldukça değişken blok ayak izlerine izin vererek, texture başına bellek ve görsel kalite üzerinde granüler kontrol sağlar.
Adım 4: Dağıtık Backend Mimarisi ve Server Sharding
Devasa bir metropol tek bir fiziksel makinede çalışamaz. MMO ölçeğinde bir şehir tasarlarken, dünya birden fazla sunucu instance'ı (shard veya node) arasında fiziksel olarak bölünmelidir. Bir oyuncu Downtown Node'undan Slums Node'una giden bir köprüyü geçtiğinde, istemci bağlantısı ve dünya durumu iki tamamen farklı sunucu süreci arasında sorunsuz bir şekilde el değiştirmelidir.
Bunu kendiniz inşa etmek; Agones gibi sistemlerle orkestra edilen Kubernetes cluster'ları kurmayı, sunucu node'ları arasında oyuncu durumunu aktarmak için Redis ile veritabanı sharding işlemini ve kesintisiz bağlantı geçişleri için özel UDP load balancer'ları gerektirir. Bunu, oyuncuların geçiş sırasında eşya kaybetmeyeceği şekilde sağlam bir şekilde tasarlamak devasa bir iştir; kıdemli bir mühendislik ekibi için kolayca 4-6 aylık özel DevOps çalışması demektir.
Bu geçişler sırasında RPC kuyruklarını ve veritabanı yazma işlemlerini düzgün yönetmezseniz, kaçınılmaz olarak veri bozulması (state corruption) ile karşılaşırsınız. Daha önce Unreal Engine'deki durumlarınızı bozan RPC replication sorununu çözme mekaniklerini ele almıştık ve bu aynı prensipler, sunucu node'ları arasındaki spatial geçişler için de doğrudan geçerlidir.
Platform çözümlerinin parladığı nokta burasıdır. horizOn ile bu yüksek eş zamanlı Backend servisleri, gerçek zamanlı veritabanı senkronizasyonları ve Dedicated Server orkestrasyonları önceden yapılandırılmış olarak gelir. Kubernetes ağ kurallarını mimari etmek ve hata ayıklamak için vaktinizi harcamak yerine, tamamen şehrinizin gameplay döngülerini ve istemci optimizasyonlarını oluşturmaya odaklanabilirsiniz.
Mobil Şehir Dünya Tasarımı İçin En İyi Uygulamalar
Şehrinizin milyonlarca toplam kullanıcıya sorunsuz bir şekilde ölçeklenmesini ve bütçe dostu cihazlarda yüksek kare hızlarını korumasını sağlamak için şu mimari kurallara sıkı sıkıya uyun:
- Agresif Instance Pooling: Oynanış sırasında araçlar, yayalar veya mermiler gibi geçici nesneler için asla
Instantiate()veyaSpawnActorkullanmayın. Mobil CPU'lar bellek tahsisi (allocation) ve Garbage Collection işlemlerinde çok zorlanır. Nesne havuzlarını (object pools) yükleme ekranında önceden hazırlayın (pre-warm) ve bunları sürekli döngüye sokun. - Şehir Blokları İçin Texture Atlasing: Draw call'lar, mobil GPU'ların (Tile-Based Deferred Rendering kullanan) başlıca katilidir. Tüm genel sokak objelerinin (çöp kutuları, banklar, sokak lambaları) texture'larını tek bir büyük texture atlas içinde birleştirin. Bu, motorun yüzlerce objeyi tek bir draw call ile batch etmesini sağlar.
- Her Chunk İçin Katı Poligon Bütçeleri: Sert limitler uygulayın. Tek bir mobil şehir chunk'ı (örneğin 100x100 metrelik bir alan) ideal olarak 300.000 görünür üçgenin altında kalmalıdır. Mimari detayları simüle etmek için ham geometri yerine normal map'lere yoğunlaşın.
- Sunucu Tarafı Hibernasyon (Hibernation) Uygulayın: Haritanın %80'inin boş olduğu devasa bir şehir için Dedicated Server çalıştırmak, stüdyonuzu iflasa sürüklemenin en hızlı yoludur. Atıl durumdaki grid koordinatlarını uyutmak ve bir oyuncu yaklaştığında anında uyandırmak için Fortnite sunucu optimizasyonu hibernasyon önerisi analizinden ilham alan agresif instance yönetimine ihtiyacınız var.
- Çarpışmayı (Collision) Görsel Mesh'ten Ayırın: Sunucu tarafındaki collision hesaplamaları için asla karmaşık görsel mesh'leri kullanmayın. Sunucu şehri sadece bir dizi düşük poligonlu ilkel şekil (box, capsule, sphere) olarak anlamalıdır. Bu, sunucu bellek ayak izini minimumda tutar ve fizik hesaplamalarını milisaniyenin altına indirir.
Kaçınılması Gereken Yaygın Hatalar
- RPC Flooding Tuzağı: Geliştiriciler genellikle görsel efektler (bir araba kazasından çıkan kıvılcım gibi) için sunucudan istemciye Remote Procedure Calls (RPC'ler) tetikler. Bunu yapmayın. Sunucu sadece arabanın durumunu replicate etmelidir (örneğin
bIsCrashed = true). İstemci, bir OnRep/property hook aracılığıyla bu durum değişikliğini bağımsız olarak gözlemlemeli ve kıvılcım VFX'ini yerel olarak tetiklemelidir. Bu, devasa miktarda ağ bant genişliği tasarrufu sağlar. - Bölge Geçişlerinde Bellek Sızıntısı: Mobil cihazlarda bir şehir chunk'ını stream-out ederken, Garbage Collection'ı aktif olarak zorladığınızdan veya asset bundle'larını manuel olarak bellekten boşalttığınızdan emin olun. Oyuncu bölgeler arasında hareket ederken her seferinde bellekte birkaç Megabaytlık sahipsiz texture bırakırsanız, 20 dakikalık oynanıştan sonra kaçınılmaz olarak crash yaşayacaklardır.
Sonuç
Gerçek mobil oyun ölçeklendirme optimizasyonu bir dengeleme sanatıdır. Her bir megabayt istemci RAM'i için savaşmayı, network relevancy kurallarını sıkıca düzenlemeyi ve sunucu yükünü ölçeklenebilir Backend node'ları arasında dağıtmayı gerektirir. Spatial Hashing, dinamik güncelleme frekansları ve asenkron asset streaming uygulayarak, yıllanmış mobil donanımlarda bile sorunsuz çalışan devasa, yaşayan şehirler inşa edebilirsiniz.
Ancak, binlerce eş zamanlı bağlantıyı yönlendirmek ve kesintisiz sunucu geçişlerini yönetmek için ölçeklenebilir altyapı inşa etmek genellikle oyunun kendisini inşa etmekten daha zordur. DevOps kabusu yaşamadan Multiplayer Backend'inizi ölçeklendirmeye hazır mısınız? horizOn'u ücretsiz deneyin veya yüksek eş zamanlı mimariyi kutudan çıktığı gibi nasıl yönettiğimizi görmek için API dokümantasyonuna göz atın.
Kaynak: Designing Large-Scale Mobile Game Cities: Production, Optimization, & Worldbuilding Expertise