Bir oyun dünyasını her on saniyede bir ilerletmek birkaç satır zamanlayıcı kodu olabilir. Zor soru, süreç tam kayıt sırasında kapanınca aynı oyun gününün yeniden uygulanıp uygulanmayacağı. Arşivimdeki dünya adımı, gün sonu ve olay üretimi yöntemlerini açıklarken döngünün zamanlamasını kalıcı ilerleme kararından ayırıyorum.
Döngü ne zaman çağıracağını bilir
WorldLoop, PeriodicTimer ile uygun anlarda store'u çağırıyor. IWorldStore ise “bir sonraki günü en fazla bir kez uygula” sözleşmesini taşıyor. Bu metot adı garanti uygulaması değildir; örnekte gerçek veritabanı kodu yok. Microsoft.Extensions.Hosting bağlamında bir store uygulaması ve DI kaydıyla host'a yerleştirilecek bir parçadır.
using Microsoft.Extensions.Hosting;
public interface IWorldStore
{
Task ApplyNextDayOnceAsync(CancellationToken token);
}
public sealed class WorldLoop(IWorldStore store):BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken token)
{
using var timer=new PeriodicTimer(TimeSpan.FromSeconds(10));
while(await timer.WaitForNextTickAsync(token))
await store.ApplyNextDayOnceAsync(token);
}
}
Her adım await edildiği için bu tek döngü kendi önceki adımı bitmeden ikinci çağrıyı başlatmaz. Bu, aynı sunucunun başka bir örneğinin veya başka bir worker'ın aynı dünyayı ilerletemeyeceği anlamına gelmez. Zamanlayıcı düzeyindeki sıralama ile veri düzeyindeki tekilleştirme farklı sınırlar.
Takvim tikleriyle oyun zamanını karıştırmamak
On saniyelik aralık, oyundaki bir günün gerçek süresi olarak seçilmiş bir gösterim. Bir adım yavaşlarsa “kaç oyun günü geçti?” sorusuna yalnız callback sayısıyla cevap vermek yetmeyebilir. PeriodicTimer gecikmiş zamanları sınırsız bir kalıcı kuyruk gibi saklamaz; uygulama kapalıyken ilerleme istiyorsam son tamamlanan oyun günü kalıcı tutulmalı.
Gerçek geçen zamana göre telafi yapılacak mı, dünya uygulama kapalıyken duracak mı? Bu ürün kararıdır. Aynı store'a ardışık günleri kontrollü şekilde uygulatmakla bir anda birçok günü atlamak ekonomi, üretim ve olay kurallarında farklı sonuçlar doğurabilir. Zamanlayıcının varsayılanı oyun tasarımını seçmemeli.
ApplyNextDayOnceAsync nasıl bir sınır ister?
Kalıcı tarafta dünya kimliği, mevcut gün ve beklenen sürüm üzerinden koşullu bir güncelleme düşünülebilir. Dünya 17. gündeyken 18. günü uygulayan iki istekten yalnız biri kabul edilmeli. O güne ait kaynak üretimi, oyuncu durumu ve olaylar da aynı atomik işin parçalarıysa birlikte commit edilmeli. Yalnız gün sayısını artırmak işlemin tamamını tekilleştirmez.
Bir adım yarıda kalıp yeniden denenirse rastgele olayların yeniden çekilmesi sonucu değiştirebilir. Tekrar üretilebilir bir seed, kayıtlı olay kararı veya tamamlanmış adım kaydı bu tasarımda değerlendirilebilir. Hangi yöntemin seçildiği oyunun yeniden oynatma ve denetleme ihtiyacına bağlıdır; örnekte böyle bir sistemin tamamlandığı iddia edilmiyor.
Store bağımlılığı ve scope
Hosted service uzun ömürlüdür. Gerçek store Scoped bir DbContext kullanıyorsa doğrudan constructor'da yakalanıp bütün ömür boyunca tutulmamalı; her adım uygun scope içinde çözülmelidir. Kodun arayüz üzerinden konuşması bu sahiplik kararını görünür kılar ama DI yaşam süresini kendiliğinden doğru hale getirmez.
İptal ve hata politikasını açık seçmek
WaitForNextTickAsync ve store çağrısı aynı token'ı alıyor. Kapanış sırasında gelen beklenen iptal, “dünya bozuldu” hatası gibi değerlendirilmemeli. Buna karşılık store'daki beklenmeyen exception'ı sessizce yutup bir sonraki güne geçmek tutarsızlık yaratabilir. Hangi hatada host duracak, hangisinde aynı adım güvenli biçimde tekrar denenecek, uygulama bunu açıkça seçmeli.
Testte tek döngüde çağrıların çakışmadığını, iki worker yarışında günün bir kez uygulandığını ve işlem ortası hata sonrası adımın yarım kalmadığını kontrol ederim. Saat bekleten testler yerine sahte store ve kontrol edilebilir zaman kaynağı işi daha deterministik sınamayı sağlar. Başarılı kontrol, yalnız timer'ın tik üretmesi değil dünyanın doğru sonraki duruma geçmesidir.