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

news2024/12/23 14:10:35

从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

  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 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)

  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 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()
}
测试效果
  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 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)
	}
}
测试效果

在这里插入图片描述

启动服务

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/841248.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue2:组件基础(下)

Vue2&#xff1a;组件基础&#xff08;下&#xff09; Date: April 12, 2023 Sum: props验证、计算属性、自定义时间、组件上的v-model、任务列表案例 Tags: * 目标&#xff1a; 能够知道如何对 props 进行验证 能够知道如何使用计算属性 令能够知道如何为组件自定义事件 …

K8S系列文章 之 容器网络基础 Docker0

什么是Docker0 使用ip addr命令看一下网卡&#xff1a; rootKitDevVps:~# ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00inet 127.0.0.1/8 scope host…

火车头标题伪原创【php源码】

大家好&#xff0c;给大家分享一下python怎么读取文件中的数据&#xff0c;很多人还不知道这一点。下面详细解释一下。现在让我们来看看&#xff01; 火车头采集ai伪原创插件截图&#xff1a; python是一门非常火爆且相对易学的编程语言&#xff0c;应用在各种场景。许多人想学…

VS code:Task

Task 微软官方连接&#xff1a; https://code.visualstudio.com/docs/editor/tasks what is Task 我们知道&#xff0c;vscode可以支持许多编程语言&#xff0c;很多语言是需要进行编译的&#xff0c;打包&#xff0c;测试… 有许多已有的工具支持这些流程&#xff0c;例如A…

Nginx(1)

目录 1.Nginx概述2.Nginx的特点3.Nginx主要功能1.反向代理2.负载均衡 1.Nginx概述 Nginx (engine x) 是一个自由的、开源的、高性能的HTTP服务器和反向代理服务器&#xff0c;也是一个IMAP、POP3、SMTP代理服务器。 Nginx是一个强大的web服务器软件&#xff0c;用于处理高并发…

【windows】windows上如何使用linux命令?

前言 windows上的bat命令感觉不方便&#xff0c;想在windows上使用linux命令。 有人提供了轮子&#xff0c;本文简单介绍一些该轮子的安装与使用&#xff0c;希望能够帮助到和我有一起需求的网友。 我的答案是busybox。 1.安装busybox.exe 在这个网站上安装busybox busyb…

---------------- 部署 Zookeeper 集群 ----------------

部署 Zookeeper 集群 1.安装前准备2.安装 Zookeeper修改配置文件在每个节点上创建数据目录和日志目录在每个节点的dataDir指定的目录下创建一个 myid 的文件配置 Zookeeper 启动脚本 //准备 3 台服务器做 Zookeeper 集群 192.168.109.1 192.168.109.2 192.168.109.3 1.安装前准…

前端学习---vue2--选项/数据--data-computed-watch-methods-props

写在前面&#xff1a; vue提供了很多数据相关的。 文章目录 data 动态绑定介绍使用使用数据 computed 计算属性介绍基础使用计算属性缓存 vs 方法完整使用 watch 监听属性介绍使用 methodspropspropsData data 动态绑定 介绍 简单的说就是进行双向绑定的区域。 vue实例的数…

Abaqus 中最常用的子程序有哪些 硕迪科技

在ABAQUS中&#xff0c;用户定义的子程序是一种重要的构件&#xff0c;可以将其插入到Abaqus分析中以增强该软件的功能和灵活性。这些子程序允许用户在分析过程中添加自定义材料模型、边界条件、初始化、加载等特定操作&#xff0c;以便更精准地模拟分析中的现象和现象。ABAQUS…

AirPods 充电接触不良问题修复

AirPods 充电接触不良问题修复 问题现象 从充电盒拿出耳机&#xff0c;一只耳机电量不满甚至完全没有电放入充电盒不充电&#xff0c;指示灯是绿色而非橙色多次尝试耳机能充电&#xff0c;但是合上盖子就无法充电 如果你的耳机出现上述症状&#xff0c;基本就是耳机充电接触…

一篇万能英语作文范文怎么写?聪明灵犀英语作文写作工具分享

一篇万能英语作文范文怎么写&#xff1f;英语作文是英语学习中不可或缺的一环&#xff0c;但是对于很多人来说&#xff0c;写作并不是一件容易的事情。本文将分享一些实用的英语作文写作工具&#xff0c;帮助你更好地写作。 1. 明确主题 写作之前&#xff0c;首先需要明确主题…

恒运资本:另类投资业务火热 券商“投行+”模式盛行

自科创板实行保荐组织跟投准则以来&#xff0c;各家券商纷繁开端树立特殊出资子公司以参加科创板项目的跟投&#xff0c;近期星展证券也取得经过树立子公司从事特殊出资事务的资历。到现在&#xff0c;至少有82家券商获批树立特殊出资子公司。 多位券商负责人表明&#xff0c;新…

python编程人工智能小例子,《人工智能python课程》

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;python编程人工智能小例子&#xff0c;《人工智能python课程》&#xff0c;现在让我们一起来看看吧&#xff01; python 怎么实现人工智能 一、Python是解释语言&#xff0c;程序写起来非常方便写程序方便对做机器学习的…

极客教程 scrapy和selenium

selenium 极客教程 使用python 调用scrapy的 爬虫Spider并且相互之间可以正常传参实现全局 常规情况创建&#xff0c;使用命令 scrapy genspider baidu "baidu.com"Python中Scrapy框架详解 浏览器调试模式下&#xff08;F12 或 右键检查&#xff09;Command sh…

ospf 案例

OSPF 基础案例 配置 S1 交换机 [S1]int LoopBack 0 [S1-LoopBack0]ip addr 1.1.1.1 32[S1]vlan batch 20 30 Info: This operation may take a few seconds. Please wait for a moment...done.[S1]int vlanif 20 [S1-Vlanif20]ip addr 192.168.20.1 24 [S1-Vlanif20]int vlani…

新版Android Studio模拟器浮动

&#xff08;水一篇&#xff0c;但其实很多入门同学不知道&#xff09; 安装新版Andorid Studio后会发现模拟器是内嵌在AS中的&#xff0c;如何让她浮动

创建型模式-单例模式

文章目录 一、创建型模式1. 单例设计模式1.1 单例模式的结构1.2 单例模式的实现&#xff08;1&#xff09;饿汉式-方式1&#xff08;静态变量方式&#xff09;&#xff08;2&#xff09;饿汉式-方式2&#xff08;静态代码块方式&#xff09;&#xff08;3&#xff09;懒汉式-方…

lammps拉伸初始应力不为零原因及解决办法

拉伸模拟是研究材料拉伸性能的常用方法。通常情况下&#xff0c;初始应力为零&#xff0c;以获得准确的应力-应变曲线。 然而&#xff0c;在某些情况下&#xff0c;模拟得到的初始应力大于零或者小于零。 &#xff08;1&#xff09;初始应力不为零的原因 根本原因是在拉伸前…

SpringBoot + Docker 实现一次构建到处运行~

一、容器化部署的好处 图片 Docker 作为一种新兴的虚拟化方式&#xff0c;它可以更高效的利用系统资源&#xff0c;不需要进行硬件虚拟以及运行完整操作系统等额外开销。 传统的虚拟机技术启动应用服务往往需要数分钟&#xff0c;而 Docker 容器应用&#xff0c;由于直接运行…

【Linux 网络】网络层协议之IP协议

IP协议 IP协议所处的位置网络层要解决的问题IP协议格式分片与组装网段划分特殊的IP地址IP地址的数量限制私网IP地址和公网IP地址路由 IP协议所处的位置 IP指网际互连协议&#xff0c;Internet Protocol的缩写&#xff0c;是TCP/IP体系中的网络层协议。 网络层要解决的问题 网络…