Skip to article Developer portalAnnounce, upload and sell your game

GAMENIGHT / DOCUMENTATION

Godot 4

The Godot addon handles lifecycle messages, runtime controller frames and live player profiles over WebSocket. Your game connects these to its simulation and rendering.

Install

Copy sdk/godot/addons/gamenight into your project’s addons directory and enable GameNight in Project Settings → Plugins. The plugin registers GameNight and GameNightScreen autoloads.

Godot has no package manager, so every game keeps its own copy of the addon, and copies fall behind. Run python sdk/godot/sync.py path/to/your/game from a GameNight checkout to update it, and add the drift check to your game's CI. It fails as soon as your copy differs from the SDK on main:

jobs:
  gamenight-sdk:
    uses: ontola/gamenight/.github/workflows/godot-sdk-check.yml@main
    with:
      path: .  # the folder that holds project.godot

The connection reads the launch environment automatically. Keep standalone menus separate from the managed path using GameNight.launched_by_daemon.

Lifecycle signals

These declarations come directly from the addon:

signal prepared(session_id: String, seats: Array, players: Array)
## The transition landed on you. Start the match NOW.
signal started(session_id: String)
signal paused(session_id: String)
signal resumed(session_id: String)
## Tear the session down; the id will never be used again.
signal disposed(session_id: String)

Source: sdk/godot/addons/gamenight/gamenight.gd

Connect each signal to your game. Prepare loads a level while hidden; Start begins play. Pause must freeze gameplay, timers and audio. Resume continues the same state. Dispose clears session resources but permits a later Prepare.

The autoload processes while the scene tree is paused, so it can receive Resume. In managed mode, losing the host clears input and exits the game. Standalone development connections may reconnect automatically.

Report preparation progress

The addon provides this implementation:

func notify_progress(session_id: String, percent: int, label: String = "") -> void:
	var msg := {"type": "progress", "session": session_id, "percent": clampi(percent, 0, 100)}
	if label != "":
		msg["label"] = label
	_send(msg)

Source: sdk/godot/addons/gamenight/gamenight.gd

Call GameNight.notify_ready(session_id) only after your level and first frame are prepared. notify_finished reports a round; your game still owns the results screen and next round.

Controllers and live profiles

Use the runtime stream in managed play. A seat's controller token is opaque; it is not a Godot joystick index. frame_for_seat resolves the token from the latest roster, returns a copy, and returns an empty frame while paused or when input is more than 250 ms old.

func _physics_process(delta: float) -> void:
    if GameNight.phase != "running":
        return
    var frame = GameNight.frame_for_seat(0)
    var movement = Vector2(GameNight.axis(frame, 0), GameNight.axis(frame, 1))
    # Apply movement to the character assigned to seat 0.
    if GameNight.button(frame, 0): # A, held; add your own press-edge detection.
        pass

Use GameNight.roster_changed to refresh seat ownership, names, skin colour and avatar artwork. The current snapshot is available in GameNight.party. devices_for_local_seats() is only for standalone play and returns no native devices in managed mode. Back/Select belongs to the host; do not bind native Back to a second pause/resume handler.

For circular faces, use face.gd with a head centre and radius. See the face helper example. The helper draws the skin circle under the artwork and supports historic avatar anchors.

Screen ownership

GameNightScreen follows Start, Pause, Resume and Dispose. OS focus does not request Start or Resume. A game preparing in the background stays quiet until the runtime explicitly starts it. The helper handles the window and master audio bus; your lifecycle callbacks must still pause the simulation and timers.

Godot lobby example

The SDK includes a runnable Living Room project using a separate authenticated lobby client. It shows live player names and faces and can join, queue, play and resume. See Build a lobby for launch instructions and the runtime-owned controller contract. Its client is independent of the game autoloads.

Before release

Check the lifecycle contract and controller stream, then run packaged integration checks. Run python scripts/test-godot-sdk.py --godot /path/to/godot for the headless adapter regression checks. The lobby has a separate real-daemon flow test. These tests and source-checked documentation do not verify your game callbacks, physical controllers or platform window behaviour. Enabling the plugin alone does not certify a game.

Optional performance diagnostics

The GameNight autoload samples application frame intervals while running and reports them every ten active seconds, with CPU/GPU model, OS, physical RAM and window dimensions. Rebuild your game to ship this adapter update. See the protocol reference for counters, limits and missing-data semantics. These diagnostics contain no accounts, behavioral history or recommendation logic.

Yield your soundtrack to host music

The welcome snapshot and subsequent GameNight.party_updated snapshots include now_playing when GameNight detects a host music player. Check playing, not just whether a track exists: paused tracks still have metadata. When it is true, mute your music bus only. Keep effects audible and preserve the player's own music preference. Restore that preference when host music pauses or disappears.

func _ready():
    GameNight.party_updated.connect(_host_music)
    _host_music(GameNight.party)

func _host_music(party: Dictionary):
    var track = party.get("now_playing", {})
    var playing = track is Dictionary and bool(track.get("playing", false))
    AudioServer.set_bus_mute(AudioServer.get_bus_index("Music"), playing)

Use a dedicated Music bus. Growing Guns uses MusicA and MusicB for its two soundtrack layers. Do not mute Master: that also silences game effects.

For a game that prepares before being played, set display/window/size/mode to 1 (Minimized) and display/window/size/no_focus to true at engine startup. Minimizing in _ready() is too late to prevent the initial window appearing. Use GameNightScreen for Start/Resume and explicitly claim the screen in your standalone launch path. OS focus must never request gameplay.

While warming, a minimized window never draws, so never await RenderingServer.frame_post_draw before notify_ready(): it does not return and the lobby waits on "preloading" forever. Await get_tree().process_frame instead whenever DisplayServer.window_get_mode() is WINDOW_MODE_MINIMIZED. Leave the window itself to GameNightScreen. Godot refuses hide() on the main window, and on Windows setting borderless shows the window again.