前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(下)

ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(下)

作者头像
为少
发布于 2021-05-27 10:49:51
发布于 2021-05-27 10:49:51
2.7K00
代码可运行
举报
文章被收录于专栏:黑客下午茶黑客下午茶
运行总次数:0
代码可运行

快速上手多人游戏服务器开发。后续会基于 Google Agones,更新相关 K8S 运维、大规模快速扩展专用游戏服务器的文章。拥抱☁️原生? Cloud-Native!

系列

状态处理

Colyseus 中,room handlers 是 有状态(stateful) 的。每个房间都有自己的状态。状态的突变会自动同步到所有连接的客户端。

序列化方法

  • Schema (default)

状态同步时

  • user 成功加入 room 后,他将从服务器接收到完整状态。
  • 在每个 patchRate 处,状态的二进制补丁会发送到每个客户端(默认值为50ms
  • 从服务器接收到每个补丁后,在客户端调用 onStateChange
  • 每种序列化方法都有自己处理传入状态补丁的特殊方式。

Schema

SchemaSerializer 是从 Colyseus 0.10 开始引入的,它是默认的序列化方法。

Schema 结构只用于房间的状态(可同步数据)。对于不能同步的算法中的数据,您不需要使用 Schema 及其其他结构。

服务端

要使用 SchemaSerializer,你必须:

  • 有一个扩展 Schema 类的状态类
  • @type() 装饰器注释你所有的可同步属性
  • 为您的房间实例化状态(this.setState(new MyState()))
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Schema, type } from "@colyseus/schema";

class MyState extends Schema {
    @type("string")
    currentTurn: string;
}

原始类型

这些是您可以为 @type() 装饰器提供的类型及其限制。

如果您确切地知道 number 属性的范围,您可以通过为其提供正确的原始类型来优化序列化。否则,请使用 "number",它将在序列化过程中添加一个额外的字节来标识自己。

Type

Description

Limitation

"string"

utf8 strings

maximum byte size of 4294967295

"number"

auto-detects the int or float type to be used. (adds an extra byte on output)

0 to 18446744073709551615

"boolean"

true or false

0 or 1

"int8"

signed 8-bit integer

-128 to 127

"uint8"

unsigned 8-bit integer

0 to 255

"int16"

signed 16-bit integer

-32768 to 32767

"uint16"

unsigned 16-bit integer

0 to 65535

"int32"

signed 32-bit integer

-2147483648 to 2147483647

"uint32"

unsigned 32-bit integer

0 to 4294967295

"int64"

signed 64-bit integer

-9223372036854775808 to 9223372036854775807

"uint64"

unsigned 64-bit integer

0 to 18446744073709551615

"float32"

single-precision floating-point number

-3.40282347e+38 to 3.40282347e+38

"float64"

double-precision floating-point number

-1.7976931348623157e+308 to 1.7976931348623157e+308

子 schema 属性

您可以在 "root" 状态定义中定义更多自定义数据类型,如直接引用(direct reference)、映射(map)或数组(array)。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Schema, type } from "@colyseus/schema";

class World extends Schema {
    @type("number")
    width: number;

    @type("number")
    height: number;

    @type("number")
    items: number = 10;
}

class MyState extends Schema {
    @type(World)
    world: World = new World();
}

ArraySchema

ArraySchema 是内置 JavaScript Array 类型的可同步版本。

可以从数组中使用更多的方法。看看数组的 MDN 文档。

示例:自定义 Schema 类型的数组

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Schema, ArraySchema, type } from "@colyseus/schema";

class Block extends Schema {
    @type("number")
    x: number;

    @type("number")
    y: number;
}

class MyState extends Schema {
    @type([ Block ])
    blocks = new ArraySchema<Block>();
}

示例:基本类型的数组

您不能在数组内混合类型。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Schema, ArraySchema, type } from "@colyseus/schema";

class MyState extends Schema {
    @type([ "string" ])
    animals = new ArraySchema<string>();
}
array.push()

在数组的末尾添加一个或多个元素,并返回该数组的新长度。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const animals = new ArraySchema<string>();
animals.push("pigs", "goats");
animals.push("sheeps");
animals.push("cows");
// output: 4
array.pop()

从数组中删除最后一个元素并返回该元素。此方法更改数组的长度。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
animals.pop();
// output: "cows"

animals.length
// output: 3
array.shift()

从数组中删除第一个元素并返回被删除的元素。这个方法改变数组的长度。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
animals.shift();
// output: "pigs"

animals.length
// output: 2
array.unshift()

将一个或多个元素添加到数组的开头,并返回数组的新长度。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
animals.unshift("pigeon");
// output: 3
array.indexOf()

返回给定元素在数组中的第一个下标,如果不存在则返回 -1

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const itemIndex = animals.indexOf("sheeps");
array.splice()

通过删除或替换现有元素和/或在适当位置添加新元素来更改数组的内容。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// find the index of the item you'd like to remove
const itemIndex = animals.findIndex((animal) => animal === "sheeps");

// remove it!
animals.splice(itemIndex, 1);
array.forEach()

迭代数组中的每个元素。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
this.state.array1 = new ArraySchema<string>('a', 'b', 'c');

this.state.array1.forEach(element => {
    console.log(element);
});
// output: "a"
// output: "b"
// output: "c"

MapSchema

MapSchema 是内置 JavaScript Map 类型的一个可同步版本。

建议使用 MapsID 跟踪您的游戏实体(entities),例如玩家(players),敌人(enemies)等。

"目前仅支持字符串 key":目前,MapSchema 只允许您提供值类型。key 类型总是 string

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Schema, MapSchema, type } from "@colyseus/schema";

class Player extends Schema {
    @type("number")
    x: number;

    @type("number")
    y: number;
}

class MyState extends Schema {
    @type({ map: Player })
    players = new MapSchema<Player>();
}
map.get()

通过 key 获取一个 map 条目:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const map = new MapSchema<string>();
const item = map.get("key");

OR

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//
// NOT RECOMMENDED
//
// This is a compatibility layer with previous versions of @colyseus/schema
// This is going to be deprecated in the future.
//
const item = map["key"];
map.set()

key 设置 map 项:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const map = new MapSchema<string>();
map.set("key", "value");

OR

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//
// NOT RECOMMENDED
//
// This is a compatibility layer with previous versions of @colyseus/schema
// This is going to be deprecated in the future.
//
map["key"] = "value";
map.delete()

key 删除一个 map 项:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
map.delete("key");

OR

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//
// NOT RECOMMENDED
//
// This is a compatibility layer with previous versions of @colyseus/schema
// This is going to be deprecated in the future.
//
delete map["key"];
map.size

返回 MapSchema 对象中的元素数量。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const map = new MapSchema<number>();
map.set("one", 1);
map.set("two", 2);

console.log(map.size);
// output: 2
map.forEach()

按插入顺序遍历 map 的每个 key/value 对。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
this.state.players.forEach((value, key) => {
    console.log("key =>", key)
    console.log("value =>", value)
});

"所有 Map 方法":您可以从 Maps 中使用更多的方法。看一看 MDN 文档的 Maps。

CollectionSchema

"CollectionSchema 仅用 JavaScript 实现":目前为止,CollectionSchema 只能用于 JavaScript。目前还不支持 Haxec#LUAc++ 客户端。

CollectionSchemaArraySchema 的工作方式相似,但需要注意的是您无法控制其索引。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Schema, CollectionSchema, type } from "@colyseus/schema";

class Item extends Schema {
    @type("number")
    damage: number;
}

class Player extends Schema {
    @type({ collection: Item })
    items = new CollectionSchema<Item>();
}
collection.add()

item 追加到 CollectionSchema 对象。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const collection = new CollectionSchema<number>();
collection.add(1);
collection.add(2);
collection.add(3);
collection.at()

获取位于指定 index 处的 item

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const collection = new CollectionSchema<string>();
collection.add("one");
collection.add("two");
collection.add("three");

collection.at(1);
// output: "two"
collection.delete()

根据 item 的值删除 item

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
collection.delete("three");
collection.has()

返回一个布尔值,无论该 item 是否存在于 set 中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (collection.has("two")) {
    console.log("Exists!");
} else {
    console.log("Does not exist!");
}
collection.size

返回 CollectionSchema 对象中的元素数量。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const collection = new CollectionSchema<number>();
collection.add(10);
collection.add(20);
collection.add(30);

console.log(collection.size);
// output: 3
collection.forEach()

对于 CollectionSchema 对象中的每个 index/value 对,forEach() 方法按插入顺序执行所提供的函数一次。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
collection.forEach((value, at) => {
    console.log("at =>", at)
    console.log("value =>", value)
});

SetSchema

"SetSchema 只在 JavaScript 中实现":SetSchema 目前只能在 JavaScript 中使用。目前还不支持 HaxeC#LUAC++ 客户端。

SetSchema 是内置 JavaScript Set 类型的可同步版本。

"更多":你可以从 Sets 中使用更多的方法。看一下 MDN 文档的 Sets。

SetSchema 的用法与 [CollectionSchema] 非常相似,最大的区别是 Sets 保持唯一的值。Sets 没有直接访问值的方法。(如collection.at())

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Schema, SetSchema, type } from "@colyseus/schema";

class Effect extends Schema {
    @type("number")
    radius: number;
}

class Player extends Schema {
    @type({ set: Effect })
    effects = new SetSchema<Effect>();
}
set.add()

SetSchema 对象追加一个 item

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const set = new CollectionSchema<number>();
set.add(1);
set.add(2);
set.add(3);
set.at()

获取位于指定 index 处的项。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const set = new CollectionSchema<string>();
set.add("one");
set.add("two");
set.add("three");

set.at(1);
// output: "two"
set.delete()

根据项的值删除项。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
set.delete("three");
set.has()

返回一个布尔值,无论该项是否存在于集合中。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (set.has("two")) {
    console.log("Exists!");
} else {
    console.log("Does not exist!");
}
set.size

返回 SetSchema 对象中的元素数量。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const set = new SetSchema<number>();
set.add(10);
set.add(20);
set.add(30);

console.log(set.size);
// output: 3

过滤每个客户端的数据

"这个特性是实验性的":@filter()/@filterChildren() 是实验性的,可能无法针对快节奏的游戏进行优化。

过滤旨在为特定客户端隐藏状态的某些部分,以避免在玩家决定检查来自网络的数据并查看未过滤状态信息的情况下作弊。

数据过滤器是每个客户端和每个字段(或每个子结构,在 @filterChildren 的情况下)都会触发的回调。如果过滤器回调返回 true,字段数据将为该特定客户端发送,否则,数据将不为该客户端发送。

请注意,如果过滤函数的依赖关系发生变化,它不会自动重新运行,但只有在过滤字段(或其子字段)被更新时才会重新运行。请参阅此问题以了解解决方法。

@filter() property decorator

@filter() 属性装饰器可以用来过滤掉整个 Schema 字段。

下面是 @filter() 签名的样子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class State extends Schema {
    @filter(function(client, value, root) {
        // client is:
        //
        // the current client that's going to receive this data. you may use its
        // client.sessionId, or other information to decide whether this value is
        // going to be synched or not.

        // value is:
        // the value of the field @filter() is being applied to

        // root is:
        // the root instance of your room state. you may use it to access other
        // structures in the process of decision whether this value is going to be
        // synched or not.
    })
    @type("string") field: string;
}

@filterChildren() 属性装饰器

@filterChildren() 属性装饰器可以用来过滤出 arraysmapssets 等内部的项。它的签名与 @filter() 非常相似,只是在 value 之前增加了 key 参数 — 表示 ArraySchemaMapSchemaCollectionSchema 等中的每一项。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class State extends Schema {
    @filterChildren(function(client, key, value, root) {
        // client is:
        //
        // the current client that's going to receive this data. you may use its
        // client.sessionId, or other information to decide whether this value is
        // going to be synched or not.

        // key is:
        // the key of the current value inside the structure

        // value is:
        // the current value inside the structure

        // root is:
        // the root instance of your room state. you may use it to access other
        // structures in the process of decision whether this value is going to be
        // synched or not.
    })
    @type([Cards]) cards = new ArraySchema<Card>();
}

例子: 在一场纸牌游戏中,每张纸牌的相关资料只应供纸牌拥有者使用,或在某些情况下(例如纸牌已被丢弃)才可使用。

查看 @filter() 回调签名:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Client } from "colyseus";

class Card extends Schema {
    @type("string") owner: string; // contains the sessionId of Card owner
    @type("boolean") discarded: boolean = false;

    /**
     * DO NOT USE ARROW FUNCTION INSIDE `@filter`
     * (IT WILL FORCE A DIFFERENT `this` SCOPE)
     */
    @filter(function(
        this: Card, // the instance of the class `@filter` has been defined (instance of `Card`)
        client: Client, // the Room's `client` instance which this data is going to be filtered to
        value: Card['number'], // the value of the field to be filtered. (value of `number` field)
        root: Schema // the root state Schema instance
    ) {
        return this.discarded || this.owner === client.sessionId;
    })
    @type("uint8") number: number;
}

向后/向前兼容性

向后/向前兼容性可以通过在现有结构的末尾声明新的字段来实现,以前的声明不被删除,但在需要时被标记为 @deprecated()

这对于原生编译的目标特别有用,比如 C#, C++, Haxe 等 — 在这些目标中,客户端可能没有最新版本的 schema 定义。

限制和最佳实践

  • 每个 Schema 结构最多可以容纳 64 个字段。如果需要更多字段,请使用嵌套的 Schema 结构。
  • NaNnull 数字被编码为 0
  • null 字符串被编码为 ""
  • Infinity 被编码为 Number.MAX_SAFE_INTEGER 的数字。
  • 不支持多维数组。了解如何将一维数组用作多维数组
  • ArraysMaps 中的项必须都是同一类型的实例。
  • @colyseus/schema 只按照指定的顺序编码字段值。
    • encoder(服务器)和decoder(客户端)必须有相同的 schema 定义。
    • 字段的顺序必须相同。

客户端

Callbacks

您可以在客户端 schema 结构中使用以下回调来处理来自服务器端的更改。

  • onAdd (instance, key)
  • onRemove (instance, key)
  • onChange (changes) (on Schema instance)
  • onChange (instance, key) (on collections: MapSchema, ArraySchema, etc.)
  • listen()

"C#, C++, Haxe":当使用静态类型语言时,需要根据 TypeScript schema 定义生成客户端 schema 文件。参见在客户端生成 schema

onAdd (instance, key)

onAdd 回调只能在 maps (MapSchema)和数组(ArraySchema)中使用。调用 onAdd 回调函数时,会使用添加的实例及其 holder 对象上的 key 作为参数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
room.state.players.onAdd = (player, key) => {
    console.log(player, "has been added at", key);

    // add your player entity to the game world!

    // If you want to track changes on a child object inside a map, this is a common pattern:
    player.onChange = function(changes) {
        changes.forEach(change => {
            console.log(change.field);
            console.log(change.value);
            console.log(change.previousValue);
        })
    };

    // force "onChange" to be called immediatelly
    player.triggerAll();
};

onRemove (instance, key)

onRemove 回调只能在 maps (MapSchema) 和 arrays (ArraySchema) 中使用。调用 onRemove 回调函数时,会使用被删除的实例及其 holder 对象上的 key 作为参数。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
room.state.players.onRemove = (player, key) => {
    console.log(player, "has been removed at", key);

    // remove your player entity from the game world!
};

onChange (changes: DataChange[])

onChange 对于直接 Schema 引用和集合结构的工作方式不同。关于集合结构 (array,map 等)的 onChange,请点击这里

您可以注册 onChange 来跟踪 Schema 实例的属性更改。onChange 回调是由一组更改过的属性以及之前的值触发的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
room.state.onChange = (changes) => {
    changes.forEach(change => {
        console.log(change.field);
        console.log(change.value);
        console.log(change.previousValue);
    });
};

你不能在未与客户端同步的对象上注册 onChange 回调。


onChange (instance, key)

onChange 对于直接 Schema 引用和 collection structures 的工作方式不同。

每当 primitive 类型(string, number, boolean等)的集合更新它的一些值时,这个回调就会被触发。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
room.state.players.onChange = (player, key) => {
    console.log(player, "have changes at", key);
};

如果您希望检测 non-primitive 类型(包含 Schema 实例)集合中的更改,请使用onAdd 并在它们上注册 onChange

"onChangeonAddonRemove 是 exclusive(独占) 的": onAddonRemove 期间不会触发 onChange 回调。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
如果在这些步骤中还需要检测更改,请考虑注册 `onAdd``onRemove`

.listen(prop, callback)

监听单个属性更改。

.listen() 目前只适用于 JavaScript/TypeScript

参数:

  • property: 您想要监听更改的属性名。
  • callback: 当 property 改变时将被触发的回调。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
state.listen("currentTurn", (currentValue, previousValue) => {
    console.log(`currentTurn is now ${currentValue}`);
    console.log(`previous value was: ${previousValue}`);
});

.listen() 方法返回一个用于注销监听器的函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const removeListener = state.listen("currentTurn", (currentValue, previousValue) => {
    // ...
});

// later on, if you don't need the listener anymore, you can call `removeListener()` to stop listening for `"currentTurn"` changes.
removeListener();

listenonChange 的区别是什么?

.listen() 方法是单个属性上的 onChange 的简写。下面是

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
state.onChange = function(changes) {
    changes.forEach((change) => {
        if (change.field === "currentTurn") {
            console.log(`currentTurn is now ${change.value}`);
            console.log(`previous value was: ${change.previousValue}`);
        }
    })
}

客户端 schema 生成

这只适用于使用静态类型语言(如 C#、C++ 或 Haxe)的情况。

在服务器项目中,可以运行 npx schema-codegen 自动生成客户端 schema 文件。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
npx schema-codegen --help

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
schema-codegen [path/to/Schema.ts]

Usage (C#/Unity)
    schema-codegen src/Schema.ts --output client-side/ --csharp --namespace MyGame.Schema

Valid options:
    --output: fhe output directory for generated client-side schema files
    --csharp: generate for C#/Unity
    --cpp: generate for C++
    --haxe: generate for Haxe
    --ts: generate for TypeScript
    --js: generate for JavaScript
    --java: generate for Java

Optional:
    --namespace: generate namespace on output code

Built-in room » Lobby Room

"大厅房间的客户端 API 将在 Colyseus 1.0.0 上更改":

  • 内置的大厅房间目前依赖于发送消息来通知客户可用的房间。当 @filter() 变得稳定时,LobbyRoom 将使用 state 代替。

服务器端

内置的 LobbyRoom 将自动通知其连接的客户端,每当房间 "realtime listing" 有更新。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { LobbyRoom } from "colyseus";

// Expose the "lobby" room.
gameServer
  .define("lobby", LobbyRoom);

// Expose your game room with realtime listing enabled.
gameServer
  .define("your_game", YourGameRoom)
  .enableRealtimeListing();

onCreate()onJoin()onLeave()onDispose() 期间,会自动通知 LobbyRoom

如果你已经更新了你房间的metadata,并且需要触发一个 lobby 的更新,你可以在元数据更新之后调用 updateLobby()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Room, updateLobby } from "colyseus";

class YourGameRoom extends Room {

  onCreate() {

    //
    // This is just a demonstration
    // on how to call `updateLobby` from your Room
    //
    this.clock.setTimeout(() => {

      this.setMetadata({
        customData: "Hello world!"
      }).then(() => updateLobby(this));

    }, 5000);

  }

}

客户端

您需要通过从 LobbyRoom 发送给客户端的信息来跟踪正在添加、删除和更新的房间。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Client, RoomAvailable } from "colyseus.js";

const client = new Client("ws://localhost:2567");
const lobby = await client.joinOrCreate("lobby");

let allRooms: RoomAvailable[] = [];

lobby.onMessage("rooms", (rooms) => {
  allRooms = rooms;
});

lobby.onMessage("+", ([roomId, room]) => {
  const roomIndex = allRooms.findIndex((room) => room.roomId === roomId);
  if (roomIndex !== -1) {
    allRooms[roomIndex] = room;

  } else {
    allRooms.push(room);
  }
});

lobby.onMessage("-", (roomId) => {
  allRooms = allRooms.filter((room) => room.roomId !== roomId);
});

Built-in room » Relay Room

内置的 RelayRoom 对于简单的用例非常有用,在这些用例中,除了连接到它的客户端之外,您不需要在服务器端保存任何状态。

通过简单地中继消息(将消息从客户端转发给其他所有人) — 服务器端不能验证任何消息 — 客户端应该执行验证。

RelayRoom 的源代码非常简单。一般的建议是在您认为合适的时候使用服务器端验证来实现您自己的版本。

服务器端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { RelayRoom } from "colyseus";

// Expose your relayed room
gameServer.define("your_relayed_room", RelayRoom, {
  maxClients: 4,
  allowReconnectionTime: 120
});

客户端

请参阅如何注册来自 relayed room 的玩家加入、离开、发送和接收消息的回调。

连接到房间

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Client } from "colyseus.js";

const client = new Client("ws://localhost:2567");

//
// Join the relayed room
//
const relay = await client.joinOrCreate("your_relayed_room", {
  name: "This is my name!"
});

在玩家加入和离开时注册回调

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//
// Detect when a player joined the room
//
relay.state.players.onAdd = (player, sessionId) => {
  if (relay.sessionId === sessionId) {
    console.log("It's me!", player.name);

  } else {
    console.log("It's an opponent", player.name, sessionId);
  }
}

//
// Detect when a player leave the room
//
relay.state.players.onRemove = (player, sessionId) => {
  console.log("Opponent left!", player, sessionId);
}

//
// Detect when the connectivity of a player has changed
// (only available if you provided `allowReconnection: true` in the server-side)
//
relay.state.players.onChange = (player, sessionId) => {
  if (player.connected) {
    console.log("Opponent has reconnected!", player, sessionId);

  } else {
    console.log("Opponent has disconnected!", player, sessionId);
  }
}

发送和接收消息

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//
// By sending a message, all other clients will receive it under the same name
// Messages are only sent to other connected clients, never the current one.
//
relay.send("fire", {
  x: 100,
  y: 200
});

//
// Register a callback for messages you're interested in from other clients.
//
relay.onMessage("fire", ([sessionId, message]) => {

  //
  // The `sessionId` from who sent the message
  //
  console.log(sessionId, "sent a message!");

  //
  // The actual message sent by the other client
  //
  console.log("fire at", message);
});

Colyseus 的最佳实践

这一部分需要改进和更多的例子!每一段都需要有自己的一页,有详尽的例子和更好的解释。

  • 保持你的 room 类尽可能小,没有游戏逻辑
  • 使可同步的数据结构尽可能小
    • 理想情况下,扩展 Schema 的每个类应该只有字段定义。
    • 自定义 getter 和 setter 方法可以实现,只要它们中没有游戏逻辑。
  • 你的游戏逻辑应该由其他结构来处理,例如:
    • 了解如何使用命令模式。
    • 一个 Entity-Component 系统。我们目前缺少一个与 Colyseus 兼容的 ECS 包,一些工作已经开始尝试将ECSY@colyseus/schema 结合起来。

为什么?

  • Models (@colyseus/schema) 应该只包含数据,不包含游戏逻辑。
  • Rooms 应该有尽可能少的代码,并将动作转发给其他结构

命令模式有几个优点,例如:

  • 它将调用该操作的类与知道如何执行该操作的对象解耦。
  • 它允许你通过提供一个队列系统来创建一个命令序列。
  • 实现扩展来添加一个新的命令很容易,可以在不改变现有代码的情况下完成。
  • 严格控制命令的调用方式和调用时间。
  • 由于命令简化了代码,因此代码更易于使用、理解和测试。

用法

安装

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
npm install --save @colyseus/command

在您的 room 实现中初始化 dispatcher

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { Room } from "colyseus";
import { Dispatcher } from "@colyseus/command";

import { OnJoinCommand } from "./OnJoinCommand";

class MyRoom extends Room<YourState> {
  dispatcher = new Dispatcher(this);

  onCreate() {
    this.setState(new YourState());
  }

  onJoin(client, options) {
    this.dispatcher.dispatch(new OnJoinCommand(), {
        sessionId: client.sessionId
    });
  }

  onDispose() {
    this.dispatcher.stop();
  }
}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const colyseus = require("colyseus");
const command = require("@colyseus/command");

const OnJoinCommand = require("./OnJoinCommand");

class MyRoom extends colyseus.Room {

  onCreate() {
    this.dispatcher = new command.Dispatcher(this);
    this.setState(new YourState());
  }

  onJoin(client, options) {
    this.dispatcher.dispatch(new OnJoinCommand(), {
        sessionId: client.sessionId
    });
  }

  onDispose() {
    this.dispatcher.stop();
  }
}

命令实现的样子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// OnJoinCommand.ts
import { Command } from "@colyseus/command";

export class OnJoinCommand extends Command<YourState, {
    sessionId: string
}> {

  execute({ sessionId }) {
    this.state.players[sessionId] = new Player();
  }

}
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// OnJoinCommand.js
const command = require("@colyseus/command");

exports.OnJoinCommand = class OnJoinCommand extends command.Command {

  execute({ sessionId }) {
    this.state.players[sessionId] = new Player();
  }

}

查看更多

  • 参阅 命令定义
    • https://github.com/colyseus/command/blob/master/test/scenarios/CardGameScenario.ts
  • 参阅 用法
    • https://github.com/colyseus/command/blob/master/test/Test.ts
  • 参阅 实现
    • https://github.com/colyseus/command/blob/master/src/index.ts

Refs

中文手册同步更新在:

  • https:/colyseus.hacker-linner.com
代码语言:javascript
代码运行次数:0
运行
复制
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 黑客下午茶 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
Colyseus:轻量级多人游戏免费开源解决方案
这个框架也是最近接触,自己在摸索过程中遇到很多坑,因此记录下。文章基于 Express + TS 演示及说明。
青年码农
2023/03/01
2.5K1
Colyseus:轻量级多人游戏免费开源解决方案
ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(中)
快速上手多人游戏服务器开发。后续会基于 Google Agones,更新相关 K8S 运维、大规模快速扩展专用游戏服务器的文章。拥抱☁️原生? Cloud-Native! 系列 ColyseusJS
为少
2021/05/27
2K0
ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(上)
快速上手多人游戏服务器开发。后续会基于 Google Agones ,更新相关 K8S 运维、大规模快速扩展专用游戏服务器的文章。拥抱☁️原生? Cloud-Native! 快速开始 在开始之前,
为少
2021/05/27
1.7K0
ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(上)
ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(系统保障篇)
快速上手多人游戏服务器开发。后续会基于 Google Agones,更新相关 K8S 运维、大规模快速扩展专用游戏服务器的文章。拥抱☁️原生? Cloud-Native! 系列 ColyseusJS
为少
2021/05/27
8.3K0
ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(系统保障篇)
探索 Golang 云原生游戏服务器开发,5 分钟上手 Nano 游戏服务器框架
nano 是一个轻量级的服务器框架,它最适合的应用领域是网页游戏、社交游戏、移动游戏的服务端。当然还不仅仅是游戏,用 nano 开发高实时 web 应用也非常合适。
为少
2021/05/27
7.8K0
探索 Golang 云原生游戏服务器开发,5 分钟上手 Nano 游戏服务器框架
快速搭建饥荒游戏服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
cnguu
2020/10/23
6K0
快速搭建饥荒游戏服务器
Go开源游戏服务器框架——Pitaya
Pitaya是一款由国外游戏公司topfreegames使用golang进行编写,易于使用,快速且轻量级的开源分布式游戏服务器框架 Pitaya使用etcd作为默认的服务发现组件,提供使用nats和grpc进行远程调用(server to server)的可选配置,并提供在docker中运行以上组件(etcd、nats)的docker-compose配置
歪歪梯
2020/09/24
8.1K0
教你从头写游戏服务器框架(3)
使用异步非阻塞编程,确实能获得很好的性能。但是在代码上,确非常不直观。因为任何一个可能阻塞的操作,都必须要要通过“回调”函数来链接。比如一个玩家登录,你需要先读数据库,然后读一个远程缓冲服务器(如 redis),然后返回登录结果:用户名、等级……在这个过程里,有两个可能阻塞的操作,你就必须把这个登录的程序,分成三个函数来编写:一个是收到客户端数据包的回调,第二个是读取数据库后的回调,第三个是读取缓冲服务器后的回调。
韩伟
2019/05/08
2.9K3
教你从头写游戏服务器框架(3)
教你从头写游戏服务器框架
大概已经有差不多一年没写技术文章了,原因是今年投入了一些具体游戏项目的开发。这些新的游戏项目,比较接近独立游戏的开发方式。我觉得公司的“祖传”服务器框架技术不太适合,所以从头写了一个游戏服务器端的框架,以便获得更好的开发效率和灵活性。现在项目将近上线,有时间就想总结一下,这样一个游戏服务器框架的设计和实现过程。
韩伟
2019/01/30
4.3K1
教你从头写游戏服务器框架
流动图书馆小程序实战
流动图书馆是一个图书漂流和借阅工具,旨在共享闲置图书,并链接趣味相投的小伙伴。
疯狂的小程序
2018/01/25
2.2K1
在Ubuntu上部署一个基于webrtc的多人视频聊天服务
最近研究webrtc视频直播技术,网上找了些教程最终都不太能顺利跑起来的,可能是文章写的比较老,使用的一些开源组件已经更新了,有些配置已经不太一样了,所以按照以前的步骤会有问题。折腾了一阵终于跑起来了,记录一下。
MJ.Zhou
2020/03/19
1.2K0
腾讯云快速搭建微信小程序服务
小程序后台服务需要通过 HTTPS 访问,在实验开始之前,我们要准备域名和 SSL 证书。
云上云
2019/07/01
33.7K1
腾讯云快速搭建微信小程序服务
92.精读《React PowerPlug 源码》
React PowerPlug 是利用 render props 进行更好状态管理的工具库。
黄子毅
2022/03/14
1.2K0
腾讯云大学大咖分享 | 小游戏联机对战引擎实践
腾讯云大学本期直播课程邀请到了腾讯云Web前端工程师通过两个小游戏demo,讲解了小游戏联机对战引擎中帧同步和状态同步两种应用场景。「腾讯云大学」联合「云加社区」为大家整理了课程精彩干货!
可可爱爱没有脑袋
2019/09/11
4.5K0
腾讯云大学大咖分享 | 小游戏联机对战引擎实践
探索使用 Golang 和 Webassembly 构建一个多人游戏服务器
https://www.youtube.com/watch?v=ZyGw1yLNO9E(原创整理) 什么是 WebAssembly?由 Google、Microsoft、Mozilla、Apple 等
为少
2021/05/27
1.2K0
探索使用 Golang 和 Webassembly 构建一个多人游戏服务器
游戏服务器之内存数据库redis客户端应用(下)
(3)存储一个角色的基础信息(使用命令set) 存储结构: key:BASE角色id ,value 角色基础信息 int playerId = player->get_player_base()->m_player_id; char tmpBuf[64]; memset(tmpBuf,0,64); sprintf(tmpBuf,"BASE%d",playerId); string key(tmpBuf); CRWRedisClient redisClient; redis::cli
李海彬
2018/03/22
6.3K0
Pixel Stream 源码分析
2021年10月18日。备份一下像素流源代码,防止以后万一GitHub用不了了,代码给整没了就不好办了,顺便讲解一下文件的组织架构。https://github.com/xosg/PixelStreamer
Jean
2021/10/27
2.3K0
Pixel Stream 源码分析
TRTC学习之旅(二)-- 使用vue+ts集成TRTC实现多人会议室
根据上回学习了官方TRTC demo之后,已经了解了一个基础的多人会议室创建的流程,接下来我需要将自己学到的转换为自己能够运用的。
黑眼圈云豆
2020/06/24
4K3
TRTC学习之旅(二)--  使用vue+ts集成TRTC实现多人会议室
用 Node.js 写一个多人游戏服务器引擎 [每日前端夜话0x31]
听说过文字冒险游戏吗? 如果你的年龄足够大的话(就像我一样),那么你可能听说过、甚至玩过“back in zhe day”。在本文中,我将向你展示编写的整个过程。这不仅仅是一个文本冒险游戏,而是一个能让你和你的朋友们一起玩的,可以进行任何剧情的文本冒险游戏引擎。 没错,我们将通过在添加多人游戏功能来增加它的趣味性。
疯狂的技术宅
2019/03/27
2.4K0
用 Node.js 写一个多人游戏服务器引擎 [每日前端夜话0x31]
Node 开发一个多人对战的射击游戏(实战长文)
https://juejin.cn/post/6960096410305822751
coder_koala
2021/05/28
2.6K0
Node 开发一个多人对战的射击游戏(实战长文)
推荐阅读
相关推荐
Colyseus:轻量级多人游戏免费开源解决方案
更多 >
LV.1
这个人很懒,什么都没有留下~
目录
  • 系列
  • 状态处理
    • 序列化方法
    • 状态同步时
    • Schema
    • 服务端
      • 原始类型
      • 子 schema 属性
      • ArraySchema
      • MapSchema
      • CollectionSchema
      • SetSchema
    • 过滤每个客户端的数据
      • @filter() property decorator
      • @filterChildren() 属性装饰器
      • 向后/向前兼容性
      • 限制和最佳实践
    • 客户端
      • Callbacks
    • 客户端 schema 生成
  • Built-in room » Lobby Room
    • 服务器端
    • 客户端
  • Built-in room » Relay Room
    • 服务器端
    • 客户端
      • 连接到房间
      • 在玩家加入和离开时注册回调
      • 发送和接收消息
  • Colyseus 的最佳实践
    • 用法
    • 查看更多
    • Refs
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档