diff --git a/components/icons/platforms.tsx b/components/icons/platforms.tsx index d85fd9c9..240bf37d 100644 --- a/components/icons/platforms.tsx +++ b/components/icons/platforms.tsx @@ -65,6 +65,12 @@ export const wechat = (props: IconProps = {}) => WeChat +export const gamemaker = (props: IconProps = {}) => GameMaker + /** * BRANDS */ diff --git a/images/icons/gamemaker.png b/images/icons/gamemaker.png new file mode 100644 index 00000000..519aec85 Binary files /dev/null and b/images/icons/gamemaker.png differ diff --git a/pages/getting-started/_meta.tsx b/pages/getting-started/_meta.tsx index d83a2a01..72be51c6 100644 --- a/pages/getting-started/_meta.tsx +++ b/pages/getting-started/_meta.tsx @@ -1,4 +1,4 @@ -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, gamemaker } from '../../components/icons/platforms' export default { "typescript": { title: {typescript({ width: '19px', marginRight: '2px' })} TypeScript }, @@ -9,6 +9,7 @@ export default { "construct3": { title: {construct3({ width: '19px', marginRight: '2px' })} Construct 3 }, "cocos": { title: {cocos({ width: '19px', marginRight: '2px' })} Cocos Creator }, "haxe": { title: {haxe({ width: '19px', marginRight: '2px' })} Haxe }, + "gamemaker": { title: {gamemaker({ width: '19px', marginRight: '2px' })} GameMaker }, "discord-activity": { title: {discord({ width: '19px', marginRight: '2px' })} Discord Activity }, "wechat": { title: {wechat({ width: '19px', marginRight: '2px' })} WeChat }, } \ No newline at end of file diff --git a/pages/getting-started/gamemaker.mdx b/pages/getting-started/gamemaker.mdx new file mode 100644 index 00000000..2c305a20 --- /dev/null +++ b/pages/getting-started/gamemaker.mdx @@ -0,0 +1,264 @@ +--- +title: "GameMaker" +--- +import { Callout } from "nextra/components"; +import { DevicesIcon } from '@primer/octicons-react' + +# GameMaker + + + The GameMaker extension is **experimental**, and may not be stable. Please [report any issues you find](https://github.com/colyseus/native-sdk/issues/16). + + +The GameMaker SDK is built on top of the shared [Colyseus Native SDK](https://github.com/colyseus/native-sdk), which provides cross-platform support for Colyseus across different engines. The work on Native SDK is still in progress, so expect some breaking changes as we go. + +## Platforms + +- Windows +- macOS +- Linux +- HTML5 + +## Installation + +- Download the latest GameMaker SDK from [GitHub Releases](https://github.com/colyseus/native-sdk/releases?q=gamemaker+sdk&expanded=true) +- Extract the extension and `Colyseus.gml` script into your GameMaker project +- Add the extension's native library (`.dll` / `.dylib` / `.so`) to your project's Extension + +## Project Setup + +The SDK uses an **event queue** pattern to integrate with GameMaker's frame loop. You need: + +1. A **Script** resource containing the `Colyseus.gml` helper (handles event dispatch and callback registration) +2. An **Object** with **Create**, **Step**, and **Clean Up** events to manage the connection lifecycle + +You must call `colyseus_process()` once per frame in the **Step** event to poll and dispatch all queued events. + +## Quick Example + +This example shows how to connect to a room, listen for state changes, send messages, and leave the room. + +```js filename="Create_0.gml" +/// Create Event — initialize client and join room + +client = colyseus_client_create("http://localhost:2567"); +room = colyseus_client_join_or_create(client, "my_room", "{}"); +callbacks = -1; + +// --- Room event handlers --- + +colyseus_on_join(room, function(_room) { + show_debug_message("Joined room: " + colyseus_room_get_id(_room)); + show_debug_message("Session ID: " + colyseus_room_get_session_id(_room)); + + // Create callbacks manager for state change tracking + callbacks = colyseus_callbacks_create(_room); + + // Listen to root state properties (use 0 for root state instance) + colyseus_listen(callbacks, 0, "currentTurn", function(value, prev) { + show_debug_message("Turn changed: " + string(prev) + " -> " + string(value)); + }); + + // Listen for players added to the "players" map + colyseus_on_add(callbacks, 0, "players", function(instance, key) { + show_debug_message("Player joined: " + key); + + // Listen to nested properties on each player + colyseus_listen(callbacks, instance, "x", function(v, p) { + show_debug_message("Player x: " + string(v)); + }); + colyseus_listen(callbacks, instance, "y", function(v, p) { + show_debug_message("Player y: " + string(v)); + }); + + // Listen to nested collections + colyseus_on_add(callbacks, instance, "items", function(item, k) { + show_debug_message("Item added: " + string(item)); + }); + }); + + // Listen for players removed from the map + colyseus_on_remove(callbacks, 0, "players", function(instance, key) { + show_debug_message("Player left: " + key); + }); +}); + +colyseus_on_state_change(room, function(_room) { + var _state = colyseus_room_get_state(_room); + if (_state != 0) { + show_debug_message("State changed"); + + // Access schema fields + var _host = colyseus_schema_get_ref(_state, "host"); + var _my_player = colyseus_map_get(_state, "players", colyseus_room_get_session_id(_room)); + show_debug_message("Is host: " + string(_host == _my_player)); + } +}); + +colyseus_on_error(room, function(code, msg) { + show_debug_message("Room error [" + string(code) + "]: " + msg); +}); + +colyseus_on_leave(room, function(code, reason) { + show_debug_message("Left room [" + string(code) + "]: " + reason); +}); + +colyseus_on_message(room, function(_room, _type, _data) { + // _data is auto-decoded: struct for maps, string/number for primitives + show_debug_message("Message [" + _type + "]: " + json_stringify(_data)); +}); +``` + +```js filename="Step_0.gml" +/// Step Event — poll events and send messages + +colyseus_process(); + +if (colyseus_room_is_connected(room)) { + var _dx = keyboard_check(vk_right) - keyboard_check(vk_left); + var _dy = keyboard_check(vk_down) - keyboard_check(vk_up); + if (_dx != 0 || _dy != 0) { + x += _dx * 4; + y += _dy * 4; + colyseus_send(room, "move", { x: x, y: y }); + } +} +``` + +```js filename="CleanUp_0.gml" +/// Clean Up Event — free resources + +if (room != 0) { + colyseus_room_leave(room); + colyseus_room_free(room); // also frees associated callbacks +} +if (client != 0) { + colyseus_client_free(client); +} +``` + +## SDK API + +### Client + +| Function | Description | +| --- | --- | +| `colyseus_client_create(endpoint)` | Create a client. Returns a client handle. | +| `colyseus_client_free(client)` | Free client resources. | +| `colyseus_client_join_or_create(client, room_name, options_json)` | Join or create a room. Returns a room handle. | +| `colyseus_client_join(client, room_name, options_json)` | Join an existing room. | +| `colyseus_client_create_room(client, room_name, options_json)` | Create a new room. | +| `colyseus_client_join_by_id(client, room_id, options_json)` | Join a room by its ID. | +| `colyseus_client_reconnect(client, reconnection_token)` | Reconnect using a token from a previous session. | + +### Room + +| Function | Description | +| --- | --- | +| `colyseus_room_is_connected(room)` | Returns `1` if connected, `0` otherwise. | +| `colyseus_room_get_id(room)` | Get the room's unique ID. | +| `colyseus_room_get_session_id(room)` | Get your session ID in the room. | +| `colyseus_room_get_name(room)` | Get the room type name. | +| `colyseus_room_leave(room)` | Leave the room gracefully. | +| `colyseus_room_free(room)` | Free room resources (also frees callbacks). | + +### Room Events (GML helpers) + +Register callbacks for room lifecycle events. These are dispatched by `colyseus_process()`. + +| Function | Callback signature | +| --- | --- | +| `colyseus_on_join(room, handler)` | `handler(room)` | +| `colyseus_on_state_change(room, handler)` | `handler(room)` | +| `colyseus_on_message(room, handler)` | `handler(room, type, data)` — `data` is auto-decoded | +| `colyseus_on_error(room, handler)` | `handler(code, message)` | +| `colyseus_on_leave(room, handler)` | `handler(code, reason)` | + +### Sending Messages + +| Function | Description | +| --- | --- | +| `colyseus_send(room, type, struct)` | Send a struct as a message. Values must be strings or numbers. | +| `colyseus_room_send(room, type, data)` | Send raw encoded bytes with a string type. | +| `colyseus_room_send_bytes(room, type, data, length)` | Send raw bytes. | +| `colyseus_room_send_int(room, type, data)` | Send raw encoded bytes with a numeric type. | + +For advanced message construction: + +| Function | Description | +| --- | --- | +| `colyseus_message_create_map()` | Create a message builder. Returns a message handle. | +| `colyseus_message_put_str(msg, key, value)` | Add a string field. | +| `colyseus_message_put_number(msg, key, value)` | Add a number field. | +| `colyseus_message_put_bool(msg, key, value)` | Add a boolean field (`1`/`0`). | +| `colyseus_room_send_message(room, type, msg)` | Send and free the constructed message. | +| `colyseus_message_free(msg)` | Free a message (only needed if not sent). | + +### State & Schema + +Access fields from schema instances. Use `colyseus_room_get_state(room)` to get the root state handle. + +| Function | Description | +| --- | --- | +| `colyseus_room_get_state(room)` | Get the root state instance handle (`0` if unavailable). | +| `colyseus_schema_get_string(instance, field)` | Get a string field value. | +| `colyseus_schema_get_number(instance, field)` | Get a numeric field value (all numeric types). | +| `colyseus_schema_get_ref(instance, field)` | Get a child schema instance handle. | +| `colyseus_map_get(instance, field, key)` | Get an item from a MapSchema by key. | + +### State Change Callbacks (GML helpers) + +Create a callbacks manager to listen for state mutations. Use `0` as the instance handle to refer to the root state. + +| Function | Callback signature | +| --- | --- | +| `colyseus_callbacks_create(room)` | Create a callbacks manager. Returns a callbacks handle. | +| `colyseus_listen(callbacks, instance, property, handler)` | `handler(value, previous_value)` | +| `colyseus_on_add(callbacks, instance, property, handler)` | `handler(instance, key)` | +| `colyseus_on_remove(callbacks, instance, property, handler)` | `handler(instance, key)` | +| `colyseus_callbacks_remove_handle(callbacks, handle)` | Stop listening for a specific callback. | +| `colyseus_callbacks_free(callbacks)` | Free callbacks (auto-freed when room is freed). | + +### Event Processing + +| Function | Description | +| --- | --- | +| `colyseus_process()` | **Call once per frame.** Polls the event queue and dispatches registered handlers. | + +### Running the test server locally + +To run the test server locally, you will need to run the following commands in your terminal: + +```sh filename="Terminal" +git clone https://github.com/colyseus/sdks-test-server +cd sdks-test-server +npm install +npm start +``` + + + You can see the source code for the test server [here](https://github.com/colyseus/sdks-test-server). + + +You should be able to see the server running at `http://localhost:2567`, and the example project will be able to connect to it. + +## Debugging + +### Breakpoints + +If you set a breakpoint in your application while the WebSocket connection is open, the connection will be closed automatically after 3 seconds due to inactivity. To prevent the WebSocket connection from dropping, use `pingInterval: 0` during development: + +```ts filename="app.config.ts" +import { defineServer } from "colyseus"; +import { WebSocketTransport } from "@colyseus/ws-transport"; + +const server = defineServer({ + // ... + transport: new WebSocketTransport({ + pingInterval: 0, + }), + // ... +}); +``` + +Make sure to have a `pingInterval` higher than `0` on production. The default `pingInterval` value is `3000`.