Ga naar hoofdinhoud

SpacetimeDB SDK

Integratie met SpacetimeDB voor real-time multiplayer.

Overzicht

De game gebruikt de SpacetimeDB Godot SDK (addon) voor:

  • Real-time synchronisatie tussen spelers
  • Persistente game state
  • Server-side game logica (reducers)
  • Automatisch gegenereerde type bindings

Addon Structuur

addons/SpacetimeDB/
├── core/
│ ├── spacetimedb_client.gd # Hoofd client class
│ ├── spacetimedb_connection.gd # WebSocket verbinding
│ ├── bsatn_serializer.gd # Data serialisatie
│ ├── bsatn_deserializer.gd # Data deserialisatie
│ ├── module_table.gd # Tabel interface
│ └── local_database.gd # Lokale cache
├── codegen/
│ ├── codegen.gd # Binding generator
│ └── schema_parser.gd # Schema parsing
└── nodes/
└── row_receiver/ # Data receiver nodes

Verbinding Maken

Basis Setup

extends Node

var client: SpacetimeDBClient

func _ready() -> void:
client = SpacetimeDBClient.new()
add_child(client)

# Event handlers
client.connected.connect(_on_connected)
client.disconnected.connect(_on_disconnected)
client.identity_received.connect(_on_identity)

# Verbinden
client.connect_to_server(
"ws://localhost:3000", # Server URL
"milenas-treehouse" # Module naam
)

func _on_connected() -> void:
print("Verbonden met SpacetimeDB!")

func _on_identity(identity: String) -> void:
print("Mijn identity: ", identity)

Database Tabellen

Subscriben op Data

func _on_connected() -> void:
# Subscribe op tabellen
client.subscribe([
"SELECT * FROM player",
"SELECT * FROM player_online",
"SELECT * FROM player_position",
"SELECT * FROM chat_message"
])

# Callback voor initiële data
client.initial_subscription_applied.connect(_on_data_loaded)

Lezen van Tabellen

# Alle rijen itereren
for player in client.db.player.iter():
print(player.username)

# Zoeken op primary key
var player = client.db.player.identity().find(my_identity)

# Zoeken op unique column
var player = client.db.player.username().find("milena123")

Updates Ontvangen

func _setup_listeners() -> void:
var db = client.db

# Nieuwe rij toegevoegd
db.player_online.subscribe_to_inserts(&"PlayerOnline", _on_player_joined)

# Rij gewijzigd
db.player_position.subscribe_to_updates(&"PlayerPosition", _on_player_moved)

# Rij verwijderd
db.player_online.subscribe_to_deletes(&"PlayerOnline", _on_player_left)

func _on_player_joined(player_online) -> void:
print("Speler online: ", player_online.identity)

func _on_player_moved(old_pos, new_pos) -> void:
print("Speler bewoog naar: ", new_pos.x, ", ", new_pos.y)

func _on_player_left(player_online) -> void:
print("Speler offline: ", player_online.identity)

Reducers Aanroepen

Reducers zijn server-side functies:

# Registreren
client.reducers.register_player_with_pin("milena", "Milena", "123456")

# Positie updaten
client.reducers.update_position("game", x, y, direction, is_moving)

# Chat bericht sturen
client.reducers.send_chat("Hallo allemaal!", "global", null)

# Item verplaatsen
client.reducers.move_item(from_container, from_slot, to_container, to_slot, 1)

Reducer Resultaten

# Luisteren naar reducer resultaten
client.reducer_callback.connect(_on_reducer_result)

func _on_reducer_result(reducer_name: String, status: String, message: String) -> void:
if status == "failed":
print("Reducer ", reducer_name, " mislukt: ", message)
else:
print("Reducer ", reducer_name, " succesvol")

Inventory Manager Voorbeeld

Zie scripts/inventory/inventory_manager.gd:

class_name InventoryManager extends Node

signal containers_changed
signal resources_changed

var _client = null
var _player_identity: String = ""
var _containers: Array = []
var _resources: Dictionary = {}

func setup(client, player_identity: String) -> void:
_client = client
_player_identity = player_identity
_connect_to_db_updates()

func _connect_to_db_updates() -> void:
var db = _client.db

# Container updates
db.player_container.subscribe_to_inserts(&"Container", _on_container_inserted)
db.player_container.subscribe_to_updates(&"Container", _on_container_updated)

# Resource updates
db.player_resource.subscribe_to_inserts(&"Resource", _on_resource_inserted)

func load_inventory() -> void:
_containers.clear()
for container in _client.db.player_container.iter():
if str(container.owner) == _player_identity:
_containers.append(container)
containers_changed.emit()

Auto-Generated Bindings

De SDK genereert automatisch GDScript classes voor alle tabellen:

spacetime_bindings/schema/types/
├── milenas_treehouse_v_2_player.gd
├── milenas_treehouse_v_2_player_container.gd
├── milenas_treehouse_v_2_container_item.gd
├── milenas_treehouse_v_2_item_definition.gd
└── ...

Bindings Genereren

# Na wijzigingen aan server schema
cd server
spacetime build
spacetime generate --lang gdscript --out-dir ../spacetime_bindings

Offline Modus

Als SpacetimeDB niet beschikbaar is:

var is_offline: bool = false

func _on_connection_failed() -> void:
is_offline = true
# Fallback naar lokale opslag
_load_from_local_storage()

func _save_locally() -> void:
var file = FileAccess.open("user://save.json", FileAccess.WRITE)
file.store_string(JSON.stringify(game_data))

Best Practices

1. Reducer Validatie

Alle business logica zit in reducers - de client stuurt alleen requests:

# Goed: Server valideert
client.reducers.move_item(from, to, 1)

# Fout: Client valideert (kan worden omzeild)
if _has_item(from): # Niet doen!
_move_item_locally(from, to)

2. Subscribe Filtering

Alleen data ophalen die je nodig hebt:

# Goed: Gefilterd
client.subscribe([
"SELECT * FROM player_position WHERE scene = 'game'"
])

# Minder goed: Alles ophalen
client.subscribe([
"SELECT * FROM player_position"
])

3. Optimistische Updates

Voor snellere UI feedback:

func move_item(from: int, to: int) -> void:
# Lokaal direct updaten (optimistisch)
_update_ui_immediately(from, to)

# Server call
client.reducers.move_item(from, to, 1)

# Bij fout: rollback (via update callback)

Debugging

Logs Bekijken

# Server logs
spacetime logs milenas-treehouse -f

# In Godot: Debug print
print(client.db.player.iter().size(), " players in database")

Connection Status

func _process(_delta) -> void:
if client.is_connected():
connection_label.text = "Online"
else:
connection_label.text = "Offline"

Volgende Stappen