ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(下)
快速上手多人游戏服务器开发。后续会基于
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())
)
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 intor floattype to be used. (adds an extra byte on output) |
0to 18446744073709551615 |
"boolean" |
trueor false |
0or 1 |
"int8" |
signed 8-bit integer | -128to 127 |
"uint8" |
unsigned 8-bit integer | 0to 255 |
"int16" |
signed 16-bit integer | -32768to 32767 |
"uint16" |
unsigned 16-bit integer | 0to 65535 |
"int32" |
signed 32-bit integer | -2147483648to 2147483647 |
"uint32" |
unsigned 32-bit integer | 0to 4294967295 |
"int64" |
signed 64-bit integer | -9223372036854775808to 9223372036854775807 |
"uint64" |
unsigned 64-bit integer | 0to 18446744073709551615 |
"float32" |
single-precision floating-point number | -3.40282347e+38to 3.40282347e+38 |
"float64" |
double-precision floating-point number | -1.7976931348623157e+308to 1.7976931348623157e+308 |
子 schema 属性
您可以在 "root" 状态定义中定义更多自定义数据类型,如直接引用(
direct reference)、映射(
map)或数组(
array)。
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是内置
JavaScriptArray 类型的可同步版本。
可以从数组中使用更多的方法。看看数组的 MDN 文档。
示例:自定义 Schema
类型的数组
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>(); }
示例:基本类型的数组
您不能在数组内混合类型。
import { Schema, ArraySchema, type } from "@colyseus/schema"; class MyState extends Schema { @type([ "string" ]) animals = new ArraySchema<string>(); }
array.push()
在数组的末尾添加一个或多个元素,并返回该数组的新长度。
const animals = new ArraySchema<string>(); animals.push("pigs", "goats"); animals.push("sheeps"); animals.push("cows"); // output: 4
array.pop()
从数组中删除最后一个元素并返回该元素。此方法更改数组的长度。
animals.pop(); // output: "cows" animals.length // output: 3
array.shift()
从数组中删除第一个元素并返回被删除的元素。这个方法改变数组的长度。
animals.shift(); // output: "pigs" animals.length // output: 2
array.unshift()
将一个或多个元素添加到数组的开头,并返回数组的新长度。
animals.unshift("pigeon"); // output: 3
array.indexOf()
返回给定元素在数组中的第一个下标,如果不存在则返回
-1
const itemIndex = animals.indexOf("sheeps");
array.splice()
通过删除或替换现有元素和/或在适当位置添加新元素来更改数组的内容。
// 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()
迭代数组中的每个元素。
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 类型的一个可同步版本。
建议使用
Maps按
ID跟踪您的游戏实体(
entities),例如玩家(
players),敌人(
enemies)等。
"目前仅支持字符串 key":目前,
MapSchema只允许您提供值类型。
key类型总是
string。
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条目:
const map = new MapSchema<string>(); const item = map.get("key");
OR
// // 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项:
const map = new MapSchema<string>(); map.set("key", "value");
OR
// // 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项:
map.delete("key");
OR
// // 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对象中的元素数量。
const map = new MapSchema<number>(); map.set("one", 1); map.set("two", 2); console.log(map.size); // output: 2
map.forEach()
按插入顺序遍历
map的每个
key/value对。
this.state.players.forEach((value, key) => { console.log("key =>", key) console.log("value =>", value) });
"所有 Map 方法":您可以从 Maps 中使用更多的方法。看一看 MDN 文档的 Maps。
CollectionSchema
"
CollectionSchema仅用 JavaScript 实现":目前为止,
CollectionSchema只能用于
JavaScript。目前还不支持
Haxe,
c#,
LUA和
c++客户端。
CollectionSchema与
ArraySchema的工作方式相似,但需要注意的是您无法控制其索引。
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对象。
const collection = new CollectionSchema<number>(); collection.add(1); collection.add(2); collection.add(3);
collection.at()
获取位于指定
index处的
item。
const collection = new CollectionSchema<string>(); collection.add("one"); collection.add("two"); collection.add("three"); collection.at(1); // output: "two"
collection.delete()
根据
item的值删除
item。
collection.delete("three");
collection.has()
返回一个布尔值,无论该
item是否存在于
set中。
if (collection.has("two")) { console.log("Exists!"); } else { console.log("Does not exist!"); }
collection.size
返回
CollectionSchema对象中的元素数量。
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()方法按插入顺序执行所提供的函数一次。
collection.forEach((value, at) => { console.log("at =>", at) console.log("value =>", value) });
SetSchema
"
SetSchema只在 JavaScript 中实现":
SetSchema目前只能在
JavaScript中使用。目前还不支持
Haxe,
C#,
LUA和
C++客户端。
SetSchema是内置 JavaScript Set 类型的可同步版本。
"更多":你可以从
Sets中使用更多的方法。看一下 MDN 文档的 Sets。
SetSchema的用法与 [
CollectionSchema] 非常相似,最大的区别是
Sets保持唯一的值。
Sets没有直接访问值的方法。(如collection.at())
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。
const set = new CollectionSchema<number>(); set.add(1); set.add(2); set.add(3);
set.at()
获取位于指定
index处的项。
const set = new CollectionSchema<string>(); set.add("one"); set.add("two"); set.add("three"); set.at(1); // output: "two"
set.delete()
根据项的值删除项。
set.delete("three");
set.has()
返回一个布尔值,无论该项是否存在于集合中。
if (set.has("two")) { console.log("Exists!"); } else { console.log("Does not exist!"); }
set.size
返回
SetSchema对象中的元素数量。
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()签名的样子:
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()属性装饰器可以用来过滤出
arrays、
maps、
sets等内部的项。它的签名与
@filter()非常相似,只是在
value之前增加了
key参数 — 表示
ArraySchema、
MapSchema、
CollectionSchema等中的每一项。
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()回调签名:
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
结构。 NaN
或null
数字被编码为0
null
字符串被编码为""
Infinity
被编码为Number.MAX_SAFE_INTEGER
的数字。- 不支持多维数组。了解如何将一维数组用作多维数组
Arrays
和Maps
中的项必须都是同一类型的实例。@colyseus/schema
只按照指定的顺序编码字段值。encoder
(服务器)和decoder
(客户端)必须有相同的schema
定义。- 字段的顺序必须相同。
客户端
Callbacks
您可以在客户端
schema结构中使用以下回调来处理来自服务器端的更改。
onAdd (instance, key)
onRemove (instance, key)
onChange (changes)
(onSchema
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作为参数。
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作为参数。
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回调是由一组更改过的属性以及之前的值触发的。
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等)的集合更新它的一些值时,这个回调就会被触发。
room.state.players.onChange = (player, key) => { console.log(player, "have changes at", key); };
如果您希望检测 non-primitive 类型(包含
Schema实例)集合中的更改,请使用
onAdd并在它们上注册
onChange。
"
onChange、
onAdd和
onRemove是 exclusive(独占) 的":
onAdd或
onRemove期间不会触发
onChange回调。
如果在这些步骤中还需要检测更改,请考虑注册 `onAdd` 和 `onRemove`。
.listen(prop, callback)
监听单个属性更改。
.listen()目前只适用于JavaScript/TypeScript。
参数:
property
: 您想要监听更改的属性名。callback
: 当property
改变时将被触发的回调。
state.listen("currentTurn", (currentValue, previousValue) => { console.log(`currentTurn is now ${currentValue}`); console.log(`previous value was: ${previousValue}`); });
.listen()方法返回一个用于注销监听器的函数:
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();
listen
和 onChange
的区别是什么?
.listen()方法是单个属性上的
onChange的简写。下面是
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文件。
npx schema-codegen --help
输出:
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" 有更新。
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():
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发送给客户端的信息来跟踪正在添加、删除和更新的房间。
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
的源代码非常简单。一般的建议是在您认为合适的时候使用服务器端验证来实现您自己的版本。
服务器端
import { RelayRoom } from "colyseus"; // Expose your relayed room gameServer.define("your_relayed_room", RelayRoom, { maxClients: 4, allowReconnectionTime: 120 });
客户端
请参阅如何注册来自
relayed room的玩家加入、离开、发送和接收消息的回调。
连接到房间
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!" });
在玩家加入和离开时注册回调
// // 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); } }
发送和接收消息
// // 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 应该有尽可能少的代码,并将动作转发给其他结构
命令模式有几个优点,例如:
- 它将调用该操作的类与知道如何执行该操作的对象解耦。
- 它允许你通过提供一个队列系统来创建一个命令序列。
- 实现扩展来添加一个新的命令很容易,可以在不改变现有代码的情况下完成。
- 严格控制命令的调用方式和调用时间。
- 由于命令简化了代码,因此代码更易于使用、理解和测试。
用法
安装
npm install --save @colyseus/command
在您的
room实现中初始化
dispatcher:
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(); } }
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(); } }
命令实现的样子:
// OnJoinCommand.ts import { Command } from "@colyseus/command"; export class OnJoinCommand extends Command<YourState, { sessionId: string }> { execute({ sessionId }) { this.state.players[sessionId] = new Player(); } }
// OnJoinCommand.js const command = require("@colyseus/command"); exports.OnJoinCommand = class OnJoinCommand extends command.Command { execute({ sessionId }) { this.state.players[sessionId] = new Player(); } }
查看更多
Refs
中文手册同步更新在:
- https:/colyseus.hacker-linner.com
我是为少 微信:uuhells123 公众号:黑客下午茶 加我微信(互相学习交流),关注公众号(获取更多学习资料~)
- ColyseusJS 轻量级多人游戏服务器开发框架 - 中文手册(系统保障篇)
- android游戏开发框架libgdx的使用(三)--中文显示与汉字绘制
- 深入浅出node.js游戏服务器开发1——基础架构与框架介绍
- 深入浅出node.js游戏服务器开发1——基础架构与框架介绍
- Twisted python 开发游戏的服务器框架
- android游戏开发框架libgdx的使用(三)--中文显示与汉字绘制
- 深入浅出node.js游戏服务器开发——Pomelo框架的设计动机与架构介绍
- (pomelo系列入门教材)深入浅出node.js游戏服务器开发1——基础架构与框架介绍
- Koa框架教程,Koa框架开发指南,Koa框架中文使用手册,Koa框架中文文档
- 深入浅出node.js游戏服务器开发——基础架构与框架介绍
- Leaf - 一个由 Go 语言编写的开发效率和执行效率并重的开源游戏服务器框架
- 深入浅出node.js游戏服务器开发1——基础架构与框架介绍
- 网易游戏服务器开发框架 Pomelo
- android游戏开发框架libgdx的使用(二十一)—使用TTF字库支持中文
- (pomelo系列入门教程)深入浅出node.js游戏服务器开发——Pomelo框架的设计动机与架构介绍
- Leaf - 一个由 Go 语言编写的开发效率和执行效率并重的开源游戏服务器框架
- 深入浅出node.js游戏服务器开发1——基础架构与框架介绍
- 【Zinx应用-MMO游戏案例-(2)AOI兴趣点算法】刘丹冰-传智播客-Golang轻量级并发服务器框架
- android游戏开发框架libgdx的使用(三)--中文显示与汉字绘制
- 分享PHP的GUI开发框架PHP-GTK中文开发手册