Skip to main content

What You Build

A minimal multiplayer integration that:
  • extends CraftyGame,
  • uses server-authoritative gameplay updates,
  • syncs player-visible state with set_synced,
  • passes local testing and submission pipeline.

Step 1: Create manifest.json

{
  "id": "first-crafty-game",
  "name": "First Crafty Game",
  "version": "1.0.0",
  "crafty_sdk": "1.0",
  "entry_scene": "main.tscn",
  "player_scene": "player.tscn",
  "min_players": 1,
  "max_players": 8
}

Step 2: Choose Your Player Path

3D Action Template (fastest start)

  • set player.tscn root script to res://sdk/crafty_character_3d.gd
  • use apply_default_movement(...) from CraftyGame.

Custom Genre Path (2D/card/RTS/puzzle)

  • make your own script extending CraftyPlayer
  • sync your own state model (set_synced("hand_count", ...), etc.).

Step 3: Build main.gd

extends CraftyGame

const ROUND_SECONDS := 120.0
const MOVE_SPEED := 7.0
const GRAVITY := 20.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)

func _game_start() -> void:
    Crafty.set_time_limit(ROUND_SECONDS)
    Crafty.send_announcement("Round started")

func _game_end() -> void:
    Crafty.send_announcement("Game over")

func _player_joined(player) -> void:
    if player.has_method("respawn"):
        player.respawn(get_random_spawn_point())
    player.set_synced("score", 0)

func _player_left(_player) -> void:
    pass

func _process(delta: float) -> void:
    super._process(delta)
    if not Crafty.is_server():
        return
    for p in get_players():
        if p.has_method("respawn"):
            apply_default_movement(p, delta, MOVE_SPEED, GRAVITY)

Step 4: Add One Multiplayer Rule

Example scoring rule:
func score_point(player) -> void:
    var current := int(player.get_synced("score") if player.get_synced("score") != null else 0)
    current += 1
    player.set_synced("score", current)
    Crafty.send_announcement("%s scored (%d)" % [str(player.get("display_name")), current])

Step 5: Local Test

Use the two-process test runner flow:
  • starts local headless server,
  • starts client and connects to localhost,
  • validates real multiplayer behavior.
Guide:

Step 6: Export and Upload

Export a game-only .pck, then upload with manifest.json. Guides:

What “Done” Looks Like

  • All lifecycle hooks implemented.
  • Server authority enforced (Crafty.is_server()).
  • Player-visible state comes from set_synced.
  • Local test runner path passes.
  • Upload passes scanner + enters review.