YG

Stack the World: React Native ve Skia ile vinçli kule oyunu geliştirmek

20 Ekim 2026·5 dk okuma·Yusuf GÜNER

Stack the World'ü Expo, React Native ve React Native Skia ile yazdım. Oyunda fizik motoru yok; kule, saf TypeScript ile yazılmış küçük bir simülasyon; Skia yalnızca onu çiziyor. Bu yazıyı yazarken oyun Google Play'de kapalı testte, iOS sürümü ise 1 Ekim'den beri App Store incelemesini bekliyor.

React Native ile oyun yazılır mı diye soranlar için kısa cevabım evet, ama türüne bağlı. Tek dokunuşla oynanan, 2D, sahnesi kontrollü bir oyunsa yeterli. Aşağıda neyi nasıl çözdüğümü, nerede takıldığımı ve ölçtüğüm rakamları yazdım.

Oyun ne yapıyor

Ekranın üstünde bir vinç var ve kancasında, kule yükseldikçe daha hızlı ve daha geniş sallanan bir kat bekliyor, sen de onu bırakacağın anı kolluyorsun. Doğru anda dokunuyorsun, kat kopuyor ve kulenin tepesine düşüyor. Tam ortaya oturursa "mükemmel" sayılıyor ve kombo artıyor. Biraz kayarsa kalıyor ama kulenin ağırlık merkezi kayıyor. Iskalarsan kat devrilip düşüyor, bir can gidiyor.

İlk sürümde Türkiye, Almanya ve Fransa var. Her ülkede üç şehir, her şehirde on bina var ve şehrin son binası o şehrin simge yapısı. Paris'te Eyfel Kulesi'ni kat kat dikiyorsun. Bir de sınırsız kat çıktığın rekor modu var.

Fizik motoru kullanmadım

Tasarım dokümanına ilk yazdığım kurallardan biri "fizik motoru yok, matter-js kullanılmaz" oldu.

Sebebi his. Bu tür oyunlarda kulenin nasıl sallandığını, katın nasıl oturduğunu saniyesine kadar ayarlayabilmen gerekiyor. Gerçek bir rigid body motoru her şeyi doğru hesaplıyor, fakat kulenin ne zaman ve ne kadar sallanacağını sana bırakmıyor.

Bunun yerine kule src/game/sim/ altında saf TypeScript ile yazıldı. Bu klasör React'i de Skia'yı da tanımıyor. Fonksiyonların başında 'worklet' direktifi var, böylece aynı kod hem Jest testlerinde hem de Reanimated'in UI thread'inde çalışıyor. Ekranlar değişse bile oyunun kalbi cihaz olmadan test ediliyor.

Döngü sabit 60 Hz adımla ilerliyor. Kare atlansa bile biriken zaman adımlara bölünüyor, 120 Hz bir ekranda da oyun hızlanmıyor. Vinç kancası basit bir sarkaç, kule eğimi ise yay-sönüm (spring-damper) sistemi:

// Temsili özet, gerçek koddan sadeleştirildi
export function advance(s: SimState, dtMs: number) {
  'worklet';
  s.acc += dtMs;
  while (s.acc >= STEP_MS) {
    s.t += STEP_MS / 1000;
    s.hookAngle = s.amp * Math.sin(s.omega * s.t);
    stepSway(s, STEP_MS / 1000);
    s.acc -= STEP_MS;
  }
}

Kule yükseldikçe sarkacın genliği ve hızı artıyor, zorluk eğrisi bu iki sayıdan geliyor. Durum Reanimated shared value içinde duruyor, Skia sahneyi oradan çiziyor.

React her karede render olmuyor, yalnız bir olay olduğunda çalışıyor.

Çizim ile fizik aynı genişliği kullanmalı

Eyfel turunu ilk yaptığımda kancada yanlış parça görünüyordu, taban plakaya oturmuyor, platformlar yana taşıyordu. Kök neden basitti. Parçalar farklı kat kutularında çiziliyordu ve çizilen genişlik fiziğin kullandığı genişlikten ayrışmıştı.

O günden beri kural tek satır. Çizilen genişlik, fiziğin kullandığı genişliğe eşit olmak zorunda. Ayrıntı gerekiyorsa çizim tarafında çözülüyor. Eyfel'in tabanı iki kata yayılarak çiziliyor ama fizikte her kat yine tek blok.

Affedicilik bir sayı

Bir katın ne kadar kayıkken oturacağına uzun süre karar veremedim. Yüzde 50 fiziksel olarak doğruya yakındı ama sertti, varsayılan yüzde 30 ise fazla affediciydi. Yüzde 40'ta karar kıldım. Sonradan bir hata daha çıktı. İlk kat platformun sağına atılınca denge göstergesi daha ilk hamlede düşüyordu. Şimdi her kat yalnız altındaki kata göre ölçülüyor ve ilk kat referans sayılıyor.

Performansı ölçerek düzelttim

Ölçümü Redmi Note 12 Pro+ üzerinde perfetto, gfxinfo ve meminfo ile yaptım. Oyun ekranı yaklaşık 119 fps veriyordu. Kare aralığı p50'de 8,2 ms, p99'da 28,5 ms çıktı. Skia çizimi kare başına ortalama 6,2 ms tutuyordu, yani 120 Hz bütçesinin dörtte üçü.

Asıl sorun ilk girişlerdeydi. Android uygulama her güncellendiğinde shader önbelleğini sildiği için her Skia efekti ilk çizildiğinde GPU programı UI thread'de yeniden derleniyor ve oyuncu güncellemeden sonra açtığı ilk ekranlarda kısa ama fark edilen donmalar yaşıyordu. Güncellemeden sonra haritaya ilk girişte 441 ms, mağazada 298 ms, oyuna girişte 230 ms takılma ölçtüm.

Çözüm, açılış ekranı dururken görünmez bir yüzeye oyunun kullandığı efektleri birer kez çizmek oldu. Skia.Surface.MakeOffscreen ile küçük bir yüzey açılıyor, gradyanlar, blur, renk matrisi ve sahne boyaları çiziliyor, sonra yüzey bırakılıyor. RN Skia her thread'e tek bir GPU bağlamı verdiği için derlenen programlar ekrandaki tuvallerde de kullanılıyor. Her adım kendi try/catch'inde, hata açılışı durdurmuyor ve bir üst süre sınırı var.

Isıtmadan sonra bu ekranların hiçbirinde 60 ms'yi aşan kare kalmadı.

Denediğim ama geri aldığım şeyler de oldu. İkonları PNG'ye çevirip normal <Image> ile göstermek durumu kötüleştirdi, her girişte 25-87 ms'lik kareler çıktı. Skia doku önbelleğini 96 MB'a sınırlamak belleği düşürdü ama native yama gerektiriyordu, değmedi.

Bir de arka plan meselesi vardı. Uygulama arka plandayken menüdeki maskot ve buton animasyonları saniyede 77 kadar kare callback'i çalıştırmaya devam ediyordu. Döngü kancasına AppState dinlemeyi ekleyince hepsi tek değişiklikle durdu; emülatörde arka plan CPU kullanımı yaklaşık yüzde 90 düştü. Bu rakamların bir sınırı var. Hepsi tek bir cihazda ölçüldü, zayıf bir telefonda tekrar bakmam gerekiyor.

11 dil ve fontlar

Oyun İngilizce, Türkçe, Almanca, Fransızca, İspanyolca, Portekizce, Rusça, Japonca, Korece, Çince ve Endonezce çalışıyor. Dil telefonun ayarından geliyor, oyunun içinden de değiştirilebiliyor ve yeni bir dil eklemek için locales klasörüne tek bir JSON dosyası koymak yetiyor.

Çeviriden çok fontlar uğraştırdı, çünkü Japonca, Korece ve Çince fontları tam hâliyle pakete koyarsan her biri megabaytlarca yer tutuyor ve oyunun indirme boyutu gereksiz yere büyüyor. Bir script her dilin JSON'unda geçen karakterleri toplayıp alt küme font üretiyor, hedef dil başına 40-120 KB. Bunun bir yan etkisi var. Metin değişince font yeniden üretilmezse yeni karakter kutu olarak görünüyor, nitekim rüzgâr sistemindeki terimleri düzelttiğimde ja ve zh fontlarını yeniden üretmem gerekti. Şimdi testler her CJK karakterinin fontta olup olmadığını kontrol ediyor.

Metnin hiçbiri resme gömülmüyor. Tek satırlık buton ve başlıklar sığmazsa adjustsFontSizeToFit ile en fazla yüzde 70'e küçülüyor. Almanca ve Rusça uzun, Japonca ise sıkışık olduğu için bu üç dili 360 px genişlikte bütün ekranlarda kontrol ediyorum.

Mağaza süreci: gerçekte olanlar

Google Play'de kişisel geliştirici hesabı açınca üretime çıkmadan önce kapalı test şartı geliyor. En az 12 test kullanıcısının 14 gün boyunca kesintisiz testte kalması gerekiyor. Burada öğrendiğim şey, listeye eklenen herkesin sayılmadığıydı. Sadece opt-in linkine tıklayan kişiler sayıma giriyor. Tester çağrısını buna göre sadeleştirdim ve herkesten gruba katıldıktan sonra opt-in linkine de tıklamasını, ardından uygulamayı Play'den yüklemesini istedim. Şartlar dolunca 19 Ekim'de üretim erişimine başvurdum, sonucu bekliyorum.

Kapalı testte bir de can sıkıcı hata çıktı. Google ile giriş Play'den kuran herkeste "bağlantı kurulamadı" diyordu. Logcat'e bakınca gerçek sebep çıktı, uygulama OAuth için kayıtlı değildi. Play uygulamayı kendi anahtarıyla yeniden imzaladığı için yükleme anahtarının SHA-1'i yetmiyor, Play Console'daki uygulama imzalama SHA-1'ini Google Cloud'da Android istemcisi olarak eklemek gerekiyor. Ekledikten sonra hemen çalıştı.

Göremezsin bunu ekrandaki genel hata mesajından.

iOS tarafı daha sessiz. Sürüm 1 Ekim'den beri "Waiting for Review" durumunda, Apple'dan ne ret ne soru geldi. Muhtemel sebeplerden biri güncellenen geliştirici sözleşmesinin hesap sahibi tarafından kabul edilmemiş olmasıydı; kabul edildi, takipteyim. Yeniden göndermeyi en son çare olarak tutuyorum, çünkü sırayı kaybettirebilir. Bu sırada fark ettiğim bir kısıt daha var. Sürüm incelemede beklerken ekran görüntülerini değiştiremiyorsun, yeni görselleri hazırlayıp onayı bekliyorum.

Bugün nerede

Oyunun 1.0 kapsamı hazır, artık iş büyük ölçüde mağazaların sırası ve gerçek oyunculardan gelecek geri bildirimle hangi şehirlerin ve hangi dengelerin değişeceğine bağlı. Jest test sayısı 7.000'i geçti; her birleştirmeden önce tsc ve testlerin tamamı çalışıyor. Sırada zayıf bir Android cihazda performansı tekrar ölçmek ve App Store sonucunu almak var.

React Native ile bir oyun düşünüyorsan, simülasyonu ekrandan ayırmakla başla. Cihaz olmadan test edebildiğin bir kule, ayar yapmayı da hata bulmayı da çok kolaylaştırıyor.

React NativeSkiamobil oyunExpoperformans

Diğer yazılar