├── .env.development ├── .env.production ├── .gitignore ├── README.md ├── loadtest └── example.ts ├── package.json ├── src ├── app.config.ts ├── index.ts └── rooms │ ├── MyRoom.ts │ └── schema │ └── MyRoomState.ts └── tsconfig.json /.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV=development -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV=production -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .vscode 4 | package-lock.json 5 | .idea/ 6 | lib 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PlayCanvas: Real-time Multiplayer with Colyseus 2 | 3 | This is the server code for a step-by-step tutorial on how to use PlayCanvas + Colyseus together. 4 | 5 | - [See step-by-step Tutorial](https://developer.playcanvas.com/en/tutorials/real-time-multiplayer-colyseus//) 6 | - [See PlayCanvas Project](https://playcanvas.com/project/859259/overview/colyseus-demo) 7 | - [See Colyseus documentation](https://docs.colyseus.io/) 8 | 9 | ## How to run the server 10 | 11 | - Download and install [Node.js LTS](https://nodejs.org/en/download/) 12 | - Clone or download this repository. 13 | - Run `npm install` 14 | - Run `npm start` 15 | 16 | The WebSocket server should be available locally at `ws://localhost:2567`, and [http://localhost:2567](http://localhost:2567) should be accessible. 17 | 18 | ## License 19 | 20 | MIT 21 | -------------------------------------------------------------------------------- /loadtest/example.ts: -------------------------------------------------------------------------------- 1 | import { Client, Room } from "colyseus.js"; 2 | import { cli, Options } from "@colyseus/loadtest"; 3 | 4 | export async function main(options: Options) { 5 | const client = new Client(options.endpoint); 6 | const room: Room = await client.joinOrCreate(options.roomName, { 7 | // your join options here... 8 | }); 9 | 10 | console.log("joined successfully!"); 11 | 12 | room.onMessage("message-type", (payload) => { 13 | // logic 14 | }); 15 | 16 | room.onStateChange((state) => { 17 | console.log("state change:", state); 18 | }); 19 | 20 | room.onLeave((code) => { 21 | console.log("left"); 22 | }); 23 | } 24 | 25 | cli(main); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "my-app", 4 | "version": "1.0.0", 5 | "description": "npm init template for bootstrapping an empty Colyseus project", 6 | "main": "lib/index.js", 7 | "scripts": { 8 | "start": "tsx watch src/index.ts", 9 | "loadtest": "tsx loadtest/example.ts --room my_room --numClients 2", 10 | "build": "npm run clean && tsc", 11 | "clean": "rimraf build", 12 | "test": "mocha -r tsx test/**_test.ts --exit --timeout 15000" 13 | }, 14 | "engines": { 15 | "node": "^22.0.0" 16 | }, 17 | "author": "", 18 | "license": "UNLICENSED", 19 | "bugs": { 20 | "url": "https://github.com/colyseus/create-colyseus/issues" 21 | }, 22 | "homepage": "https://github.com/colyseus/create-colyseus#readme", 23 | "devDependencies": { 24 | "@colyseus/loadtest": "^0.16.1", 25 | "@colyseus/testing": "^0.16.0", 26 | "@types/cors": "^2.8.6", 27 | "@types/express": "^4.17.1", 28 | "@types/mocha": "^8.2.3", 29 | "copyfiles": "^2.4.1", 30 | "mocha": "^9.0.2", 31 | "rimraf": "^2.7.1", 32 | "tsx": "^4.19.4", 33 | "typescript": "^5.8.3" 34 | }, 35 | "dependencies": { 36 | "@colyseus/monitor": "^0.16.0", 37 | "@colyseus/playground": "^0.16.4", 38 | "@colyseus/tools": "^0.16.0", 39 | "colyseus": "^0.16.0", 40 | "cors": "^2.8.5", 41 | "express": "^4.16.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app.config.ts: -------------------------------------------------------------------------------- 1 | import config from "@colyseus/tools"; 2 | 3 | import { LobbyRoom } from "colyseus"; 4 | import { playground } from "@colyseus/playground"; 5 | import { monitor } from "@colyseus/monitor"; 6 | 7 | import { MyRoom } from "./rooms/MyRoom"; 8 | 9 | export default config({ 10 | initializeGameServer: (gameServer) => { 11 | // Expose MyRoom with realtime listing (lobby listing) 12 | gameServer 13 | .define('my_room', MyRoom) 14 | .enableRealtimeListing(); 15 | 16 | // Expose LobbyRoom 17 | gameServer 18 | .define('lobby', LobbyRoom); 19 | }, 20 | 21 | initializeExpress: (app) => { 22 | app.use("/", playground()); 23 | app.use("/monitor", monitor()); 24 | }, 25 | 26 | beforeListen: () => { 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * IMPORTANT: 3 | * --------- 4 | * Do not manually edit this file if you'd like to host your server on Colyseus Cloud 5 | * 6 | * If you're self-hosting (without Colyseus Cloud), you can manually 7 | * instantiate a Colyseus Server as documented here: 8 | * 9 | * See: https://docs.colyseus.io/server/api/#constructor-options 10 | */ 11 | import { listen } from "@colyseus/tools"; 12 | 13 | // Import arena config 14 | import app from "./app.config"; 15 | 16 | // Listen on 2567 (or PORT environment variable.) 17 | listen(app); 18 | -------------------------------------------------------------------------------- /src/rooms/MyRoom.ts: -------------------------------------------------------------------------------- 1 | import { Room, Client } from "@colyseus/core"; 2 | import { MyRoomState, Player } from "./schema/MyRoomState"; 3 | 4 | export class MyRoom extends Room { 5 | maxClients = 5; 6 | state = new MyRoomState(); 7 | 8 | onCreate(options: any) { 9 | console.log("MyRoom created."); 10 | 11 | this.onMessage("updatePosition", (client, data) => { 12 | console.log("update received -> "); 13 | console.debug(JSON.stringify(data)); 14 | const player = this.state.players.get(client.sessionId); 15 | player.x = data["x"]; 16 | player.y = data['y']; 17 | player.z = data["z"]; 18 | }); 19 | } 20 | 21 | onJoin(client: Client, options: any) { 22 | console.log(client.sessionId, "joined!"); 23 | 24 | // create Player instance 25 | const player = new Player(); 26 | 27 | // place Player at a random position in the floor 28 | const FLOOR_SIZE = 4; 29 | player.x = -(FLOOR_SIZE / 2) + (Math.random() * FLOOR_SIZE); 30 | player.y = 1.031; 31 | player.z = -(FLOOR_SIZE / 2) + (Math.random() * FLOOR_SIZE); 32 | 33 | // place player in the map of players by its sessionId 34 | // (client.sessionId is unique per connection!) 35 | this.state.players.set(client.sessionId, player); 36 | 37 | console.log("new player =>", player.toJSON()); 38 | } 39 | 40 | onLeave(client: Client, consented: boolean) { 41 | this.state.players.delete(client.sessionId); 42 | console.log(client.sessionId, "left!"); 43 | } 44 | 45 | onDispose() { 46 | console.log("room", this.roomId, "disposing..."); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/rooms/schema/MyRoomState.ts: -------------------------------------------------------------------------------- 1 | import { MapSchema, Schema, type } from "@colyseus/schema"; 2 | 3 | export class Player extends Schema { 4 | @type("number") x: number; 5 | @type("number") y: number; 6 | @type("number") z: number; 7 | } 8 | 9 | export class MyRoomState extends Schema { 10 | @type({ map: Player }) players = new MapSchema(); 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "target": "es6", 5 | "module": "commonjs", 6 | "strict": true, 7 | "allowJs": true, 8 | "strictNullChecks": false, 9 | "esModuleInterop": true, 10 | "experimentalDecorators": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "include": [ 15 | "src" 16 | ] 17 | } --------------------------------------------------------------------------------