Multiplayer Overzicht
Architectuur en concepten voor multiplayer functionaliteit.
Architectuur
┌─────────────────────────────────────────────────────────────────┐
│ Multiplayer Architectuur │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Client 1 │ │ Client 2 │ │
│ │ (Godot) │ │ (Godot) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ │ WebSocket │ WebSocket │
│ │ │ │
│ └────────────┬───────────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ SpacetimeDB │ │
│ │ Server │ │
│ │ (Rust WASM) │ │
│ └───────────────┘ │
│ │
│ Data Flow: │
│ 1. Client stuurt reducer call │
│ 2. Server valideert & update database │
│ 3. Server broadcast changes naar subscribed clients │
│ 4. Clients ontvangen updates via callbacks │
│ │
└─────────────────────────────────────────────────────────────────┘
SpacetimeDB Integratie
Verbinding Maken
# spacetime_client.gd
extends Node
signal connected()
signal disconnected()
signal connection_error(error: String)
const SERVER_URL = "wss://mainnet.spacetimedb.com"
const MODULE_NAME = "milenas_treehouse"
var _client: SpacetimeDBClient = null
var _identity: String = ""
var is_connected: bool = false
func _ready() -> void:
_client = SpacetimeDBClient.new()
add_child(_client)
_client.on_connect.connect(_on_connected)
_client.on_disconnect.connect(_on_disconnected)
_client.on_error.connect(_on_error)
func connect_to_server() -> void:
var token = _load_auth_token()
_client.connect_to_db(
SERVER_URL,
MODULE_NAME,
token
)
func _on_connected(identity: String, token: String) -> void:
_identity = identity
is_connected = true
_save_auth_token(token)
_setup_subscriptions()
connected.emit()
func _on_disconnected() -> void:
is_connected = false
disconnected.emit()
Subscriptions
func _setup_subscriptions() -> void:
# Subscribe naar relevante tabellen
_client.subscribe([
"SELECT * FROM Player WHERE identity = :identity",
"SELECT * FROM PlayerCharacter WHERE player_id = :player_id",
"SELECT * FROM InventoryItem WHERE player_id = :player_id",
"SELECT * FROM OnlinePlayer", # Alle online spelers
"SELECT * FROM ChatMessage WHERE timestamp > :last_seen",
"SELECT * FROM FriendRequest WHERE target_id = :player_id"
])
Online Players
Tracking
# online_players_manager.gd
extends Node
signal player_joined(player_data: Dictionary)
signal player_left(player_id: int)
signal player_updated(player_id: int, data: Dictionary)
var online_players: Dictionary = {} # player_id -> player_data
func _ready() -> void:
SpacetimeDB.OnlinePlayer.on_insert.connect(_on_player_online)
SpacetimeDB.OnlinePlayer.on_delete.connect(_on_player_offline)
SpacetimeDB.OnlinePlayer.on_update.connect(_on_player_update)
func _on_player_online(row: OnlinePlayer) -> void:
# Skip self
if row.player_id == GameState.player_id:
return
var player_data = {
"player_id": row.player_id,
"display_name": row.display_name,
"current_scene": row.current_scene,
"position": Vector2(row.position_x, row.position_y),
"character_data": _parse_character_data(row.character_data)
}
online_players[row.player_id] = player_data
player_joined.emit(player_data)
func _on_player_offline(row: OnlinePlayer) -> void:
if row.player_id in online_players:
online_players.erase(row.player_id)
player_left.emit(row.player_id)
func _on_player_update(old_row: OnlinePlayer, new_row: OnlinePlayer) -> void:
if new_row.player_id not in online_players:
return
var player_data = online_players[new_row.player_id]
player_data.current_scene = new_row.current_scene
player_data.position = Vector2(new_row.position_x, new_row.position_y)
player_updated.emit(new_row.player_id, player_data)
Remote Player Instantiation
# scene_manager.gd
func _on_player_joined(player_data: Dictionary) -> void:
# Only spawn if same scene
if player_data.current_scene != current_scene_name:
return
_spawn_remote_player(player_data)
func _spawn_remote_player(player_data: Dictionary) -> void:
var remote_player = REMOTE_PLAYER_SCENE.instantiate()
remote_player.player_id = player_data.player_id
remote_player.display_name = player_data.display_name
remote_player.position = player_data.position
remote_player.setup_character(player_data.character_data)
_remote_players[player_data.player_id] = remote_player
players_container.add_child(remote_player)
Multiplayer Features
Gedeelde Wereld
┌─────────────────────────────────────────────────────────────┐
│ Shared World Data │
├─────────────────────────────────────────────────────────────┤
│ │
│ Persistent (Database): Real-time (Subscriptions): │
│ ├── Player inventories ├── Player positions │
│ ├── Farm plots ├── Player animations │
│ ├── Club data ├── Chat messages │
│ ├── Quest progress ├── Emotes/reactions │
│ └── Pet data └── Activity indicators │
│ │
└─────────────────────────────────────────────────────────────┘
Club System
# Spelers kunnen clubs joinen en samen spelen
func get_club_members_online() -> Array:
var members = []
var club = ClubManager.get_current_club()
if club.is_empty():
return members
for player_id in online_players:
var player = online_players[player_id]
if player.club_id == club.id:
members.append(player)
return members
func teleport_to_club_member(player_id: int) -> void:
if player_id not in online_players:
return
var target = online_players[player_id]
SceneManager.change_scene(target.current_scene, target.position)
Samen Activiteiten
# Farming samen
func can_help_farm(plot: FarmPlot) -> bool:
# Andere spelers kunnen helpen met water geven
return plot.owner_id != GameState.player_id and not plot.is_watered
func help_water_plot(plot: FarmPlot) -> void:
SpacetimeDB.call_reducer("help_water_farm_plot", [
plot.owner_id,
plot.plot_index
])
# Geeft friendship bonus aan beide spelers
Connection States
enum ConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED,
RECONNECTING,
ERROR
}
var connection_state: ConnectionState = ConnectionState.DISCONNECTED
func _update_connection_ui() -> void:
match connection_state:
ConnectionState.DISCONNECTED:
connection_indicator.modulate = Color.GRAY
connection_label.text = "Offline"
ConnectionState.CONNECTING:
connection_indicator.modulate = Color.YELLOW
connection_label.text = "Verbinden..."
ConnectionState.CONNECTED:
connection_indicator.modulate = Color.GREEN
connection_label.text = "Online"
ConnectionState.RECONNECTING:
connection_indicator.modulate = Color.ORANGE
connection_label.text = "Opnieuw verbinden..."
ConnectionState.ERROR:
connection_indicator.modulate = Color.RED
connection_label.text = "Verbindingsfout"
Reconnection Logic
const MAX_RECONNECT_ATTEMPTS = 5
const RECONNECT_DELAY = 2.0
var reconnect_attempts: int = 0
func _on_disconnected() -> void:
connection_state = ConnectionState.RECONNECTING
_attempt_reconnect()
func _attempt_reconnect() -> void:
if reconnect_attempts >= MAX_RECONNECT_ATTEMPTS:
connection_state = ConnectionState.ERROR
_show_reconnect_dialog()
return
reconnect_attempts += 1
await get_tree().create_timer(RECONNECT_DELAY * reconnect_attempts).timeout
connect_to_server()
func _on_connected(identity: String, token: String) -> void:
reconnect_attempts = 0
connection_state = ConnectionState.CONNECTED
# ...
Offline Mode
# Beperkte functionaliteit wanneer offline
func is_feature_available(feature: String) -> bool:
if not SpacetimeClient.is_connected:
match feature:
"chat": return false
"friends": return false
"clubs": return false
"trading": return false
"farm": return true # Lokale cache
"inventory": return true # Lokale cache
_: return true
return true
func _show_offline_notice(feature: String) -> void:
NotificationManager.toast(
"Je moet online zijn voor deze functie",
"warning"
)