Tarayıcıda akıcı bir dövüş sahnesi çizmek ile maç sonucunu tutarlı hesaplamak farklı işler. OfficeFighters bu ayrımı Angular uygulaması, Phaser sahnesi, saf .NET simülasyonu ve onu çalıştıran hosting katmanı üzerinden kuruyor. Kaynak incelemesinde en öğretici bölüm, bu parçaların farklı saatlerle çalışmasına rağmen aynı maç durumunu paylaşması.
Angular uygulamayı, Phaser sahneyi yönetiyor
Angular giriş, yönetim ekranları, HTTP servisleri ve bağlantı yaşam döngüsünü taşıyor. Phaser görsel oyun sahnesini ve çizim tarafını yönetiyor. NetBridge arayüzü oyun katmanının ağla konuşma sınırı: oyun tarafı Angular servisine doğrudan bağlanmak yerine bu sözleşmeden veri alıyor. HubNetBridge uygulamanın SignalR tarafını bu sınırla buluşturuyor.
Bu düzenin pratik etkisi, bir ağ olayını işlemek için Phaser sahnesine servis enjeksiyonu ayrıntılarını taşımamak. Sahneyi kapatırken abonelikleri ve kaynakları bırakmak da aynı sınırın görevi. Yeni sahne açıldığında eskisinin dinleyicileri yaşamaya devam ederse aynı snapshot iki kez işlenebilir veya sesler iki kez tetiklenebilir.
Saf simülasyonda uzunluk ve zaman birimleri açık
OfficeFighters.Game içindeki SimMath bir pikseli bin mpx olarak temsil ediyor; zaman tick cinsinden ilerliyor. Yüzde hesabı tamsayıya sıfıra doğru kesilerek dönüyor. Çarpmanın ara sonucunda long kullanılması, int aralığındaki iki değeri çarparken sonuca ulaşmadan taşmayı önlüyor; ardından Saturate sonucu int aralığında tutuyor.
// SimMath: source arithmetic, with a 64-bit intermediate.
public const int MpxPerPx = 1000;
public static int Pct(int value, int pct) =>
Saturate((long)value * pct / 100);
// C# integer division truncates toward zero:
// Pct(-7, 50) == -3Sıfıra doğru kesme, negatif değerlerde aşağı yuvarlamadan farklı sonuç doğurur. Örneğin -7’nin yüzde 50’si bu sözleşmede -3’tür; aşağı yuvarlama -4 üretirdi. Testlerde yalnız pozitif hızları kullanmak bu farkı saklayabilir. Sabit noktalı aritmetik, bütün simülasyonun kendiliğinden deterministik olduğunu kanıtlamaz; işlem sırası ve rastgelelik girdisi de denetlenmeli.
Odanın tek bir döngü sahibi var
GameLoopShard kendisine atanmış odaları ayrılmış iş parçacığında sırayla güncelliyor. ShardPool otomatik seçimde işlemci sayısının yarısını bir ile dört arasına sıkıştırıyor; yeni oda en az yüklü parçaya veriliyor. Oda maç boyunca o parçada kalıyor. Dışarıdan gelen komutlar kuyruklara yazılıyor ve durum değişikliği oda döngüsünde gerçekleşiyor.
Böylece odanın simülasyonunu aynı anda farklı çağrıların ilerletmesi engelleniyor. Shard sayısı bir kapasite sihri değil; oda başına iş miktarı ve dağılımı hâlâ önemli. Kaynakta döngü süresi, gecikme ve düşürülen tick göstergelerinin bulunması bu nedenle anlamlı.
Gecikmiş tick borcu sınırlı işleniyor
GameRoom monoton zamana göre bir sonraki tick anını tutuyor. Döngü gecikirse MaxCatchUpTicks sınırına kadar ardışık adım çalıştırıyor. Sınırdan sonra kalan gecikme düşürülen tick olarak sayılıp takvim ileri alınıyor. Bu davranış, bir turdaki gecikmenin sonraki bütün turları sonsuz yakalama işine çevirmesini önlüyor. Duvar saatinin değişmesiyle oynanmayan monoton saat, bu hesap için ayrı bir bağımlılık.
Snapshot ritmi ile render ritmi ayrılıyor
Sunucunun her simülasyon adımında ağ paketi üretmesi gerekmiyor. GameRoom tick oranı ile snapshot oranından bir gönderim aralığı çıkarıyor. İstemci daha seyrek gelen durumları SnapshotBuffer içinde tutuyor ve renderTick’in öncesindeki ve sonrasındaki paketleri buluyor. İki nokta arasındaki doğrusal değer, çizimde küçük adımlarla ilerlemek için kullanılıyor.
// Interpolation math used by the renderer.
const t = Math.min(1, Math.max(0,
(renderTick - a.tick) / (b.tick - a.tick)));
const x = a.x + (b.x - a.x) * t;
// a: tick 100, x 1000; b: tick 102, x 1400
// renderTick 101 => t 0.5 => x 1200Kaynakta x, y, vx ve vy gibi alanlar ara değerleniyor. Sağlık, yön, hareket kimliği ve dövüş durumu gibi ayrık alanlar önceki snapshot’tan alınıyor. Canı iki değer arasında kesirli dolaştırmak ya da saldırı kimliğini sayısal olarak ortalamak oyun sözleşmesini bozardı.
Tur geçişi, hitstop ve tampon tükenmesi
İki snapshot farklı turlara aitse konumlar arasında interpolasyon yapılmıyor; yeni tur başındaki sıfırlamanın karakteri sahnede süzerek göstermesi engelleniyor. Aynı hareket durumu devam ederken stateTick animasyon için ilerleyebiliyor, hitstop sırasında ise sabit tutuluyor. Tamponun sonuna gelinirse son doğrulanmış snapshot korunuyor ve starved bilgisi dönüyor.
İyi bir kontrol seti eşit tick, tur değişimi, negatif koordinat, aynı konum, hitstop ve tampon tükenmesini kapsar. Kaynakta netcode testleri bu saf hesapları Phaser çiziminden ayrı ele alıyor. Bu yazı mimari ve kaynak davranışını açıklıyor; belirli bir cihazda FPS veya eşzamanlı oyuncu kapasitesi ölçüldüğü iddiası taşımıyor.
Kaynaklar / References
OfficeFighters serisinin diğer yazıları
- OfficeFighters — Supabase Storage ve S3 ile oyun varlıklarını yönetmek
- OfficeFighters — SignalR ve WebSocket ile gerçek zamanlı oyun iletişimi
- OfficeFighters — MailKit ve outbox ile dayanıklı e-posta gönderimi
- OfficeFighters — JWT, refresh token rotasyonu ve çok sekmeli oturum yönetimi
- OfficeFighters — PostgreSQL, EF Core ve DB-first veri modeli