anasayfa / blog / araçlar
Yayın: 22 Ekim 2025 10 dk okuma araçlar
Oyun geliştirmede CI/CD: gece build'i neyi değiştirir

Oyun geliştirmede CI/CD: gece build'i neyi değiştirir

Küçük bir ekipte build otomasyonunu kurmak beş gün sürdü; kırık bir commit'in ortalama ömrü 2,5 günden 9 saate indi ve sürüm günleri sakinleşti.

Cuma gecesi bozulan sürüm

Geçen yıl yayıncıya demo göndereceğimiz cuma akşamı, sürüm alabilen tek makine benim bilgisayarımdı. Ekipte dört kişiydik ve build almanın nasıl yapıldığını yalnızca ben biliyordum. Saat 19:40'ta build Shader error in 'Custom/Water': undeclared identifier '_TimeScale' diyerek düştü. O satır üç gün önce commit edilmişti ve kimse fark etmemişti, çünkü o shader varyantı editörde hiç derlenmiyordu.

Hatayı bulmak 25 dakika, düzeltip yeniden build almak 70 dakika sürdü. Sorunlu değişikliği bulmak için 14 commit'lik bir aralığı elle taramak zorunda kaldım. Demo gece 23:10'da gitti. Asıl kötü olan gecikme değildi; üç gün boyunca hiçbirimizin projenin gerçekten derlenip derlenmediğini bilmemesiydi.

Oyun geliştirme CI/CD kurulumumuz bu akşamdan çıktı. Hedef şık bir pipeline değildi: hangi commit bozdu sorusunu üç gün yerine bir gecede cevaplamaktı. İlk sürümü GitHub Actions ile kurduk, çünkü depo zaten oradaydı; ikinci build makinesini eklediğimizde Jenkins'e geçtik.

Gece build'i neyi değiştirir

Otomatik build'in tek işi, sabah masaya oturduğunuzda dün geceki kodun çalışan bir sürümünün hazır olması. Bizde 03:00'te tetikleniyor, 06:20 civarında bitiyor ve Slack'e tek satır düşüyor: commit hash, süre, paket boyutu, test sonucu. Bundan uzun bir rapor gönderdiğimiz iki ay boyunca kimsenin mesajı açtığını görmedim, o yüzden tek satırda kaldık. Koşu hafta sonlarını da kapsıyor; pazartesi sabahı bozuk bir depo bulmak, cuma akşamı bulmaktan çok daha iyi.

Ölçebildiğim en somut kazanç şu: kırık bir commit'in ortalama ömrü 2,5 günden 9 saate indi. Hata ne kadar erken yakalanırsa üzerine yığılan commit sayısı o kadar az; git bisect çalıştırmak yerine tek bir diff'e bakıyorsunuz. Düzeltme maliyeti de düşüyor, çünkü kodu yazan kişi bağlamı hâlâ hatırlıyor.

İkinci kazanç kod yazmayan tarafta. Build makinesi her sabah oynanabilir bir exe ürettiği için "bende çalışıyordu" cümlesi büyük ölçüde bitti; level designer da QA da aynı sürümü açıyor. Elle sürüm hazırlamak haftada yaklaşık 4 saatimi alıyordu, o saatler de geri geldi. Yayıncıya cuma günü göndereceğimiz sürüm artık perşembe gecesi hazır oluyor ve son gün kalan tek iş sürüm notu yazmak.

Unity ve Unreal'de komut satırı build

Unity tarafı basit görünür ama ilk tuzak -quit bayrağında. -batchmode -nographics -quit -executeMethod zincirinde editör, sizin asenkron işiniz bitmeden kapanabiliyor ve exit kodu her zaman 0 dönüyor. Biz -quit kullanmayıp metodun sonunda EditorApplication.Exit(code) çağırıyoruz; böylece derleme hatası CI'da gerçekten kırmızı oluyor. Log'u -logFile - ile stdout'a yönlendirmek de şart, dosyaya yazdığınızda hata mesajı CI arayüzünde hiç görünmüyor.

Sürüme hangi sahnelerin gireceğini EditorBuildSettings.scenes yerine kendi yazdığımız BuildProfile ScriptableObject'inden okuyoruz. Sebep pratik: sahne listesi bir asset olduğu için sürekli merge conflict çıkarıyordu ve iki kez kimse fark etmeden test sahnesi sürüme girdi. Aynı asset define symbol'leri ve sürüm numarasını da taşıyor, yani bir build'in neyle üretildiği tek bir yerde duruyor.

Unreal tarafında iş RunUAT.bat BuildCookRun ile dönüyor. -clientconfig=Development -cook -stage -pak -archive ile tam bir tur 48 dakika sürüyor; -iterativecooking açıkken değişmemiş asset'ler atlanıp 11-14 dakikaya iniyor. Iterative cook'a gece build'inde güvenmiyoruz, sadece pull request build'lerinde açıyoruz. Gece koşusunda ayrıca -nocompileeditor kullanıyoruz, o da 6-7 dakika kazandırıyor.

build.yml
# Nightly game build - self-hosted runner, 03:00 local time name: nightly-build on: schedule: - cron: "0 3 * * *" workflow_dispatch: jobs: win64-player: runs-on: [self-hosted, windows, unity-6000] timeout-minutes: 90 steps: - uses: actions/checkout@v4 with: lfs: true # A cold Library/ import costs ~26 min, so we keep it on the machine - name: Restore Library cache run: robocopy D:\cache\Library Library /MIR /NJH /NJS /NFL /NDL; exit 0 # No -quit: BuildRunner calls EditorApplication.Exit with the real code - name: Build player run: > "$env:UNITY_PATH\Unity.exe" -batchmode -nographics -projectPath . -logFile - -buildTarget Win64 -executeMethod BuildRunner.BuildWin64 - name: Smoke test and perf gate run: | .\Build\Game.exe -runScene SmokeTest -timeout 180 python tools\check_frametime.py --budget 16.6 --p99 22.0
yamlbuild.yml

Perforce, Git LFS ve önbellek disiplini

40 GB'lık bir depo Git'in tasarlandığı şey değil. Git LFS ile ilk denememizde .psd ve .fbx dosyaları pointer'a döndü ama yeni bir makinenin ilk klonu yine de 55 dakika sürdü. Sparse checkout ve lfs.fetchexclude ile bunu 12 dakikaya indirdik. Sanatçılardan yalnızca ihtiyaç duydukları klasörleri çekmelerini istemek işe yaramadı; elle komut hatırlaması gereken hiç kimse hatırlamıyor.

Asıl sorun indirme değil kilitleme. İki kişi aynı Environment_Rocks.fbx dosyasına dokunduğunda merge diye bir şey yok, biri işini kaybediyor. Perforce'a geçme sebebimiz teknik üstünlük değildi; sanatçılar onu zaten biliyordu ve exclusive checkout orada varsayılan davranış. Lisans ve sunucu bakımı yıllık bir maliyet getiriyor ama kaybedilen iş saatlerinin yanında ucuz kalıyor. Kullandığım eşik basit: proje 20 GB'ın altındaysa Git LFS yeter, üstündeyse Perforce.

Önbellek tarafında sayılar acımasız: temiz bir Library/ klasörünü sıfırdan üretmek bizim projede 26 dakika, oysa asıl derleme 9 dakika. Bu yüzden Library/ build makinesinde kalıyor, ama körü körüne değil: Unity sürümü değiştiğinde Library/PackageCache siliniyor ve haftada bir tam temiz build alıyoruz. Yoksa önbelleğin gizlediği bir hata aylar sonra sürüm gününde ortaya çıkıyor. Temiz build'i pazar gecesine aldık, çünkü hafta içi o 26 dakikayı geri vermek istemiyoruz.

Unreal'de aynı rolü DerivedDataCache oynuyor ve onu paylaşımlı bir ağ klasöründe tutuyoruz. Shader derlemesi soğuk başlangıçta 38 dakika, DDC doluyken 4 dakika. Ağ üzerinden okumak yerel diskten yavaş olduğu için makinede ikinci bir yerel DDC katmanı daha var. Klasör 180 GB'ı geçince eski girdileri temizleyen bir görev ekledik; disk dolduğunda build hata vermiyor, sessizce yavaşlıyor ve bunu fark etmemiz iki haftamızı almıştı.

Smoke test ve performans regresyon eşiği

Otomatik test derken kapsamlı bir suite kastetmiyorum. SmokeTest sahnemiz oyunu açıyor, ana menüden ilk bölüme giriyor, 30 saniyelik kayıtlı input'u oynatıyor ve temiz çıkıyor. Bu kadarı bile bir yılda yaşadığımız 9 sürüm bozulmasının 6'sını sabah 07:00'de haber verdi. Kalan 3 tanesi belirli bir GPU'da veya ikinci bölümde çıkıyordu; onlar için smoke test'i büyütmek yerine QA'nın sabah listesine iki madde ekledik.

İkinci kapı performans. Aynı sahnede frame sürelerini topluyoruz ve ortalamaya değil p99'a bakıyoruz; hedef 16,6 ms, p99 eşiği 22 ms. Ortalama, tek bir 40 ms'lik takılmayı gizliyor ve oyuncunun şikayet ettiği tam olarak o takılma. Ölçümü ilk 120 frame'i atarak başlatıyoruz, çünkü shader warm-up o aralıkta oluyor ve dahil edilirse p99 anlamsız şişiyor.

Eşiği tek build'de kırmızı yapmıyoruz, iki ardışık build gerekiyor. Build makinesini başka hiçbir iş için kullanmamamıza ve ölçümü üç kez alıp medyanı almamıza rağmen yaklaşık %6 gürültü kaldı; eşiği bu gürültüye göre koymazsanız ekip bir ay içinde uyarıları görmezden gelmeye başlıyor. Eşiği aşan build'i de silmiyoruz, artefakt duruyor ki bir önceki gecenin sürümüyle karşılaştırmalı profil alınabilsin. Son 14 gecenin p99 değerini basit bir CSV dosyasında tutuyoruz, eğilimi görmek tek bir sayıya bakmaktan daha faydalı.

Steam ve TestFlight'a otomatik gönderim

Dağıtım adımı teknik olarak en kolay, operasyonel olarak en sinir bozucu kısım. Steam'e yükleme steamcmd +run_app_build app_build.vdf ile tek satır, ama build makinesinde bir kez elle giriş yapıp Steam Guard'ın bıraktığı ssfn dosyasını saklamazsanız pipeline her gece o adımda takılıyor. Gece build'lerini yalnızca parolalı bir internal dalına gönderiyoruz. 9 GB'lık depo yüklemesi ortalama 7 dakika sürüyor, ilk seferden sonra yalnızca değişen parçalar gidiyor.

iOS tarafında xcodebuild -exportArchive ve xcrun altool zinciri çalışıyor. Buradaki tek gerçek kural CFBundleVersion alanını CI çalıştırma numarasına bağlamak; aynı build numarasını ikinci kez gönderdiğinizde App Store Connect reddediyor ve hata mesajı 20 dakika sonra e-posta ile geliyor. TestFlight'ın işleme süresi 8-20 dakika arasında değişiyor, yani sabah kahvesinden önce hazır olmuyor. Sertifikaları ve provisioning profile'ları makinenin anahtarlığına kurup pipeline'a security unlock-keychain adımını eklemek, ilk haftanın en çok vakit alan işiydi.

Toplam kurulum bize tek kişiyle 5 gün sürdü: 1 gün komut satırı build, 1 gün makine ve runner, 1,5 gün önbellek ve sürüm kontrolü, 1 gün test, 0,5 gün mağaza yükleme. Sırayı da bu şekilde koruyun. İlk hafta yalnızca "derleniyor mu" sorusunu cevaplayan bir pipeline kurun, artefaktı bir klasöre atsın yeter; smoke test ve mağaza adımlarını sonraki aylara bırakın. Pipeline'ın kendisi de bakım istiyor, bizde ayda yaklaşık yarım gün gidiyor ve bu kalemi baştan hesaba katmazsanız sistem altı ay içinde çürüyor. Kimse ilk gün eksiksiz bir sistem kurmuyor; çalışan yarısı, çalışmayan tamamından değerlidir.

← Tüm yazılar