当前位置: 首页 > news >正文

从0到1开发go-tcp框架【4实战片— — 开发MMO之玩家聊天篇】

从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 coreimport ("fmt""sync"
)/*
一个AOI地图中的格子类型
*/
type Grid struct {//格子IDGID 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 coreimport "fmt"// 定义AOI地图大小
const (AOI_MIN_X  int = 85AOI_MAX_X  int = 410AOI_CNTS_X int = 10AOI_MIN_Y  int = 75AOI_MAX_Y  int = 400AOI_CNTS_Y int = 20
)type AOIManager struct {//区域的左边界坐标MinX int//区域的右界坐标MaxX int//X方向格子的数量CountsX int//区域的上边界坐标MinY int//区域的下边界坐标MaxY int//Y方向上格子的数量CountsY int//当前区域中有哪些格子map-key=格子的IDgrids 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 + idxgid := 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) {//根据横纵坐标得到当前坐标属于哪个格子IDgID := 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 coreimport ("fmt""testing"
)func TestNewAOIManager(t *testing.T) {//初始化AOIManageraoiMgr := NewAOIManager(0, 250, 5, 0, 250, 5)//打印AOIManagerfmt.Println(aoiMgr)
}//根据GID获取九宫格
func TestAOIManager_GetSurroundGridsByGid(t *testing.T) {//初始化AOIManageraoiMgr := 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

  1. json:可读性比较强;编解码比较耗时【web领域】
  2. xml:基于标签【前端/网页】
  3. 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

其中:

  1. –proto_path,指定了 .proto 文件导包时的路径,可以有多个,如果忽略则默认当前目录。
  2. –go_out, 指定了生成的go语言代码文件放入的文件夹
  3. 允许使用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"; //配置包依赖路径

在这里插入图片描述

  1. 编译时,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 mainimport ("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)

  1. 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

在这里插入图片描述

①玩家上线

在这里插入图片描述

  • 创建一个玩家的方法:
  1. 编写proto文件
  2. 定义玩家对象player.go
  • 玩家可以和客户端通信的发送消息的方法:
  1. 将msg的proto格式进行序列化改成二进制
  2. 通过zinx框架提供的sendMsg将数据进行TLV格式的打包发包
  • 实现上线业务功能:
  1. 给server注册一个创建连接之后的hook函数
  2. 给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 coreimport ("fmt""google.golang.org/protobuf/proto""math/rand"pb "myTest/mmo_game_zinx/pb""myTest/zinx/ziface""sync"
)// 玩家
type Player struct {Pid  int32              //玩家IDConn 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 := PidGenPidGen++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 mainimport ("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()
}
测试效果
  1. 启动服务端
  2. 启动客户端(client.exe)
    在这里插入图片描述

连续启动多个查看效果:

在这里插入图片描述
服务端控制台打印:
在这里插入图片描述

②世界聊天

在这里插入图片描述

  1. proto3聊天协议的定义
  2. 聊天业务的实现:
  • 解析聊天的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 apisimport ("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 mainimport ("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 coreimport "sync"/*
当前游戏的世界总管理模块
*/
type WorldManager struct {//AOIManager 当前世界地图AOI的管理模块AoiMgr *AOIManager//当前全部在线的players集合Players map[int32]*Player//保护Players集合的锁pLock sync.RWMutex
}// 提供一个对外的世界管理模块的句柄
var WorldMgrObj *WorldManagerfunc 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] = playerwm.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 coreimport ("fmt""google.golang.org/protobuf/proto""math/rand"pb "myTest/mmo_game_zinx/pb""myTest/zinx/ziface""sync"
)// 玩家
type Player struct {Pid  int32              //玩家IDConn 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 := PidGenPidGen++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)}
}
测试效果

在这里插入图片描述

启动服务

相关文章:

从0到1开发go-tcp框架【4实战片— — 开发MMO之玩家聊天篇】

从0到1开发go-tcp框架【实战片— — 开发MMO】 MMO&#xff08;MassiveMultiplayerOnlineGame&#xff09;&#xff1a;大型多人在线游戏&#xff08;多人在线网游&#xff09; 1 AOI兴趣点的算法 游戏中的坐标模型&#xff1a; 场景相关数值计算 ● 场景大小&#xff1a; 250…...

无重复字符的最长子串 LeetCode热题100

题目 给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长连续子字符串 的长度。 思路 使用滑动窗口&#xff0c;记录窗口区间的长度大小&#xff0c;取最大值。用map存储滑动窗口内所有字符&#xff0c;字符作为key&#xff0c;每个字符的数量作为value。遍历…...

Docker搭建zookeeper

问题背景 前言 本文参考自&#xff1a;docker-compose快速搭建Zookeeper集群还有一种更加详细更加全面的部署方式&#xff1a;Docker之docker-compose一键部署Zookeeper集群&#xff0c;但笔者还未验证&#xff0c;先记录下来 搭建 安装docker-ce 此处不赘述 安装docker-co…...

LeetCode 热题 100 JavaScript--160. 相交链表

/*** Definition for singly-linked list.* function ListNode(val) {* this.val val;* this.next null;* }*//*** param {ListNode} headA* param {ListNode} headB* return {ListNode}*/// 1、暴力解法 var getIntersectionNode function(headA, headB) {var p1 …...

AWS S3 协议对接 minio/oss 等

使用亚马逊 S3 协议访问对象存储 [s3-API](https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/API/API_Operations_Amazon_Simple_Storage_Service.html)- 兼容S3协议的对象存储有- minio- 似乎是完全兼容 [兼容文档](https://www.minio.org.cn/product/s3-compatibility.htm…...

手机便签内容不见了怎么恢复正常?

在日常生活和工作中&#xff0c;很多人都需要随手记录事情&#xff0c;例如家庭琐事、孩子相关的事情、指定时间需要完成的工作任务、会议安排等。当我们需要随时随地记录事情的时候&#xff0c;手机便签应用就是非常不多的选择&#xff0c;我们直接打开手机上的便签APP就可以新…...

【架构】Java 系统架构演进的思考

文章目录 1 前言2 单体应用架构3 垂直应用架构4 分布式架构5 SOA 架构6 微服务云架构7 总结 1 前言 随着移动互联的发展&#xff0c;网站、H5、移动端的应用规模也不断扩大&#xff0c;不管是应用的数量还是质量都得到了指数级的提升。开发者的数量与日俱增&#xff0c;应用的…...

Python爬虫——解析_jsonpath

jsonpath的安装 pip install jsonpathjsonpath的使用&#xff1a; obj json.load(open(json文件, r, encodingutf-8)) ret jsonpath.jsonpath(obj, jsonpath语法)json文件&#xff1a; { "store": {"book": [{ "category": "末世"…...

华为发布数字资产继承功能

在华为开发者大会2023&#xff08;HDC.Together&#xff09;上&#xff0c;华为常务董事、终端BG CEO、智能汽车解决方案BU CEO余承东正式发布了数字资产继承功能&#xff0c;HarmonyOS提供了安全便捷的数字资产继承路径。 在鸿蒙世界中&#xff0c;我们每个人在每台设备、应用…...

阿里云NAS文件存储基本介绍与购买使用

文章目录 1.NAS文件存储基本概念1.1.什么是NAS文件存储1.2.NAS的应用场景1.3.NAS、OSS、EBS的区别 2.购买NAS文件存储2.1.开通NAS服务2.2.创建NAS文件系统2.3.配置NAS文件系统属性2.4.查看购买的NAS服务 3.NAS文件存储基本使用3.1.修改NAS文件系统默认的名称3.2.NAS的权限组3.3…...

大模型使用——超算上部署LLAMA-2-70B-Chat

大模型使用——超算上部署LLAMA-2-70B-Chat 前言 1、本机为Inspiron 5005&#xff0c;为64位&#xff0c;所用操作系统为Windos 10。超算的操作系统为基于Centos的linux&#xff0c;GPU配置为A100&#xff0c;所使用开发环境为Anaconda。 2、本教程主要实现了在超算上部署LLAM…...

机器学习笔记:李宏毅ChatGPT课程1:刨析ChatGPT

ChatGPT——Chat Generative Pre-trained Transformer 1 文字接龙 每次输出一个概率分布&#xff0c;根据概率sample一个答案 ——>因为是根据概率采样&#xff0c;所以ChatGPT每次的答案是不一样的&#xff08;把生成式学习拆分成多个分类问题&#xff09;将生成的答案加到…...

Llama 2 with langchain项目详解(三)

Llama 2 with langchain项目详解(三) 17.3 Llama 2 with langchain基础 本节讲解在LangChain中使用Llama 2模型的基础知识,展示如何运行LangChain的代码,及在云端运行Llama 2的700亿模型。 首先,使用Python的pip管理器安装一系列库,包括huggingface/transformers、datase…...

牛客 AB30 排序(快排模板)

描述 给定一个长度为 n 的数组&#xff0c;请你编写一个函数&#xff0c;返回该数组按升序排序后的结果。 数据范围&#xff1a; 0≤&#xfffd;≤11030≤n≤1103&#xff0c;数组中每个元素都满足 0≤&#xfffd;&#xfffd;&#xfffd;≤1090≤val≤109 要求&#xff1…...

【Linux旅行记】第一个小程序“进度条“!

文章目录 一、预备知识1.1回车换行1.2缓冲区 二、倒计时三、进度条3.1普通版本源代码3.2高级版本源代码 &#x1f340;小结&#x1f340; &#x1f389;博客主页&#xff1a;小智_x0___0x_ &#x1f389;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &…...

DeepMind将AI用于可控核聚变:将等离子体形状模拟精度提高65%

近日&#xff0c;英国AI公司DeepMind宣布取得了一项新的突破&#xff0c;成功实现了AI可控核聚变。这一技术能够在高温等离子体环境下实现精准放电&#xff0c;为核聚变技术的发展提供了新的思路和创新。 长期以来&#xff0c;相关领域的科学家们&#xff0c;一直在寻找清洁、取…...

Scrum是什么意思,Scrum敏捷项目管理工具有哪些?

一、什么是Scrum&#xff1f; Scrum是一种敏捷项目管理方法&#xff0c;旨在帮助团队高效地开展软件开发和项目管理工作。 Scrum强调迭代和增量开发&#xff0c;通过将项目分解为多个短期的开发周期&#xff08;称为Sprint&#xff09;&#xff0c;团队可以更好地应对需求变…...

【从零单排Golang】第十三话:使用WaitGroup等待多路并行的异步任务

在后端开发当中&#xff0c;经常会遇到这样的场景&#xff1a;请求给了批量的输入&#xff0c;对于每一个输入&#xff0c;我们都要给外部发请求等待返回&#xff0c;然后才能继续其它自己的业务逻辑。在这样的case下&#xff0c;如果每一个输入串行处理的话&#xff0c;那么很…...

WSL2安装CentOS7和CentOS8

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、下载ZIP包&#xff1f;二、安装1.打开Windows子系统支持2.安装到指定位置3.管理虚拟机4.配置虚拟机1.配置国内源2.安装软件3.安装第三方源 5.配置用户1.创建…...

不平衡电网条件下基于变频器DG操作的多目标优化研究(Matlab代码Simulink实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f308;4 Matlab代码&Simulink实现&文章讲解 &#x1f4a5;1 概述 文献来源&#xff1a; 最近&#xff0c;利用并网转换器&#xff08;GCC&#xff09;克服电网故障并支撑电网电压已…...

工业安全零事故的智能守护者:一体化AI智能安防平台

前言&#xff1a; 通过AI视觉技术&#xff0c;为船厂提供全面的安全监控解决方案&#xff0c;涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面&#xff0c;能够实现对应负责人反馈机制&#xff0c;并最终实现数据的统计报表。提升船厂…...

解锁数据库简洁之道:FastAPI与SQLModel实战指南

在构建现代Web应用程序时&#xff0c;与数据库的交互无疑是核心环节。虽然传统的数据库操作方式&#xff08;如直接编写SQL语句与psycopg2交互&#xff09;赋予了我们精细的控制权&#xff0c;但在面对日益复杂的业务逻辑和快速迭代的需求时&#xff0c;这种方式的开发效率和可…...

Linux-07 ubuntu 的 chrome 启动不了

文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了&#xff0c;报错如下四、启动不了&#xff0c;解决如下 总结 问题原因 在应用中可以看到chrome&#xff0c;但是打不开(说明&#xff1a;原来的ubuntu系统出问题了&#xff0c;这个是备用的硬盘&a…...

LLM基础1_语言模型如何处理文本

基于GitHub项目&#xff1a;https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken&#xff1a;OpenAI开发的专业"分词器" torch&#xff1a;Facebook开发的强力计算引擎&#xff0c;相当于超级计算器 理解词嵌入&#xff1a;给词语画"…...

12.找到字符串中所有字母异位词

&#x1f9e0; 题目解析 题目描述&#xff1a; 给定两个字符串 s 和 p&#xff0c;找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义&#xff1a; 若两个字符串包含的字符种类和出现次数完全相同&#xff0c;顺序无所谓&#xff0c;则互为…...

分布式增量爬虫实现方案

之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面&#xff0c;避免重复抓取&#xff0c;以节省资源和时间。 在分布式环境下&#xff0c;增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路&#xff1a;将增量判…...

基于Java Swing的电子通讯录设计与实现:附系统托盘功能代码详解

JAVASQL电子通讯录带系统托盘 一、系统概述 本电子通讯录系统采用Java Swing开发桌面应用&#xff0c;结合SQLite数据库实现联系人管理功能&#xff0c;并集成系统托盘功能提升用户体验。系统支持联系人的增删改查、分组管理、搜索过滤等功能&#xff0c;同时可以最小化到系统…...

Go 并发编程基础:通道(Channel)的使用

在 Go 中&#xff0c;Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式&#xff0c;用于在多个 Goroutine 之间传递数据&#xff0c;从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...

GitHub 趋势日报 (2025年06月06日)

&#x1f4ca; 由 TrendForge 系统生成 | &#x1f310; https://trendforge.devlive.org/ &#x1f310; 本日报中的项目描述已自动翻译为中文 &#x1f4c8; 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...

三分算法与DeepSeek辅助证明是单峰函数

前置 单峰函数有唯一的最大值&#xff0c;最大值左侧的数值严格单调递增&#xff0c;最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值&#xff0c;最小值左侧的数值严格单调递减&#xff0c;最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...