Godot XR Multiplayer: Spatial SyncとTime-Rewind Netcodeの仕組みを探る
要点まとめ
VRマルチプレイヤー開発における最大の課題である「空間的な同期遅れ(Spatial Sync)」と「3D酔い」を克服するためのNetcode設計について解説した記事です。Godot XR Game Jamの優秀作品から得られた5つの空間同期の知見や、GDScriptによるデルタ圧縮空間記録システムの実装例を紹介しています。さらに、WebRTCや低レイテンシーUDPを統合した「horizOn」を活用して、複雑なインフラ構築コストをかけずにスケーラブルなXRマルチプレイヤー体験を構築する手法を提案しています。
Building a godot xr multiplayer game is a fast track to producing severe physical motion sickness if your spatial synchronization falls behind by even a single frame. When a player moves their head or hands at a native 90Hz refresh rate, any networked jitter or late-state synchronization creates a jarring disconnect between their physical body and the virtual world. Resolving this spatial latency requires bypassing traditional flat-screen netcode paradigms and implementing custom predictive tracking buffers.
godot xr multiplayer ゲームの開発において、空間的な同期(Spatial Sync)がわずか1フレームでも遅延すれば、プレイヤーは即座に深刻な3D酔い(モーションシックネス)に襲われることになります。プレイヤーがネイティブの90Hzリフレッシュレートで頭や手を動かすとき、ネットワーク上のJitterや同期の遅れは、物理的な身体と仮想世界との間に不快な乖離を生み出します。この空間的なレイテンシーを解決するためには、従来のフラットスクリーン向けの Netcode パラダイムを回避し、カスタムの予測トラッキングバッファ(predictive tracking buffers)を実装しなければなりません。
By examining advanced temporal mechanics and custom netcode patterns, you can keep client cameras and physics entities running in absolute sync without overloading your server bandwidth.
高度な時間的メカニズムとカスタムの Netcode パターンを検証することで、サーバーの帯域幅(Bandwidth)に過度な負荷をかけることなく、クライアントのカメラと物理エンティティを完全に同期させ続けることができます。
Under the Hood: The Extreme Challenge of Spatial Sync in VR Multiplayer
舞台裏:VR MultiplayerにおけるSpatial Syncの極限の課題
In a typical flat-screen multiplayer game, you synchronize a single character capsule collider consisting of a 3D position vector (12 bytes) and a 1D rotation float (4 bytes). Sending this 16-byte payload at 30Hz results in roughly 480 bytes per second of raw data. In a godot xr multiplayer context, you must synchronize three separate high-frequency tracked components simultaneously: the head-mounted display (HMD) and two motion controllers. Each tracked point requires a 3D position vector (12 bytes) and a 3D orientation quaternion (16 bytes), totaling 28 bytes per point.
一般的なフラットスクリーン向けの Multiplayer ゲームでは、3D位置ベクトル(12バイト)と1D回転float(4バイト)からなる単一のキャラクターのカプセルコライダーを同期させます。この16バイトのペイロードを30Hzで送信すると、生データは1秒あたり約480バイトになります。しかし、godot xr multiplayer のコンテキストでは、ヘッドマウントディスプレイ(HMD)と2つのモーションコントローラーという、高頻度でトラッキングされる3つの独立したコンポーネントを同時に同期させる必要があります。トラッキングされる各ポイントには、3D位置ベクトル(12バイト)と3D向きクォータニオン(16バイト)が必要で、1ポイントあたり計28バイトに達します。
Synchronizing all three points represents 84 bytes per frame. At a standard 90Hz refresh rate, this demands 7,560 bytes (7.56 KB) per second of raw payload. Add standard UDP packet overhead (28 bytes per header), and a single client saturates up to 10.08 KB/s. In an 8-player lobby, the server must process and broadcast up to 564 KB/s of high-frequency data, creating massive downstream congestion and packet queue delays.
これら3つのポイントすべてを同期すると、1フレームあたり84バイトになります。標準的な90Hzのリフレッシュレートでは、1秒あたり7,560バイト(7.56 KB)の生ペイロードが必要になります。さらに標準的な UDP パケットのオーバーヘッド(ヘッダーあたり28バイト)を加えると、単一のクライアントだけで最大10.08 KB/sに達します。8プレイヤーのロビー(Lobby)の場合、サーバーは最大564 KB/sもの高頻度データを処理してブロードキャストしなければならず、下りの帯域の逼迫やパケットキューの遅延といった甚大な問題を引き起こします。
While standard flat-screen games can tolerate minor discrepancies—as detailed in our guide on how to fix player location desync in Unreal Engine multiplayer—VR games are completely unforgiving. Even a 3-frame spatial desync can trigger acute vestibular mismatch, leaving players physically nauseous. To combat this, developers must transition from rigid server-authoritative replication to client-side prediction schemes with historical delta reconciliation.
一般的なフラットスクリーンゲームであれば、多少のズレは許容できます(詳細は、Unreal Engineのマルチプレイヤーでプレイヤー位置の同期ズレを修正する方法のガイドで解説しています)。しかし、VRゲームにおいては一切の妥協が許されません。わずか3フレームの空間的な同期ズレ(desync)であっても、急激な前庭感覚の不一致を引き起こし、プレイヤーに深刻な吐き気をもたらします。これに対処するため、開発者は厳格な Server-Authoritative(サーバー権限)型のレプリケーションから、履歴デルタ調整(historical delta reconciliation)を伴う Client-Side Prediction(クライアントサイド予測)方式へと移行する必要があります。
5 Spatial Lessons from Godot XR Community Game Jam V Entries
Godot XR Community Game Jam V 提出作品から学ぶ5つの空間同期のレッスン
The fifth Godot XR Community Game Jam V featured 98 participants submitting 23 games focused on the theme "Rewind". These projects pushed the boundaries of Godot 4's spatial architectures, demonstrating creative applications of temporal manipulation in immersive spaces. Below are the key spatial synchronization and netcode lessons we extracted from the top five entries of the jam.
第5回 Godot XR Community Game Jam V には98名が参加し、「Rewind(巻き戻し)」というテーマに焦点を当てた23本のゲームが提出されました。これらのプロジェクトは Godot 4 の空間アーキテクチャの限界に挑み、没入型空間における時間操作のクリエイティブな応用を示しました。以下は、このゲームジャムの上位5作品から私たちが抽出した、主要な Spatial Sync と Netcode に関するレッスンです。
1. Tactile Physics and Interaction States (Inspired by Rewind Tower)
1. 触覚的な物理挙動とインタラクション状態(Rewind Tower からのインスピレーション)
The tournament-winning entry, Rewind Tower, features a tactile tower defense system where players wind up mechanical units on a coin-operated board and rewind them to keep them in battle. In a multiplayer scenario, synchronizing physical grabs and analog spring-winding states introduces critical race conditions.
見事優勝を飾った作品 Rewind Tower は、コイン式のボード上でプレイヤーが機械ユニットのゼンマイを巻き上げ、それらを巻き戻して戦闘を維持するという、触覚的なタワーディフェンスシステムを採用しています。Multiplayer のシナリオにおいて、物理的な掴み(grab)やアナログなゼンマイ巻きの状態を同期することは、深刻な競合状態(Race Conditions)を引き起こします。
If two players attempt to grab the same unit simultaneously, a naive replication system will cause the object to rapidly teleport between the two players as they battle for network authority. To solve this, developers must implement a deterministic priority queue on the server that temporarily designates one client as the physical authority, locking out external grab inputs until the object is released.
もし2人のプレイヤーが同時に同じユニットを掴もうとした場合、単純なレプリケーションシステムでは、ネットワーク権限(Network Authority)を奪い合うことでオブジェクトが両プレイヤー間を激しく瞬間移動(テレポート)することになります。これを解決するために、開発者はサーバー上に決定論的な優先度キュー(deterministic priority queue)を実装し、オブジェクトがリリースされるまで外部からの掴み入力をロックアウトして、一時的に1つのクライアントに物理権限を割り当てる必要があります。
2. Hand-Held Analog Controls and Continuous Input Sync (Inspired by Chrono Crank)
2. 手持ちのアナログ制御と連続入力の同期(Chrono Crank からのインスピレーション)
Taking second place, Chrono Crank relies on a steampunk-style handheld device with a large physical crank used to manipulate the flow of time. Replicating physical crank rotation requires synchronizing a high-precision, continuous float value across all connected players.
第2位に輝いた Chrono Crank は、時間の流れを操作するために使用する巨大な物理クランクを備えた、スチームパンク風の手持ちデバイスを中心に構成されています。物理的なクランクの回転をレプリケートするには、接続されているすべてのプレイヤー間で高精度かつ連続的な float 値を同期しなければなりません。
Analog levers and knobs require continuous position updates rather than binary state triggers. Attempting to sync these interactive physics coordinates using high-frequency RPCs is a common pitfall that mirrors the exact issues developers face when fixing the multiplayer RPC replication issues that break world states.
アナログのレバーやノブは、バイナリの状態トリガーではなく、連続的な位置更新を必要とします。こうしたインタラクティブな物理座標を高頻度の RPC を使って同期しようとすることは、開発者がワールド状態を破壊するマルチプレイヤーのRPCレプリケーション問題を修正する際に直面する課題とまったく同じ落とし穴です。
Instead, you should synchronize the dial's localized rotation angle as a compressed float variable, interpolating the state on receiving clients using Hermite spline interpolation.
代わりに、ダイヤルのローカル回転角を圧縮された float 変数として同期し、受信側のクライアント上でエルミートスプライン補間(Hermite spline interpolation)を用いて状態を補間する必要があります。
3. Spatial Recording and Scrubbing (Inspired by NeuroCorp Training Demo)
3. 空間の記録とスクラブ操作(NeuroCorp Training Demo からのインスピレーション)
In the third-place project, NeuroCorp Training Demo, players are teleported into immersive recordings of past physical events, allowing them to scrub backward and forward through time to complete tasks. In a shared lobby, synchronization of these temporal scrub events requires a global state buffer.
第3位のプロジェクト NeuroCorp Training Demo では、プレイヤーは過去の物理的イベントの没入型記録の中にテレポートし、時間を前後にスクラブ(巻き戻し・早送り)してタスクを完了します。共有ロビーにおいて、これらの時間的スクラブイベントを同期するには、グローバルな状態バッファ(global state buffer)が必要になります。
When a player scrubs the recording, the system must synchronize the playback timestamp across all clients. Because loading an entire room's physical history frame-by-frame consumes substantial memory, the network layer must serialize frame data using index-based keyframe structures, allowing clients to interpolate locally between discrete position samples rather than streaming continuous transform frames.
プレイヤーが記録をスクラブした際、システムはすべてのクライアント間で再生タイムスタンプを同期しなければなりません。部屋全体の物理的な履歴をフレーム単位でロードすると膨大なメモリを消費するため、ネットワークレイヤーはインデックスベースのキーフレーム構造を使用してフレームデータをシリアライズする必要があります。これにより、クライアントは連続的な Transform フレームをストリーミングするのではなく、離散的な位置サンプル間でローカルに補間できるようになります。
4. Parallel Spatial Timelines (Inspired by Last Minute)
4. 並行する空間タイムライン(Last Minute からのインスピレーション)
Last Minute, which finished in fourth place, is an escape room game where players manipulate time to solve puzzles in real-time. Managing asynchronous time states inside a multiplayer game room is an architectural nightmare.
4位でフィニッシュした Last Minute は、プレイヤーがリアルタイムで時間を操作してパズルを解く脱出ゲームです。Multiplayer ゲームのルーム内で非同期の時間状態を管理することは、アーキテクチャ上の悪夢と言えます。
If Player A rewinds a puzzle box to its state 10 seconds ago while Player B is currently holding a key from that box, the server must decouple the spatial timeline of the box from the global player timeline. To achieve this, the game world must be structured into independent temporal zones, allowing individual spatial nodes to process history buffers independently while keeping active players locked to the current system tick rate.
もしプレイヤーAがパズルボックスの状態を10秒前に巻き戻している間に、プレイヤーBがそのボックスから取り出した鍵を現在持っているとすれば、サーバーはそのボックスの空間的タイムラインをグローバルなプレイヤーのタイムラインから切り離さなければなりません。これを実現するためには、ゲームワールドを独立した時間的ゾーン(temporal zones)に構造化し、アクティブなプレイヤーを現在のシステムティックレートにロックしたまま、個々の空間ノードが履歴バッファを独立して処理できるようにする必要があります。
5. Multi-Entity Prediction and Platform Synced Motion (Inspired by ScrewTheTime)
5. マルチエンティティ予測と足場に同期した移動(ScrewTheTime からのインスピレーション)
ScrewTheTime secured fifth place as a VR platformer where the player controls both their character and the temporal state of moving platforms. Synchronizing a player standing on a moving, rewinding platform in VR requires parent-child coordinate space transforms.
ScrewTheTime は、プレイヤーが自身のキャラクターと動く足場の時間状態の両方を操作するVRプラットフォーマーで、5位を獲得しました。VRにおいて、動きながら巻き戻る足場の上に立っているプレイヤーを同期させるには、親子関係の座標空間トランスフォーム(parent-child coordinate space transforms)が必要です。
If the server only synchronizes absolute global coordinates, the tiny latency variations between the platform's movement and the player's HMD updates will cause the player to slide off the platform or shake uncontrollably. To fix this, child the player's spatial origin point to the moving platform node on the client-side, translating all local head movement calculations relative to the platform's local basis before broadcasting updates to the server.
もしサーバーが絶対的なグローバル座標のみを同期する場合、足場の移動とプレイヤーのHMDの更新との間の極小のレイテンシーのブレによって、プレイヤーが足場から滑り落ちたり、制御不能なほど激しく揺れたりします。これを修正するには、クライアントサイドでプレイヤーの空間の原点(spatial origin point)を動く足場のノードの小(Child)に設定し、すべてのローカルな頭部移動の計算を足場のローカル基底(local basis)に対する相対的なものに変換した上で、サーバーへ更新情報をブロードキャストします。
Deep Dive: Building a Delta-Compressed Spatial Recording System in Godot 4
ディープダイブ:Godot 4におけるDelta-Compressed Spatial Recording Systemの構築
To build a highly responsive godot xr multiplayer experience with rewind capabilities, you must implement a spatial recording buffer that compresses tracked positions. The following production-grade GDScript 4 class records local tracking data (HMD and controllers) into a ring buffer, executes delta compression to minimize network traffic, and performs spherical linear interpolation (SLERP) for smooth playback on remote clients.
巻き戻し機能を備えた応答性の高い godot xr multiplayer 体験を構築するには、追跡された位置情報を圧縮する空間記録バッファを実装する必要があります。以下のプロダクションコード品質の GDScript 4 クラスは、ローカルのトラッキングデータ(HMDおよびコントローラー)をリングバッファに記録し、ネットワークトラフィックを最小限に抑えるためのデルタ圧縮を実行し、リモートクライアント上でスムーズな再生を行うための球面線形補間(SLERP)を行います。
# 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
This system ensures that even if you drop frames or experience network latency variance, you can reconstruct past transforms accurately. Remote clients can seamlessly interpolate historical tracking states, keeping VR hand gestures and interactions looking incredibly fluid.
このシステムにより、フレーム落ちやネットワークのレイテンシー変動が発生した場合でも、過去のトランスフォームを正確に再構築できます。リモートクライアントは過去のトラッキング状態をシームレスに補間できるため、VRの手のジェスチャーやインタラクションを非常に滑らかに表示し続けることができます。
Implementing Time-Rewind in Multiplayer: The Manual Nightmare
MultiplayerにおけるTime-Rewindの実装:手動構築という悪夢
If you choose to write this entire system by hand, you face significant infrastructure overhead. You must set up room orchestrators, host custom authoritative server instances in Linux environments, and configure raw UDP sockets via ENetMultiplayerPeer. To avoid player disconnects on home networks, you must build custom STUN/TURN traversal servers to bypass Carrier-Grade NAT (CGNAT) firewalls.
このシステム全体を自前で実装しようとすると、莫大なインフラ構築コストに直面することになります。ルームのオーケストレーターをセットアップし、Linux環境でカスタムの Authoritative(オーソリテイティブ)サーバーインスタンスをホストし、ENetMultiplayerPeer を介して生の UDP ソケットを設定しなければなりません。また、家庭内ネットワークでの接続切断を防ぐために、Carrier-Grade NAT(CGNAT)ファイアウォールを回避するカスタムの STUN/TURN トラバーサルサーバーを構築する必要があります。
Additionally, maintaining a persistent global state database that can process thousands of high-frequency records per second requires specialized clustering configurations. You will spend 4 to 6 weeks of dedicated engineering time just writing backend infrastructure rather than designing your game.
さらに、1秒間に数千もの高頻度レコードを処理できる永続的なグローバル状態データベースを維持するには、特別なクラスタリング設定が必要です。ゲームのデザインに集中する代わりに、バックエンドインフラの記述だけで4〜6週間の開発期間を費やすことになります。
Building this yourself requires setting up load balancers, database sharding, and SSL cert management — easily 4-6 weeks of work. With horizOn, these backend services come pre-configured, letting you ship your game instead of your infrastructure.
自作する場合は、Load Balancing(ロードバランサーの設定)、Database Sharding(データベースのシャード分割)、および SSL 証明書の管理などが必要になり、軽く4〜6週間はかかります。horizOn を使えば、これらの Backend サービスがあらかじめ構成された状態で提供されるため、インフラ開発ではなくゲームのリリースに集中できます。
Simplifying Godot XR Multiplayer with horizOn
horizOn による Godot XR Multiplayer の簡素化
Instead of wasting development cycles on complex low-level networking and server provisioning, you can integrate horizOn directly into your project. The platform's real-time room engine provides optimized WebRTC and low-latency UDP pipelines designed explicitly for high-frequency VR data serialization.
複雑な低レベルのネットワーキングやサーバープロビジョニングに開発サイクルを無駄にする代わりに、horizOn を直接プロジェクトに統合することができます。このプラットフォームのリアルタイムルームエンジンは、高頻度の VR データシリアライズ専用に設計された、最適化された WebRTC および低レイテンシーの UDP パイプラインを提供します。
By leveraging horizOn's pre-configured room structures, your game can automatically pair users based on regional latency metrics, keeping spatial synchronizations tight. Because all backend routing, security tokens, and player matchmaking are managed via a simple SDK, you can deploy a scalable, multi-region VR experience in minutes.
horizOn の事前設定済みルーム構造を活用することで、ゲームは地域のレイテンシー指標に基づいてユーザーを自動的にペアリングし、空間同期(Spatial Sync)を強固に保ちます。すべての Backend ルーティング、セキュリティトークン、プレイヤーの Matchmaking(マッチメイキング)がシンプルな SDK を通じて管理されるため、スケーラブルなマルチリージョン VR 体験をわずか数分でデプロイできます。
4 Netcode Best Practices for Godot XR Multiplayer
Godot XR Multiplayerにおける4つのNetcodeベストプラクティス
Applying proper netcode conventions ensures that your game performs cleanly regardless of individual client network quality. Incorporate these four techniques into your next spatial layout:
適切な Netcode の規約を適用することで、個々のクライアントのネットワーク品質に関係なく、ゲームのパフォーマンスをクリーンに保つことができます。次の空間レイアウト設計に、これら4つの手法を取り入れてみてください。
- Decouple Network Tick Rate from Physics Frame Rate: Never lock your network replication tick rate directly to the client's HMD refresh rate. Run your game logic and transform synchronization at a stable 30Hz or 45Hz, and use client-side linear and spherical linear interpolation to reconstruct the native 90Hz/120Hz frames locally.
- Network Tick RateをPhysics Frame Rateからデカップルする:ネットワークレプリケーションのティックレートを、クライアントの HMD リフレッシュレートに直接ロックしてはいけません。ゲームロジックとトランスフォームの同期は安定した30Hzまたは45Hzで実行し、クライアントサイドの線形補間および球面線形補間(SLERP)を使用して、ネイティブの90Hz/120Hzフレームをローカルで再構築します。
- Prioritize HMD Tracking Updates Over Controllers: Head tracking is the primary contributor to VR simulator sickness. Allocate higher network priority and larger packet bandwidth slices to the user's camera transform, while applying heavier delta compression and lower priority limits to hand transforms.
- コントローラーよりもHMDトラッキングの更新を優先する:ヘッドトラッキングは、VR酔い(シミュレータ酔い)を引き起こす最大の要因です。ユーザーのカメラのトランスフォームに、より高いネットワーク優先度と大きなパケット帯域幅スライスを割り当てる一方で、手のトランスフォームにはより強力なデルタ圧縮と低い優先度制限を適用します。
- Enforce Distance and Orientation Thresholds: Do not broadcast packets if a player's spatial changes are negligible. Set up explicit delta-compression guard bands (e.g., 5mm of position movement or 0.02 radians of rotation) to eliminate idle updates and conserve bandwidth.
- 距離と向きのしきい値を強制する:プレイヤーの空間的な変化がごくわずかである場合は、パケットをブロードキャストしないでください。明示的なデルタ圧縮ガードバンド(例:位置移動5mm、または回転0.02ラジアン)を設定して無駄な更新を排除し、帯域幅を節約します。
- Employ Client-Side Authority for Physical Grabs: When a player grabs an interactive object, instantly switch the node's network authority to the grabbing client. This prevents local lag from creating a visible separation between their controller and the held item, while allowing the server to asynchronously audit final object placement.
- 物理的な掴みアクションにClient-Side Authority(クライアント権限)を採用する:プレイヤーがインタラクティブなオブジェクトを掴んだ際、ノードのネットワーク権限(Network Authority)を即座に掴んだ側のクライアントへと切り替えます。これにより、ローカルの遅延によってコントローラーと保持されたアイテムの間に目に見える乖離が生じるのを防ぎつつ、サーバーが非同期でオブジェクトの最終的な配置を監査できるようにします。
Next Steps: Ship Your XR Multiplayer Experience
次のステップ:あなたのXR Multiplayerエクスペリエンスをリリースしよう
Developing high-frequency multiplayer environments in VR requires a careful balance of visual stability and strict bandwidth management. By incorporating delta compression, local historical buffering, and client-side authority, you can build immersive environments that keep players stable and connected.
VRで高頻度の Multiplayer 環境を開発するには、視覚的な安定性と厳密な帯域幅管理の慎重なバランスが求められます。デルタ圧縮、ローカル履歴バッファリング、Client-Side Authorityを取り入れることで、プレイヤーの接続性を保ち、酔いのない没入型環境を構築できます。
Ready to scale your multiplayer backend? Try horizOn for free or check out the API docs.
マルチプレイヤーの Backend をスケールさせる準備はできましたか?無料で horizOn を試すか、API docs をご確認ください。
Source: Godot XR Community Game Jam V