What This Guide Covers
This guide shows a persistent multiplayer game loop:- Load player save data on join
- Keep inventory, quest progress, and level in persistent storage
- Auto-save while players are connected
- Award and spend credits with
Crafty.economy
Input Actions Used
Configure actions in your project/manifest:quest_completebuy_potion
Complete Game Script (quest_arena.gd)
Copy
extends CraftyGame
const MOVE_SPEED := 7.0
const GRAVITY := 20.0
const AUTO_SAVE_INTERVAL := 15.0
const QUEST_XP_REWARD := 25
const QUEST_CREDIT_REWARD := 10
const POTION_COST := 8
var _auto_save_accum := 0.0
func get_prediction_params() -> Dictionary:
return {"move_speed": MOVE_SPEED, "gravity": GRAVITY}
func _game_init() -> void:
var spawn_root := get_node_or_null("SpawnPoints")
if spawn_root:
for child in spawn_root.get_children():
if child is Node3D:
spawn_points.append(child.global_position)
if spawn_points.is_empty():
spawn_points = [
Vector3(-6, 1, -6),
Vector3(6, 1, -6),
Vector3(-6, 1, 6),
Vector3(6, 1, 6)
]
func _game_start() -> void:
Crafty.set_time_limit(1800.0)
Crafty.send_announcement("Quest Arena started")
func _game_end() -> void:
for p in get_players():
_save_player_snapshot(p)
Crafty.send_announcement("Quest Arena session saved")
func _player_joined(player: CraftyPlayer) -> void:
player.respawn(get_random_spawn_point())
_load_player_snapshot(player)
_sync_player_hud_fields(player)
func _player_left(player: CraftyPlayer) -> void:
_save_player_snapshot(player)
func _process(delta: float) -> void:
super._process(delta)
if not Crafty.is_server():
return
for p in get_players():
apply_default_movement(p, delta, MOVE_SPEED, GRAVITY)
_handle_player_actions(p)
_auto_save_accum += delta
if _auto_save_accum >= AUTO_SAVE_INTERVAL:
_auto_save_accum = 0.0
for p in get_players():
_save_player_snapshot(p)
func _handle_player_actions(player: CraftyPlayer) -> void:
if player.input.is_action_just_pressed("quest_complete"):
_complete_quest(player)
if player.input.is_action_just_pressed("buy_potion"):
_buy_potion(player)
func _complete_quest(player: CraftyPlayer) -> void:
var progress := Crafty.data.load(player, "quest_progress")
if progress == null:
progress = {"main": 0}
progress["main"] = int(progress.get("main", 0)) + 1
Crafty.data.save(player, "quest_progress", progress)
var xp := int(Crafty.data.load(player, "xp") if Crafty.data.load(player, "xp") != null else 0)
xp += QUEST_XP_REWARD
Crafty.data.save(player, "xp", xp)
var level := 1 + int(xp / 100)
Crafty.data.save(player, "level", level)
var awarded := await Crafty.economy.award(player, QUEST_CREDIT_REWARD, "quest_complete")
if awarded:
Crafty.send_announcement("%s completed a quest (+%d credits)" % [player.display_name, QUEST_CREDIT_REWARD])
_sync_player_hud_fields(player)
func _buy_potion(player: CraftyPlayer) -> void:
var purchased := await Crafty.economy.spend(player, POTION_COST, "potion_purchase")
if not purchased:
Crafty.send_announcement("%s cannot afford a potion" % player.display_name)
return
var inventory := Crafty.data.load(player, "inventory")
if inventory == null:
inventory = {"potions": 0}
inventory["potions"] = int(inventory.get("potions", 0)) + 1
Crafty.data.save(player, "inventory", inventory)
Crafty.send_announcement("%s bought a potion" % player.display_name)
_sync_player_hud_fields(player)
func _load_player_snapshot(player: CraftyPlayer) -> void:
var level = Crafty.data.load(player, "level")
if level == null:
level = 1
var xp = Crafty.data.load(player, "xp")
if xp == null:
xp = 0
var inventory = Crafty.data.load(player, "inventory")
if inventory == null:
inventory = {"potions": 0}
var quest_progress = Crafty.data.load(player, "quest_progress")
if quest_progress == null:
quest_progress = {"main": 0}
Crafty.data.save(player, "level", level)
Crafty.data.save(player, "xp", xp)
Crafty.data.save(player, "inventory", inventory)
Crafty.data.save(player, "quest_progress", quest_progress)
func _save_player_snapshot(player: CraftyPlayer) -> void:
# Data API is write-through to in-memory cache; backend flush is handled by SDK.
var level = Crafty.data.load(player, "level")
var xp = Crafty.data.load(player, "xp")
var inventory = Crafty.data.load(player, "inventory")
var quest_progress = Crafty.data.load(player, "quest_progress")
Crafty.data.save(player, "level", 1 if level == null else level)
Crafty.data.save(player, "xp", 0 if xp == null else xp)
Crafty.data.save(player, "inventory", {"potions": 0} if inventory == null else inventory)
Crafty.data.save(player, "quest_progress", {"main": 0} if quest_progress == null else quest_progress)
func _sync_player_hud_fields(player: CraftyPlayer) -> void:
var level := int(Crafty.data.load(player, "level"))
var xp := int(Crafty.data.load(player, "xp"))
var inventory := Crafty.data.load(player, "inventory")
var quest_progress := Crafty.data.load(player, "quest_progress")
var balance := await Crafty.economy.get_balance(player)
player.set_synced("level", level)
player.set_synced("xp", xp)
player.set_synced("potions", int(inventory.get("potions", 0)))
player.set_synced("quest_main", int(quest_progress.get("main", 0)))
player.set_synced("credits", balance)
Suggested manifest.json
Copy
{
"id": "quest-arena",
"name": "Quest Arena",
"version": "1.0.0",
"crafty_sdk": "1.0",
"entry_scene": "quest_arena.tscn",
"player_scene": "player.tscn",
"min_players": 1,
"max_players": 12,
"tick_rate": 60,
"description": "Persistent quest progression with economy rewards.",
"tags": ["rpg", "quest", "persistence"],
"input_actions": {
"quest_complete": {"keyboard": "Q", "gamepad": "Y"},
"buy_potion": {"keyboard": "B", "gamepad": "X"}
}
}

