diff --git a/components/SDKTabs.tsx b/components/SDKTabs.tsx index ef4e16d8..bb8da233 100644 --- a/components/SDKTabs.tsx +++ b/components/SDKTabs.tsx @@ -1,7 +1,7 @@ import { Tabs } from 'nextra/components' export function SDKTabs({ children }: { children: React.ReactNode }) { - return + return {children} } diff --git a/components/icons/platforms.tsx b/components/icons/platforms.tsx index d85fd9c9..8b06e3a3 100644 --- a/components/icons/platforms.tsx +++ b/components/icons/platforms.tsx @@ -65,6 +65,12 @@ export const wechat = (props: IconProps = {}) => WeChat +export const godot = (props: IconProps = {}) => Godot + /** * BRANDS */ diff --git a/images/icons/godot.png b/images/icons/godot.png new file mode 100644 index 00000000..46c68e93 Binary files /dev/null and b/images/icons/godot.png differ diff --git a/pages/getting-started.mdx b/pages/getting-started.mdx index 00331af5..7adc620c 100644 --- a/pages/getting-started.mdx +++ b/pages/getting-started.mdx @@ -1,5 +1,5 @@ import { Cards, Tabs, Steps } from 'nextra/components' -import { javascript, typescript, react, unity, defold, construct3, cocos, haxe, discord, wechat } from '../components/icons/platforms' +import { javascript, typescript, react, unity, defold, construct3, cocos, haxe, discord, wechat, godot } from '../components/icons/platforms' import { SyncIcon, RowsIcon } from '@primer/octicons-react' # Getting Started @@ -13,6 +13,7 @@ Colyseus is unopinionated on the game engine or framework you use in the fronten + diff --git a/pages/getting-started/_meta.tsx b/pages/getting-started/_meta.tsx index d83a2a01..477b437c 100644 --- a/pages/getting-started/_meta.tsx +++ b/pages/getting-started/_meta.tsx @@ -1,10 +1,11 @@ -import { javascript, typescript, react, unity, defold, construct3, cocos, haxe, discord, wechat } from '../../components/icons/platforms' +import { javascript, typescript, react, unity, defold, construct3, cocos, haxe, discord, wechat, godot } from '../../components/icons/platforms' export default { "typescript": { title: {typescript({ width: '19px', marginRight: '2px' })} TypeScript }, "javascript": { title: {javascript({ width: '19px', marginRight: '2px' })} JavaScript }, "react": { title: {react({ width: '19px', marginRight: '2px' })} React }, "unity": { title: {unity({ width: '19px', marginRight: '2px' })} Unity }, + "godot": { title: {godot({ width: '19px', marginRight: '2px' })} Godot }, "defold": { title: {defold({ width: '19px', marginRight: '2px' })} Defold }, "construct3": { title: {construct3({ width: '19px', marginRight: '2px' })} Construct 3 }, "cocos": { title: {cocos({ width: '19px', marginRight: '2px' })} Cocos Creator }, diff --git a/pages/getting-started/godot.mdx b/pages/getting-started/godot.mdx new file mode 100644 index 00000000..4e2b5716 --- /dev/null +++ b/pages/getting-started/godot.mdx @@ -0,0 +1,146 @@ +--- +title: "Godot Engine" +--- +import { Callout } from "nextra/components"; +import { DevicesIcon } from '@primer/octicons-react' + +# Godot Engine + + + The GDExtension is **very experimental**, and may not be stable. Please [report any issues you find](https://github.com/colyseus/native-sdk/issues/8). + + +We are experimenting with our shared [Colyseus Native SDK](https://github.com/colyseus/native-sdk) for cross-platform support for Colyseus across different engines. Godot is the first engine to support this. The work on Native SDK is still in progress, so expect some breaking changes as we go. + +## Platforms + +- Desktop (Windows, macOS, Linux) +- iOS +- Android +- Web (HTML5) + +## Installation + +- Download the latest Godot SDK from [GitHub Releases](https://github.com/colyseus/native-sdk/releases?q=godot+sdk&expanded=true) +- Extract the addons folder into your Godot project root +- Enable the plugin in **Project Settings** → **Plugins** + +## Web Builds + +When exporting your project via **Project** → **Export** → **Web (Runnable)**, make sure to enable **Extensions Support**. + +![Settings](/getting-started/godot-web.png) + +## SDK API + +Navigate to the [ Client SDK](/sdk) for API Reference, and select the **Godot** tab. + +## Quick Example / Reference + +This example shows how to connect to a room, listen for state changes, send messages and leave the room. + +```gdscript filename="Network.gd" +extends Node + +var client: ColyseusClient +var room: ColyseusRoom +var callbacks: ColyseusCallbacks + +func _ready(): + # Create and connect client + client = Colyseus.create_client() + client.set_endpoint("ws://localhost:2567") + + print("Connecting to: ", client.get_endpoint()) + + # Join or create a room + room = client.join_or_create("test_room") + + # Connect signals + if room: + room.joined.connect(_on_room_joined) + room.state_changed.connect(_on_state_changed) + room.message_received.connect(_on_message_received) + room.error.connect(_on_room_error) + room.left.connect(_on_room_left) + +func _process(delta): + # Poll the client for messages + # (Only required for web builds) + ColyseusClient.poll() + +func _on_room_joined(): + print("✓ Joined room: ", room.get_id()) + print(" Session ID: ", room.get_session_id()) + print(" Room name: ", room.get_name()) + + # Get callbacks container for the room + callbacks = Colyseus.callbacks(room) + + # Listen to root state property changes + callbacks.listen("currentTurn", _on_turn_change) + + # Listen to collection additions/removals + callbacks.on_add("players", _on_player_add) + callbacks.on_remove("players", _on_player_remove) + + # Send a message + var message = "Hello from Godot!".to_utf8_buffer() + room.send_message("add_item", {"name": "MY NEW ITEM"}) + +func _on_turn_change(current_value, previous_value): + print("↻ Turn changed: ", previous_value, " -> ", current_value) + +func _on_player_add(player: Dictionary, key: String): + print("+ Player joined: ", key) + # Listen to nested schema properties + callbacks.listen(player, "hp", _on_player_hp_change) + # Listen to nested collections + callbacks.on_add(player, "items", _on_item_add) + +func _on_player_remove(player: Dictionary, key: String): + print("- Player left: ", key) + +func _on_player_hp_change(current_hp, previous_hp): + print(" HP changed: ", previous_hp, " -> ", current_hp) + +func _on_item_add(item: Dictionary, index: int): + print(" Item added at index: ", index, " -> ", item) + + callbacks.listen(item, "name", func(name, _prev): + print(" Item name: ", name)) + +func _on_state_changed(): + print("↻ Room state changed") + # Access state as Dictionary + var state = room.get_state() + if state: + print(" State: ", state) + +func _on_message_received(type: Variant, data: Variant): + # type is the message type (String or int for numeric types) + print("✉ Message received - type: ", type, " data: ", data) + +func _on_room_error(code: int, message: String): + printerr("✗ Room error [", code, "]: ", message) + +func _on_room_left(code: int, reason: String): + print("← Left room [", code, "]: ", reason) + +func _exit_tree(): + # Clean up when node is removed + if room and room.has_joined(): + room.leave() +``` + +### State Schema Codegen + +It is not required to use the State Schema Codegen, but it is recommended to use it to get type safety and autocomplete in your IDE. + +```sh filename="Terminal" +npx schema-codegen src/rooms/schema/* --gdscript --bundle --output ../colyseus/schema/ +``` + + + See the full [State Schema Codegen documentation](/sdk/state-sync-callbacks#frontend-schema-generation) for more options and details. + diff --git a/pages/sdk.mdx b/pages/sdk.mdx index d764df1d..80d915e7 100644 --- a/pages/sdk.mdx +++ b/pages/sdk.mdx @@ -17,7 +17,7 @@ The Client SDK provides everything you need to connect to a Colyseus server from The `Client` instance is your entry point to connect to the server. - + ```ts filename="client.ts" @@ -62,6 +62,13 @@ The `Client` instance is your entry point to connect to the server. ``` + + ```gdscript filename="client.gd" + var client: ColyseusClient = Colyseus.create_client() + client.set_endpoint("ws://localhost:2567") + ``` + + --- @@ -78,7 +85,7 @@ The most common way to connect. Joins an existing room if available, or creates client.joinOrCreate (roomName: string, options: any) ``` - + ```ts filename="client.ts" @@ -142,15 +149,14 @@ client.joinOrCreate (roomName: string, options: any) - ```cpp filename="client.cpp" - client->joinOrCreate("battle", {/* options */}, [=](std::string err, Room* room) { - if (err != "") { - std::cout << "join error: " << err << std::endl; - return; - } + ```gdscript filename="client.gd" + var room: ColyseusRoom = client.join_or_create("battle") - std::cout << "joined successfully" << std::endl; - }); + room.joined.connect(func(): + print("joined successfully")) + + room.error.connect(func(code: int, message: String): + printerr("join error: ", message)) ``` @@ -168,7 +174,7 @@ Always creates a new room instance, even if others exist. client.create (roomName: string, options: any) ``` - + ```ts filename="client.ts" @@ -232,15 +238,14 @@ client.create (roomName: string, options: any) - ```cpp filename="client.cpp" - client->create("battle", {/* options */}, [=](std::string err, Room* room) { - if (err != "") { - std::cout << "join error: " << err << std::endl; - return; - } + ```gdscript filename="client.gd" + var room: ColyseusRoom = client.create("battle") - std::cout << "joined successfully" << std::endl; - }); + room.joined.connect(func(): + print("joined successfully")) + + room.error.connect(func(code: int, message: String): + printerr("join error: ", message)) ``` @@ -257,7 +262,7 @@ Joins an existing room by room name. Fails if no room is available. client.join (roomName: string, options: any) ``` - + ```ts filename="client.ts" @@ -321,15 +326,14 @@ client.join (roomName: string, options: any) - ```cpp filename="client.cpp" - client->join("battle", {/* options */}, [=](std::string err, Room* room) { - if (err != "") { - std::cout << "join error: " << err << std::endl; - return; - } + ```gdscript filename="client.gd" + var room: ColyseusRoom = client.join("battle") - std::cout << "joined successfully" << std::endl; - }); + room.joined.connect(func(): + print("joined successfully")) + + room.error.connect(func(code: int, message: String): + printerr("join error: ", message)) ``` @@ -346,7 +350,7 @@ Joins a specific room by its unique ID. This is useful for invite links or rejoi client.joinById (roomId: string, options: any) ``` - + ```ts filename="client.ts" @@ -410,15 +414,14 @@ client.joinById (roomId: string, options: any) - ```cpp filename="client.cpp" - client->joinById("battle", {/* options */}, [=](std::string err, Room* room) { - if (err != "") { - std::cout << "join error: " << err << std::endl; - return; - } + ```gdscript filename="client.gd" + var room: ColyseusRoom = client.join_by_id("KRYAKzRo2") - std::cout << "joined successfully" << std::endl; - }); + room.joined.connect(func(): + print("joined successfully")) + + room.error.connect(func(code: int, message: String): + printerr("join error: ", message)) ``` @@ -446,7 +449,7 @@ A **"seat reservation"** is a token generated by the server that allows a client client.consumeSeatReservation (reservation) ``` - + ```ts filename="client.ts" @@ -510,15 +513,14 @@ client.consumeSeatReservation (reservation) - ```cpp filename="client.cpp" - client->consumeSeatReservation(reservation, [=](std::string err, Room* room) { - if (err != "") { - std::cout << "join error: " << err << std::endl; - return; - } + ```gdscript filename="client.gd" + var room: ColyseusRoom = client.consume_seat_reservation(reservation) - std::cout << "joined successfully" << std::endl; - }); + room.joined.connect(func(): + print("joined successfully")) + + room.error.connect(func(code: int, message: String): + printerr("join error: ", message)) ``` @@ -541,7 +543,7 @@ Send messages to the room handler. Messages are encoded with MsgPack and can hol room.send (type, message) ``` - + ```ts filename="client.ts" // @@ -611,6 +613,16 @@ room.send (type, message) room.send(0, { direction: "left" }); ``` + + + ```gdscript filename="client.gd" + # sending message with string type + room.send_message("move", {"direction": "left"}) + + # sending message with number type + room.send_message(0, {"direction": "left"}) + ``` + @@ -641,7 +653,7 @@ room.sendBytes("some-bytes", [ 172, 72, 101, 108, 108, 111, 32, 119, 111, 114, 1 Listen for messages sent from the server. - + ```ts filename="client.ts" @@ -693,11 +705,12 @@ Listen for messages sent from the server. - ```cpp filename="client.cpp" - room.onMessage("powerup", [=](msgpack::object message) -> void { - std::cout << "message received from server" << std::endl; - std::cout << message << std::endl; - }); + ```gdscript filename="client.gd" + room.message_received.connect(_on_message_received) + + func _on_message_received(type: Variant, message: Variant): + print("message received from server") + print(message) ``` @@ -725,7 +738,7 @@ room.state: any The `onStateChange` event fires whenever the server synchronizes state updates. - + ```ts filename="client.ts" room.onStateChange.once((state) => { @@ -779,11 +792,12 @@ The `onStateChange` event fires whenever the server synchronizes state updates. - ```cpp filename="client.cpp" - room.onStateChange = [=](State>* state) { - std::cout << "new state" << std::endl; - // ... - }; + ```gdscript filename="client.gd" + room.state_changed.connect(_on_state_changed) + + func _on_state_changed(): + var state = room.get_state() + print("the room state has been updated: ", state) ``` @@ -794,34 +808,41 @@ For better performance and control, listen to specific property changes instead - ```ts {10} filename="client.ts" + ```ts filename="client.ts" // get state callbacks handler const callbacks = Callbacks.get(room); ``` - ```cs {10} filename="client.cs" + ```cs filename="client.cs" // get state callbacks handler var callbacks = Colyseus.Schema.Callbacks.Get(room); ``` - ```lua {10} filename="client.lua" + ```lua filename="client.lua" -- get state callbacks handler local callbacks = ColyseusSDK.callbacks(room) ``` - ```haxe {2,11} filename="client.hx" + ```haxe filename="client.hx" import io.colyseus.serializer.schema.Callbacks; // get state callbacks handler var callbacks = Callbacks.get(room); ``` + + + ```gdscript filename="client.gd" + # get state callbacks handler + var callbacks: ColyseusCallbacks = Colyseus.callbacks(room) + ``` + @@ -846,7 +867,7 @@ Disconnect from the room. Use `consented: true` (default) for intentional leaves room.leave (consented: boolean) ``` - + ```ts filename="client.ts" // consented leave @@ -896,6 +917,13 @@ room.leave (consented: boolean) room.leave(false); ``` + + + ```gdscript filename="client.gd" + # consented leave + room.leave() + ``` + @@ -906,7 +934,7 @@ room.leave (consented: boolean) Triggered when the client leaves the room (either intentionally or due to disconnection). - + ```ts filename="client.ts" @@ -949,10 +977,11 @@ Triggered when the client leaves the room (either intentionally or due to discon - ```cpp filename="client.cpp" - room.onLeave = [=]() -> void { - std::cout << "client left the room" << std::endl; - }; + ```gdscript filename="client.gd" + room.left.connect(_on_room_left) + + func _on_room_left(code: int, reason: String): + print("client left the room: ", reason) ``` @@ -975,7 +1004,7 @@ The SDK automatically attempts to reconnect when the connection is unexpectedly Triggered when the connection is unexpectedly dropped. The SDK will automatically attempt to reconnect. - + ```ts filename="client.ts" room.onDrop((code, reason) => { @@ -1020,6 +1049,15 @@ Triggered when the connection is unexpectedly dropped. The SDK will automaticall }; ``` + + + ```gdscript filename="client.gd" + # The Godot SDK handles reconnection automatically + # Use the error signal to detect connection issues + room.error.connect(func(code: int, message: String): + print("connection error: ", message)) + ``` + @@ -1038,7 +1076,7 @@ Triggered when the connection is unexpectedly dropped. The SDK will automaticall Triggered when the client successfully reconnects after a connection drop. While disconnected, your `room.send()` calls will be queued and sent to the server when the client reconnects. The maximum number of queued messages is configurable using `room.reconnection.maxEnqueuedMessages`. - + ```ts filename="client.ts" room.onReconnect(() => { @@ -1078,6 +1116,15 @@ Triggered when the client successfully reconnects after a connection drop. While }; ``` + + + ```gdscript filename="client.gd" + # The Godot SDK handles reconnection automatically + # The joined signal will fire again after successful reconnection + room.joined.connect(func(): + print("connected/reconnected to the room!")) + ``` + #### Configuration Options @@ -1107,7 +1154,7 @@ You may configure the reconnection behavior using `room.reconnection`. **Customizing reconnection behavior:** - + ```ts filename="client.ts" const room = await client.joinOrCreate("battle"); @@ -1162,11 +1209,18 @@ You may configure the reconnection behavior using `room.reconnection`. }); ``` + + + ```gdscript filename="client.gd" + # Reconnection is handled automatically by the Godot SDK + # Configuration options may vary - check the SDK documentation + ``` + **Custom backoff function:** - + ```ts filename="client.ts" room.reconnection.backoff = (attempt: number, delay: number) => { @@ -1206,6 +1260,13 @@ You may configure the reconnection behavior using `room.reconnection`. }; ``` + + + ```gdscript filename="client.gd" + # Custom backoff functions are not currently exposed in the Godot SDK + # The SDK uses a default exponential backoff strategy + ``` + ### Manual Reconnection @@ -1219,7 +1280,7 @@ For more control, you can manually reconnect using a cached reconnection token. client.reconnect (reconnectionToken) ``` - + ```ts filename="client.ts" @@ -1283,15 +1344,14 @@ client.reconnect (reconnectionToken) - ```cpp filename="client.cpp" - client->reconnect(cachedReconnectionToken, [=](std::string err, Room* room) { - if (err != "") { - std::cout << "join error: " << err << std::endl; - return; - } + ```gdscript filename="client.gd" + var room: ColyseusRoom = client.reconnect(cached_reconnection_token) - std::cout << "joined successfully" << std::endl; - }); + room.joined.connect(func(): + print("reconnected successfully")) + + room.error.connect(func(code: int, message: String): + printerr("reconnect error: ", message)) ``` @@ -1300,7 +1360,7 @@ client.reconnect (reconnectionToken) Listen for errors that occur in the room. - + ```ts filename="client.ts" room.onError((code, message) => { @@ -1347,10 +1407,11 @@ Listen for errors that occur in the room. - ```cpp filename="client.cpp" - room.onError = [=] (int code, std::string message) => void { - std::cout << "oops, error ocurred: " << message << std::endl; - }; + ```gdscript filename="client.gd" + room.error.connect(_on_room_error) + + func _on_room_error(code: int, message: String): + printerr("oops, error occurred: ", message) ``` @@ -1383,7 +1444,7 @@ client.getLatency (options?: LatencyOptions): Promise - `options.pingCount`: Number of pings to send (default: `1`). Returns the average latency when greater than 1. - + ```ts filename="client.ts" const latency = await client.getLatency(); @@ -1444,6 +1505,13 @@ client.getLatency (options?: LatencyOptions): Promise }); ``` + + + ```gdscript filename="client.gd" + # Latency measurement API may vary in the Godot SDK + # Check the SDK documentation for available methods + ``` + #### On an Active Room @@ -1454,7 +1522,7 @@ Measure round-trip time on an existing connection. room.ping (callback: (ms: number) => void) ``` - + ```ts filename="client.ts" room.ping((latency) => { @@ -1494,6 +1562,13 @@ room.ping (callback: (ms: number) => void) }); ``` + + + ```gdscript filename="client.gd" + # Ping API may vary in the Godot SDK + # Check the SDK documentation for available methods + ``` + @@ -1514,7 +1589,7 @@ Client.selectByLatency (endpoints: Array): Promise + ```ts filename="client.ts" import { Client } from "@colyseus/sdk"; @@ -1598,6 +1673,22 @@ Client.selectByLatency (endpoints: Array): Promise + + + ```gdscript filename="client.gd" + # Multi-region server selection may vary in the Godot SDK + # You can manually test latency to multiple endpoints: + var endpoints = [ + "wss://us-east.gameserver.com", + "wss://eu-west.gameserver.com", + "wss://asia.gameserver.com" + ] + + # Select endpoint with lowest latency and create client + var client = Colyseus.create_client() + client.set_endpoint(endpoints[0]) # Set your preferred endpoint + ``` + diff --git a/pages/sdk/state-sync-callbacks.mdx b/pages/sdk/state-sync-callbacks.mdx index 5f00487b..109cc121 100644 --- a/pages/sdk/state-sync-callbacks.mdx +++ b/pages/sdk/state-sync-callbacks.mdx @@ -83,6 +83,21 @@ In order to register callbacks to Schema instances, you must access the instance }); ``` + + + ```gdscript {10} filename="client.gd" + # initialize SDK + var client: ColyseusClient = Colyseus.create_client() + client.set_endpoint("ws://localhost:2567") + + # join room + var room: ColyseusRoom = client.join_or_create("my_room") + + room.joined.connect(func(): + # get state callbacks handler + var callbacks: ColyseusCallbacks = Colyseus.callbacks(room)) + ``` + ### Register the callbacks @@ -184,6 +199,30 @@ In order to register callbacks to Schema instances, you must access the instance }); ``` + + + ```gdscript filename="client.gd" + callbacks.listen("currentTurn", _on_turn_change) + + func _on_turn_change(current_value, previous_value): + pass # ... + + # when an entity was added (ArraySchema or MapSchema) + callbacks.on_add("entities", _on_entity_add) + + func _on_entity_add(entity: Dictionary, session_id: String): + print("entity added: ", entity) + + callbacks.listen(entity, "hp", func(current_hp, previous_hp): + print("entity ", session_id, " changed hp to ", current_hp)) + + # when an entity was removed (ArraySchema or MapSchema) + callbacks.on_remove("entities", _on_entity_remove) + + func _on_entity_remove(entity: Dictionary, session_id: String): + print("entity removed: ", entity) + ``` + @@ -236,6 +275,16 @@ Listens for a single property change within a `Schema` instance. }); ``` + + + ```gdscript filename="client.gd" + callbacks.listen("currentTurn", _on_turn_change) + + func _on_turn_change(current_value, previous_value): + print("currentTurn is now ", current_value) + print("previous value was: ", previous_value) + ``` + **Removing the callback:** The `.listen()` method returns a function that, when called, removes the attached callback: @@ -284,6 +333,15 @@ Listens for a single property change within a `Schema` instance. unbindCallback(); ``` + + + ```gdscript filename="client.gd" + var unbind_callback = callbacks.listen("currentTurn", _on_turn_change) + + # stop listening for "currentTurn" changes + unbind_callback.call() + ``` + --- @@ -335,6 +393,15 @@ Bind properties directly to `targetObject`, whenever the client receives an upda ``` + + ```gdscript filename="client.gd" + # + # callbacks.bind_to() is not implemented in Godot SDK yet + # contributions are very welcome! + # + ``` + + --- @@ -390,6 +457,18 @@ The On Change callback is invoked whenever a direct property of a `Schema` insta }); ``` + + + ```gdscript filename="client.gd" + callbacks.on_add("entities", _on_entity_add) + + func _on_entity_add(entity: Dictionary, session_id: String): + # ... + callbacks.on_change(entity, func(): + # some property changed inside entity + pass) + ``` + --- @@ -466,6 +545,22 @@ By default, the callback is called immediately for existing items in the collect }); ``` + + + ```gdscript filename="client.gd" + callbacks.on_add("players", _on_player_add) + + func _on_player_add(player: Dictionary, session_id: String): + print("player has been added at ", session_id) + + # add your player entity to the game world! + + # detecting changes on object properties + callbacks.listen(player, "field_name", func(value, previous_value): + print(value) + print(previous_value)) + ``` + --- @@ -515,6 +610,17 @@ The `onRemove` callback is called with the removed item and its key on holder ob }); ``` + + + ```gdscript filename="client.gd" + callbacks.on_remove("players", _on_player_remove) + + func _on_player_remove(player: Dictionary, session_id: String): + print("player has been removed at ", session_id) + + # remove your player entity from the game world! + ``` + --- @@ -547,8 +653,10 @@ Usage (C#/Unity) Valid options: --output: fhe output directory for generated frontend schema files + --c: generate for C --csharp: generate for C#/Unity --cpp: generate for C++ + --gdscript: generate for Godot --haxe: generate for Haxe --ts: generate for TypeScript --js: generate for JavaScript diff --git a/public/getting-started/godot-web.png b/public/getting-started/godot-web.png new file mode 100644 index 00000000..e996017b Binary files /dev/null and b/public/getting-started/godot-web.png differ