Nanolink Cocos2d-x 接入概要

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 // 断开连接
					

获取 APPKEY

1,登录 天梯实时对战服务 后台,在 游戏列表 界面,点击 “创建游戏”。如图:

创建新游戏截图

2,输入游戏的相关信息后,点击 “提交”,即可。如图:

提交游戏截图

3,在 游戏列表,可以点击“编辑”某个游戏,查看唯一的 APPKEY,接入 SDK 时使用。如图:

APPKEY 截图

iOS SDK 导入

Cocos 导入, Xcode 环境配置

下载 SDK 解压至本地,demos/*/sdk 目录下包含下面文件:

nanolink.h

// Cocos-C++ 环境下加入
nanoclient.h
nanoclient.cpp

// Cocos-Lua 环境下加入
nanolink_lua.h
nanolink_lua.cpp

// Cocos-JS 环境下加入
nanolink_js.h
nanolink_js.cpp
1,在工程中加入 SDK 接口文件 和 nanolink.h 头文件

把 demos/*/sdk 目录中 nanolink.h 文件加入工程中。

Cocos-C++ 开发环境下,需要把 nanoclient.h, nanoclient.cpp 文件加入到工程中。

Cocos-Lua 开发环境下,需要把 nanolink_lua.h, nanolink_lua.cpp 文件加入到工程中。

Cocos-JS 开发环境下,需要把 nanolink_js.h, nanolink_js.cpp 文件加入到工程中。

2,设置库路径

在 Build Settings -> Search Paths 加入 libs/iOS 库路径。(Header Search Paths 和 Library Search Paths 具体配置的路径根据自身环境配置即可)

Header Search Paths 添加 $(SRCROOT)/../../nanolink/sdk

Library Search Paths 添加 $(SRCROOT)/../../nanolink/libs/iOS

Cocos-JS 环境下,Header Search Paths 还需要添加 $(SRCROOT)/../../cocos2d-x/cocos/scripting/js-bindings/manual,支持 ScriptingCore.h

3,设置链接库

在 Build Phases -> Link Binary With Libraries 添加 nanolink 静态库。

使用 LLVM C++ Standard Library 或者 Complier Default 的开发者,可以直接使用 libnanolink.a。

由于开发者选用的 C++ 标准库可能有所不同,如果使用 GNU C++ Standard Library,可以联系客服咨询 (QQ: 2803816871)。

提示: 在 Build Settings 中搜索 C++ Standard Library,即可查看自己的设置。

Android SDK 导入

编译配置

在 Android 工程加载自己项目 .so 的地方,添加对 libnanolink.so 的引用。

						
static {
    System.loadLibrary("nanolink");
    System.loadLibrary("your_app_lib");
}
					
Cocos-C++ 开发环境

下载 SDK 解压至本地,demos/c++/sdk 目录下包含下面文件,将文件导入到工程中:

nanolink.h
nanoclient.h
nanoclient.cpp
1,修改 Android.mk 文件

/frameworks/runtime-src/proj.android/jni/Android.mk

						
LOCAL_PATH := $(call my-dir)
#——— 开始添加, 加载 libnanolink.so ———
#——— 确保加载的 .so 文件(路径) 正确加载 ———
include $(CLEAR_VARS)
LOCAL_MODULE:=libnanolink
LOCAL_SRC_FILES:=../../../nanolink/libs/android/$(TARGET_ARCH_ABI)/libnanolink.so
include $(PREBUILT_SHARED_LIBRARY)
#——— 结束添加 ———

include $(CLEAR_VARS)

...


#——— 开始添加, Cocos-C++ 开发环境加入以下接口文件 ———

LOCAL_SRC_FILES := hellolua/main.cpp \
                   ../../Classes/AppDelegate.cpp \
                   ../../../nanolink/sdk/nanoclient.cpp

LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes \
                   $(LOCAL_PATH)/../../../nanolink/sdk
#——— 结束添加 ———

...

#——— 开始添加, 设置链接库 ———
#放在 BUILD_SHARED_LIBRARY 前
LOCAL_SHARED_LIBRARIES := libnanolink
#——— 结束添加 ———

include $(BUILD_SHARED_LIBRARY)

					
Cocos-Lua 开发环境

下载 SDK 解压至本地,demos/lua/sdk 目录下包含下面文件,将文件导入到工程中:

nanolink.h
nanolink_lua.h
nanolink_lua.cpp
1,修改 Android.mk 文件
						
LOCAL_PATH := $(call my-dir)
#——— 开始添加, 加载 libnanolink.so ———
#——— 确保加载的 .so 文件(路径) 正确加载 ———
include $(CLEAR_VARS)
LOCAL_MODULE:=libnanolink
LOCAL_SRC_FILES:=../../../nanolink/libs/android/$(TARGET_ARCH_ABI)/libnanolink.so
include $(PREBUILT_SHARED_LIBRARY)
#——— 结束添加 ———

include $(CLEAR_VARS)

...

ifeq ($(USE_ARM_MODE),1)
LOCAL_ARM_MODE := arm
endif

#——— 开始添加, Cocos-Lua 开发环境加入以下接口文件 ———

LOCAL_SRC_FILES := hellolua/main.cpp \
                   ../../Classes/AppDelegate.cpp \
                   ../../../nanolink/sdk/nanolink_lua.cpp

LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes \
                   $(LOCAL_PATH)/../../../nanolink/sdk
#——— 结束添加 ———

...

#——— 开始添加, 设置链接库 ———
#放在 BUILD_SHARED_LIBRARY 前
LOCAL_SHARED_LIBRARIES := libnanolink
#——— 结束添加 ———

include $(BUILD_SHARED_LIBRARY)

					
2,初始化代码 (Cocos2d-Lua),修改 AppDelegate.cpp

在开始位置加上 #include "nanolink_lua.h"

register_all_packages() 后加入 nanolink_lua_init(L); 。其中 L 为当前 LuaEngine 的 lua_State。

Cocos-JS 开发环境

下载 SDK 解压至本地,demos/js/sdk 目录下包含下面文件,将文件导入到工程中:

nanolink.h
nanolink_js.h
nanolink_js.cpp
1,修改 Android.mk 文件
						
LOCAL_PATH := $(call my-dir)
#——— 开始添加, 加载 libnanolink.so ———
#——— 确保加载的 .so 文件(路径) 正确加载 ———
include $(CLEAR_VARS)
LOCAL_MODULE:=libnanolink
LOCAL_SRC_FILES:=../../../nanolink/libs/android/$(TARGET_ARCH_ABI)/libnanolink.so
include $(PREBUILT_SHARED_LIBRARY)
#——— 结束添加 ———

include $(CLEAR_VARS)

...

ifeq ($(USE_ARM_MODE),1)
LOCAL_ARM_MODE := arm
endif

#——— 开始添加, Cocos-JS 开发环境加入以下接口文件 ———

LOCAL_SRC_FILES := hellojavascript/main.cpp \
                   ../../Classes/AppDelegate.cpp \
                   ../../../nanolink/sdk/nanolink_js.cpp

LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes \
                   $(LOCAL_PATH)/../../../nanolink/sdk
#——— 结束添加 ———

...

#——— 开始添加, 设置链接库 ———
#放在 BUILD_SHARED_LIBRARY 前
LOCAL_SHARED_LIBRARIES := libnanolink
#——— 结束添加 ———

include $(BUILD_SHARED_LIBRARY)

					
2,初始化代码 (Cocos2d-JS),修改 AppDelegate.cpp

在开始位置加上 #include "nanolink_js.h"

sc->start () 前加入 sc->addRegisterCallback(nanolink_js::registerJSFunctions);

修改 Android.mk 文件 LOCAL_SRC_FILESLOCAL_C_INCLUDES 时,根据自身环境添加相应的 C++, Lua,JS 接口即可。

快速接入

参考 Unity

示例程序中用到Nanolink 自带的 NanoBuffers 序列化工具, 你也可以使用 Protobuf 等序列化工具, 具体参考 序列化与反序列化

SDK 初始化

定义
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无效)

连接

等级匹配 connect
定义
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。

局域网匹配 和 房间号匹配 connect2
定义
void connect2 (const char* group, int32_t timeout=15000);

group: 匹配的房间名称

如果 group 为空,通过局域网广播方式连接。

如果 group 不为空,通过服务器建立相同 group 之间用户的连接。

timeout: 匹配的等待时间(ms),超过时间取消匹配,缺省 15000 毫秒 (15 秒)。

功能

局域网对战 或 创建房间号(或邀请好友)的对战方式。

group 名称可以重用,如果 group 在服务器不存在,创建并等待其它 玩家连接。

如果 group 首字母为 ‘+’,表示被邀请加入。此时如果 group 在服务器 不存在,立即返回连接失败,错误代码 404。

断开连接 disconnect
定义
void disconnect ();
功能

断开已经建立的连接 或 取消正在进行的匹配。

断开当前连接时,SDK 会统计本次连接的数据到统计后台。包括:连接时长,出入流量,发送的数据包数,延迟,发送成功率,重发率 等数据。所以离开本次连接或者离开当前对战场景时,尽量确保调用 disconnect。

数据收发

发送数据 send
定义
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, 无法添加发送任务(发送队列满)

发送协议“标记” sendmark
定义
int32_t sendmark (uint8_t mark=0);

mark: 一个字节的 mark 标识, 当前可以忽略。

功能

用在多人联网对战, 用于和 resync 指令配合使用

发送一个标记指令, 用于告知其他客户端该数据之后可以重构本客户端完整状态。

返回值

无法添加发送任务(发送队列满), 返回 -1。

多人对战时, 一个客户端数据接收遇到不可修复的问题时, 将发送 resync 指令请求重新同步完整数据。

其他客户端收到 resync 指令需要发送一个 mark 指令, 然后开始发送该客户端的数据。

接收数据 recv
定义
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。

更新玩家位置 update
定义
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

获取协议参数

整型参数 getInt
定义
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 参数明细:
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 判断当前客户端是否是主机,主机可能根据网络延迟切换。(1为主机)
time-scale 回放的时间系数, getInt("time-scale") 的时候, 1.0 转换为 100。
time-elapsed 回放经过时间。
time-remaining 回放剩余时间。
字符串参数 getString
定义
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 参数明细:
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 获得协议的基本信息及统计数据。

协议配置 config

定义
void config (const char* name, const char* value);
功能

配置协议的参数。

name 参数明细:
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 回放剩余时间,只读

房间配置 config

定义
void config (const char* name, const char* value);
功能

配置房间的参数。

房间配置参数需要在建立连接后配置,通常可以在事件处理函数 onConnected() 中调用
name 参数明细:
name 描述
room-atime 多人入场时间,缺省 60000(60秒)。
room-btime 多人最大战斗持续时间,缺省 360秒。
room-master 设置当前房间的主机索引号。
255 为根据延迟自动切换主机;
0-254 为指定的主机索引号;
缺省为0。
room-players 多人最大人数,范围 2-64,缺省 8。(0为解散房间)
"room-xxx" 参数修改时, 会自动同步到服务器,
例如, 多个玩家确认"开始"时, 由客户端主机 config ("room-atime", "1"), 防止其他玩家再进入游戏,
建议仅由客户端主机修改房间参数。

应用协议事件处理

NanoClient 类定义了 onConnected, onDisconnected, onStatusChanged, onMessage, onResync, onPlayer 等事件处理函数接口, 开发者继承 NanoClient 类并实现以上接口可以快速实现实时对战。

连接建立 onConnected

连接建立完成, 可以开始进行数据收发。

onConnected ()
连接断开 onDisconnected

连接断开时,触发的事件。会返回连接断开时的错误码。

onDisconnected (int error)

error: 断开时获得的 lastError,通过 lastError 可以判断网络断开的原因。错误码说明

状态变化 onStatusChanged

连接状态发生改变时触发。

onStatusChanged (string newStatus, string oldStatus)

newStatus: 最新状态。

oldStatus: 上一个状态。

接收数据事件 onMessage [必须]

接收到一个事件时的处理接口。

onMessage (byte[] data, byte fromIndex)

data: 接收到的数据缓冲区。

fromIndex: 数据发送者的索引号 clientIndex。

resync 指令

当某个客户端遇到不可修复的传输故障,向其他客户端请求重新发送当前最新数据。例如,某个玩家中途进入游戏,之前的数据没有接收到,SDK 会自动发送一个 resync 指令,向其他客户端请求数据。

通常在多人对战模式中使用,客户端通常在 onResync 中发送包括玩家昵称等初始信息。

onResync (byte fromIndex)

fromIndex: 出现丢包或者中途进入游戏的客户端的索引号 clientIndex。

玩家事件 onPlayer

玩家新加入房间"new", 离开房间"left", 重连回到房间"return"。

onPlayer (byte clientIndex, string e)

clientIndex: 当前事件的玩家索引号 clientIndex。

e: 事件, 包括: 玩家新加入("new"), 离开("left"), 回来("return"), "lag"

序列化与反序列化 NanoBuffers

Nanolink 传输二进制数据, 开发者可以使用熟悉的序列化方式, 例如 Protobuf, JSON, XML, ....

在实时联网时, 数据收发频率高, 发送数据越小越好, 建议使用 protobuf 等二进制序列化工具, 也可以使用 Nanolink SDK 内置的 NanoBuffers 序列化工具。

NanoBuffers 是个简单的二进制序列化工具

压缩: 支持 Varint 压缩, 支持浮点数压缩

简洁: 直接通过链式 get, put 接口读写数据

方便: 直接使用, 不需要预先定义 schema再编译生成类代码

开源: 提供C++, C#, JS, Lua 等语言的实现, 可以扩展新的数据类型

NanoBuffers 的基本用法
						
// 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人以上, 建议支持区域广播, 节约流量

如何实现区域广播
1, 每个玩家向服务器注册自己所在的位置, 空间

NanoClient.update (x, y, z, k) 更新位置, 空间, 接收增益

x, y 是玩家的虚拟坐标, 是个256*256的网格, 通常由玩家在游戏中的实际坐标转换而来

空间(z) 和 接收增益(k) 用于实现高级的区域广播特性, 建议先用默认参数

2, 玩家发送数据时, 可以指定"发送强度", 来决定多大范围内的玩家可以接收到数据

NanoClient.send (data, strength) 其中 strength 指定"发送强度"

玩家的不同事件的影响范围不一样, 例如, 走路的强度是3, 跑步的强度是5, 枪声的强度是8, 手榴弹15...

send 的 strength 参数补充说明:

0: 特殊值, 仅发送给主机 (is-master 获取的主机)

255: 特殊值, 广播给所有空间的所有玩家

254: 特殊值, 广播给相同空间的所有玩家

1-253: 广播给相同空间的范围内玩家

3, 玩家发送数据时, 服务器根据玩家间的相互位置计算数据的接收者
// 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) {
        // 在接收范围内
    }
}
					

存档回放

数据保存 (录制) save

记录网络传输数据(录制)功能默认是关闭的,如果需要开启录制功能,需要 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。

加载回放 load
定义
int32_t load (uint8_t* data, int32_t size = -1);
功能

加载协议数据或保存协议数据,支持从文件或内存中加载/保存数据。

data: 加载数据缓冲区。

size: 加载数据缓冲区长度

size = -1: data 指定的是文件路径 (data != NULL)。

size > 0: data 指定的是一块内存指针, 数据将从该内存指针位置读取。

返回值

加载的数据长度。

如果返回值为-1, 表明加载失败。