Synchronisatie
Data synchronisatie tussen client en server.
Overzicht
┌─────────────────────────────────────────────────────────────────┐
│ Sync Architectuur │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Client Server │
│ ────── ────── │
│ │
│ Local State Authoritative State │
│ ├── Optimistic Updates ├── Validation │
│ ├── Prediction ├── Persistence │
│ └── Rollback └── Broadcast │
│ │
│ Flow: │
│ 1. Client: Optimistic update (instant feedback) │
│ 2. Client: Send reducer call │
│ 3. Server: Validate & process │
│ 4. Server: Broadcast result │
│ 5. Client: Confirm or rollback │
│ │
└─────────────────────────────────────────────────────────────────┘
Position Sync
Player Position Broadcasting
# position_sync.gd
extends Node
const SYNC_INTERVAL: float = 0.1 # 100ms
const MIN_MOVE_DISTANCE: float = 5.0
var _sync_timer: float = 0.0
var _last_synced_position: Vector2 = Vector2.ZERO
var _last_synced_scene: String = ""
func _process(delta: float) -> void:
if not SpacetimeClient.is_connected:
return
_sync_timer += delta
if _sync_timer >= SYNC_INTERVAL:
_sync_timer = 0.0
_check_position_sync()
func _check_position_sync() -> void:
var current_pos = GameState.player_position
var current_scene = SceneManager.current_scene_name
# Scene changed?
if current_scene != _last_synced_scene:
_sync_position(current_pos, current_scene)
return
# Moved enough?
if current_pos.distance_to(_last_synced_position) >= MIN_MOVE_DISTANCE:
_sync_position(current_pos, current_scene)
func _sync_position(pos: Vector2, scene: String) -> void:
_last_synced_position = pos
_last_synced_scene = scene
SpacetimeDB.call_reducer("update_player_position", [
int(pos.x),
int(pos.y),
scene
])
Remote Player Interpolation
# remote_player.gd
extends Node2D
const INTERPOLATION_SPEED: float = 10.0
var target_position: Vector2 = Vector2.ZERO
var player_id: int = 0
func _process(delta: float) -> void:
# Smooth interpolation naar target position
position = position.lerp(target_position, INTERPOLATION_SPEED * delta)
# Animation based on movement
var velocity = target_position - position
if velocity.length() > 1.0:
_play_walk_animation(velocity.normalized())
else:
_play_idle_animation()
func update_position(new_pos: Vector2) -> void:
target_position = new_pos
func _play_walk_animation(direction: Vector2) -> void:
animated_sprite.play("walk")
animated_sprite.flip_h = direction.x < 0
Inventory Sync
Optimistic Updates
# inventory_manager.gd
func add_item(item_id: String, quantity: int) -> int:
# 1. Optimistic local update
var added = _add_item_local(item_id, quantity)
if added > 0:
# 2. Sync to server
SpacetimeDB.call_reducer("add_inventory_item", [item_id, added])
return added
func remove_item(item_id: String, quantity: int) -> bool:
# 1. Check locally
if not has_item(item_id, quantity):
return false
# 2. Optimistic removal
_remove_item_local(item_id, quantity)
# 3. Sync to server
SpacetimeDB.call_reducer("remove_inventory_item", [item_id, quantity])
return true
Server Confirmation
func _ready() -> void:
# Listen voor server updates
SpacetimeDB.InventoryItem.on_insert.connect(_on_item_inserted)
SpacetimeDB.InventoryItem.on_update.connect(_on_item_updated)
SpacetimeDB.InventoryItem.on_delete.connect(_on_item_deleted)
func _on_item_inserted(row: InventoryItem) -> void:
if row.player_id != GameState.player_id:
return
# Server bevestigt: update lokale state
var local_item = _find_local_item(row.item_id, row.container_id, row.slot_index)
if not local_item:
# Nieuw item van server (bijv. multiplayer gift)
_add_item_from_server(row)
NotificationManager.toast("Item ontvangen!", "success")
func _on_item_deleted(row: InventoryItem) -> void:
if row.player_id != GameState.player_id:
return
# Server bevestigt removal
_confirm_item_removal(row.item_id, row.container_id, row.slot_index)
Conflict Resolution
# Bij sync conflicten: server wins
func _reconcile_inventory() -> void:
# Haal complete inventory op van server
var server_items = SpacetimeDB.InventoryItem.filter_by_player_id(GameState.player_id)
# Reset lokale state
_clear_local_inventory()
# Rebuild from server
for item in server_items:
_add_item_from_server(item)
NotificationManager.toast("Inventory gesynchroniseerd", "info")
Farm Sync
Plot State Sync
# farm_manager.gd
func plant_seed(plot_index: int, seed_id: String) -> void:
# Optimistic update
var plot = _get_plot(plot_index)
plot.seed_id = seed_id
plot.growth_stage = 0
plot.planted_timestamp = Time.get_unix_time_from_system()
plot.update_visuals()
# Server sync
SpacetimeDB.call_reducer("plant_seed", [plot_index, seed_id])
func _on_farm_plot_updated(old: FarmPlotRow, new: FarmPlotRow) -> void:
if new.player_id != GameState.player_id:
return
var plot = _get_plot(new.plot_index)
# Update from server (handles multi-device sync)
if new.growth_stage > plot.growth_stage:
plot.growth_stage = new.growth_stage
plot.update_visuals()
plot.play_growth_effect()
Real-time Growth Updates
# Growth wordt server-side berekend
func _ready() -> void:
SpacetimeDB.FarmPlot.on_update.connect(_on_farm_plot_updated)
# Periodic check voor growth updates
var timer = Timer.new()
timer.wait_time = 30.0
timer.autostart = true
timer.timeout.connect(_request_growth_check)
add_child(timer)
func _request_growth_check() -> void:
SpacetimeDB.call_reducer("check_farm_growth", [])
Quest Sync
# quest_manager.gd
func _on_quest_progress_updated(old: PlayerQuestRow, new: PlayerQuestRow) -> void:
if new.player_id != GameState.player_id:
return
var quest_id = new.quest_id
# Update local state
if quest_id in active_quests:
var quest = active_quests[quest_id]
var old_progress = quest.objectives_progress.duplicate()
quest.objectives_progress = JSON.parse_string(new.objectives_json)
quest.status = new.status
# Check for new completions
for obj_id in quest.objectives_progress:
var old_val = old_progress.get(obj_id, 0)
var new_val = quest.objectives_progress[obj_id]
if new_val > old_val:
quest_progress.emit(quest_id, obj_id, new_val, _get_objective_target(quest_id, obj_id))
# Quest completed?
if new.status == "completed" and active_quests.has(quest_id):
_handle_quest_completed(quest_id)
Subscription Management
Efficient Subscriptions
# Alleen subscriben op relevante data
func _update_subscriptions_for_scene(scene_name: String) -> void:
# Unsubscribe van vorige scene data
_client.unsubscribe(_current_scene_subscription)
# Subscribe op nieuwe scene data
match scene_name:
"treehouse":
_current_scene_subscription = _client.subscribe([
"SELECT * FROM OnlinePlayer WHERE current_scene = 'treehouse'",
"SELECT * FROM FarmPlot WHERE player_id = :player_id"
])
"world_map":
_current_scene_subscription = _client.subscribe([
"SELECT * FROM OnlinePlayer WHERE current_scene = 'world_map'"
])
"club_hall":
_current_scene_subscription = _client.subscribe([
"SELECT * FROM OnlinePlayer WHERE club_id = :club_id",
"SELECT * FROM ClubProject WHERE club_id = :club_id"
])
Subscription Batching
# Batch multiple subscriptions
func setup_initial_subscriptions() -> void:
_client.subscribe([
# Player data
"SELECT * FROM Player WHERE identity = :identity",
"SELECT * FROM PlayerCharacter WHERE player_id = :player_id",
# Inventory
"SELECT * FROM Container WHERE player_id = :player_id",
"SELECT * FROM InventoryItem WHERE player_id = :player_id",
# Social
"SELECT * FROM Friend WHERE player_id = :player_id OR friend_id = :player_id",
"SELECT * FROM FriendRequest WHERE target_id = :player_id",
# Quests
"SELECT * FROM PlayerQuest WHERE player_id = :player_id AND status = 'active'",
# Chat (recent only)
"SELECT * FROM ChatMessage WHERE timestamp > :recent_timestamp ORDER BY timestamp DESC LIMIT 100"
])
Delta Compression
# Alleen wijzigingen sturen, niet volledige state
func _sync_character_appearance(changes: Dictionary) -> void:
if changes.is_empty():
return
# Stuur alleen gewijzigde velden
SpacetimeDB.call_reducer("update_character_appearance", [
JSON.stringify(changes)
])
# Voorbeeld: alleen haar kleur veranderd
# changes = {"hair_color": "#FF5500"}
# In plaats van hele character data
Latency Compensation
# Timestamp-based ordering
func _on_chat_message_received(row: ChatMessage) -> void:
# Insert op juiste positie gebaseerd op server timestamp
var insert_index = _find_insert_index(row.timestamp)
_chat_messages.insert(insert_index, row)
_update_chat_ui()
func _find_insert_index(timestamp: int) -> int:
for i in range(_chat_messages.size() - 1, -1, -1):
if _chat_messages[i].timestamp <= timestamp:
return i + 1
return 0