SlideShare ist ein Scribd-Unternehmen logo
1 von 92
Making
Online Multiplayer Game
Experiences
With Colyseus and Node.js
This presentation is from Jan 2020.
It was supposed to happen on Node Atlanta 2020,
but unfortunately it got cancelled.
Hi, I’m Endel 👋
👨💻 Software Engineer and Game Developer from Brazil
🎓 Game Development & Digital Entertainment
GitHub / Twitter: @endel
I DON’T LIKE SOCCER
Summary
● Introduction
● Basic concepts
○ How Online Multiplayer Games Work
○ Authoritative Game Servers
● Colyseus in-depth
○ Match-making
○ Game State & Serialization
○ Infrastructure
● Client-side Techniques
A naive beginning (2015)
● Socket.io
● Not authoritative
● Synchronization issues between clients
Server
Broadcasts messages back
to clients
Client 1.
State
Client 2.
State
Client 3.
State
100ms 100ms 150ms
A naive beginning (2015)
Not authoritative
A naive beginning (2015)
All I wanted
● Be able to handle multiple game sessions
● Manipulate data structures in the server
○ Have them automatically synchronized with the clients
Colyseus
Why Colyseus?
● Multiplayer games should be easier to make
● Should be more accessible
● Should work on as many platforms as possible
● Should be open-source!
Client-side Integration
● HTML5 (JavaScript / TypeScript)
● Unity3D (C#)
● Defold Engine (LUA)
● Haxe
● Cocos2d-X (C++)
How Online Multiplayer Games Work?
● Peer-to-peer
● Client as a host
● Hybrid (of two above)
● Client / Server
Server
Dumb client Dumb client Dumb client
Responsibilities of the Server
● Hold the Game State
● Hold the Game Logic
● Validate client inputs
● Send the Game State to the clients
Server
Dumb client Dumb client Dumb client
Responsibilities of the Client
● Visual representation
● Apply game state updates in a pleasant manner
● Send inputs (actions) to the Server
Server
Dumb client Dumb client Dumb client
Authoritative Game Servers
My new position is
now [x, y]
*mxlvl.com: This game was made by @x100
Authoritative Game Servers
My new position is
now [x, y]
*mxlvl.com: This game was made by @x100
Authoritative Game Servers
I’m pointing at angle
X and moving
forward
*mxlvl.com: This game was made by @x100
Authoritative Game Servers
I’m pointing at angle
X and moving
forward
*mxlvl.com: This game was made by @x100
Colyseus
● Uses HTTP/WebSockets
● Game Rooms
● Match-making
● Room State Serialization
● Scalability*
Colyseus
Game Rooms
Colyseus
Game Rooms
The match-maker has
instantiated this room.
Rooms are in-memory, and
live on a particular Node.js
process
Colyseus
Game Rooms
Client is asking to join
into this room
Colyseus
Game Rooms
Client sent a message to
this room
Colyseus
Game Rooms
Client has been
disconnected
(or left intentionally)
Colyseus
Game Rooms
The room is being
destroyed.
(It’s a good place to persist
things on the database)
Rooms are disposed
automatically when the last
client disconnects
(unless autoDispose=false)
Colyseus
Game Rooms
Colyseus
Game Rooms
Unique identifier for
this room type
Colyseus
Game Rooms
● Rooms are created during matchmaking
● A client can be connected to multiple rooms
● Each room connection has its own WebSocket connection
● client.join( “game”, { options } )
● client.create( “game”, { options } )
● client.joinOrCreate( “game”, { options } )
● client.joinById( roomId, { options } )
● client.reconnect( roomId, sessionId )
Colyseus
Matchmaking (Client-side)
Colyseus
Matchmaking (Client-side)
try {
const room = await client.joinOrCreate("game", {
name: "Jake Badlands"
});
} catch (e) {
console.log("Could not join!");
console.error(e);
}
Colyseus
Matchmaking
ient.joinOrCreate("game", {
s"
t join!");
class MyRoom extends Room {
maxClients = 4;
async onAuth(client, options) {
// retrieve user data from auth provider
return true;
}
onJoin(client, options) {
// mutate the this.state
console.log(options.name, "joined!");
}
}
Client-side: Server-side:
http: request seat reservation
roomId / sessionId
WebSocket request
Connection established
Colyseus
Matchmaking
Full Room’s State
... State patches
Connection established
Colyseus
Room State & Serialization
... State patches
● The Room State is MUTABLE
● Patches are broadcasted at every 50ms
○ Customizable via this.setPatchRate(ms)
( But first, a bit of a background… )
Colyseus
Room State & Serialization
👎 Serialization: back in v0.1 ~ v0.3 👎
● Deep JavaScript Object Comparison
● JSON Patch (RFC6902)
● Patch size is too large
[
{ "op": "remove", "path": "/players/N150OHMve" },
{ "op": "add", "path": "/players/NkfeqSGPx", "value": {"x": 10, "y": 10} },
{ "op": "replace", "path": "/players/NkfeqSGPx/x", "value": 5 }
]
Previous State ⇔ Current State
👎 Serialization: back in v0.4 ~ v0.9 👎
● “Fossil Delta” Binary Diff Compression
● Hard to detect a change in the state
● Creates a new copy when decoding
var currentState = this.state;
var currentStateEncoded = msgpack.encode( currentState );
// skip if state has not changed.
if ( currentStateEncoded.equals( this._previousStateEncoded ) ) {
return false;
}
var patches = delta.create(this._previousStateEncoded, currentStateEncoded);
CPU Intensive
Serialization: v0.10+
● @colyseus/schema
● Schema-based
● Strongly-typed
● Binary serialization
● Inspired by protobuf / flatbuffers / avro
Serialization: v0.10+
● @colyseus/schema
● Schema-based
● Strongly-typed
● Binary serialization
● Inspired by protobuf / flatbuffers / avro
🔲 Encode/Decode only fields that have changed
🔲 No bottleneck to detect state changes
🔲 Mutations should be cheap
🔲 Avoid decoding large objects that haven't been patched
🔲 Better developer experience on statically-typed languages (C#, C++)
Serialization: v0.10+
Checklist
✅ Encode/Decode only fields that have changed
✅ No bottleneck to detect state changes
❓ Mutations should be cheap (need more benchmarks, so far seems ✅)
✅ Avoid decoding large objects that haven't been patched
✅ Better developer experience on statically-typed languages (C#, C++)
Serialization: v0.10+
Checklist
class Player extends Schema {
@type("number") x: number;
@type("number") y: number;
}
const player = new Player();
player.x = 10;
player.y = 20;
player.encode()
[ 0, 10, 1, 20 ]
Definition Usage
Serialization: v0.10+
Demo
class Player extends Schema {
@type("number") x: number;
@type("number") y: number;
}
const player = new Player();
player.x = 10;
player.y = 20;
player.encode()
[ 0, 10, 1, 20 ]
Definition Usage
Serialization: v0.10+
Demo
class Player extends Schema {
@type("number") x: number;
@type("number") y: number;
}
const player = new Player();
player.x = 10;
player.y = 20;
player.encode()
[ 0, 10, 1, 20 ]
Definition Usage
Serialization: v0.10+
Demo
@colyseus/schema
Colyseus
Room State & Serialization
import { Schema, type, MapSchema } from "@colyseus/schema";
class Player extends Schema {
@type("number")
position: number = 0;
}
class GameState extends Schema {
@type({ map: Player })
players = new MapSchema<Player>();
}
Game State Example
class MyRoom extends Room {
onCreate(options) {
this.setState(new GameState());
}
onJoin(client, options) {
this.state.players[client.sessionId] = new Player();
}
onMessage(client, message) {
if (message.move) {
this.state.players[client.sessionId].position++;
}
Colyseus
Room State & Serialization
Room State Example
Colyseus
Room State & Serialization
const room = await client.joinOrCreate("game");
room.onStateChange((state) => {
console.log("You are:", state.players[ room.sessionId ])
});
Client-side: Receiving Patches
Colyseus
Room State & Serialization
const room = await client.joinOrCreate("game");
room.onStateChange((state) => {
console.log("You are:", state.players[ room.sessionId ])
});
Client-side: Receiving Patches
What exactly has changed?
Colyseus
Room State & Serialization
const room = await client.joinOrCreate("game");
room.state.players.onAdd = (player, sessionId) => {
console.log("player joined", sessionId);
player.listen("position", (position) => {
console.log("position changed", position);
});
}
room.state.players.onRemove = (player, sessionId) {
console.log("player left", sessionId);
}
Colyseus
Room State & Serialization
const room = await client.joinOrCreate("game");
room.state.players.onAdd = (player, sessionId) => {
console.log("player joined", sessionId);
player.listen("position", (position) => {
console.log("position changed", position);
});
}
room.state.players.onRemove = (player, sessionId) {
console.log("player left", sessionId);
}
Player joined
Player left
Colyseus
Room State & Serialization
const room = await client.joinOrCreate("game");
room.state.players.onAdd = (player, sessionId) => {
console.log("player joined", sessionId);
player.listen("position", (position) => {
console.log("position changed", position);
});
}
room.state.players.onRemove = (player, sessionId) {
console.log("player left", sessionId);
}
Particular
property
changes
Colyseus
Room State & Serialization
const room = await client.joinOrCreate<GameState>("game");
room.state.players.onAdd = (player, sessionId) => {
console.log("player joined", sessionId);
player.listen("position", (position) => {
console.log("position changed", position);
});
}
room.state.players.onRemove = (player, sessionId) {
console.log("player left", sessionId);
}
Autocompletion
💖 TypeScript 💖
Colyseus
Room State & Serialization
const room = await client.joinOrCreate("game");
room.state.players.onAdd = (player, sessionId) => {
console.log("player joined", sessionId);
player.listen("position", (position) => {
console.log("position changed", position);
});
}
room.state.players.onRemove = (player, sessionId) {
console.log("player left", sessionId);
}
(Consistency?!)
● By default, the entire state is sent to everyone
○ This way no “private” data can be stored in the state
● @filter() let you define a filter callback per property, per client
Colyseus
State Filters! (Experimental)
Colyseus
State Filters! (Experimental)
class Entity extends Schema {
@filter(function(client, value, root) {
// this = instance of Entity
// client = the client which this value is being filtered for
// value = `secret` value
// root = the Root State
return false;
})
@type("number") secret: number;
}
Colyseus
State Filters! (Experimental)
class Entity extends Schema {
@filter(function(client, value, root) {
// this = instance of Entity
// client = the client which this value is being filtered for
// value = `secret` value
// root = the Root State
return false;
})
@type("number") secret: number;
}
Colyseus
State Filters! (Experimental)
class Entity extends Schema {
@filter(function(client, value, root) {
// this = instance of Entity
// client = the client which this value is being filtered for
// value = `secret` value
// root = the Root State
return false;
})
@type("number") secret: number;
}
Colyseus
State Filters! (Experimental)
class Card extends Schema {
@filter(function(this: Card, client, value, root?: RootState) {
return root.
players[client.sessionId].
cards.indexOf(this) !== -1;
})
@type("number") number: number;
}
Card Game Example:
Only Card owners can see their own Card data
Colyseus
State Filters! (Experimental)
class Player extends Schema {
sessionId: string;
@filter(function(this: Player, client: any, value: Card) {
return this.sessionId === client.sessionId;
})
@type([Card]) cards = new ArraySchema<Card>();
}
⚠️ Array and Map filters are not currently supported ⚠️
(Planned for version 1.0)
Colyseus
State Filters! (Experimental)
class State extends Schema {
@filter(function(this: State, client: any, value: Player) {
const player = this.players[client.sessionId]
const a = value.x - player.x;
const b = value.y - player.y;
return (Math.sqrt(a * a + b * b)) <= 10;
})
@type({ map: Player }) players = new MapSchema<Player>();
}
⚠️ Array and Map filters are not currently supported ⚠️
(Planned for version 1.0)
using Colyseus.Schema;
public class State : Schema {
[Type("string")]
public string fieldString = "";
[Type("number")]
public float fieldNumber = 0;
[Type("ref", typeof(Player))]
public Player player = new Player(
[Type("array", typeof(ArraySchema<
public ArraySchema<Player> arrayOf
ArraySchema<Player>();
[Type("map", typeof(MapSchema<Play
Client-side integration (C#)
● npx schema-codegen 
State.ts 
--csharp 
--output client-state
generated: State.cs
Colyseus
Room State & Serialization
using namespace colyseus::schema;
class State : public Schema {
public:
string fieldString = "";
varint_t fieldNumber = 0;
Player *player = new Player();
ArraySchema<Player*> *arrayOfPlaye
ArraySchema<Player*>();
MapSchema<Player*> *mapOfPlayers =
State() {
this->_indexes = {{0, "fiel
this->_types = {{0, "strin
this->_childPrimitiveTypes
Client-side integration (C++)
● npx schema-codegen 
State.ts 
--cpp 
--output client-state
generated: State.hpp
Colyseus
Room State & Serialization
import io.colyseus.serializer.schema.Schema;
class State extends Schema {
@:type("string")
public var fieldString: String = ""
@:type("number")
public var fieldNumber: Dynamic = 0
@:type("ref", Player)
public var player: Player = new Pla
@:type("array", Player)
public var arrayOfPlayers: ArraySch
Client-side integration (Haxe)
● npx schema-codegen 
State.ts 
--haxe 
--output client-state
generated: State.hx
Colyseus
Room State & Serialization
Colyseus
Sending Messages
● Messages use MsgPack by default
room.onMessage((message) => {
console.log("received:", message)
})
room.send({ action: "hello" })
Client-side
onMessage(client, message) {
if (message.action === "hello") {
this.broadcast("world!");
this.send(client, "world!");
}
}
Server-side
Colyseus
Sending Messages
● Messages use MsgPack by default
room.onMessage((message) => {
console.log("received:", message)
})
room.send({ action: "hello" })
Client-side
onMessage(client, message) {
if (message.action === "hello") {
this.broadcast("world!");
this.send(client, "world!");
}
}
Server-side
Send message to everyone
Colyseus
Sending Messages
● Messages use MsgPack by default
room.onMessage((message) => {
console.log("received:", message)
})
room.send({ action: "hello" })
Client-side
onMessage(client, message) {
if (message.action === "hello") {
this.broadcast("world!");
this.send(client, "world!");
}
}
Server-side
Send message to a single
client
Colyseus
Handling reconnection
● What if one of my clients drops the connection?
○ Closed/Refreshed Browser
○ Switched from Wifi / 3G
○ Unstable Internet Connection
○ Etc.
class MyRoom extends Room {
async onLeave (client, consented: boolean) {
try {
if (consented) {
throw new Error("consented leave");
}
await this.allowReconnection(client, 20);
console.log("Client successfully reconnected!");
} catch (e) {
console.log("Could not reconnect.");
}
}
Colyseus
Handling reconnection (Server-side)
class MyRoom extends Room {
async onLeave (client, consented: boolean) {
try {
if (consented) {
throw new Error("consented leave");
}
await this.allowReconnection(client, 20);
console.log("Client successfully reconnected!");
} catch (e) {
console.log("Could not reconnect.");
}
}
Colyseus
Handling reconnection (Server-side)
room.leave() was
called from the
client
class MyRoom extends Room {
async onLeave (client, consented: boolean) {
try {
if (consented) {
throw new Error("consented leave");
}
await this.allowReconnection(client, 20);
console.log("Client successfully reconnected!");
} catch (e) {
console.log("Could not reconnect.");
}
}
Colyseus
Handling reconnection (Server-side)
Hold client’s seat
reservation
(sessionId)
for 20 seconds
class MyRoom extends Room {
async onLeave (client, consented: boolean) {
try {
if (consented) {
throw new Error("consented leave");
}
await this.allowReconnection(client, 20);
console.log("Client successfully reconnected!");
} catch (e) {
console.log("Could not reconnect.");
}
}
Colyseus
Handling reconnection (Server-side)
No reconnection
in 20 seconds,
promise rejected
class MyRoom extends Room {
async onLeave (client, consented: boolean) {
try {
if (consented) {
throw new Error("consented leave");
}
await this.allowReconnection(client, 20);
console.log("Client successfully reconnected!");
} catch (e) {
console.log("Could not reconnect.");
}
}
Colyseus
Handling reconnection (Server-side)
Reconnected!
const room = await client.joinOrCreate("game", {});
// Cache roomId and sessionId
localStorage.setItem("lastRoomId", room.id);
localStorage.setItem("lastSessionId", room.sessionId);
// (CLOSE BROWSER TAB)
const lastRoomId = localStorage.getItem("lastRoomId");
const lastSessionId = localStorage.getItem("lastSessionId");
const room = await client.reconnect(lastRoomId, lastSessionId);
Colyseus
Handling reconnection (Client-side)
const room = await client.joinOrCreate("game", {});
// Cache roomId and sessionId
localStorage.setItem("lastRoomId", room.id);
localStorage.setItem("lastSessionId", room.sessionId);
// (CLOSE BROWSER TAB)
const lastRoomId = localStorage.getItem("lastRoomId");
const lastSessionId = localStorage.getItem("lastSessionId");
const room = await client.reconnect(lastRoomId, lastSessionId);
Colyseus
Handling reconnection (Client-side)
Client-side Techniques
Client Server
p = [10, 10] p = [10, 10]
p = [11, 10]
p = [11, 10]
Delay
(~100ms)
Client-side Techniques
Client Server
p = [10, 10] p = [10, 10]
p = [11, 10]
p = [11, 10]
Delay
(~100ms)
Client-side Techniques
● Linear Interpolation
(Lerp)
https://mazmorra.io
tick() {
sprite.x = lerp(sprite.x, serverX, 0.07);
sprite.y = lerp(sprite.y, serverY, 0.07);
}
Client-side Techniques
● Delay to process actions
● Simulation on other client
happens 1 second later
https://poki.com/en/g/raft-wars-multiplayer
Client-side Techniques
● Client-side prediction
● Process player’s input immediately
● Enqueue player’s actions
in the server
● Process the queue at every
server tick
https://github.com/halftheopposite/tosios
Infrastructure
● How many CCU can Colyseus handle?
○ It depends!
● You have CPU and Memory limits
○ Optimize Your Game Loop
● 1000 clients in a single room?
○ Probably no!
● 1000 clients distributed across many rooms?
○ Probably yes!
Scalability ?
Scalability !
Scalability
● Colyseus’ rooms are STATEFUL
● Games are (generally) STATEFUL
● Rooms are located on a specific server and/or process.
Scalability
Colyseus
Communication between Game Rooms
onCreate() {
/**
* Pub/Sub
*/
this.presence.subscribe("global-action", (data) => {
console.log(data);
});
this.presence.publish("global-action", "hello!");
/**
* "Remote Procedure Call"
*/
matchMaker.remoteRoomCall(roomId, "methodName", [...])
}
Colyseus
Tools
@colyseus/monitor
Colyseus
Tools
@colyseus/loadtest
$ npx colyseus-loadtest bot.ts --room my_room --numClients 10
● Realtime
● Phaser/JavaScript
● Built by @tinydobbings
● ~350 concurrent players
Built with Colyseus
GunFight.io
https://gunfight.io
● Turn-based
● JavaScript version
● Defold Engine version (@selimanac)
Built with Colyseus
Tic-Tac-Toe
https://github.com/endel/colyseus-tic-tac-toe
● “The Open-Source IO Shooter”
● Realtime shooter
● PixiJS / TypeScript
● Built by @halftheopposite
Built with Colyseus
TOSIOS
https://github.com/halftheopposite/tosios
● Turn-based
● Defold Engine / LUA
● ~250 concurrent players
● (soon on mobile!)
Built with Colyseus
Raft Wars Multiplayer
https://poki.com/en/g/raft-wars-multiplayer
● Instant Game (Messenger)
● Realtime
Built with Colyseus
Chaos Shot
https://www.facebook.com/ChaosShot.io/
Plans for v1.0
● Better State Filters (on Arrays and Maps)
● Allow more sophisticated matchmaking
● Transport-independent (TCP/UDP)
● ...while keeping all client-side integrations up-to-date!
Thank You! ❤
● Twitter/GitHub: @endel

Weitere ähnliche Inhalte

Ähnlich wie NodeAtlanta 2020 - Making Multiplayer Games with Colyseus and Node.js.pptx

Mobile webapplication development
Mobile webapplication developmentMobile webapplication development
Mobile webapplication development
Ganesh Gembali
 
Realtime html5 multiplayer_games_with_node_js
Realtime html5 multiplayer_games_with_node_jsRealtime html5 multiplayer_games_with_node_js
Realtime html5 multiplayer_games_with_node_js
Mario Gonzalez
 

Ähnlich wie NodeAtlanta 2020 - Making Multiplayer Games with Colyseus and Node.js.pptx (20)

NetRacer for the Commodore 64
NetRacer for the Commodore 64NetRacer for the Commodore 64
NetRacer for the Commodore 64
 
TDC2017 | São Paulo - Trilha Programação Funcional How we figured out we had ...
TDC2017 | São Paulo - Trilha Programação Funcional How we figured out we had ...TDC2017 | São Paulo - Trilha Programação Funcional How we figured out we had ...
TDC2017 | São Paulo - Trilha Programação Funcional How we figured out we had ...
 
Akka for realtime multiplayer mobile games
Akka for realtime multiplayer mobile gamesAkka for realtime multiplayer mobile games
Akka for realtime multiplayer mobile games
 
Mobile webapplication development
Mobile webapplication developmentMobile webapplication development
Mobile webapplication development
 
Realtime html5 multiplayer_games_with_node_js
Realtime html5 multiplayer_games_with_node_jsRealtime html5 multiplayer_games_with_node_js
Realtime html5 multiplayer_games_with_node_js
 
Game Programming - Cloud Development
Game Programming - Cloud DevelopmentGame Programming - Cloud Development
Game Programming - Cloud Development
 
Multiplayer Java Game
Multiplayer Java GameMultiplayer Java Game
Multiplayer Java Game
 
Building fast,scalable game server in node.js
Building fast,scalable game server in node.jsBuilding fast,scalable game server in node.js
Building fast,scalable game server in node.js
 
RandomGuessingGame
RandomGuessingGameRandomGuessingGame
RandomGuessingGame
 
Realtime html5 multiplayer_games_with_node_js
Realtime html5 multiplayer_games_with_node_jsRealtime html5 multiplayer_games_with_node_js
Realtime html5 multiplayer_games_with_node_js
 
Clojure ♥ cassandra
Clojure ♥ cassandra Clojure ♥ cassandra
Clojure ♥ cassandra
 
On Space-Scarce Economy In Blockchain Systems
On Space-Scarce Economy In Blockchain SystemsOn Space-Scarce Economy In Blockchain Systems
On Space-Scarce Economy In Blockchain Systems
 
Game server development in node.js in jsconf eu
Game server development in node.js in jsconf euGame server development in node.js in jsconf eu
Game server development in node.js in jsconf eu
 
Building Multiplayer Games (w/ Unity)
Building Multiplayer Games (w/ Unity)Building Multiplayer Games (w/ Unity)
Building Multiplayer Games (w/ Unity)
 
Monte Carlo C++
Monte Carlo C++Monte Carlo C++
Monte Carlo C++
 
BINARY DATA ADVENTURES IN BROWSER JAVASCRIPT
BINARY DATA ADVENTURES  IN BROWSER JAVASCRIPTBINARY DATA ADVENTURES  IN BROWSER JAVASCRIPT
BINARY DATA ADVENTURES IN BROWSER JAVASCRIPT
 
Parallel Futures of a Game Engine
Parallel Futures of a Game EngineParallel Futures of a Game Engine
Parallel Futures of a Game Engine
 
Akka for realtime multiplayer mobile games
Akka for realtime multiplayer mobile gamesAkka for realtime multiplayer mobile games
Akka for realtime multiplayer mobile games
 
The Challenge of Bringing FEZ to PlayStation Platforms
The Challenge of Bringing FEZ to PlayStation PlatformsThe Challenge of Bringing FEZ to PlayStation Platforms
The Challenge of Bringing FEZ to PlayStation Platforms
 
Game programming-help
Game programming-helpGame programming-help
Game programming-help
 

Kürzlich hochgeladen

ALCOHOL PRODUCTION- Beer Brewing Process.pdf
ALCOHOL PRODUCTION- Beer Brewing Process.pdfALCOHOL PRODUCTION- Beer Brewing Process.pdf
ALCOHOL PRODUCTION- Beer Brewing Process.pdf
Madan Karki
 
Activity Planning: Objectives, Project Schedule, Network Planning Model. Time...
Activity Planning: Objectives, Project Schedule, Network Planning Model. Time...Activity Planning: Objectives, Project Schedule, Network Planning Model. Time...
Activity Planning: Objectives, Project Schedule, Network Planning Model. Time...
Lovely Professional University
 

Kürzlich hochgeladen (20)

EMPLOYEE MANAGEMENT SYSTEM FINAL presentation
EMPLOYEE MANAGEMENT SYSTEM FINAL presentationEMPLOYEE MANAGEMENT SYSTEM FINAL presentation
EMPLOYEE MANAGEMENT SYSTEM FINAL presentation
 
Insurance management system project report.pdf
Insurance management system project report.pdfInsurance management system project report.pdf
Insurance management system project report.pdf
 
Lesson no16 application of Induction Generator in Wind.ppsx
Lesson no16 application of Induction Generator in Wind.ppsxLesson no16 application of Induction Generator in Wind.ppsx
Lesson no16 application of Induction Generator in Wind.ppsx
 
Artificial Intelligence Bayesian Reasoning
Artificial Intelligence Bayesian ReasoningArtificial Intelligence Bayesian Reasoning
Artificial Intelligence Bayesian Reasoning
 
ALCOHOL PRODUCTION- Beer Brewing Process.pdf
ALCOHOL PRODUCTION- Beer Brewing Process.pdfALCOHOL PRODUCTION- Beer Brewing Process.pdf
ALCOHOL PRODUCTION- Beer Brewing Process.pdf
 
analog-vs-digital-communication (concept of analog and digital).pptx
analog-vs-digital-communication (concept of analog and digital).pptxanalog-vs-digital-communication (concept of analog and digital).pptx
analog-vs-digital-communication (concept of analog and digital).pptx
 
Seismic Hazard Assessment Software in Python by Prof. Dr. Costas Sachpazis
Seismic Hazard Assessment Software in Python by Prof. Dr. Costas SachpazisSeismic Hazard Assessment Software in Python by Prof. Dr. Costas Sachpazis
Seismic Hazard Assessment Software in Python by Prof. Dr. Costas Sachpazis
 
Intelligent Agents, A discovery on How A Rational Agent Acts
Intelligent Agents, A discovery on How A Rational Agent ActsIntelligent Agents, A discovery on How A Rational Agent Acts
Intelligent Agents, A discovery on How A Rational Agent Acts
 
How to Design and spec harmonic filter.pdf
How to Design and spec harmonic filter.pdfHow to Design and spec harmonic filter.pdf
How to Design and spec harmonic filter.pdf
 
RM&IPR M5 notes.pdfResearch Methodolgy & Intellectual Property Rights Series 5
RM&IPR M5 notes.pdfResearch Methodolgy & Intellectual Property Rights Series 5RM&IPR M5 notes.pdfResearch Methodolgy & Intellectual Property Rights Series 5
RM&IPR M5 notes.pdfResearch Methodolgy & Intellectual Property Rights Series 5
 
SLIDESHARE PPT-DECISION MAKING METHODS.pptx
SLIDESHARE PPT-DECISION MAKING METHODS.pptxSLIDESHARE PPT-DECISION MAKING METHODS.pptx
SLIDESHARE PPT-DECISION MAKING METHODS.pptx
 
Instruct Nirmaana 24-Smart and Lean Construction Through Technology.pdf
Instruct Nirmaana 24-Smart and Lean Construction Through Technology.pdfInstruct Nirmaana 24-Smart and Lean Construction Through Technology.pdf
Instruct Nirmaana 24-Smart and Lean Construction Through Technology.pdf
 
Introduction to Artificial Intelligence and History of AI
Introduction to Artificial Intelligence and History of AIIntroduction to Artificial Intelligence and History of AI
Introduction to Artificial Intelligence and History of AI
 
Diploma Engineering Drawing Qp-2024 Ece .pdf
Diploma Engineering Drawing Qp-2024 Ece .pdfDiploma Engineering Drawing Qp-2024 Ece .pdf
Diploma Engineering Drawing Qp-2024 Ece .pdf
 
Quiz application system project report..pdf
Quiz application system project report..pdfQuiz application system project report..pdf
Quiz application system project report..pdf
 
ChatGPT Prompt Engineering for project managers.pdf
ChatGPT Prompt Engineering for project managers.pdfChatGPT Prompt Engineering for project managers.pdf
ChatGPT Prompt Engineering for project managers.pdf
 
Activity Planning: Objectives, Project Schedule, Network Planning Model. Time...
Activity Planning: Objectives, Project Schedule, Network Planning Model. Time...Activity Planning: Objectives, Project Schedule, Network Planning Model. Time...
Activity Planning: Objectives, Project Schedule, Network Planning Model. Time...
 
Supermarket billing system project report..pdf
Supermarket billing system project report..pdfSupermarket billing system project report..pdf
Supermarket billing system project report..pdf
 
NEWLETTER FRANCE HELICES/ SDS SURFACE DRIVES - MAY 2024
NEWLETTER FRANCE HELICES/ SDS SURFACE DRIVES - MAY 2024NEWLETTER FRANCE HELICES/ SDS SURFACE DRIVES - MAY 2024
NEWLETTER FRANCE HELICES/ SDS SURFACE DRIVES - MAY 2024
 
Online book store management system project.pdf
Online book store management system project.pdfOnline book store management system project.pdf
Online book store management system project.pdf
 

NodeAtlanta 2020 - Making Multiplayer Games with Colyseus and Node.js.pptx

  • 1. Making Online Multiplayer Game Experiences With Colyseus and Node.js This presentation is from Jan 2020. It was supposed to happen on Node Atlanta 2020, but unfortunately it got cancelled.
  • 2. Hi, I’m Endel 👋 👨💻 Software Engineer and Game Developer from Brazil 🎓 Game Development & Digital Entertainment GitHub / Twitter: @endel I DON’T LIKE SOCCER
  • 3. Summary ● Introduction ● Basic concepts ○ How Online Multiplayer Games Work ○ Authoritative Game Servers ● Colyseus in-depth ○ Match-making ○ Game State & Serialization ○ Infrastructure ● Client-side Techniques
  • 4. A naive beginning (2015) ● Socket.io ● Not authoritative ● Synchronization issues between clients
  • 5. Server Broadcasts messages back to clients Client 1. State Client 2. State Client 3. State 100ms 100ms 150ms A naive beginning (2015) Not authoritative
  • 6. A naive beginning (2015) All I wanted ● Be able to handle multiple game sessions ● Manipulate data structures in the server ○ Have them automatically synchronized with the clients
  • 8. Why Colyseus? ● Multiplayer games should be easier to make ● Should be more accessible ● Should work on as many platforms as possible ● Should be open-source!
  • 9. Client-side Integration ● HTML5 (JavaScript / TypeScript) ● Unity3D (C#) ● Defold Engine (LUA) ● Haxe ● Cocos2d-X (C++)
  • 10. How Online Multiplayer Games Work? ● Peer-to-peer ● Client as a host ● Hybrid (of two above) ● Client / Server Server Dumb client Dumb client Dumb client
  • 11. Responsibilities of the Server ● Hold the Game State ● Hold the Game Logic ● Validate client inputs ● Send the Game State to the clients Server Dumb client Dumb client Dumb client
  • 12. Responsibilities of the Client ● Visual representation ● Apply game state updates in a pleasant manner ● Send inputs (actions) to the Server Server Dumb client Dumb client Dumb client
  • 13. Authoritative Game Servers My new position is now [x, y] *mxlvl.com: This game was made by @x100
  • 14. Authoritative Game Servers My new position is now [x, y] *mxlvl.com: This game was made by @x100
  • 15. Authoritative Game Servers I’m pointing at angle X and moving forward *mxlvl.com: This game was made by @x100
  • 16. Authoritative Game Servers I’m pointing at angle X and moving forward *mxlvl.com: This game was made by @x100
  • 17. Colyseus ● Uses HTTP/WebSockets ● Game Rooms ● Match-making ● Room State Serialization ● Scalability*
  • 19. Colyseus Game Rooms The match-maker has instantiated this room. Rooms are in-memory, and live on a particular Node.js process
  • 20. Colyseus Game Rooms Client is asking to join into this room
  • 21. Colyseus Game Rooms Client sent a message to this room
  • 22. Colyseus Game Rooms Client has been disconnected (or left intentionally)
  • 23. Colyseus Game Rooms The room is being destroyed. (It’s a good place to persist things on the database) Rooms are disposed automatically when the last client disconnects (unless autoDispose=false)
  • 26. Colyseus Game Rooms ● Rooms are created during matchmaking ● A client can be connected to multiple rooms ● Each room connection has its own WebSocket connection
  • 27. ● client.join( “game”, { options } ) ● client.create( “game”, { options } ) ● client.joinOrCreate( “game”, { options } ) ● client.joinById( roomId, { options } ) ● client.reconnect( roomId, sessionId ) Colyseus Matchmaking (Client-side)
  • 28. Colyseus Matchmaking (Client-side) try { const room = await client.joinOrCreate("game", { name: "Jake Badlands" }); } catch (e) { console.log("Could not join!"); console.error(e); }
  • 29. Colyseus Matchmaking ient.joinOrCreate("game", { s" t join!"); class MyRoom extends Room { maxClients = 4; async onAuth(client, options) { // retrieve user data from auth provider return true; } onJoin(client, options) { // mutate the this.state console.log(options.name, "joined!"); } } Client-side: Server-side:
  • 30. http: request seat reservation roomId / sessionId WebSocket request Connection established Colyseus Matchmaking
  • 31. Full Room’s State ... State patches Connection established Colyseus Room State & Serialization ... State patches
  • 32. ● The Room State is MUTABLE ● Patches are broadcasted at every 50ms ○ Customizable via this.setPatchRate(ms) ( But first, a bit of a background… ) Colyseus Room State & Serialization
  • 33. 👎 Serialization: back in v0.1 ~ v0.3 👎 ● Deep JavaScript Object Comparison ● JSON Patch (RFC6902) ● Patch size is too large [ { "op": "remove", "path": "/players/N150OHMve" }, { "op": "add", "path": "/players/NkfeqSGPx", "value": {"x": 10, "y": 10} }, { "op": "replace", "path": "/players/NkfeqSGPx/x", "value": 5 } ] Previous State ⇔ Current State
  • 34. 👎 Serialization: back in v0.4 ~ v0.9 👎 ● “Fossil Delta” Binary Diff Compression ● Hard to detect a change in the state ● Creates a new copy when decoding var currentState = this.state; var currentStateEncoded = msgpack.encode( currentState ); // skip if state has not changed. if ( currentStateEncoded.equals( this._previousStateEncoded ) ) { return false; } var patches = delta.create(this._previousStateEncoded, currentStateEncoded); CPU Intensive
  • 35. Serialization: v0.10+ ● @colyseus/schema ● Schema-based ● Strongly-typed ● Binary serialization ● Inspired by protobuf / flatbuffers / avro
  • 36. Serialization: v0.10+ ● @colyseus/schema ● Schema-based ● Strongly-typed ● Binary serialization ● Inspired by protobuf / flatbuffers / avro
  • 37. 🔲 Encode/Decode only fields that have changed 🔲 No bottleneck to detect state changes 🔲 Mutations should be cheap 🔲 Avoid decoding large objects that haven't been patched 🔲 Better developer experience on statically-typed languages (C#, C++) Serialization: v0.10+ Checklist
  • 38. ✅ Encode/Decode only fields that have changed ✅ No bottleneck to detect state changes ❓ Mutations should be cheap (need more benchmarks, so far seems ✅) ✅ Avoid decoding large objects that haven't been patched ✅ Better developer experience on statically-typed languages (C#, C++) Serialization: v0.10+ Checklist
  • 39. class Player extends Schema { @type("number") x: number; @type("number") y: number; } const player = new Player(); player.x = 10; player.y = 20; player.encode() [ 0, 10, 1, 20 ] Definition Usage Serialization: v0.10+ Demo
  • 40. class Player extends Schema { @type("number") x: number; @type("number") y: number; } const player = new Player(); player.x = 10; player.y = 20; player.encode() [ 0, 10, 1, 20 ] Definition Usage Serialization: v0.10+ Demo
  • 41. class Player extends Schema { @type("number") x: number; @type("number") y: number; } const player = new Player(); player.x = 10; player.y = 20; player.encode() [ 0, 10, 1, 20 ] Definition Usage Serialization: v0.10+ Demo
  • 43. Colyseus Room State & Serialization import { Schema, type, MapSchema } from "@colyseus/schema"; class Player extends Schema { @type("number") position: number = 0; } class GameState extends Schema { @type({ map: Player }) players = new MapSchema<Player>(); } Game State Example
  • 44. class MyRoom extends Room { onCreate(options) { this.setState(new GameState()); } onJoin(client, options) { this.state.players[client.sessionId] = new Player(); } onMessage(client, message) { if (message.move) { this.state.players[client.sessionId].position++; } Colyseus Room State & Serialization Room State Example
  • 45. Colyseus Room State & Serialization const room = await client.joinOrCreate("game"); room.onStateChange((state) => { console.log("You are:", state.players[ room.sessionId ]) }); Client-side: Receiving Patches
  • 46. Colyseus Room State & Serialization const room = await client.joinOrCreate("game"); room.onStateChange((state) => { console.log("You are:", state.players[ room.sessionId ]) }); Client-side: Receiving Patches What exactly has changed?
  • 47. Colyseus Room State & Serialization const room = await client.joinOrCreate("game"); room.state.players.onAdd = (player, sessionId) => { console.log("player joined", sessionId); player.listen("position", (position) => { console.log("position changed", position); }); } room.state.players.onRemove = (player, sessionId) { console.log("player left", sessionId); }
  • 48. Colyseus Room State & Serialization const room = await client.joinOrCreate("game"); room.state.players.onAdd = (player, sessionId) => { console.log("player joined", sessionId); player.listen("position", (position) => { console.log("position changed", position); }); } room.state.players.onRemove = (player, sessionId) { console.log("player left", sessionId); } Player joined Player left
  • 49. Colyseus Room State & Serialization const room = await client.joinOrCreate("game"); room.state.players.onAdd = (player, sessionId) => { console.log("player joined", sessionId); player.listen("position", (position) => { console.log("position changed", position); }); } room.state.players.onRemove = (player, sessionId) { console.log("player left", sessionId); } Particular property changes
  • 50. Colyseus Room State & Serialization const room = await client.joinOrCreate<GameState>("game"); room.state.players.onAdd = (player, sessionId) => { console.log("player joined", sessionId); player.listen("position", (position) => { console.log("position changed", position); }); } room.state.players.onRemove = (player, sessionId) { console.log("player left", sessionId); } Autocompletion 💖 TypeScript 💖
  • 51. Colyseus Room State & Serialization const room = await client.joinOrCreate("game"); room.state.players.onAdd = (player, sessionId) => { console.log("player joined", sessionId); player.listen("position", (position) => { console.log("position changed", position); }); } room.state.players.onRemove = (player, sessionId) { console.log("player left", sessionId); } (Consistency?!)
  • 52. ● By default, the entire state is sent to everyone ○ This way no “private” data can be stored in the state ● @filter() let you define a filter callback per property, per client Colyseus State Filters! (Experimental)
  • 53. Colyseus State Filters! (Experimental) class Entity extends Schema { @filter(function(client, value, root) { // this = instance of Entity // client = the client which this value is being filtered for // value = `secret` value // root = the Root State return false; }) @type("number") secret: number; }
  • 54. Colyseus State Filters! (Experimental) class Entity extends Schema { @filter(function(client, value, root) { // this = instance of Entity // client = the client which this value is being filtered for // value = `secret` value // root = the Root State return false; }) @type("number") secret: number; }
  • 55. Colyseus State Filters! (Experimental) class Entity extends Schema { @filter(function(client, value, root) { // this = instance of Entity // client = the client which this value is being filtered for // value = `secret` value // root = the Root State return false; }) @type("number") secret: number; }
  • 56. Colyseus State Filters! (Experimental) class Card extends Schema { @filter(function(this: Card, client, value, root?: RootState) { return root. players[client.sessionId]. cards.indexOf(this) !== -1; }) @type("number") number: number; } Card Game Example: Only Card owners can see their own Card data
  • 57. Colyseus State Filters! (Experimental) class Player extends Schema { sessionId: string; @filter(function(this: Player, client: any, value: Card) { return this.sessionId === client.sessionId; }) @type([Card]) cards = new ArraySchema<Card>(); } ⚠️ Array and Map filters are not currently supported ⚠️ (Planned for version 1.0)
  • 58. Colyseus State Filters! (Experimental) class State extends Schema { @filter(function(this: State, client: any, value: Player) { const player = this.players[client.sessionId] const a = value.x - player.x; const b = value.y - player.y; return (Math.sqrt(a * a + b * b)) <= 10; }) @type({ map: Player }) players = new MapSchema<Player>(); } ⚠️ Array and Map filters are not currently supported ⚠️ (Planned for version 1.0)
  • 59. using Colyseus.Schema; public class State : Schema { [Type("string")] public string fieldString = ""; [Type("number")] public float fieldNumber = 0; [Type("ref", typeof(Player))] public Player player = new Player( [Type("array", typeof(ArraySchema< public ArraySchema<Player> arrayOf ArraySchema<Player>(); [Type("map", typeof(MapSchema<Play Client-side integration (C#) ● npx schema-codegen State.ts --csharp --output client-state generated: State.cs Colyseus Room State & Serialization
  • 60. using namespace colyseus::schema; class State : public Schema { public: string fieldString = ""; varint_t fieldNumber = 0; Player *player = new Player(); ArraySchema<Player*> *arrayOfPlaye ArraySchema<Player*>(); MapSchema<Player*> *mapOfPlayers = State() { this->_indexes = {{0, "fiel this->_types = {{0, "strin this->_childPrimitiveTypes Client-side integration (C++) ● npx schema-codegen State.ts --cpp --output client-state generated: State.hpp Colyseus Room State & Serialization
  • 61. import io.colyseus.serializer.schema.Schema; class State extends Schema { @:type("string") public var fieldString: String = "" @:type("number") public var fieldNumber: Dynamic = 0 @:type("ref", Player) public var player: Player = new Pla @:type("array", Player) public var arrayOfPlayers: ArraySch Client-side integration (Haxe) ● npx schema-codegen State.ts --haxe --output client-state generated: State.hx Colyseus Room State & Serialization
  • 62. Colyseus Sending Messages ● Messages use MsgPack by default room.onMessage((message) => { console.log("received:", message) }) room.send({ action: "hello" }) Client-side onMessage(client, message) { if (message.action === "hello") { this.broadcast("world!"); this.send(client, "world!"); } } Server-side
  • 63. Colyseus Sending Messages ● Messages use MsgPack by default room.onMessage((message) => { console.log("received:", message) }) room.send({ action: "hello" }) Client-side onMessage(client, message) { if (message.action === "hello") { this.broadcast("world!"); this.send(client, "world!"); } } Server-side Send message to everyone
  • 64. Colyseus Sending Messages ● Messages use MsgPack by default room.onMessage((message) => { console.log("received:", message) }) room.send({ action: "hello" }) Client-side onMessage(client, message) { if (message.action === "hello") { this.broadcast("world!"); this.send(client, "world!"); } } Server-side Send message to a single client
  • 65. Colyseus Handling reconnection ● What if one of my clients drops the connection? ○ Closed/Refreshed Browser ○ Switched from Wifi / 3G ○ Unstable Internet Connection ○ Etc.
  • 66. class MyRoom extends Room { async onLeave (client, consented: boolean) { try { if (consented) { throw new Error("consented leave"); } await this.allowReconnection(client, 20); console.log("Client successfully reconnected!"); } catch (e) { console.log("Could not reconnect."); } } Colyseus Handling reconnection (Server-side)
  • 67. class MyRoom extends Room { async onLeave (client, consented: boolean) { try { if (consented) { throw new Error("consented leave"); } await this.allowReconnection(client, 20); console.log("Client successfully reconnected!"); } catch (e) { console.log("Could not reconnect."); } } Colyseus Handling reconnection (Server-side) room.leave() was called from the client
  • 68. class MyRoom extends Room { async onLeave (client, consented: boolean) { try { if (consented) { throw new Error("consented leave"); } await this.allowReconnection(client, 20); console.log("Client successfully reconnected!"); } catch (e) { console.log("Could not reconnect."); } } Colyseus Handling reconnection (Server-side) Hold client’s seat reservation (sessionId) for 20 seconds
  • 69. class MyRoom extends Room { async onLeave (client, consented: boolean) { try { if (consented) { throw new Error("consented leave"); } await this.allowReconnection(client, 20); console.log("Client successfully reconnected!"); } catch (e) { console.log("Could not reconnect."); } } Colyseus Handling reconnection (Server-side) No reconnection in 20 seconds, promise rejected
  • 70. class MyRoom extends Room { async onLeave (client, consented: boolean) { try { if (consented) { throw new Error("consented leave"); } await this.allowReconnection(client, 20); console.log("Client successfully reconnected!"); } catch (e) { console.log("Could not reconnect."); } } Colyseus Handling reconnection (Server-side) Reconnected!
  • 71. const room = await client.joinOrCreate("game", {}); // Cache roomId and sessionId localStorage.setItem("lastRoomId", room.id); localStorage.setItem("lastSessionId", room.sessionId); // (CLOSE BROWSER TAB) const lastRoomId = localStorage.getItem("lastRoomId"); const lastSessionId = localStorage.getItem("lastSessionId"); const room = await client.reconnect(lastRoomId, lastSessionId); Colyseus Handling reconnection (Client-side)
  • 72. const room = await client.joinOrCreate("game", {}); // Cache roomId and sessionId localStorage.setItem("lastRoomId", room.id); localStorage.setItem("lastSessionId", room.sessionId); // (CLOSE BROWSER TAB) const lastRoomId = localStorage.getItem("lastRoomId"); const lastSessionId = localStorage.getItem("lastSessionId"); const room = await client.reconnect(lastRoomId, lastSessionId); Colyseus Handling reconnection (Client-side)
  • 73. Client-side Techniques Client Server p = [10, 10] p = [10, 10] p = [11, 10] p = [11, 10] Delay (~100ms)
  • 74. Client-side Techniques Client Server p = [10, 10] p = [10, 10] p = [11, 10] p = [11, 10] Delay (~100ms)
  • 75. Client-side Techniques ● Linear Interpolation (Lerp) https://mazmorra.io tick() { sprite.x = lerp(sprite.x, serverX, 0.07); sprite.y = lerp(sprite.y, serverY, 0.07); }
  • 76. Client-side Techniques ● Delay to process actions ● Simulation on other client happens 1 second later https://poki.com/en/g/raft-wars-multiplayer
  • 77. Client-side Techniques ● Client-side prediction ● Process player’s input immediately ● Enqueue player’s actions in the server ● Process the queue at every server tick https://github.com/halftheopposite/tosios
  • 78. Infrastructure ● How many CCU can Colyseus handle? ○ It depends! ● You have CPU and Memory limits ○ Optimize Your Game Loop ● 1000 clients in a single room? ○ Probably no! ● 1000 clients distributed across many rooms? ○ Probably yes!
  • 81. Scalability ● Colyseus’ rooms are STATEFUL ● Games are (generally) STATEFUL ● Rooms are located on a specific server and/or process.
  • 83. Colyseus Communication between Game Rooms onCreate() { /** * Pub/Sub */ this.presence.subscribe("global-action", (data) => { console.log(data); }); this.presence.publish("global-action", "hello!"); /** * "Remote Procedure Call" */ matchMaker.remoteRoomCall(roomId, "methodName", [...]) }
  • 85. Colyseus Tools @colyseus/loadtest $ npx colyseus-loadtest bot.ts --room my_room --numClients 10
  • 86. ● Realtime ● Phaser/JavaScript ● Built by @tinydobbings ● ~350 concurrent players Built with Colyseus GunFight.io https://gunfight.io
  • 87. ● Turn-based ● JavaScript version ● Defold Engine version (@selimanac) Built with Colyseus Tic-Tac-Toe https://github.com/endel/colyseus-tic-tac-toe
  • 88. ● “The Open-Source IO Shooter” ● Realtime shooter ● PixiJS / TypeScript ● Built by @halftheopposite Built with Colyseus TOSIOS https://github.com/halftheopposite/tosios
  • 89. ● Turn-based ● Defold Engine / LUA ● ~250 concurrent players ● (soon on mobile!) Built with Colyseus Raft Wars Multiplayer https://poki.com/en/g/raft-wars-multiplayer
  • 90. ● Instant Game (Messenger) ● Realtime Built with Colyseus Chaos Shot https://www.facebook.com/ChaosShot.io/
  • 91. Plans for v1.0 ● Better State Filters (on Arrays and Maps) ● Allow more sophisticated matchmaking ● Transport-independent (TCP/UDP) ● ...while keeping all client-side integrations up-to-date!
  • 92. Thank You! ❤ ● Twitter/GitHub: @endel

Hinweis der Redaktion

  1. This game was made using Colyseus by @x100, a community member
  2. This is how a room definition looks like All these methods can be async
  3. I’ll talk a bit about state serialization before going into handling the game state I hope it’s not going to be too much information, because it’s kind of a deep topic
  4. Fossil Delta is really good for small binary diffs CPU Intensive detecting changes Explain client-side new allocations and garbage collection
  5. Why would you re-implement these great tools Incremental encoding
  6. This is the checklist I had before going ahead and implementing the serializer
  7. Because we’re using Proxies to be able to catch changes on the state, mutations have this cost
  8. The .encode() method is just to illustrate how it works, you usually don’t need to call it manually, as the room does automatically at the patch interval
  9. The .encode() method is just to illustrate how it works, you usually don’t need to call it manually, as the room does automatically at the patch interval
  10. The .encode() method is just to illustrate how it works, you usually don’t need to call it manually, as the room does automatically at the patch interval
  11. Has been released early last year I’ve a good chunk of time last year fixing bugs on it, and improving things
  12. I’ll talk a bit about state serialization before going into handling the game state
  13. During onStateChange, you can’t really know exactly what has changed
  14. During onStateChange, you can’t really know exactly what has changed
  15. Mention that “player” references can be passed around, as it is going to be always the same
  16. Here you actually get autocompletion for the field, and the callback will have the right type you’re listening for
  17. Filters are experimental, because their performance is not that great currently
  18. This is the signature of the @filter callback
  19. Delays are
  20. This is a trickier one. Mention turn-based
  21. This is a trickier one.