Godot XR Multiplayer: Spatial Sync ve Time-Rewind Netcode'unun Derinlikleri
Özet olarak
Godot XR Multiplayer projelerinde spatial sync (uzamsal senkronizasyon) ve düşük gecikmeli netcode zorluklarını ele alan bu teknik makale, VR ortamlarında motion sickness sorununu önlemenin yollarını açıklıyor. Godot XR Community Game Jam V projelerinden elde edilen uzamsal dersler ve delta-compression tabanlı GDScript 4 kod örneği ile yüksek frekanslı verilerin nasıl optimize edileceği gösteriliyor. Manuel backend altyapısı kurmanın zorluklarına değinilirken, WebRTC destekli horizOn platformunun bu karmaşık süreçleri nasıl kolaylaştırdığı vurgulanıyor.
Bir godot xr multiplayer oyunu geliştirmek, spatial synchronization'ınız tek bir kare bile geride kaldığında ciddi fiziksel motion sickness üretmenin en hızlı yoludur. Bir oyuncu kafasını veya ellerini yerel 90Hz yenileme hızında (refresh rate) hareket ettirdiğinde, herhangi bir ağ kaynaklı jitter veya gecikmiş senkronizasyon (late-state synchronization), fiziksel bedenleri ile sanal dünya arasında sarsıcı bir kopukluk yaratır. Bu spatial latency (uzamsal gecikme) problemini çözmek, geleneksel düz ekran netcode paradigmalarını baypas etmeyi ve özel predictive tracking buffer'ları (öngörücü takip tamponları) uygulamayı gerektirir.
Gelişmiş temporal mekanikleri ve özel netcode modellerini inceleyerek, sunucu bandwidth'inizi (bant genişliği) aşırı yüklemeden client kameralarını ve fizik entity'lerini tamamen senkronize halde çalıştırabilirsiniz.
Under the Hood: VR Multiplayer'da Spatial Sync'in Ekstrem Zorlukları
Tipik bir düz ekran multiplayer oyunda, 3D position vektöründen (12 byte) ve 1D rotation float değerinden (4 byte) oluşan tek bir karakter kapsül collider'ını senkronize edersiniz. Bu 16 byte'lık payload'u 30Hz'de göndermek, saniyede yaklaşık 480 byte'lık ham veri üretir. Bir godot xr multiplayer bağlamında ise, aynı anda üç ayrı yüksek frekanslı tracked bileşeni senkronize etmeniz gerekir: head-mounted display (HMD) ve iki hareket kontrolcüsü (motion controller). Takip edilen her bir nokta, 3D position vektörü (12 byte) ve 3D orientation quaternion (16 byte) gerektirir; bu da nokta başına toplam 28 byte demektir.
Üç noktanın tamamını senkronize etmek, kare başına 84 byte anlamına gelir. Standart bir 90Hz refresh rate değerinde bu, saniyede 7.560 byte (7,56 KB) ham payload gerektirir. Standart UDP packet overhead'ini (header başına 28 byte) eklediğinizde, tek bir client 10,08 KB/s'ye kadar alanı doldurur. 8 oyunculu bir lobby'de server, saniyede 564 KB/s'ye kadar yüksek frekanslı veriyi işlemek ve broadcast etmek zorunda kalır; bu da devasa downstream sıkışıklığına ve packet queue gecikmelerine yol açar.
Unreal Engine multiplayer'da oyuncu konumu desync sorununu çözme kılavuzumuzda ayrıntılı olarak açıkladığımız gibi, standart düz ekran oyunlar küçük tutarsızlıkları tolere edebilirken, VR oyunları tamamen affetmezdir. 3 karelik bir spatial desync bile akut vestibular uyumsuzluğu tetikleyerek oyuncuların fiziksel olarak midesinin bulanmasına neden olabilir. Bununla mücadele etmek için geliştiriciler, katı server-authoritative replication yapısından, geçmiş delta mutabakatına (historical delta reconciliation) sahip client-side prediction şemalarına geçiş yapmalıdır.
Godot XR Community Game Jam V Çalışmalarından 5 Uzamsal Ders
Beşinci Godot XR Community Game Jam V bünyesinde, "Rewind" temasına odaklanan 23 oyun sunan 98 katılımcı yer aldı. Bu projeler Godot 4'ün uzamsal mimarilerinin sınırlarını zorlayarak, sürükleyici alanlarda zamansal manipülasyonun yaratıcı uygulamalarını sergiledi. Aşağıda, jam'in en iyi beş projesinden çıkardığımız temel spatial synchronization ve netcode derslerini bulabilirsiniz.
1. Tactile Physics ve Etkileşim Durumları (Rewind Tower'dan Esinlenilmiştir)
Turnuvayı kazanan proje Rewind Tower, oyuncuların madeni parayla çalışan bir pano üzerinde mekanik üniteleri kurup savaşta tutmak için onları geri sardıkları dokunsal bir tower defense sistemine sahiptir. Multiplayer bir senaryoda, fiziksel grab'leri ve analog spring-winding durumlarını senkronize etmek kritik race condition'lara (yarış durumlarına) yol açar.
Eğer iki oyuncu aynı üniteyi aynı anda grab etmeye çalışırsa, basit bir replication sistemi, onlar network authority için mücadele ederken nesnenin iki oyuncu arasında hızla teleport olmasına neden olacaktır. Bunu çözmek için geliştiriciler, nesne bırakılana kadar harici grab girdilerini kilitleyerek geçici olarak bir client'ı fiziksel authority olarak atayan, server üzerinde çalışan deterministik bir priority queue uygulamalıdır.
2. Hand-Held Analog Kontroller ve Kesintisiz Input Senkronizasyonu (Chrono Crank'ten Esinlenilmiştir)
İkinci sırayı alan Chrono Crank, zamanın akışını manipüle etmek için kullanılan büyük bir fiziksel krankına sahip steampunk tarzı bir el cihazına dayanıyor. Fiziksel krank rotasyonunu replicate etmek, bağlı tüm oyuncular arasında yüksek hassasiyetli, kesintisiz bir float değerinin senkronize edilmesini gerektirir.
Analog kollar ve butonlar, binary state trigger'lar yerine sürekli position güncellemeleri gerektirir. Yüksek frekanslı RPC'ler kullanarak bu etkileşimli fizik koordinatlarını senkronize etmeye çalışmak, geliştiricilerin dünya durumlarını bozan multiplayer RPC replication sorunlarını düzeltme kılavuzunda dünyayı bozan multiplayer RPC replication sorunlarını çözerken karşılaştıkları sorunlarla tamamen aynı olan yaygın bir hatadır.
Bunun yerine, kadrandaki lokalize rotation açısını sıkıştırılmış bir float değişkeni olarak senkronize etmeli ve Hermite spline interpolation kullanarak alıcı client'lar üzerinde bu durumu interpolate etmelisiniz.
3. Spatial Recording ve Scrubbing (NeuroCorp Training Demo'dan Esinlenilmiştir)
Üçüncü sıradaki proje NeuroCorp Training Demo'da oyuncular, görevleri tamamlamak için zamanı ileri ve geri sarabilmelerine (scrub) olanak tanıyan, geçmiş fiziksel olayların sürükleyici kayıtlarına teleport edilirler. Ortak bir lobby'de, bu temporal scrub olaylarının senkronizasyonu global bir state buffer gerektirir.
Tüm bir odanın fiziksel geçmişini frame-by-frame (kare kare) yüklemek önemli ölçüde bellek tükettiğinden, ağ katmanı frame verilerini indeks tabanlı keyframe yapıları kullanarak serialize etmelidir; böylece client'lar sürekli transform frame'leri stream etmek yerine lokalde ayrık position örnekleri arasında interpolate yapabilir.
4. Paralel Spatial Timeline'lar (Last Minute'tan Esinlenilmiştir)
Dördüncü sırada bitiren Last Minute, oyuncuların bulmacaları gerçek zamanlı çözmek için zamanı manipüle ettikleri bir escape room oyunudur. Bir multiplayer oyun odası içinde asenkron zaman durumlarını yönetmek mimari bir kabustur.
Eğer Oyuncu A bir bulmaca kutusunu 10 saniye önceki durumuna geri sararken Oyuncu B o kutudan aldığı bir anahtarı tutuyorsa, server kutunun spatial timeline'ını global oyuncu timeline'ından ayırmalıdır. Bunu başarmak için oyun dünyası bağımsız temporal zone'lar halinde yapılandırılmalı ve aktif oyuncuları mevcut sistem tick rate değerine kilitli tutarken, bireysel spatial node'ların history buffer'ları bağımsız olarak işlemesine izin verilmelidir.
5. Multi-Entity Prediction ve Platform Synced Motion (ScrewTheTime'dan Esinlenilmiştir)
ScrewTheTime, oyuncunun hem kendi karakterini hem de hareketli platformların temporal durumunu kontrol ettiği bir VR platform oyunu olarak beşinci sırayı aldı. VR'da hareket eden ve zamanı geri sarılan bir platform üzerinde duran bir oyuncuyu senkronize etmek, parent-child koordinat alanı transform'larını gerektirir.
Eğer server yalnızca mutlak global koordinatları senkronize ederse, platformun hareketi ile oyuncunun HMD güncellemeleri arasındaki küçük gecikme (latency) farklılıkları, oyuncunun platformdan kaymasına veya kontrolsüz şekilde titremesine neden olur. Bunu düzeltmek için, client tarafında oyuncunun spatial origin noktasını hareketli platform node'una child yapın ve güncellemeleri server'a broadcast etmeden önce tüm lokal kafa hareketi hesaplamalarını platformun lokal basis değerine göre translate edin.
Deep Dive: Godot 4'te Delta-Compressed Spatial Recording Sistemi İnşa Etmek
Rewind yeteneklerine sahip, son derece hızlı yanıt veren bir godot xr multiplayer deneyimi oluşturmak için, takip edilen position'ları sıkıştıran bir spatial recording buffer uygulamalısınız. Aşağıdaki üretim kalitesindeki (production-grade) GDScript 4 sınıfı, lokal takip verilerini (HMD ve kontrolcüler) bir ring buffer'a kaydeder, ağ trafiğini en aza indirmek için delta compression yürütür ve uzak client'larda akıcı bir oynatma için spherical linear interpolation (SLERP) gerçekleştirir.
# xr_spatial_sync_manager.gd
extends Node
class_name XRSpatialSyncManager
# Inner class to store high-frequency spatial tracking data for a single frame
class TrackingFrame extends RefCounted:
var timestamp: float = 0.0
var head_pos: Vector3 = Vector3.ZERO
var head_rot: Quaternion = Quaternion.IDENTITY
var left_hand_pos: Vector3 = Vector3.ZERO
var left_hand_rot: Quaternion = Quaternion.IDENTITY
var right_hand_pos: Vector3 = Vector3.ZERO
var right_hand_rot: Quaternion = Quaternion.IDENTITY
# Compress the frame into a lightweight dictionary for network serialization
func serialize() -> Dictionary:
return {
"t": timestamp,
"hp": [head_pos.x, head_pos.y, head_pos.z],
"hr": [head_rot.x, head_rot.y, head_rot.z, head_rot.w],
"lp": [left_hand_pos.x, left_hand_pos.y, left_hand_pos.z],
"lr": [left_hand_rot.x, left_hand_rot.y, left_hand_rot.z, left_hand_rot.w],
"rp": [right_hand_pos.x, right_hand_pos.y, right_hand_pos.z],
"rr": [right_hand_rot.x, right_hand_rot.y, right_hand_rot.z, right_hand_rot.w]
}
# Decompress a network dictionary back into a fully typed frame
static func deserialize(data: Dictionary) -> TrackingFrame:
var frame = TrackingFrame.new()
frame.timestamp = data["t"]
frame.head_pos = Vector3(data["hp"][0], data["hp"][1], data["hp"][2])
frame.head_rot = Quaternion(data["hr"][0], data["hr"][1], data["hr"][2], data["hr"][3])
frame.left_hand_pos = Vector3(data["lp"][0], data["lp"][1], data["lp"][2])
frame.left_hand_rot = Quaternion(data["lr"][0], data["lr"][1], data["lr"][2], data["lr"][3])
frame.right_hand_pos = Vector3(data["rp"][0], data["rp"][1], data["rp"][2])
frame.right_hand_rot = Quaternion(data["rr"][0], data["rr"][1], data["rr"][2], data["rr"][3])
return frame
# --- Configuration Properties ---
@export var sync_threshold_position: float = 0.005 # 5mm threshold for delta-compression
@export var sync_threshold_rotation: float = 0.02 # ~1.15 degrees threshold for angular changes
@export var buffer_max_time: float = 2.0 # Store up to 2 seconds of local history
@export var playback_interpolation_delay: float = 0.033 # 33ms interpolation window
# --- Node References ---
@export var xr_origin: XROrigin3D
@export var xr_camera: XRCamera3D
@export var left_controller: XRController3D
@export var right_controller: XRController3D
# --- Internal Buffers ---
var history_buffer: Array[TrackingFrame] = []
var last_sent_frame: TrackingFrame = null
func _ready() -> void:
if not xr_origin or not xr_camera or not left_controller or not right_controller:
push_warning("XRSpatialSyncManager: Tracked nodes are not fully assigned!")
# Record the current spatial state of the VR user
func capture_current_frame() -> TrackingFrame:
var frame = TrackingFrame.new()
frame.timestamp = Time.get_ticks_msec() / 1000.0
# Capture relative transforms inside the XR origin space
frame.head_pos = xr_camera.transform.origin
frame.head_rot = xr_camera.transform.basis.get_rotation_quaternion()
frame.left_hand_pos = left_controller.transform.origin
frame.left_hand_rot = left_controller.transform.basis.get_rotation_quaternion()
frame.right_hand_pos = right_controller.transform.origin
frame.right_hand_rot = right_controller.transform.basis.get_rotation_quaternion()
return frame
# Push a captured frame into our sliding history ring buffer
func store_frame(frame: TrackingFrame) -> void:
history_buffer.append(frame)
# Prune frames older than the maximum buffer timeline window
var expiration_time = frame.timestamp - buffer_max_time
while history_buffer.size() > 0 and history_buffer[0].timestamp < expiration_time:
history_buffer.remove_at(0)
# Evaluate if the current frame differs enough from the last sync frame to justify sending a network packet
func check_delta_compression(current: TrackingFrame) -> bool:
if last_sent_frame == null:
return true # Always send the initial setup frame
# Check spatial distance changes
var head_pos_diff = current.head_pos.distance_squared_to(last_sent_frame.head_pos)
var left_pos_diff = current.left_hand_pos.distance_squared_to(last_sent_frame.left_hand_pos)
var right_pos_diff = current.right_hand_pos.distance_squared_to(last_sent_frame.right_hand_pos)
if head_pos_diff > (sync_threshold_position * sync_threshold_position) or \
left_pos_diff > (sync_threshold_position * sync_threshold_position) or \
right_pos_diff > (sync_threshold_position * sync_threshold_position):
return true
# Check angular orientation changes
var head_rot_diff = current.head_rot.angle_to(last_sent_frame.head_rot)
var left_rot_diff = current.left_hand_rot.angle_to(last_sent_frame.left_hand_rot)
var right_rot_diff = current.right_hand_rot.angle_to(last_sent_frame.right_hand_rot)
if head_rot_diff > sync_threshold_rotation or \
left_rot_diff > sync_threshold_rotation or \
right_rot_diff > sync_threshold_rotation:
return true
return false
# Rewind or playback history buffer: interpolates between tracked frame states at a specific timestamp
func get_interpolated_state(target_time: float) -> TrackingFrame:
if history_buffer.is_empty():
return null
# Clamp target time to available history
if target_time <= history_buffer[0].timestamp:
return history_buffer[0]
if target_time >= history_buffer[-1].timestamp:
return history_buffer[-1]
# Find surrounding frames
var prev_frame = history_buffer[0]
var next_frame = history_buffer[-1]
for i in range(history_buffer.size() - 1):
if history_buffer[i].timestamp <= target_time and history_buffer[i + 1].timestamp >= target_time:
prev_frame = history_buffer[i]
next_frame = history_buffer[i + 1]
break
var span = next_frame.timestamp - prev_frame.timestamp
var factor = 0.0
if span > 0.0001:
factor = (target_time - prev_frame.timestamp) / span
# Perform linear and spherical linear interpolation
var interpolated = TrackingFrame.new()
interpolated.timestamp = target_time
interpolated.head_pos = prev_frame.head_pos.lerp(next_frame.head_pos, factor)
interpolated.head_rot = prev_frame.head_rot.slerp(next_frame.head_rot, factor)
interpolated.left_hand_pos = prev_frame.left_hand_pos.lerp(next_frame.left_hand_pos, factor)
interpolated.left_hand_rot = prev_frame.left_hand_rot.slerp(next_frame.left_hand_rot, factor)
interpolated.right_hand_pos = prev_frame.right_hand_pos.lerp(next_frame.right_hand_pos, factor)
interpolated.right_hand_rot = prev_frame.right_hand_rot.slerp(next_frame.right_hand_rot, factor)
return interpolated
Bu sistem, frame kaybetseniz veya network latency dalgalanmaları yaşasanız bile geçmiş transform'ları doğru şekilde yeniden oluşturabilmenizi sağlar. Remote client'lar, geçmiş tracking durumlarını sorunsuz bir şekilde interpolate ederek VR el hareketlerinin ve etkileşimlerinin inanılmaz derecede akıcı görünmesini sağlayabilir.
Multiplayer'da Time-Rewind Uygulamak: Manuel Bir Kabus
Eğer tüm bu sistemi elle yazmayı seçerseniz, önemli bir altyapı yükü (infrastructure overhead) ile karşı karşıya kalırsınız. Room orchestrator'lar kurmalı, Linux ortamlarında özel authoritative server instance'ları barındırmalı ve ENetMultiplayerPeer aracılığıyla ham UDP socket'leri yapılandırmalısınız. Ev ağlarındaki oyuncu bağlantı kesintilerini önlemek için, Carrier-Grade NAT (CGNAT) güvenlik duvarlarını aşacak özel STUN/TURN traversal sunucuları inşa etmeniz gerekir.
Ayrıca, saniyede binlerce yüksek frekanslı kaydı işleyebilen kalıcı bir global state veritabanını sürdürmek, özel clustering konfigürasyonları gerektirir. Oyununuzu tasarlamak yerine sadece backend altyapısını yazmak için 4 ila 6 haftalık özel mühendislik zamanı harcarsınız.
Bunu kendiniz inşa etmek, load balancer'lar, database sharding ve SSL cert yönetimi kurmayı gerektirir; bu da kolayca 4-6 haftalık bir çalışmadır. horizOn ile bu backend hizmetleri önceden yapılandırılmış olarak gelir ve altyapınız yerine oyununuzu ship etmenize olanak tanır.
horizOn ile Godot XR Multiplayer'ı Kolaylaştırmak
Geliştirme döngülerini karmaşık düşük seviyeli networking ve server hazırlığı ile boşa harcamak yerine, horizOn'u doğrudan projenize entegre edebilirsiniz. Platformun real-time room engine'i, yüksek frekanslı VR veri serileştirmesi (data serialization) için açıkça tasarlanmış optimize edilmiş WebRTC ve düşük gecikmeli (low-latency) UDP boru hatları (pipelines) sağlar.
horizOn'un önceden yapılandırılmış room yapılarını kullanarak oyununuz, bölgesel latency metriklerine göre kullanıcıları otomatik olarak eşleştirebilir ve spatial synchronization'ları sıkı tutabilir. Tüm backend yönlendirmesi (routing), güvenlik token'ları ve oyuncu matchmaking süreçleri basit bir SDK aracılığıyla yönetildiğinden, dakikalar içinde ölçeklenebilir, çok bölgeli bir VR deneyimini yayına alabilirsiniz.
Godot XR Multiplayer için 4 Netcode Best Practice'i
Doğru netcode kurallarını uygulamak, bireysel client ağ kalitesinden bağımsız olarak oyununuzun sorunsuz çalışmasını sağlar. Bir sonraki uzamsal tasarımınıza (spatial layout) bu dört tekniği dahil edin:
- Decouple Network Tick Rate from Physics Frame Rate: Ağ replication tick rate değerinizi asla doğrudan client'ın HMD refresh rate değerine kilitlemeyin. Oyun mantığınızı (game logic) ve transform senkronizasyonunu kararlı bir 30Hz veya 45Hz'de çalıştırın ve yerel olarak 90Hz/120Hz kareleri yeniden oluşturmak için client-side doğrusal (linear) ve küresel doğrusal (spherical linear) interpolation kullanın.
- Prioritize HMD Tracking Updates Over Controllers: Head tracking, VR simülatör hastalığının (simulator sickness) birincil nedenidir. El transform'larına daha ağır delta compression ve daha düşük öncelik sınırları uygularken, kullanıcının kamera transform'una daha yüksek ağ önceliği (network priority) ve daha büyük paket bandwidth dilimleri ayırın.
- Enforce Distance and Orientation Thresholds: Bir oyuncunun uzamsal değişiklikleri önemsizse paket broadcast etmeyin. Boşta kalan (idle) güncellemeleri ortadan kaldırmak ve bandwidth tasarrufu sağlamak için açık delta-compression guard band'leri (örneğin, 5 mm'lik position hareketi veya 0,02 radyanlık rotation) ayarlayın.
- Employ Client-Side Authority for Physical Grabs: Bir oyuncu etkileşimli bir nesneyi grab ettiğinde, node'un network authority'sini anında grab eden client'a geçirin. Bu durum, lokal lag'in kontrolcü ile tutulan nesne arasında görünür bir ayrılık yaratmasını engellerken, server'ın nihai nesne yerleşimini asenkron olarak denetlemesine (audit) izin verir.
Sonraki Adımlar: XR Multiplayer Deneyiminizi Ship Edin
VR'da yüksek frekanslı multiplayer ortamlar geliştirmek, görsel kararlılık ile sıkı bandwidth yönetimi arasında dikkatli bir denge gerektirir. Delta compression, local historical buffering ve client-side authority özelliklerini dahil ederek, oyuncuları sabit ve bağlı tutan sürükleyici ortamlar inşa edebilirsiniz.
Multiplayer backend altyapınızı ölçeklendirmeye hazır mısınız? horizOn'u ücretsiz deneyin veya API docs sayfasına göz atın.
Kaynak: Godot XR Community Game Jam V