15.世界聊天系统实施
#go #zinx

[zinx]

<1.Building Basic Services with Zinx Framework>
<2. Zinx-V0.2 Simple Connection Encapsulation and Binding with Business>
<3.Design and Implementation of the Zinx Framework's Routing Module>
<4.Zinx Global Configuration>
<5.Zinx Message Encapsulation Module Design and Implementation>
<6.Design and Implementation of Zinx Multi-Router Mode>
<7. Building Zinx's Read-Write Separation Model>
<8.Zinx Message Queue and Task Worker Pool Design and Implementation>
<9. Zinx Connection Management and Property Setting>

[ZINX应用程序 - MMO游戏案例研究]

<10. Application Case Study using the Zinx Framework>
<11. MMO Online Game AOI Algorithm>
<12.Data Transmission Protocol: Protocol Buffers>
<13. MMO Game Server Application Protocol>
<14. Building the Project and User Login>
<15. World Chat System Implementation>


MMO游戏源代码

https://github.com/aceld/zinx/tree/master/zinx_app_demo/mmo_game


成功登录用户后,它表明已经建立了前端和后端之间的初始连接。接下来,我们只需要实现聊天系统功能,允许玩家登录到服务器进行通信并互相发送消息。

15.1世界经理模块

要实现世界聊天系统,我们需要定义一个可以处理世界上所有玩家的经理。该经理应拥有有关当前所有在线玩家的信息以及全世界的AOI(感兴趣领域)部门规则。这种设置使玩家可以方便地互相聊天并同步他们的位置。

mmo_game/core/目录中创建一个world_manager.go文件。该文件将包含世界经理模块的设计和实现。

首先,让我们定义Worldmanager World Manager类的成员结构,如以下代码段所示:

// mmo_game/core/world_manager.go
package core

import (
    "sync"
)

/*
    The overall management module of the current game world
*/
type WorldManager struct {
    AoiMgr  *AOIManager       // The AOI planner manager for the current world map
    Players map[int32]*Player // Collection of currently online players
    pLock   sync.RWMutex       // Mutex for protecting Players for concurrent read and write
}

Worldmanager结构包括:

(1)AoiMgr,一个代表当前世界地图的AOI(感兴趣领域)经理的AOIManager

(2)Players,一个用于存储世界上所有玩家的信息的变量。这是一张键是PlayerID的地图,值是播放器对象。

(3)pLock,一种读写的静音,用于确保在阅读和写入Players时并发安全。

WorldManager结构包含用于管理游戏世界中玩家的各种方法。这些方法包括将玩家添加到世界上,通过ID删除玩家,通过ID检索玩家,并获取有关世界上所有玩家的信息。此外,还有用于配置AOI(感兴趣区域)网格的常数。

// mmo_game/core/world_manager.go

// Provide an external world management module handle
var WorldMgrObj *WorldManager

// Provide the initialization method for WorldManager
func init() {
    WorldMgrObj = &WorldManager{
        Players: make(map[int32]*Player),
        AoiMgr:  NewAOIManager(AOI_MIN_X, AOI_MAX_X, AOI_CNTS_X, AOI_MIN_Y, AOI_MAX_Y, AOI_CNTS_Y),
    }
}

// The minimum and maximum coordinates and grid counts for 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
)

// WorldManager is the overall management module of the current game world
type WorldManager struct {
    AoiMgr  *AOIManager       // The AOI planner manager for the current world map
    Players map[int32]*Player // Collection of currently online players
    pLock   sync.RWMutex       // Mutex for protecting Players for concurrent read and write
}

// AddPlayer provides a function to add a player to the player information table Players
func (wm *WorldManager) AddPlayer(player *Player) {
    // Add the player to the world manager
    wm.pLock.Lock()
    wm.Players[player.Pid] = player
    wm.pLock.Unlock()

    // Add the player to the AOI network planning
    wm.AoiMgr.AddToGridByPos(int(player.Pid), player.X, player.Z)
}

// RemovePlayerByPid removes a player from the player information table by player ID
func (wm *WorldManager) RemovePlayerByPid(pid int32) {
    wm.pLock.Lock()
    delete(wm.Players, pid)
    wm.pLock.Unlock()
}

// GetPlayerByPid retrieves player information by player ID
func (wm *WorldManager) GetPlayerByPid(pid int32) *Player {
    wm.pLock.RLock()
    defer wm.pLock.RUnlock()

    return wm.Players[pid]
}

// GetAllPlayers retrieves information about all players
func (wm *WorldManager) GetAllPlayers() []*Player {
    wm.pLock.RLock()
    defer wm.pLock.RUnlock()

    // Create a return player slice
    players := make([]*Player, 0)

    // Append to the slice
    for _, v := range wm.Players {
        players = append(players, v)
    }

    // Return
    return players
}

Worldmanager模块主要充当统一AOI(感兴趣领域)和参与者的管理的中介。它在其他模块中起着协调作用。一个重要方面是全局变量WorldMgrObj,它是管理模块的外部可访问的手柄,可用于其他模块。

使用WorldManager模块,每次播放器登录时,都应将它们添加到WorldMgrObj对象中。添加播放器的相关代码如下:

//mmo_game/server.go

// Hook function when a client establishes a connection
func OnConnecionAdd(conn ziface.IConnection)  {
    // Create a player
    player := core.NewPlayer(conn)

    // Synchronize the current PlayerID to the client using MsgID: 1
    player.SyncPid()

    // Synchronize the initial coordinates of the current player to the client using MsgID: 200
    player.BroadCastStartPosition()

    // Add the currently newly logged-in player to the WorldManager
    core.WorldMgrObj.AddPlayer(player)

    fmt.Println("=====> Player pidId = ", player.Pid, " arrived ====")
}

以这种方式,现在在服务器中实现了用于创建和添加玩家的代码。

15.2世界聊天系统实施

接下来,利用世界经理,我们将为当前服务器实施世界聊天广播功能。相互作用过程如图15.1。

图15.1

Figure 15.1

15.2.1原始协议定义

此步骤涉及定义MSGID 2和200的指令的原始结构,以及谈话和广播原始协议。在msg.proto文件中,定义这些协议的格式如下:

//mmo_game/pb/msg.proto

syntax = "proto3";                    // Proto protocol
package pb;                            // Current package name
option csharp_namespace = "Pb";   // Option provided for C#

// Synchronize the client's player ID
message SyncPid {
    int32 Pid = 1;
}

// Player position
message Position {
    float X = 1;
    float Y = 2;
    float Z = 3;
    float V = 4;
}

// Player broadcast data
message BroadCast {
    int32 Pid = 1;
    int32 Tp = 2;              // 1 - World chat, 2 - Player position
    oneof Data {
        string Content = 3;    // Chat content
        Position P = 4;        // Location of the broadcasting user
        int32 ActionData = 5;
    }
}

// Player chat data
message Talk {
    string Content = 1;    // Chat content
}

定义后,执行预先编写的build.sh脚本以生成新的msg.proto.go文件。

15.2.2。建立聊天业务API

接下来,创建MMO_GAME/API目录,该目录主要用于存储不同MSGID的服务器端业务逻辑代码。在API目录中,创建World_chat.go文件,该文件将包含World Chat System中聊天业务的逻辑。

遵循Zinx的格式,编写MSGID 2的处理程序逻辑代码。这是特定的实现:

//mmo_game/api/world_chat.go
package api

import (
    "fmt"
    "github.com/golang/protobuf/proto"
    "zinx/ziface"
    "zinx/zinx_app_demo/mmo_game/core"
    "zinx/zinx_app_demo/mmo_game/pb"
    "zinx/znet"
)

// WorldChat API for world chat routing business
type WorldChatApi struct {
    znet.BaseRouter
}

func (*WorldChatApi) Handle(request ziface.IRequest) {
    // 1. Decode the proto protocol sent by the client
    msg := &pb.Talk{}
    err := proto.Unmarshal(request.GetData(), msg)
    if err != nil {
        fmt.Println("Talk Unmarshal error ", err)
        return
    }

    // 2. Determine which player sent the message, get it from the connection's "pid" attribute
    pid, err := request.GetConnection().GetProperty("pid")
    if err != nil {
        fmt.Println("GetProperty pid error", err)
        request.GetConnection().Stop()
        return
    }

    // 3. Get the player object based on pid
    player := core.WorldMgrObj.GetPlayerByPid(pid.(int32))

    // 4. Have the player object initiate a chat broadcast request
    player.Talk(msg.Content)
}

对于MSGID 2的路由业务,有一个小细节要注意。在上面代码的步骤2中,根据连接conn检索播放器的PID属性。这需要将PID和conn绑定为玩家登录过程中的属性。您应该在OnConnecionAdd()方法中添加此绑定,该方法在建立客户端连接时将其作为钩函数执行。这是您可以绑定pid和conn的方式:

//mmo_game/server.go

// Hook function executed when a client connection is established
func OnConnecionAdd(conn ziface.IConnection)  {
    // Create a player
    player := core.NewPlayer(conn)
    // Synchronize the current PlayerID to the client using MsgID:1 message
    player.SyncPid()
    // Synchronize the initial coordinates of the current player to the client using MsgID:200 message
    player.BroadCastStartPosition()
    // Add the newly logged-in player to the worldManager
    core.WorldMgrObj.AddPlayer(player)

    // Add: Bind the PID to this connection as an attribute
    conn.SetProperty("pid", player.Pid)

    fmt.Println("=====> Player pidId = ", player.Pid, " arrived ====")
}

接下来,让我们为播放器实施谈话方法,该方法将广播消息发送给相应的客户端。代码实现如下:

// mmo_game/core/player.go

// Broadcast player chat
func (p *Player) Talk(content string) {
    // 1. Assemble the MsgId200 proto data
    msg := &pb.BroadCast{
        Pid: p.Pid,
        Tp:  1, // TP 1 represents chat broadcast
        Data: &pb.BroadCast_Content{
            Content: content,
        },
    }

    // 2. Get all currently online players in the world
    players := WorldMgrObj.GetAllPlayers()

    // 3. Send MsgId:200 messages to all players
    for _, player := range players {
        player.SendMsg(200, msg)
    }
}

主要过程涉及创建MSGID 200的原始数据协议结构。然后,它通过世界经理检索所有在线玩家的信息,并通过所有在线玩家迭代,将MSGID 200消息发送给每个播放器的相应客户端程序。

15.2.3测试世界聊天功能

我们现在已经实现了世界聊天消息的基本功能。让我们继续执行一个简单的测试,以确保此功能运行平稳。
在服务器端,使用以下命令运行服务器并观察结果:

$ go run server.go
Add api msgId = 2
[START] Server name: Zinx Game, listener at IP: 0.0.0.0, Port 8999 is starting
[Zinx] Version: V0.11, MaxConn: 3000, MaxPacketSize: 4096
Start Zinx server Zinx Game successfully, now listening...
Worker ID = 9 is started.
Worker ID = 4 is started.
Worker ID = 5 is started.
Worker ID = 6 is started.
Worker ID = 7 is started.
Worker ID = 8 is started.
Worker ID = 0 is started.
Worker ID = 1 is started.
Worker ID = 2 is started.
Worker ID = 3 is started.

然后,在Windows系统上打开两个客户端应用程序,并相互进行聊天对话。通过这样做,您已成功测试了聊天功能,如图15.2所示。

图15.2

Figure 15.2

Figure 15.3


MMO游戏源代码

https://github.com/aceld/zinx/tree/master/zinx_app_demo/mmo_game


[zinx]

<1.Building Basic Services with Zinx Framework>
<2. Zinx-V0.2 Simple Connection Encapsulation and Binding with Business>
<3.Design and Implementation of the Zinx Framework's Routing Module>
<4.Zinx Global Configuration>
<5.Zinx Message Encapsulation Module Design and Implementation>
<6.Design and Implementation of Zinx Multi-Router Mode>
<7. Building Zinx's Read-Write Separation Model>
<8.Zinx Message Queue and Task Worker Pool Design and Implementation>
<9. Zinx Connection Management and Property Setting>

[ZINX应用程序 - MMO游戏案例研究]

<10. Application Case Study using the Zinx Framework>
<11. MMO Online Game AOI Algorithm>
<12.Data Transmission Protocol: Protocol Buffers>
<13. MMO Game Server Application Protocol>
<14. Building the Project and User Login>
<15. World Chat System Implementation>


作者:
不和谐:https://discord.gg/xQ8Xxfyfcz
zinx:https://github.com/aceld/zinx
github:https://github.com/aceld
Aceld的家:https://yuque.com/aceld