After users are successfully logged in, it indicates that the initial connection between the frontend and backend has been established. Next, we only need to implement the chat system functionality, allowing players logged into the server to communicate and send messages to each other.

15.1 World Manager Module

To implement the world chat system, we need to define a manager that can handle all players in the world. This manager should have information about all currently online players and the AOI (Area of Interest) division rules for the world. This setup makes it convenient for players to chat with each other and synchronize their positions.

Create a world_manager.go file in the mmo_game/core/ directory. This file will contain the design and implementation of the world manager module.

First, let’s define the member structure of the WorldManager world manager class, as shown in the following code snippet:

// 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
}

The WorldManager structure includes:

(1) AoiMgr, an AOIManager that represents the AOI (Area of Interest) manager for the current world map.

(2) Players, a variable used to store information about all players currently in the world. It's a map where the key is the PlayerID, and the value is a Player object.

(3) pLock, a read-write mutex used to ensure concurrent safety when reading and writing to Players.

The WorldManager structure contains various methods for managing players in the game world. These methods include adding a player to the world, removing a player by ID, retrieving a player by ID, and getting information about all players in the world. Additionally, there are constants for configuring the AOI (Area of Interest) grid.

// 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
}

The WorldManager module primarily acts as an intermediary to unify the management of AOI (Area of Interest) and players. It plays a coordinating role among other modules. One important aspect is the global variable WorldMgrObj, which is an externally accessible handle for the management module, available for use by other modules.

With the WorldManager module in place, every time a player logs in, they should be added to the WorldMgrObj object. The relevant code for adding a player is as follows:

//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 ====")
}

This way, the code for creating and adding players to the World Manager is now implemented in the server.

15.2 World Chat System Implementation

Next, leveraging the World Manager, we will implement a world chat broadcast function for the current server. The interaction process is depicted in Figure 15.1.

Figure 15.1

15.2.1 Proto3 Protocol Definition

This step involves defining the Proto structures for the instructions with MsgId 2 and 200, along with the Talk and BroadCast Proto protocols. In the msg.proto file, define the format of these protocols as follows:

//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
}

Once defined, execute the pre-written build.sh script to generate the new msg.proto.go file.

15.2.2. Establish Chat Business API

Next, create the mmo_game/api directory, which is primarily used to store server-side business logic code for different MsgIDs. Within the api directory, create the world_chat.go file, which will contain the logic for the chat business in the world chat system.

Following the format of Zinx, write the Handler logic code for MsgID 2. Here is the specific implementation:

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

For the routing business with MsgID 2, there’s a small detail to note. In step 2 of the code above, the player’s PID property is retrieved based on the connection conn. This requires binding the PID and conn as an attribute during player login. You should add this binding in the OnConnecionAdd() method, which is executed as a Hook function when a client connection is established. Here's how you can bind the PID and 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 ====")
}

Next, let’s implement the Talk method for the Player, which sends broadcast messages to the corresponding clients. The code implementation is as follows:

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

The main process involves creating a Proto data protocol structure with MsgID 200 first. Then, it retrieves information about all online players through the world manager and iterates through all online players, sending the MsgID 200 message to each player’s corresponding client program.

15.2.3 Test the World Chat Functionality

We have now implemented the basic functionality for world chat messages. Let’s proceed to perform a simple test to ensure that this feature runs smoothly.
On the server side, run the Server with the following commands and observe the results:

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

Then, open two client applications on a Windows system and engage in a chat conversation with each other. By doing so, you have successfully tested the chat functionality, as shown in Figure 15.2.

Figure 15.2

--

--