从0到1开发go-tcp框架【实战片— — 开发MMO】
MMO(MassiveMultiplayerOnlineGame):大型多人在线游戏(多人在线网游)
1 AOI兴趣点的算法
游戏中的坐标模型:
场景相关数值计算
● 场景大小: 250*250 , w(x轴宽度) = 250,l(y轴长度) = 250
● x轴格子数量:nx = 5
● y轴格子数量:ny = 5
● 格子宽度: dx = w / nx = 250 / 5 = 50
● 格子长度: dy = l / ny = 250 / 5 = 50
● 格子的x轴坐标:idx
● 格子的y轴坐标:idy
● 格子编号:id = idy *nx + idx (利用格子坐标得到格子编号)
● 格子坐标:idx = id % nx , idy = id / nx (利用格子id得到格子坐标)
● 格子的x轴坐标: idx = id % nx (利用格子id得到x轴坐标编号)
● 格子的y轴坐标: idy = id / nx (利用格子id得到y轴坐标编号)
1.1 定义AOI格子(Grid)
AOI格子:
- 格子ID
- 格子的左边界坐标
- 格子的右边界坐标
- 格子的上边界坐标
- 格子的下边界坐标
- 当前格子内玩家/物体成员的ID集合
- 保护当前集合的锁
AOI格子应该有的方法:
- 初始化当前格子
- 给格子添加一个玩家
- 从格子中删除一个玩家
- 得到当前格子中所有的玩家
- 调试使用:打印出格子的基本信息
mmo_game_zinx/core/grid.go
package core
import (
"fmt"
"sync"
)
/*
一个AOI地图中的格子类型
*/
type Grid struct {
//格子ID
GID int
//格子的左边界坐标
MinX int
//格子的右边界坐标
MaxX int
//格子的上边界坐标
MinY int
//格子的下边界坐标
MaxY int
//当前格子内玩家或物体成员对的ID集合
playerIDs map[int]bool
//保护当前集合的锁
pIDLock sync.RWMutex
}
//初始化当前格子的方法
func NewGrid(gId, minX, maxX, minY, maxY int) *Grid {
return &Grid{
GID: gId,
MinX: minX,
MaxX: maxX,
MinY: minY,
MaxY: maxY,
playerIDs: make(map[int]bool),
}
}
//给格子添加一个玩家
func (g *Grid) Add(playerId int) {
g.pIDLock.Lock()
defer g.pIDLock.Unlock()
g.playerIDs[playerId] = true
}
//从格子中删除一个玩家
func (g *Grid) Remove(playerId int) {
g.pIDLock.Lock()
defer g.pIDLock.Unlock()
delete(g.playerIDs, playerId)
}
//得到当前格子中所有玩家的id
func (g *Grid) GetPlayerIds() (playerIds []int) {
g.pIDLock.RLock()
defer g.pIDLock.RUnlock()
for k, _ := range g.playerIDs {
playerIds = append(playerIds, k)
}
return
}
func (g *Grid) String() string {
return fmt.Sprintf("Grid id: %d, minX: %d, maxX: %d, minY: %d, maxY: %d, playerIds: %v",
g.GID, g.MinX, g.MaxX, g.MinY, g.MaxY, g.playerIDs)
}
1.2 AOI管理模块
AOIManager:
- 初始化一个AOI管理区域模块
- 得到每个格子在X轴方向的宽度
- 通过横纵轴得到GID
- 添加一个PlayerId到一个格子中
- 移除一个格子中的playerId
- 通过GID获取全部的PlayerID
- 通过坐标将Player添加到一个格子中
- 通过坐标把一个Player从一个格子中删除
- 通过Player坐标得到当前Player周边九宫格内全部的PlayerIDs
- 通过坐标获取对应玩家所在的GID
- 通过横纵轴得到周围的九宫格
- 根据GID获取GID周围的九宫格
- 获取思路:先求x,再求y。先根据GID判断该GID左边和右边是否有格子 。然后将X轴上的格子添加到集合中,再遍历集合判断集合中的上下是否有格子。
mmo_game_zinx/core/aoi.go
package core
import "fmt"
// 定义AOI地图大小
const (
AOI_MIN_X int = 85
AOI_MAX_X int = 410
AOI_CNTS_X int = 10
AOI_MIN_Y int = 75
AOI_MAX_Y int = 400
AOI_CNTS_Y int = 20
)
type AOIManager struct {
//区域的左边界坐标
MinX int
//区域的右界坐标
MaxX int
//X方向格子的数量
CountsX int
//区域的上边界坐标
MinY int
//区域的下边界坐标
MaxY int
//Y方向上格子的数量
CountsY int
//当前区域中有哪些格子map-key=格子的ID
grids map[int]*Grid
}
func NewAOIManager(minX, maxX, countsX, minY, maxY, countsY int) *AOIManager {
aoiMgr := &AOIManager{
MinX: minX,
MaxX: maxX,
CountsX: countsX,
MinY: minY,
MaxY: maxY,
CountsY: countsY,
grids: make(map[int]*Grid),
}
//给AOI初始化区域的所有格子进行编号和初始化
for y := 0; y < countsY; y++ {
for x := 0; x < countsX; x++ {
//计算格子的ID 根据x,y编号
//格子的编号:id = idy * countsX + idx
gid := y*countsX + x
//初始化gid格子
aoiMgr.grids[gid] = NewGrid(gid,
aoiMgr.MinX+x*aoiMgr.gridXWidth(),
aoiMgr.MinX+(x+1)*aoiMgr.gridXWidth(),
aoiMgr.MinY+y*aoiMgr.gridYLength(),
aoiMgr.MinY+(y+1)*aoiMgr.gridYLength())
}
}
return aoiMgr
}
// 得到每个格子在x轴方向的宽度
func (m *AOIManager) gridXWidth() int {
return (m.MaxX - m.MinX) / m.CountsX
}
// 得到每个格子在y轴方向的宽度
func (m *AOIManager) gridYLength() int {
return (m.MaxY - m.MinY) / m.CountsY
}
// 打印信息方法
func (m *AOIManager) String() string {
s := fmt.Sprintf("AOIManagr:\nminX:%d, maxX:%d, cntsX:%d, minY:%d, maxY:%d, cntsY:%d\n Grids in AOI Manager:\n",
m.MinX, m.MaxX, m.CountsX, m.MinY, m.MaxY, m.CountsY)
for _, grid := range m.grids {
s += fmt.Sprintln(grid)
}
return s
}
// 根据格子的gID得到当前周边的九宫格信息
func (m *AOIManager) GetSurroundGridsByGid(gID int) (grids []*Grid) {
//判断gID是否存在
if _, ok := m.grids[gID]; !ok {
return
}
//将当前gid添加到九宫格中
grids = append(grids, m.grids[gID])
//根据gid得到当前格子所在的X轴编号
idx := gID % m.CountsX
//判断当前idx左边是否还有格子
if idx > 0 {
grids = append(grids, m.grids[gID-1])
}
//判断当前的idx右边是否还有格子
if idx < m.CountsX-1 {
grids = append(grids, m.grids[gID+1])
}
//将x轴当前的格子都取出,进行遍历,再分别得到每个格子的上下是否有格子
//得到当前x轴的格子id集合
gidsX := make([]int, 0, len(grids))
for _, v := range grids {
gidsX = append(gidsX, v.GID)
}
//遍历x轴格子
for _, v := range gidsX {
//计算该格子处于第几列
idy := v / m.CountsX
//判断当前的idy上边是否还有格子
if idy > 0 {
grids = append(grids, m.grids[v-m.CountsX])
}
//判断当前的idy下边是否还有格子
if idy < m.CountsY-1 {
grids = append(grids, m.grids[v+m.CountsX])
}
}
return
}
// 通过横纵坐标获取对应的格子ID
func (m *AOIManager) GetGIDByPos(x, y float32) int {
gx := (int(x) - m.MinX) / m.gridXWidth()
gy := (int(y) - m.MinY) / m.gridYLength()
return gy*m.CountsX + gx
}
// 通过横纵坐标得到周边九宫格内的全部PlayerIDs
func (m *AOIManager) GetPIDsByPos(x, y float32) (playerIDs []int) {
//根据横纵坐标得到当前坐标属于哪个格子ID
gID := m.GetGIDByPos(x, y)
//根据格子ID得到周边九宫格的信息
grids := m.GetSurroundGridsByGid(gID)
for _, v := range grids {
playerIDs = append(playerIDs, v.GetPlayerIds()...)
fmt.Printf("===> grid ID : %d, pids : %v ====\n", v.GID, v.GetPlayerIds())
}
return
}
// 添加一个PlayerId到一个格子中
func (m *AOIManager) AddPidToGrid(pId, gId int) {
m.grids[gId].Add(pId)
}
// 移除一个格子中的playerID
func (m *AOIManager) RemovePidFromGrid(pId, gId int) {
m.grids[gId].Remove(pId)
}
// 通过GID获取全部的playerID
func (m *AOIManager) GetPidsByGid(gId int) (playerIds []int) {
playerIds = m.grids[gId].GetPlayerIds()
return
}
// 通过坐标将一个Player添加到一个格子中
func (m *AOIManager) AddToGridByPos(pId int, x, y float32) {
gId := m.GetGIDByPos(x, y)
grid := m.grids[gId]
grid.Add(pId)
}
// 通过一个坐标把一个player从一个格子中删除
func (m *AOIManager) RemoveFromGridByPos(pId int, x, y float32) {
gId := m.GetGIDByPos(x, y)
grid := m.grids[gId]
grid.Remove(pId)
}
mmo_game_zinx/core/aoi_test.go
package core
import (
"fmt"
"testing"
)
func TestNewAOIManager(t *testing.T) {
//初始化AOIManager
aoiMgr := NewAOIManager(0, 250, 5, 0, 250, 5)
//打印AOIManager
fmt.Println(aoiMgr)
}
//根据GID获取九宫格
func TestAOIManager_GetSurroundGridsByGid(t *testing.T) {
//初始化AOIManager
aoiMgr := NewAOIManager(0, 250, 5, 0, 250, 5)
for gid, _ := range aoiMgr.grids {
grids := aoiMgr.GetSurroundGridsByGid(gid)
fmt.Println("gid : ", gid, " grids len = ", len(grids))
gIds := make([]int, 0, len(grids))
for _, grid := range grids {
gIds = append(gIds, grid.GID)
}
fmt.Println("surrounding grid IDs are : ", gIds)
}
}
2 数据传输协议的选择(protobuf)
常见的传输格式:json、xml、protobuf
- json:可读性比较强;编解码比较耗时【web领域】
- xml:基于标签【前端/网页】
- protobuf(Google开发的):编解码很快、体积小、跨平台;可读性不强,传输过程中不是明文,是二进制(已经序列化完毕的)【后端应用/微服务/服务器】
2.1 安装
我这里以mac安装为例,其他os自行百度即可
# 安装protobuf
brew install protobuf
# 安装用于编译生成go文件的插件
brew install protoc-gen-go
brew install protoc-gen-go-grpc
# 查看版本
protoc --version
protoc-gen-go --version
# 安装golang插件
go get github.com/golang/protobuf/protoc-gen-go
go get -u -v github.com/golang/protobuf/protoc-gen-go
2.2 profobuf语法及使用
①语法
person.proto
syntax = "proto3"; //指定版本信息,不指定会报错
package pb; //后期生成go文件的包名
option go_package = "./;proto"; //配置包依赖路径
//message为关键字,作用为定义一种消息类型
message Person {
string name = 1; //姓名
int32 age = 2; //年龄
repeated string emails = 3; //电子邮件(repeated表示字段允许重复)【类比go中的切片】
repeated PhoneNumber phones = 4; //手机号
}
//enum为关键字,作用为定义一种枚举类型
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
//message为关键字,作用为定义一种消息类型可以被另外的消息类型嵌套使用
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
②使用步骤
- 定义一个go的与protobuf对应的结构体
- proto.Marshal进行编码序列化,得到二进制数据data
- 将data进行传输,或者发送给对方
- 对方收到data数据,将data通过proto.UnMarshal得到person结构体数据
1. 编写.proto文件
2. 执行protoc编译出对应go代码
通过如下方式调用protocol编译器,把 .proto 文件编译成代码
protoc --proto_path=IMPORT_PATH --go_out=DST_DIR path/to/file.proto
其中:
- –proto_path,指定了 .proto 文件导包时的路径,可以有多个,如果忽略则默认当前目录。
- –go_out, 指定了生成的go语言代码文件放入的文件夹
- 允许使用protoc --go_out=./ *.proto的方式一次性编译多个 .proto 文件 【.proto中需要添加option go_package选项】,否则会报:protoc-gen-go: unable to determine Go import path for “xxx.proto”
option go_package = "./;proto"; //配置包依赖路径
- 编译时,protobuf 编译器会把 .proto 文件编译成 .pd.go 文件
3. 通过proto.Marshal进行序列化(发数据)
data, err := proto2.Marshal(person)
4. 通过proto.UnMarshal进行反序列话(收数据)
err = proto2.Unmarshal(data, &newPerson)
③测试传输
在myDemo/protobuf文件夹下编写main.go进行测试
main.go
package main
import (
"fmt"
proto2 "google.golang.org/protobuf/proto"
pb "myTest/myDemo/protobuf/pb"
)
func main() {
person := &pb.Person{
Name: "ziyi",
Age: 18,
Emails: []string{"ziyi.atgmai.com", "ziyi_at163.com"},
Phones: []*pb.PhoneNumber{
&pb.PhoneNumber{
Number: "181234567",
Type: pb.PhoneType_MOBILE,
},
&pb.PhoneNumber{
Number: "33331111",
Type: pb.PhoneType_HOME,
},
},
}
//编码:将person对象编码,将protobuf的message进行序列化,得到一个[]byte数组
data, err := proto2.Marshal(person)
if err != nil {
fmt.Println("protobuf marshal err =", err)
return
}
//解码
newPerson := pb.Person{}
err = proto2.Unmarshal(data, &newPerson)
if err != nil {
fmt.Println("protobuf unmarshal err =", err)
return
}
fmt.Println("传输的数据:", person)
fmt.Println("接收到的数据:", &newPerson)
}
3 游戏相关业务
3.1 业务消息格式定义
MsgID:1(同步玩家本地登录ID)
- MsgID:1
- 同步玩家本地登录的ID(用来标识玩家),玩家登录之后,由Server端主动生成玩家ID发送给客户端
- 发起者:Server
- Pid:玩家ID
对应proto:
message SyncPid{
int32 Pid=1;
}
MsgID:2(世界聊天)
● 同步玩家本次登录的ID(用来标识玩家), 玩家登陆之后,由Server端主动生成玩家ID发送给客户端
● 发起者: Client
● Content: 聊天信息
message Talk{
string Content=1;
}
MsgID:3(移动信息)
● 移动的坐标数据
● 发起者: Client
● P: Position类型,地图的左边点
message Position{
float X=1;
float Y=2;
float Z=3;
float V=4;
}
MsgID:200(广播聊天、坐标、动作)
● 广播消息
● 发起者: Server
● Tp: 1 世界聊天, 2 坐标, 3 动作, 4 移动之后坐标信息更新
● Pid: 玩家ID
message BroadCast{
int32 Pid=1;
int32 Tp=2;
//oneof表示只能选三个中的一个
oneof Data {
string Content=3;
Position P=4;
int32 ActionData=5;
}
}
MsgID:201
● 广播消息 掉线/aoi消失在视野
● 发起者: Server
● Pid: 玩家ID
message SyncPid{
int32 Pid=1;
}
MsgID:202(同步位置信息)
● 同步周围的人位置信息(包括自己)
● 发起者: Server
● ps: Player 集合,需要同步的玩家
message SyncPlayers{
repeated Player ps=1;
}
message Player{
int32 Pid=1;
Position P=2;
}
3.2 项目模块搭建
mmo_game_zinx
- apis:存放基本用户的自定义路由业务,一个msgId对应一个业务
- conf:存放zinx.json(自定义框架的配置文件)
- pb:protobuf相关文件
- core:存放核心功能
- main.go:服务器的主入口
- game_client:unity客户端
最终项目结构
.
└── mmo_game_zinx
├── apis
├── conf
│ └── zinx.json
├── core
│ ├── aoi.go
│ ├── aoi_test.go
│ ├── grid.go
├── game_client
│ └── client.exe
├── pb
│ ├── build.sh
│ └── msg.proto
├── README.md
└── server.go
①玩家上线
- 创建一个玩家的方法:
- 编写proto文件
- 定义玩家对象player.go
- 玩家可以和客户端通信的发送消息的方法:
- 将msg的proto格式进行序列化改成二进制
- 通过zinx框架提供的sendMsg将数据进行TLV格式的打包发包
- 实现上线业务功能:
- 给server注册一个创建连接之后的hook函数
- 给Player提供两个方法:将PlayerID同步给客户端、将Player上线的初始位置同步给客户端
1. mmo_game_zinx/pb/msg.proto
定义proto文件(消息类型)
syntax = "proto3"; //Proto协议
package pb; //当前包名
option csharp_namespace = "Pb"; //给C#提供的选项[因为我们的游戏画面采用unity3D,基于C#的]
option go_package = "./;pb"; //配置包依赖路径
//同步客户端玩家ID
message SyncPid{
int32 Pid=1;
}
//玩家位置
message Position{
float X=1;
float Y=2;
float Z=3;
float V=4;
}
//玩家广播数据
message BroadCast{
int32 Pid=1;
int32 Tp=2;//1 世界聊天, 2 坐标, 3 动作, 4 移动之后坐标信息更新
oneof Data {
string Content=3;
Position P=4;
int32 ActionData=5;
}
}
为了方便后续更新proto文件,我们这里直接编写一个脚本
mmo_game_zinx/pb/build.sh:
#!/bin/bash
protoc --go_out=. *.proto
2. mmo_game_zinx/core/player.go
package core
import (
"fmt"
"google.golang.org/protobuf/proto"
"math/rand"
pb "myTest/mmo_game_zinx/pb"
"myTest/zinx/ziface"
"sync"
)
// 玩家
type Player struct {
Pid int32 //玩家ID
Conn ziface.IConnection //当前玩家的连接(用于和客户端的连接)
X float32 //平面的X坐标
Y float32 //高度
Z float32 //平面y坐标(注意:Z字段才是玩家的平面y坐标,因为unity的客户端已经定义好了)
V float32 //旋转的0-360角度
}
var PidGen int32 = 1 //用于生成玩家id
var IdLock sync.Mutex //保护PidGen的锁
func NewPlayer(conn ziface.IConnection) *Player {
IdLock.Lock()
id := PidGen
PidGen++
IdLock.Unlock()
p := &Player{
Pid: id,
Conn: conn,
X: float32(160 + rand.Intn(10)), //随机在160坐标点,基于X轴若干便宜
Y: 0,
Z: float32(140 + rand.Intn(20)), //随机在140坐标点,基于Y轴若干偏移
V: 0,
}
return p
}
/*
提供一个发送给客户端消息的方法
主要是将pb的protobuf数据序列化后,再调用zinx的sendMsg方法
*/
func (p *Player) SendMsg(msgId uint32, data proto.Message) {
//将proto Message结构体序列化 转换成二进制
msg, err := proto.Marshal(data)
if err != nil {
fmt.Println("marshal msg err: ", err)
return
}
//将二进制文件 通过zinx框架的sendMsg将数据发送给客户端
if p.Conn == nil {
fmt.Println("connection in player is nil")
return
}
if err := p.Conn.SendMsg(msgId, msg); err != nil {
fmt.Println("player send msg is err, ", err)
return
}
}
// 告知客户端玩家的pid,同步已经生成的玩家ID给客户端
func (p *Player) SyncPid() {
//组件MsgID:0的proto数据
proto_msg := &pb.SyncPid{
Pid: p.Pid,
}
//将消息发送给客户端
p.SendMsg(1, proto_msg)
}
// 广播玩家自己的出生地点
func (p *Player) BroadCastStartPosition() {
//组建MsgID:200 的proto数据
proto_msg := &pb.BroadCast{
Pid: p.Pid,
Tp: 2, //Tp2 代表广播位置的坐标
Data: &pb.BroadCast_P{
P: &pb.Position{
X: p.X,
Y: p.Y,
Z: p.Z,
V: p.V,
},
},
}
//将消息发送给客户端
p.SendMsg(200, proto_msg)
}
3. mmo_game_zinx/main.go
package main
import (
"fmt"
"myTest/mmo_game_zinx/core"
"myTest/zinx/ziface"
"myTest/zinx/znet"
)
// 当前客户端建立连接之后的hook函数
func OnConnectionAdd(conn ziface.IConnection) {
//创建一个player对象
player := core.NewPlayer(conn)
//给客户端发送MsgID:1的消息,同步当前的playerID给客户端
player.SyncPid()
//给客户端发送MsgID:200的消息,同步当前Player的初始位置给客户端
player.BroadCastStartPosition()
fmt.Println("======>Player pid = ", player.Pid, " is arrived ====")
}
func main() {
//创建服务句柄
s := znet.NewServer("MMO Game Zinx")
s.SetOnConnStart(OnConnectionAdd)
s.Serve()
}
测试效果
- 启动服务端
- 启动客户端(client.exe)
连续启动多个查看效果:
服务端控制台打印:
②世界聊天
- proto3聊天协议的定义
- 聊天业务的实现:
- 解析聊天的proto协议
- 将聊天数据广播给全部在线玩家->创建一个世界管理模块
- 初始化管理模块
- 添加一个玩家
- 删除一个玩家
- 通过玩家ID查询Player对象
- 获取全部的在线玩家
1. mmo_game_zinx/pb/msg.proto
在之前的基础上,在末尾追加:
message Talk{
string Content=1;
}
执行build.sh脚本重新编译
2. mmo_game_zinx/apis/world_chat.go
package apis
import (
"fmt"
"google.golang.org/protobuf/proto"
"myTest/mmo_game_zinx/core"
pb "myTest/mmo_game_zinx/pb"
"myTest/zinx/ziface"
"myTest/zinx/znet"
)
// 世界聊天路由业务
type WorldChatApi struct {
znet.BaseRouter
}
// 重写handler方法
func (wc *WorldChatApi) Handler(request ziface.IRequest) {
//1 解析客户端传递进来的proto协议
proto_msg := &pb.Talk{}
err := proto.Unmarshal(request.GetData(), proto_msg)
if err != nil {
fmt.Println("Talk Unmarshal err ", err)
return
}
//2 当前的聊天数据 属于哪个玩家发送的
pid, err := request.GetConnection().GetProperty("pid")
//3 根据pid得到对应的player对象
player := core.WorldMgrObj.GetPlayerByPid(pid.(int32))
//4 将这个消息广播给其他全部在线的用户
player.Talk(proto_msg.Content)
}
3. mmo_game_zinx/main.go
package main
import (
"fmt"
"myTest/mmo_game_zinx/apis"
"myTest/mmo_game_zinx/core"
"myTest/zinx/ziface"
"myTest/zinx/znet"
)
// 当前客户端建立连接之后的hook函数
func OnConnectionAdd(conn ziface.IConnection) {
//创建一个player对象
player := core.NewPlayer(conn)
//给客户端发送MsgID:1的消息,同步当前的playerID给客户端
player.SyncPid()
//给客户端发送MsgID:200的消息,同步当前Player的初始位置给客户端
player.BroadCastStartPosition()
//将当前新上线的玩家添加到WorldManager中
core.WorldMgrObj.AddPlayer(player)
//将playerId添加到连接属性中,方便后续广播知道是哪个玩家发送的消息
conn.SetProperty("pid", player.Pid)
fmt.Println("======>Player pid = ", player.Pid, " is arrived ====")
}
func main() {
//创建服务句柄
s := znet.NewServer("MMO Game Zinx")
s.SetOnConnStart(OnConnectionAdd)
//注册一些路由业务
s.AddRouter(2, &apis.WorldChatApi{})
s.Serve()
}
4. mmo_game_zinx/core/world_manager.go
package core
import "sync"
/*
当前游戏的世界总管理模块
*/
type WorldManager struct {
//AOIManager 当前世界地图AOI的管理模块
AoiMgr *AOIManager
//当前全部在线的players集合
Players map[int32]*Player
//保护Players集合的锁
pLock sync.RWMutex
}
// 提供一个对外的世界管理模块的句柄
var WorldMgrObj *WorldManager
func init() {
WorldMgrObj = &WorldManager{
//创建世界AOI地图规划
AoiMgr: NewAOIManager(AOI_MIN_X, AOI_MAX_X, AOI_CNTS_X, AOI_MIN_Y, AOI_MAX_Y, AOI_CNTS_Y),
//初始化player集合
Players: make(map[int32]*Player),
}
}
// 添加一个玩家
func (wm *WorldManager) AddPlayer(player *Player) {
wm.pLock.Lock()
wm.Players[player.Pid] = player
wm.pLock.Unlock()
//将player添加到AOIManager中
wm.AoiMgr.AddToGridByPos(int(player.Pid), player.X, player.Z)
}
// 删除一个玩家
func (wm *WorldManager) RemovePlayerByPid(pid int32) {
//得到当前的玩家
player := wm.Players[pid]
//将玩家从AOIManager中删除
wm.AoiMgr.RemoveFromGridByPos(int(pid), player.X, player.Z)
//将玩家从世界管理中删除
wm.pLock.Lock()
delete(wm.Players, pid)
wm.pLock.Unlock()
}
// 通过玩家ID查询player对象
func (wm *WorldManager) GetPlayerByPid(pid int32) *Player {
wm.pLock.RLock()
defer wm.pLock.RUnlock()
return wm.Players[pid]
}
// 获取全部的在线玩家
func (wm *WorldManager) GetAllPlayers() []*Player {
wm.pLock.Lock()
defer wm.pLock.Unlock()
players := make([]*Player, 0)
//遍历集合,将玩家添加到players切片中
for _, p := range wm.Players {
players = append(players, p)
}
return players
}
5. mmo_game_zinx/core/player.go
package core
import (
"fmt"
"google.golang.org/protobuf/proto"
"math/rand"
pb "myTest/mmo_game_zinx/pb"
"myTest/zinx/ziface"
"sync"
)
// 玩家
type Player struct {
Pid int32 //玩家ID
Conn ziface.IConnection //当前玩家的连接(用于和客户端的连接)
X float32 //平面的X坐标
Y float32 //高度
Z float32 //平面y坐标(注意:Z字段才是玩家的平面y坐标,因为unity的客户端已经定义好了)
V float32 //旋转的0-360角度
}
var PidGen int32 = 1 //用于生成玩家id
var IdLock sync.Mutex //保护PidGen的锁
func NewPlayer(conn ziface.IConnection) *Player {
IdLock.Lock()
id := PidGen
PidGen++
IdLock.Unlock()
p := &Player{
Pid: id,
Conn: conn,
X: float32(160 + rand.Intn(10)), //随机在160坐标点,基于X轴若干便宜
Y: 0,
Z: float32(140 + rand.Intn(20)), //随机在140坐标点,基于Y轴若干偏移
V: 0,
}
return p
}
/*
提供一个发送给客户端消息的方法
主要是将pb的protobuf数据序列化后,再调用zinx的sendMsg方法
*/
func (p *Player) SendMsg(msgId uint32, data proto.Message) {
//将proto Message结构体序列化 转换成二进制
msg, err := proto.Marshal(data)
if err != nil {
fmt.Println("marshal msg err: ", err)
return
}
//将二进制文件 通过zinx框架的sendMsg将数据发送给客户端
if p.Conn == nil {
fmt.Println("connection in player is nil")
return
}
if err := p.Conn.SendMsg(msgId, msg); err != nil {
fmt.Println("player send msg is err, ", err)
return
}
}
// 告知客户端玩家的pid,同步已经生成的玩家ID给客户端
func (p *Player) SyncPid() {
//组件MsgID:0的proto数据
proto_msg := &pb.SyncPid{
Pid: p.Pid,
}
//将消息发送给客户端
p.SendMsg(1, proto_msg)
}
// 广播玩家自己的出生地点
func (p *Player) BroadCastStartPosition() {
//组建MsgID:200 的proto数据
proto_msg := &pb.BroadCast{
Pid: p.Pid,
Tp: 2, //Tp2 代表广播位置的坐标
Data: &pb.BroadCast_P{
P: &pb.Position{
X: p.X,
Y: p.Y,
Z: p.Z,
V: p.V,
},
},
}
//将消息发送给客户端
p.SendMsg(200, proto_msg)
}
// 玩家广播世界聊天消息
func (p *Player) Talk(content string) {
//1 组建MsgID:200 proto数据
proto_msg := &pb.BroadCast{
Pid: p.Pid,
Tp: 1, //tp-1 代表聊天广播
Data: &pb.BroadCast_Content{
Content: content,
},
}
//2 得到当前世界所有在线的玩家
players := WorldMgrObj.GetAllPlayers()
for _, player := range players {
//player分别给对应的客户端发送消息
player.SendMsg(200, proto_msg)
}
}
测试效果
启动服务