Nanolink 是基于 UDP 的实时联网对战服务。
Nanolink SDK支持"1对1"和"多人" 联网对战, 支持回放。
主要接口以 C/C++ 形式定义, C#, Lua, JS 版有相应语言接口实现。
int32_t init (const char* appKey, int mode=2);
确保在调用其它接口之前正确调用 init 接口。
init // 初始化
config // 参数配置,可选
connect // 连接指定设备
send, recv // 接收与发送
disconnect // 断开连接
1,登录 天梯实时对战服务 后台,在 游戏列表 界面,点击 “创建游戏”。如图:
2,输入游戏的相关信息后,点击 “提交”,即可。如图:
3,在 游戏列表,可以点击“编辑”某个游戏,查看唯一的 APPKEY,接入 SDK 时使用。如图:
在 Unity 编译器中选择 Assets -> Import Package -> Custom Package 找到本地目录下的 nanolink_unity_sdk.unitypackage 文件 点击 Open 按钮,然后 Import 即可导入成功。
(快速导入:打开 Unity 工程后,直接双击本地的 .unitypackage 文件,然后 Import 即可。)
导入成功完成后,可以看到 Assets/Demo/Scenes/ 目录中有 1V1 和 多人对战的小例子,可以体验。
Platform 为 Android 时,需要开启 Unity 工程网络支持。
"File" - "Build Settings" - "Player Settings" - "Other Settings" - "Internet Access" 改为 Require。
Nanolink SDK 提供10多个 nano_xxx 形式的接口, 例如 nano_send, nano_recv, 开发者可以直接使用这些基础接口实现联网对战。
同时, Nanolink SDK通过NanoClient封装了基础接口, 开发者只需要继承 NanoClient 并实现必要的事件接口, 大大简化开发复杂度。
Nanolink SDK提供 C++, C#, JS, Lua 形式的 NanoClient 实现代码以及演示项目的参考实现代码, 开发者可以用事件接口快速开发联网对战游戏。
1,实现一个MyClient, 提供必要的事件接口。
2,实现一个MyNetwork, 管理 MyClien。
3,发送/接收数据。
4,序列化与反序列化。
从 NanoClient 继承并实现一个 MyClient, 实现必要的事件接口。
发送数据: MyClient.send 发送序列化的数据。
接收数据: MyClient.onMessage 接收到数据后派发给具体处理逻辑。
public class MyClient : NanoClient {
protected override void onMessage (byte[] data, byte fromIndex) {
// 重点: 收到数据, 解析后派发给具体处理代码
}
protected override void onResync (byte fromIndex) {
// 多人联网时, 中途进入或重连, 重新请求
// 发送当前客户端的完整数据, 例如 昵称, 头像 等数据
}
protected override void onStatusChanged (string newStatus,string oldStatus) {
// 状态变化时
}
protected override void onConnected () {
// 连接成功时
}
protected override void onDisconnected (int error) {
// 根据错误代码等信息判断断开原因, 具体参考示例代码
}
protected override void onPlayer (byte clientIndex, string e) {
// 玩家新加入房间"new", 离开房间"left", 重连回到房间"return"
}
}
初始化: 在 MyNetwork.Start 中调用 myClient.init。
运行: 在 MyNetwork.FixedUpdate 中调用 myClient.doUpdate。
public class MyNetwork: MonoBehaviour {
MyClient myClient = new MyClient ();
void Start() {
MyClient.init ("appKey", mode);
MyClient.config ("time-machine", "true"); // 可选, 用于支持存档回放
// 获得连接方式和参数
MyClient.connect (...);
}
void FixedUpdate () {
// 运行 myClient 的处理逻辑
myClient.doUpdate ();
}
}
发送事件: 在 MyPlayer.cs 中获得用户操作并发送操作事件。
接收事件: 定义事件处理接口 MyPlayer.onEvent。
派发事件: 在 MyClient.onMessage中派发事件数据到 MyPlayer.onEvent。
public class MyPlayer : MonoBehaviour {
void Update () {
// 获得键盘或鼠标操作
Hashtable values = new Hashtable ();
values.Add ("name", "move");
values.Add ("target", targetPosition);
// 序列化, 具体序列化在 MySerialize.toBytes 中实现
MyClient.send (MySerialize.toBytes (values));
}
public void onEvent(Hashtable values) {
string name = (string) values["name"];
switch (name) {
case "move":
targetPosition = (Vector3) values ["target"];
break;
default:
Debug.Log("未支持的 Player::onEvent 事件: " + name);
break;
}
}
}
示例程序中用到Nanolink 自带的 NanoBuffers 序列化工具, 你也可以使用 Protobuf 等序列化工具, 具体参考 序列化与反序列化。
int32_t init (const char* appKey, int mode=2);
appKey: 游戏的唯一标识,必须指定。在 天梯实时对战服务 后台创建游戏时自动生成,具体详见:获取 APPKEY
mode: 对战服务的连接方式,必须指定。定义如下:
mode=2: 1对1连接方式 (默认)
mode=3: 3人及以上连接方式
mode=0: 回放模式
mode=1: 单人录制模式
初始化 Nanolink SDK,确保在调用其它接口之前正确调用。
成功返回 0, 失败返回 -1 (mode无效)
void connect (uint8_t level, uint8_t mode = 0, int32_t timeout=15000);
level: 当前客户端的玩家等级(级别)。
mode: 等级匹配可以分为不同模式, 支持 0-15 模式, 缺省为0. 例如, mode=0, 代表 经典模式, mode=1, 代表冒险模式, ..
timeout: 匹配的等待时间(ms),超过时间取消匹配,缺省15000毫秒(15秒)。
指定等级匹配,可以指定匹配玩家的等级,等级会根据一定的宽容度匹配。
如果当前游戏没有玩家等级的概念,匹配完全随机的话,直接指定一个固定的等级即可,比如:8。
void connect2 (const char* group, int32_t timeout=15000);
group: 匹配的房间名称
如果 group 为空,通过局域网广播方式连接。
如果 group 不为空,通过服务器建立相同 group 之间用户的连接。
timeout: 匹配的等待时间(ms),超过时间取消匹配,缺省 15000 毫秒 (15 秒)。
局域网对战 或 创建房间号(或邀请好友)的对战方式。
group 名称可以重用,如果 group 在服务器不存在,创建并等待其它 玩家连接。
如果 group 首字母为 ‘+’,表示被邀请加入。此时如果 group 在服务器 不存在,立即返回连接失败,错误代码 404。
void disconnect ();
断开已经建立的连接 或 取消正在进行的匹配。
断开当前连接时,SDK 会统计本次连接的数据到统计后台。包括:连接时长,出入流量,发送的数据包数,延迟,发送成功率,重发率 等数据。所以离开本次连接或者离开当前对战场景时,尽量确保调用 disconnect。
int32_t send (const uint8_t* data, uint32_t size, uint8_t strength=255);
data: 待发送的数据 data
size: 发送数据的长度(size > 1)
strength: 发送广播强度, 仅在多人联网时有效
strength=0, 特殊值, 仅发送给主机 (is-master 获取的主机)
strength=255, 特殊值, 广播给所有空间的所有玩家
strength=254, 特殊值, 广播给相同空间的所有玩家
strength=1-253, 广播给相同空间的范围内玩家
发送指定长度 size 的 data 数据到目标设备。
成功: >= 0, 发送数据的索引值
失败:-1, 无法添加发送任务(发送队列满)
int32_t sendmark (uint8_t mark=0);
mark: 一个字节的 mark 标识, 当前可以忽略。
用在多人联网对战, 用于和 resync
指令配合使用
发送一个标记指令, 用于告知其他客户端该数据之后可以重构本客户端完整状态。
无法添加发送任务(发送队列满), 返回 -1。
多人对战时, 一个客户端数据接收遇到不可修复的问题时, 将发送 resync
指令请求重新同步完整数据。
其他客户端收到 resync
指令需要发送一个 mark
指令, 然后开始发送该客户端的数据。
int32_t recv (uint8_t* data, uint32_t size, uint8_t& from);
data: 接收数据缓冲区, 必须在外部分配。
size: 接收数据缓冲区的长度。
from: 用于获得数据发送者的 clientIndex。
接收新任务的数据。
返回接收数据长度。
返回接收数据长度:
-1: 没有接收到新数据。
0: resync 指令。
1: mark 指令。
>0: data 中返回收到的数据。
如果返回值大于参数 size, 表明 data 空间不够, 需要重新分配再次调用 recv。
通常接收数据长度小于 1024, 调用该接口的时候可以传入一个 1024 大小的 data, 如果返回值大于 1024 再根据size重新分配 data。
int32_t update (uint8_t x, uint8_t y, uint8_t z = 0, uint8_t k = 1);
更新客户端的位置(x,y), 空间(z), 接收增益(k), 用于配合 send 实现区域广播。
x, y 玩家的位置
z 玩家所在空间, 缺省为0, 0-255, 不同空间互相不会收到数据
k 接收增益, 接收范围的放大倍数
x, y 表示玩家的位置, 最大 256*256 的网格
z 表示空间, 不是 z 坐标, 例如, 不同的房间. 不同的空间互相不会收到数据, 除非全局广播
k 接收增益, 例如, 缺省接收增益是1, 玩家获得一个狙击枪, 接收增益扩大为10
成功返回 0
int32_t getInt (const char* name, int n = -1);
name:取值为 "mode", "latency", "last-time", "last-error", "client-index", "master-index", "is-master", "time-scale", "time-elapsed", "time-remaining"。
n: 用于配合 last-time 使用。
获得指定名称(name)的字符串类型参数值(value)。
name | 描述 |
---|---|
mode
|
获得 init 时传入的 mode 参数。 |
latency
|
获得当前客户端的网络延迟。 |
last-time
|
配合参数 n 使用: n >= 0: 获得指定玩家的最后指令时间; n = -1: 获得最后收到玩家(任意玩家)指令的时间; n = -2: 获得当前客户端最后收到服务器指令的时间; 可用于判断某个客户端是否堵塞, 为相对时间, n 毫秒之前。如果 last-time > 1000(ms), 通常意味着发生堵塞。 |
players
|
配合参数 n 使用: n = -1: 连接上房间的玩家数量 (有些玩家可能已经离开房间); n > 0: n 表示 timeout (毫秒), 扣除超时玩家, 当前还在线的玩家数量。 |
uptime
|
联网时间, 多人时表示房间创建到现在的时间 (毫秒)。 |
last-error
|
在 disconnect 时查询 last-error, 每次查询后 last-error 重置为 0。 错误码定义: 400 长度错误 401 版本不兼容 402 游戏不存在 403 服务器不存在 404 客户端不存在 405 战斗超时,超过设置的战斗时间, 会强行断开连接 409 无效指令 500 匹配超时(15 秒) 501 连接中断超时(15 秒) 502 链路闲置超时(300 秒) |
client-index
|
当前客户端在对战房间索引值, 0-64。 |
master-index
|
当前房间中主机的索引值, 0-64。 |
is-master
|
判断当前客户端是否是主机,主机可能根据网络延迟切换。 |
time-scale
|
回放的时间系数, 用 getInt("time-scale") 的时候, 1.0 转换为 100。 |
time-elapsed
|
回放经过时间。 |
time-remaining
|
回放剩余时间。 |
int32_t getString (const char* name, char* data, uint32_t size); //C++接口 string getString (string name); // C#, Lua, JS接口
name:取值为 "client-id", "target-id", "server-id", "status", "stats"。
data: 用于接收参数值。
size: 接收参数值的缓冲区大小。
获得指定名称(name)的字符串类型参数值(value)。
name | 描述 |
---|---|
client-id
|
当前客户端的 id。 |
target-id
|
1 对 1 对战时,返回对方的 id; 多人对战时,返回房间 id。 |
server-id
|
分区服务器 id。 connect 过程中完成分区时可以获得 server-id, 缺省为0。分区服务器 id 可用于实现跨区邀请域或其他自定义匹配。 |
status
|
协议状态, 包括: init 初始化 matching 正在匹配 connecting 连接中 connected 已连接上 disconnect 断开连接 |
stats
|
获得协议的基本信息及统计数据。 |
void config (const char* name, const char* value);
配置协议的参数。
name | 描述 |
---|---|
client-id
|
客户端 id,16 进制,如果没有指定自动生成一个临时 id。 |
server-id
|
服务器 id, 16 进制,用于指定接入的服务器, 仅用在外部匹配或邀请匹配的时候。 |
version
|
游戏协议版本。 |
min-version
|
最小兼容版本,范围 0-255,局域网匹配时使用,缺省 0。 |
send-tasks-size
|
发送队列最大长度,范围 8-256,缺省 32。 |
timeout0
|
自动重连超时时间,超过指定时间没有回应重新连接。 |
timeout1
|
连接超时时间,超过指定时间没有连接上则断开,缺省 15000(15秒)。 |
timeout2
|
空闲断开时间,超过指定时间没有数据则断开,缺省 300000 (300秒)。 |
time-machine
|
是否记录网络传输数据,true 或 false,缺省为 false。 |
time-scale
|
回放的时间系数,读写,越大回放速度越快,缺省为 1.0。用 getInt ("time-scale") 的时候, 1.0 转换为 100。 |
time-elapsed
|
回放经过时间,读写,设置该值可以用于快进,不可倒退。 |
time-remaining
|
回放剩余时间,只读 |
void config (const char* name, const char* value);
配置房间的参数。
name | 描述 |
---|---|
room-atime
|
多人入场时间,缺省 60000(60秒)。 |
room-btime
|
多人最大战斗持续时间,缺省 360秒。 |
room-master
|
设置当前房间的主机索引号。 255 为根据延迟自动切换主机; 0-254 为指定的主机索引号; 缺省为0。 |
room-players
|
多人最大人数,范围 2-64,缺省 8。(0为解散房间) |
NanoClient 类定义了 onConnected, onDisconnected, onStatusChanged, onMessage, onResync, onPlayer 等事件处理函数接口, 开发者继承 NanoClient 类并实现以上接口可以快速实现实时对战。
连接建立完成, 可以开始进行数据收发。
onConnected ()
连接断开时,触发的事件。会返回连接断开时的错误码。
onDisconnected (int error)
error: 断开时获得的 lastError,通过 lastError 可以判断网络断开的原因。错误码说明
连接状态发生改变时触发。
onStatusChanged (string newStatus, string oldStatus)
newStatus: 最新状态。
oldStatus: 上一个状态。
接收到一个事件时的处理接口。
onMessage (byte[] data, byte fromIndex)
data: 接收到的数据缓冲区。
fromIndex: 数据发送者的索引号 clientIndex。
当某个客户端遇到不可修复的传输故障,向其他客户端请求重新发送当前最新数据。例如,某个玩家中途进入游戏,之前的数据没有接收到,SDK 会自动发送一个 resync 指令,向其他客户端请求数据。
通常在多人对战模式中使用,客户端通常在 onResync 中发送包括玩家昵称等初始信息。
onResync (byte fromIndex)
fromIndex: 出现丢包或者中途进入游戏的客户端的索引号 clientIndex。
玩家新加入房间"new", 离开房间"left", 重连回到房间"return"。
onPlayer (byte clientIndex, string e)
clientIndex: 当前事件的玩家索引号 clientIndex。
e: 事件, 包括: 玩家新加入("new"), 离开("left"), 回来("return"), "lag"
Nanolink 传输二进制数据, 开发者可以使用熟悉的序列化方式, 例如 Protobuf, JSON, XML, ....
在实时联网时, 数据收发频率高, 发送数据越小越好, 建议使用 protobuf 等二进制序列化工具, 也可以使用 Nanolink SDK 内置的 NanoBuffers 序列化工具。
NanoBuffers 是个简单的二进制序列化工具
压缩: 支持 Varint 压缩, 支持浮点数压缩
简洁: 直接通过链式 get, put 接口读写数据
方便: 直接使用, 不需要预先定义 schema再编译生成类代码
开源: 提供C++, C#, JS, Lua 等语言的实现, 可以扩展新的数据类型
// 1, 编码
NanoWriter nanoWriter = new NanoWriter ();
nanoWriter.putInt (123)
.putFloat(3.1415927f, 3)
.putString("Hello")
.put (new Vector3 (0.1f, 0.12f, 0.123f), 3);
myClient.send (nanoWriter.getBytes ());
// 2, 解码
NanoReader nanoReader = new NanoReader (buf); // buf 是接收到的数据
int v1;
float v2;
String v3;
Vector3 v4;
nanoReader.getInt (out v1).getFloat (out v2, 3).getString (out v3).get (out v4);
NanoWriter 与 NanoReader 的 get, put 必须完全对应。
在多人联网的时候, 服务器把每个客户端发送的数据直接广播(转发)给其他客户端, 当一个房间人数较多的时候, 完全广播的方式消息会导致流量消耗巨大。Nanolink 多人联网服务支持区域广播特性, 即客户端发送的数据可以仅发送给特定范围的部分客户端
如果房间人数在12人以下建议用全局广播模式, 编程相对比较简单。
如果房间人数在16人以上, 建议支持区域广播, 节约流量
NanoClient.update (x, y, z, k) 更新位置, 空间, 接收增益
x, y 是玩家的虚拟坐标, 是个256*256的网格, 通常由玩家在游戏中的实际坐标转换而来
空间(z) 和 接收增益(k) 用于实现高级的区域广播特性, 建议先用默认参数
NanoClient.send (data, strength) 其中 strength 指定"发送强度"
玩家的不同事件的影响范围不一样, 例如, 走路的强度是3, 跑步的强度是5, 枪声的强度是8, 手榴弹15...
send 的 strength 参数补充说明:
0: 特殊值, 仅发送给主机 (is-master 获取的主机)
255: 特殊值, 广播给所有空间的所有玩家
254: 特殊值, 广播给相同空间的所有玩家
1-253: 广播给相同空间的范围内玩家
// a 是发送者, b 是接收者
// strength 是a玩家发送数据的强度, 缺省为255, 全局广播模式
// k 是 b玩家的接收增益, 缺省为1
bool 判断b玩家是否在a玩家的发送范围内 (a, b, strength, k) {
int dx = abs (a.x - b.x);
int dy = abs (a.y - b.y);
if (dx <= strength*k && dy >= strength*k) {
// 在接收范围内
}
}
记录网络传输数据(录制)功能默认是关闭的,如果需要开启录制功能,需要 config("time-machine", "true") 开启。
int32_t save (uint8_t* data, int32_t size = -1);
保存协议收发等数据。
data: 保存数据缓冲区。
size: 保存数据缓冲区的长度
size = -1, data 指定的是文件路径 (data != NULL)。
size > 0, data 指定的是一块内存指针, 数据保存到该内存缓冲区中。
保存的数据长度,如果返回值大于参数 size, 表明 data 空间不够。
保存的数据包括收发数据和其他重要事件, 经过编码和压缩, 数据大小大约是客户端实际 发送流量+接收流量 的 1/4 - 1/3。
int32_t load (uint8_t* data, int32_t size = -1);
加载协议数据或保存协议数据,支持从文件或内存中加载/保存数据。
data: 加载数据缓冲区。
size: 加载数据缓冲区长度
size = -1: data 指定的是文件路径 (data != NULL)。
size > 0: data 指定的是一块内存指针, 数据将从该内存指针位置读取。
加载的数据长度。
如果返回值为-1, 表明加载失败。