DEV Community

Aceld
Aceld

Posted on • Updated on

15. World Chat System Implementation

[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 Application - MMO Game Case Study]

<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>
<16. Online Location Information Synchronization>
<17. Moving position and non-crossing grid AOI broadcasting>
<18. Player Logout>
<19. Movement and AOI Broadcast Across Grids>


MMO-GAME Source Code

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


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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 ====")
}
Enter fullscreen mode Exit fullscreen mode

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

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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 ====")
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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

Figure 15.2

Figure 15.3


MMO-GAME Source Code

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 Application - MMO Game Case Study]

<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>
<16. Online Location Information Synchronization>
<17. Moving position and non-crossing grid AOI broadcasting>
<18. Player Logout>
<19. Movement and AOI Broadcast Across Grids>


Author:
discord: https://discord.gg/xQ8Xxfyfcz
zinx: https://github.com/aceld/zinx
github: https://github.com/aceld
aceld's home: https://yuque.com/aceld

Top comments (0)