本文说明如何使用 Ginx 启动与旧版 Unity MMO 示例项目兼容的服务端,并介绍双方使用的数据包格式、Protobuf 消息、玩家生命周期和当前限制。
Unity 客户端目录:
C:\Users\14\Desktop\mmo_demo_unity3d
客户端工程版本:
Unity 5.4.1f1
Build Settings 中的场景顺序为:
0 Assets/Scene/Login.unity
1 Assets/Scene/Main.unity
登录按钮调用 Login.OnLogin(),保存登录界面中的 IP 和端口,然后通过 SceneManager.LoadScene(1) 进入主场景。主场景中的 PlayerController.Start() 会调用 NetMgr.SendConnect() 建立 TCP 连接。
在 Ginx 项目根目录执行:
go run ./main/unityserver服务端读取 config/ginx.json,默认监听:
127.0.0.1:7777
不要使用 go run ./main/server 与 Unity 联调。main/server 是消息 ID 0 和 1 的 Ping/Hello 基础示例,不包含 MMO Protobuf 路由。
- 使用 Unity Hub 添加目录
C:\Users\14\Desktop\mmo_demo_unity3d。 - 使用 Unity
5.4.1f1或能够兼容该工程的旧版本打开项目。 - 打开
Assets/Scene/Login.unity。 - 点击 Unity 编辑器顶部的 Play。
- 在登录界面填写
127.0.0.1和7777。 - 点击登录按钮进入主场景。
- 查看 Unity Console 和 Ginx 服务端终端日志。
客户端原始默认地址是 192.168.2.225:8999,与 Ginx 默认配置不同。本机联调时需要在登录界面改成 127.0.0.1:7777。
客户端和服务端不在同一台机器时:
- 将
config/ginx.json中的Host改为服务端实际监听地址,通常可以使用0.0.0.0。 - 在防火墙中允许 TCP 端口
7777。 - 在 Unity 登录界面填写服务端局域网 IP,不要填写客户端自己的
127.0.0.1。
Unity 客户端和 Ginx 都使用小端序的 8 字节消息头:
+-------------------+-------------------+----------------------+
| DataLen uint32 | MsgID uint32 | Protobuf Data |
| 4 bytes LE | 4 bytes LE | DataLen bytes |
+-------------------+-------------------+----------------------+
DataLen表示后续 Protobuf 消息体长度,不包含 8 字节消息头。MsgID用于选择 Ginx 路由和 Unity 客户端处理逻辑。Data使用客户端Assets/Scripts/Data/Msg.cs中定义的 Protobuf 格式。- Ginx 的 TCP 分帧由
gnet.DataPack.ReadMessage()处理:先使用io.ReadFull读取完整 8 字节头,再按DataLen读取完整消息体。 - 一次
Read只拿到半个消息时,ReadMessage()会继续读取;一次Read收到多个消息时,当前调用只消费一条,后续消息留给下一次调用。 - 服务端发送时使用完整写入循环,避免底层
Write出现短写时截断一条消息。
Go 端兼容协议位于:
unity/protocol.go
| ID | 方向 | Protobuf 数据 | 作用 |
|---|---|---|---|
1 |
服务端 -> 客户端 | SyncPid |
给新连接分配玩家 PID |
2 |
客户端 -> 服务端 | Talk |
客户端发送聊天内容 |
3 |
客户端 -> 服务端 | Position |
客户端上报位置和朝向 |
200 |
服务端 -> 客户端 | BroadCast |
广播聊天、玩家加入和玩家移动 |
201 |
服务端 -> 客户端 | SyncPid |
通知其他客户端玩家已经离线 |
202 |
服务端 -> 客户端 | SyncPlayers |
给新连接同步已经在线的玩家 |
服务端实现了客户端需要的以下消息:
SyncPid
Pid int32
Position
X float
Y float
Z float
V float
Player
Pid int32
P Position
SyncPlayers
Ps repeated Player
MovePackege
P Position
ActionData int32
Talk
Content string
BroadCast
Pid int32
Tp int32
Content string
P Position
ActionData int32
Position.V 表示 Unity 玩家 Y 轴朝向。客户端当前发送消息 ID 3 时直接发送 Position,没有发送 MovePackege;服务端仍然保留了 MovePackege 编解码,方便后续扩展动作同步。
Unity 客户端通过 BroadCast.Tp 区分消息用途:
| TP | 作用 | 使用字段 |
|---|---|---|
1 |
聊天广播 | Pid、Content |
2 |
玩家加入或玩家出生 | Pid、P |
3 |
玩家位置和朝向更新 | Pid、P、ActionData |
4 |
预留的坐标移动广播 | Pid、P |
当前服务端发送 TP=1、TP=2 和 TP=3。客户端已经保留 TP=4 的处理逻辑,但服务端暂时没有使用。
新客户端完成 TCP 连接后,服务端执行以下流程:
Unity client
|
| TCP connect
v
Ginx OnConnStart
|
|-- MsgID 1: SyncPid
| 分配 PID,当前 PID 与 Ginx ConnID 相同
|
|-- MsgID 202: SyncPlayers
| 同步连接前已经在线的玩家
|
`-- MsgID 200: BroadCast(TP=2)
向全部在线客户端广播新玩家加入
Unity 客户端先把 SyncPid.Pid 加入本地玩家 ID 集合,再收到自己的 BroadCast(TP=2)。客户端会把这条消息识别为自己的初始位置更新,从而完成主玩家初始化。
客户端发送:
MsgID 2 + Talk
服务端解析聊天内容,然后向全部在线连接发送:
MsgID 200 + BroadCast(TP=1)
客户端发送:
MsgID 3 + Position
服务端更新 UnityWorld 中的玩家坐标,然后向全部在线连接发送:
MsgID 200 + BroadCast(TP=3)
广播包含发送者自身。发送者需要收到服务端确认后的移动数据,客户端才能完成初始位置和后续同步逻辑。
连接关闭时,服务端先从 UnityWorld 删除玩家,然后向剩余连接发送:
MsgID 201 + SyncPid
unity/protocol.go Unity Protobuf 编解码
unity/world.go Unity 在线玩家状态和广播
main/unityserver/Server.go Unity MMO 路由及连接 Hook
test/unity_protocol_test.go 协议单元测试
test/unity_world_test.go 玩家世界单元测试
go test ./...
go vet ./...
go build ./...协议测试覆盖坐标、广播、玩家列表、移动包和错误数据;世界状态测试覆盖玩家加入、状态更新、离线和排除指定连接的广播。
当前 UnityWorld.Broadcast() 会向全部在线玩家广播聊天和移动消息,还没有根据 AOI 过滤视野。玩家数量增加后,应使用 gcore.AOIManager 查询附近玩家,再只向进入视野范围的连接发送消息。
玩家 PID、位置和在线关系只保存在当前服务进程中,服务重启后会清空。当前实现不包含账号登录、数据库存储、断线重连和跨服状态同步。
客户端 SocketClient.OnRead() 直接处理固定大小的 byteBuffer,没有使用 EndRead() 返回的实际读取长度,也没有完整保存半包。TCP 不保证一次读取正好得到一个完整消息,因此网络波动时可能出现半包解析失败。
服务端的数据包格式已经与客户端一致,但这个客户端读取问题无法由服务端彻底规避。正式使用前应在 Unity 客户端增加接收缓存,按照 DataLen + 8 循环拆包,并保留不足一帧的数据等待下一次读取。
确认启动的是:
go run ./main/unityserver同时检查 Unity Console 是否成功收到消息 ID 1、202 和 200。
检查 Unity 登录界面地址是否为 127.0.0.1:7777,并确认 config/ginx.json 中的 Host 和 TcpPort 与客户端一致。
确认消息 ID 和 Protobuf 类型匹配。例如消息 ID 1 的数据必须是 SyncPid,不能发送普通字符串。
第二个客户端连接时应收到消息 ID 202。检查服务端日志是否执行了玩家加入流程,并检查 Unity 客户端是否成功解析 SyncPlayers.Ps。