从0到1开发go-tcp框架【5实战片— — 开发MMO之同步玩家位置信息与下线】【完结】

从0到1开发go-tcp框架【5实战片— — 开发MMO之同步玩家位置信息与下线】

完整代码:

仓库地址:https://gitee.com/Zifasdfa/zi-zinx

MMO(Massively Multiplayer Online):指的是一种游戏类型,可以容纳大量玩家同时在线,并且可以在游戏中进行大规模的多人互动。

  • 在 MMO(Massively Multiplayer Online)游戏开发中,AOI(Area of Interest)是一种常用的空间管理技术,用于处理玩家和游戏世界中其他实体之间的交互。

AOI 是一个虚拟的区域,通常是一个矩形或圆形区域,用于表示一个玩家或一个实体感兴趣的区域。当一个玩家或实体进入某个玩家的 AOI 区域时,他们会被视为在同一区域内,可以相互感知和交互。AOI 技术可以有效地减少服务器处理的工作量,提高游戏的性能和可扩展性。

1 玩家位置信息同步

从0到1开发go-tcp框架【5实战片— — 开发MMO之同步玩家位置信息与下线】【完结】_第1张图片

上线位置的信息同步(玩家上线广播)

  • 定义proto协议
  • 获取当前玩家周围的玩家有哪些
  • 将当前玩家的位置信息通过MsgID:200 发送给周围的玩家(让其他玩家看到自己)
  • 将周围的全部玩家位置信息发送给当前玩家的客户端(让自己看到其他的玩家)

①mmo_game_zinx/pb/msg.proto

这里涉及到了MsgID:202消息,我们应该在proto文件中,再添加两个消息

msg.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;
  }
}
message Talk{
  string Content=1;
}

//玩家信息
message Player{
  int32 Pid=1;
  Position P=2;
}

//同步玩家显示数据
message SyncPlayers{
  repeated Player ps=1;
}

添加完信息后,执行build.sh脚本,更新protoc生成的go代码

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

// 同步玩家上线的位置信息
func (p *Player) SyncSurrounding() {
	//1 获取当前玩家周围的玩家有哪些(九宫格)
	pids := WorldMgrObj.AoiMgr.GetPIDsByPos(p.X, p.Z)
	players := make([]*Player, 0, len(pids))
	for _, pid := range pids {
		players = append(players, WorldMgrObj.GetPlayerByPid(int32(pid)))
	}
	//2 将当前玩家的位置信息通过MsgID:200 发送给周围的玩家(让其他玩家看到自己)
	//2.1 组件MsgID:200 proto数据
	proto_msg := &pb.BroadCast{
		Pid: p.Pid,
		Tp:  2, //TP-2代表广播坐标
		Data: &pb.BroadCast_P{
			P: &pb.Position{
				X: p.X,
				Y: p.Y,
				Z: p.Z,
				V: p.V,
			},
		},
	}

	//2.2 周围全部的玩家都向各自的客户端发送200消息, proto_msg
	for _, player := range players {
		player.SendMsg(200, proto_msg)
	}
	//3 将周围全部玩家的位置信息发送给当前的MsgID:202 客户端(让自己看到其他玩家)
	//3.1 组建MsgID:202 proto数据
	//3.1.1 制作pb.Player slice
	players_proto_msg := make([]*pb.Player, 0, len(players))
	for _, player := range players {
		//制作一个message Player
		p := &pb.Player{
			Pid: player.Pid,
			P: &pb.Position{
				X: player.X,
				Y: player.Y,
				Z: player.Z,
				V: player.V,
			},
		}
		//将所有玩家的消息封装起来【切片类型】
		players_proto_msg = append(players_proto_msg, p)
	}
	//3.1.2 封装SyncPlayer protobuf数据
	SyncPlayers_proto_msg := &pb.SyncPlayers{
		Ps: players_proto_msg[:],
	}
	//3.2 将组建好的数据发送给当前玩家的客户端
	p.SendMsg(202, SyncPlayers_proto_msg)
}

测试效果

  • 启动main.go(Server端)
  • 启动game_client/client.exe(Client端,启动三个),查看是否能看到其他上线的用户

2 玩家移动信息同步(广播)

从0到1开发go-tcp框架【5实战片— — 开发MMO之同步玩家位置信息与下线】【完结】_第2张图片

注册一个业务路由,针对处理MsgID:3

  • 解析客户端传递进来的proto协议
  • 得到当前发送位置的是哪个玩家
  • 给其他玩家进行当前玩家的位置信息广播

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

// 同步玩家上线的位置信息
func (p *Player) SyncSurrounding() {
	//1 获取当前玩家周围的玩家有哪些(九宫格)
	pids := WorldMgrObj.AoiMgr.GetPIDsByPos(p.X, p.Z)
	players := make([]*Player, 0, len(pids))
	for _, pid := range pids {
		players = append(players, WorldMgrObj.GetPlayerByPid(int32(pid)))
	}
	//2 将当前玩家的位置信息通过MsgID:200 发送给周围的玩家(让其他玩家看到自己)
	//2.1 组件MsgID:200 proto数据
	proto_msg := &pb.BroadCast{
		Pid: p.Pid,
		Tp:  2, //TP-2代表广播坐标
		Data: &pb.BroadCast_P{
			P: &pb.Position{
				X: p.X,
				Y: p.Y,
				Z: p.Z,
				V: p.V,
			},
		},
	}

	//2.2 周围全部的玩家都向各自的客户端发送200消息, proto_msg
	for _, player := range players {
		player.SendMsg(200, proto_msg)
	}
	//3 将周围全部玩家的位置信息发送给当前的MsgID:202 客户端(让自己看到其他玩家)
	//3.1 组建MsgID:202 proto数据
	//3.1.1 制作pb.Player slice
	players_proto_msg := make([]*pb.Player, 0, len(players))
	for _, player := range players {
		//制作一个message Player
		p := &pb.Player{
			Pid: player.Pid,
			P: &pb.Position{
				X: player.X,
				Y: player.Y,
				Z: player.Z,
				V: player.V,
			},
		}
		//将所有玩家的消息封装起来【切片类型】
		players_proto_msg = append(players_proto_msg, p)
	}
	//3.1.2 封装SyncPlayer protobuf数据
	SyncPlayers_proto_msg := &pb.SyncPlayers{
		Ps: players_proto_msg[:],
	}
	//3.2 将组建好的数据发送给当前玩家的客户端
	p.SendMsg(202, SyncPlayers_proto_msg)
}

// 广播当前玩家的位置移动信息
func (p *Player) UpdatePos(x, y, z, v float32) {
	//更新当前玩家player的位置坐标
	p.X = x
	p.Y = y
	p.Z = z
	p.V = v
	//组建广播proto协议 MsgID:200 Tp-4
	proto_msg := &pb.BroadCast{
		Pid: p.Pid,
		Tp:  4, //类型4代表 移动之后的坐标信息(已经与unity的客户端规定好了)
		Data: &pb.BroadCast_P{
			P: &pb.Position{
				X: p.X,
				Y: p.Y,
				Z: p.Z,
				V: p.V,
			},
		},
	}
	//获取当前玩家的周边玩家的AOI九宫格之内的玩家
	players := p.GetSurroundingPlayers()
	//依次给每个玩家对应的客户端发送当前玩家位置更新的消息(让其他玩家看到当前玩家移动的状态)
	for _, player := range players {
		player.SendMsg(200, proto_msg)
	}
}

// 获取当前玩家AOI九宫格之内的其他玩家
func (p *Player) GetSurroundingPlayers() []*Player {
	//得到当前AOI九宫格内所有玩家的id
	pids := WorldMgrObj.AoiMgr.GetPIDsByPos(p.X, p.Z)
	//将所有的pid对应的player放到Players切片中
	players := make([]*Player, 0, len(pids))
	for _, pid := range pids {
		player := WorldMgrObj.GetPlayerByPid(int32(pid))
		players = append(players, player)
	}
	return players
}

②mmo_game_zinx/apis/move.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 MoveApi struct {
	znet.BaseRouter
}

func (m *MoveApi) Handler(request ziface.IRequest) {
	//解析用户传进来的proto(客户端)
	proto_msg := &pb.Position{}
	err := proto.Unmarshal(request.GetData(), proto_msg)
	if err != nil {
		fmt.Println("Move: Position Unmarshal error ", err)
		return
	}
	//得到当前发送位置的是哪个玩家
	pid, err := request.GetConnection().GetProperty("pid")
	if err != nil {
		fmt.Println("get request property pid error, ", err)
		return
	}
	fmt.Printf("Player pid=%d, move(%f,%f,%f,%f)\n", pid, proto_msg.X, proto_msg.Y, proto_msg.Z, proto_msg.V)

	//给其他玩家进行当前玩家的位置信息广播
	player := core.WorldMgrObj.GetPlayerByPid(pid.(int32))
	//广播并更新当前玩家的坐标
	player.UpdatePos(proto_msg.X, proto_msg.Y, proto_msg.Z, proto_msg.V)
}

③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)
	//同步玩家位置信息(我能看到其他玩家)
	player.SyncSurrounding()
	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.AddRouter(3, &apis.MoveApi{})
	s.Serve()
}

测试效果

客户端操作:

  • a、w、s、d是移动按键
  • 按住鼠标左键然后向左或向右拖动是移动视野
    从0到1开发go-tcp框架【5实战片— — 开发MMO之同步玩家位置信息与下线】【完结】_第3张图片
    input输入框可以查看或者切换操作按键


从0到1开发go-tcp框架【5实战片— — 开发MMO之同步玩家位置信息与下线】【完结】_第4张图片

操作client2查看移动效果和控制台打印,发现达到预期

3 玩家下线

从0到1开发go-tcp框架【5实战片— — 开发MMO之同步玩家位置信息与下线】【完结】_第5张图片

玩家下线:在链接断开之前处理玩家下线业务

  • 通过链接属性得到当前链接所绑定的pid
  • 得到当前玩家周边的九宫格都有哪些玩家
  • 给周围玩家广播MsgID:201消息
  • 将当前玩家从世界管理器删除
  • 将当前管家从AOI管理器中删除

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

// 同步玩家上线的位置信息
func (p *Player) SyncSurrounding() {
	//1 获取当前玩家周围的玩家有哪些(九宫格)
	pids := WorldMgrObj.AoiMgr.GetPIDsByPos(p.X, p.Z)
	players := make([]*Player, 0, len(pids))
	for _, pid := range pids {
		players = append(players, WorldMgrObj.GetPlayerByPid(int32(pid)))
	}
	//2 将当前玩家的位置信息通过MsgID:200 发送给周围的玩家(让其他玩家看到自己)
	//2.1 组件MsgID:200 proto数据
	proto_msg := &pb.BroadCast{
		Pid: p.Pid,
		Tp:  2, //TP-2代表广播坐标
		Data: &pb.BroadCast_P{
			P: &pb.Position{
				X: p.X,
				Y: p.Y,
				Z: p.Z,
				V: p.V,
			},
		},
	}

	//2.2 周围全部的玩家都向各自的客户端发送200消息, proto_msg
	for _, player := range players {
		player.SendMsg(200, proto_msg)
	}
	//3 将周围全部玩家的位置信息发送给当前的MsgID:202 客户端(让自己看到其他玩家)
	//3.1 组建MsgID:202 proto数据
	//3.1.1 制作pb.Player slice
	players_proto_msg := make([]*pb.Player, 0, len(players))
	for _, player := range players {
		//制作一个message Player
		p := &pb.Player{
			Pid: player.Pid,
			P: &pb.Position{
				X: player.X,
				Y: player.Y,
				Z: player.Z,
				V: player.V,
			},
		}
		//将所有玩家的消息封装起来【切片类型】
		players_proto_msg = append(players_proto_msg, p)
	}
	//3.1.2 封装SyncPlayer protobuf数据
	SyncPlayers_proto_msg := &pb.SyncPlayers{
		Ps: players_proto_msg[:],
	}
	//3.2 将组建好的数据发送给当前玩家的客户端
	p.SendMsg(202, SyncPlayers_proto_msg)
}

// 广播当前玩家的位置移动信息
func (p *Player) UpdatePos(x, y, z, v float32) {
	//更新当前玩家player的位置坐标
	p.X = x
	p.Y = y
	p.Z = z
	p.V = v
	//组建广播proto协议 MsgID:200 Tp-4
	proto_msg := &pb.BroadCast{
		Pid: p.Pid,
		Tp:  4, //类型4代表 移动之后的坐标信息(已经与unity的客户端规定好了)
		Data: &pb.BroadCast_P{
			P: &pb.Position{
				X: p.X,
				Y: p.Y,
				Z: p.Z,
				V: p.V,
			},
		},
	}
	//获取当前玩家的周边玩家的AOI九宫格之内的玩家
	players := p.GetSurroundingPlayers()
	//依次给每个玩家对应的客户端发送当前玩家位置更新的消息(让其他玩家看到当前玩家移动的状态)
	for _, player := range players {
		player.SendMsg(200, proto_msg)
	}
}

// 获取当前玩家AOI九宫格之内的其他玩家
func (p *Player) GetSurroundingPlayers() []*Player {
	//得到当前AOI九宫格内所有玩家的id
	pids := WorldMgrObj.AoiMgr.GetPIDsByPos(p.X, p.Z)
	//将所有的pid对应的player放到Players切片中
	players := make([]*Player, 0, len(pids))
	for _, pid := range pids {
		player := WorldMgrObj.GetPlayerByPid(int32(pid))
		players = append(players, player)
	}
	return players
}

// 玩家下线
func (p *Player) Offline() {
	//得到当前玩家周边的九宫格内都有哪些玩家
	players := p.GetSurroundingPlayers()
	//给周围玩家广播MsgID:201消息
	proto_msg := &pb.SyncPid{
		Pid: p.Pid,
	}
	for _, player := range players {
		player.SendMsg(201, proto_msg)
	}
	//从AOI格子中移除玩家
	WorldMgrObj.AoiMgr.RemoveFromGridByPos(int(p.Pid), p.X, p.Z)
	//从WorldMgr的map中移除玩家
	WorldMgrObj.RemovePlayerByPid(p.Pid)
}

②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)
	//同步玩家位置信息(我能看到其他玩家)
	player.SyncSurrounding()
	fmt.Println("======>Player pid = ", player.Pid, " is arrived ====")
}

// 当客户端断开连接时候的hook函数
func OnConnectionLost(connection ziface.IConnection) {
	//获取当前连接的pid属性
	pid, _ := connection.GetProperty("pid")
	//根据pid获取对应的玩家对象
	player := core.WorldMgrObj.GetPlayerByPid(pid.(int32))
	//触发玩家下线业务
	if pid != nil {
		player.Offline()
	}
	fmt.Println("=====> Player ", pid, " offline=====")
}

func main() {
	//创建服务句柄
	s := znet.NewServer("MMO Game Zinx")
	s.SetOnConnStart(OnConnectionAdd)
	//注册一些路由业务
	s.AddRouter(2, &apis.WorldChatApi{})
	//保证玩家能实时移动
	s.AddRouter(3, &apis.MoveApi{})
	//保证玩家下线
	s.SetOnConnStop(OnConnectionLost)
	s.Serve()
}

测试效果

此时关闭client1,查看效果:

从0到1开发go-tcp框架【5实战片— — 开发MMO之同步玩家位置信息与下线】【完结】_第6张图片

后续:

大家可以根据自己需求进行二开:

  1. 添加日志模块
  2. 添加定时器
  3. Server与Server之间的通信-分布式

参考:https://www.yuque.com/aceld/npyr8s/xgi5xp

你可能感兴趣的:(go,框架,golang,tcp/ip,游戏开发,教程,框架开发,unity)