DEV Community

Aceld
Aceld

Posted on • Updated on

14. Building the Project and User Login

[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


The preparation for the current project case has been completed. Next, we will build a project for the MMO game server application based on the Zinx framework.

14.1 Building the Project

Create several folders in the root directory of the "mmo_game" project: "api," "conf," "core," "game_client," "pb," etc.

(1) "api" directory: This directory is mainly for registering the Router processing modules for MMO game business logic.

(2) "conf" directory: Store configuration files for the project, such as "zinx.json."

(3) "core" directory: Store core algorithms or game control modules.

(4) "game_client" directory: This directory can be used for storing client-related code or simulating game client behaviors for testing purposes.

(5) "pb" directory: Store the Protocol Buffer message definitions.

This organizational structure will help separate different components of the MMO game server application and maintain a clear code structure.

14.1.1 Creating the Main Server Program

Create a file named server.go in the "mmo_game" directory as our main package, serving as the entry point for the server program.

// mmo_game/server.go
package main

import (
    "zinx/znet"
)

func main() {
    // Create a server handler
    s := znet.NewServer()

    // Start the server
    s.Serve()
}
Enter fullscreen mode Exit fullscreen mode

14.1.2 Adding the Zinx Configuration File

Add a configuration file named zinx.conf in the "conf" directory. The configuration might look like this:

// mmo_game/conf/zinx.conf
{
  "Name": "Zinx Game",
  "Host": "0.0.0.0",
  "TcpPort": 8999,
  "MaxConn": 3000,
  "WorkerPoolSize": 10
}
Enter fullscreen mode Exit fullscreen mode

14.1.3 Creating the Proto Protocol File

Create a file named msg.proto in the "pb" directory. Begin with the header information, and you can gradually add other information as you progress in the development. The protocol might look like this:

syntax = "proto3";                 // Proto protocol
package pb;                        // Current package name
option csharp_namespace = "Pb";   // Option for providing C# compatibility
Enter fullscreen mode Exit fullscreen mode

Note: There is an "option csharp_namespace" statement here, which is specifically provided for the client program. It is unrelated to the current application, as the client program's code is written in C#. The client also needs to generate corresponding C# code protocol files based on this Proto. If the client is not written in C#, you can disregard this statement.

Create a "build.sh" script with compilation instructions to facilitate compiling Proto protocol files into corresponding Go code:

#!/bin/bash
protoc --go_out=. *.proto
Enter fullscreen mode Exit fullscreen mode

The directory structure of the current "mmo_game" project should look like this:

.
└── mmo_game
    ├── api
    ├── conf
    │   └── zinx.json
    ├── core
    │   ├── aoi.go
    │   ├── aoi_test.go
    │   ├── grid.go
    ├── game_client
    │   └── client.exe
    ├── pb
    │   ├── build.sh
    │   └── msg.proto
    ├── README.md
    └── server.go
Enter fullscreen mode Exit fullscreen mode

14.2 User Online Process

After setting up the basic project environment, the next step is to implement the first functionality, which is the process of a player's initial login and online status in the MMO client. The interaction process between the client and the server during player login is relatively simple. When the client logs in for the first time and establishes a connection with the server, the server responds by assigning a player ID to the client. Upon receiving the confirmation from the client, indicating that the player has successfully logged in, the server broadcasts the initial online coordinates of the new player to all other online players. The specific process is illustrated in Figure 14.1.

Figure 14.1

Figure 14.1

Following the flow in the diagram above, let's implement the specific code.

14.2.1 Define Proto Protocols

The online business will involve two messages with MsgID 1 and MsgID 200. As discussed in the previous section, we need to define two Proto message types in the msg.proto file and generate corresponding Go code. Add the following to the Proto file:

//mmo_game/pb/msg.proto
syntax="proto3"; // Proto protocol
package pb; // Current package name
option csharp_namespace="Pb"; // Option for C# integration

// Sync client 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;
    oneof Data {
        string Content=3;
        Position P=4;
        int32 ActionData=5;
    }
}
Enter fullscreen mode Exit fullscreen mode

Then run the build.sh script to generate the corresponding msg.pb.go code.

14.2.2 Create the Player Module

In the mmo_game/core directory, create a file named player.go to define the code related to the player user. To implement the Player module, we need to define the Player class with its member attributes and related methods. First, define the data structure for the Player class, as shown below:

//mmo_game/core/player.go

// Player object
type Player struct {
    Pid  int32               // Player ID
    Conn ziface.IConnection // Connection of the current player
    X    float32             // X-coordinate on the plane
    Y    float32             // Height
    Z    float32             // Y-coordinate on the plane (note: not Y-axis)
    V    float32             // Rotation (0-360 degrees)
}
Enter fullscreen mode Exit fullscreen mode

A Player object should have a player user ID and a IConnection object representing the connection between the current player and the client. The remaining attributes represent the player's coordinates on the map.

Next, let's implement a constructor for the Player class to create and initialize a Player object. The code for this constructor is as follows:

//mmo_game/core/player.go

/*
    Player ID Generator
*/
var PidGen int32 = 1 // Counter for generating player IDs
var IdLock sync.Mutex // Mutex to protect PidGen

// Create a player object
func NewPlayer(conn ziface.IConnection) *Player {
    // Generate a PID
    IdLock.Lock()
    id := PidGen
    PidGen++
    IdLock.Unlock()

    p := &Player{
        Pid: id,
        Conn: conn,

        // Randomly generate coordinates around point (160, 134)
        X: float32(160 + rand.Intn(10)),
        Y: 0, // Height is 0
        Z: float32(134 + rand.Intn(17)),
        V: 0, // Angle is 0, not yet implemented
    }

    return p
}
Enter fullscreen mode Exit fullscreen mode

The NewPlayer() function creates a Player object. In this example, the player's ID is generated using a global serial number. This is done to quickly and simply build the example; for more complex projects, consider using distributed IDs or custom ID protocols. The initial coordinates of the player are randomly generated within a certain range for debugging purposes.

NewPlayer() depends on the passed IConnection to ensure that the connection has been successfully established before calling NewPlayer() to create a Player object.

Next, since the Player frequently needs to send messages to the client, it's necessary to provide a SendMsg() method for the Player. Let's implement the method that the Player uses to send messages to the client. The code is as follows:

//mmo_game/core/player.go

/*
    Send message to the client,
    primarily by serializing the protobuf data and sending it
*/
func (p *Player) SendMsg(msgId uint32, data proto.Message) {
    // Serialize the proto Message structure
    msg, err := proto.Marshal(data)
    if err != nil {
        fmt.Println("marshal msg err: ", err)
        return
    }

    if p.Conn == nil {
        fmt.Println("connection in player is nil")
        return
    }

    // Call the SendMsg method of the Zinx framework to send the packet
    if err := p.Conn.SendMsg(msgId, msg); err != nil {
        fmt.Println("Player SendMsg error !")
        return
    }

    return
}
Enter fullscreen mode Exit fullscreen mode

Note that SendMsg() serializes the data to be sent using Proto and then calls the SendMsg() method of the Zinx framework to send it to the remote client.

14.2.3 Implementing the Online Process

In the main entry point of the server (server.go), we need to bind a Hook function to connections that are created. Since during online process, the server automatically responds to the client with the player's ID and coordinates, we need to automatically trigger this process after the connection is established. We can achieve this by using the SetOnConnStart() method provided by the Zinx framework. Create a file named server.go in the mmo_game directory, and define the main() function as follows:

// mmo_game/server.go
package main

import (
    "fmt"
    "zinx/ziface"
    "zinx/zinx_app_demo/mmo_game/core"
    "znet"
)

// Hook function when a client establishes a connection
func OnConnectionAdd(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 coordinate information of the current player to the client using MsgID:200 message
    player.BroadCastStartPosition()

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

func main() {
    // Create the server handle
    s := znet.NewServer()

    // Register functions for client connection establishment and disconnection
    s.SetOnConnStart(OnConnectionAdd)

    // Start the service
    s.Serve()
}
Enter fullscreen mode Exit fullscreen mode

Based on the above flow analysis, when a client establishes a connection, the server needs to automatically respond to the client with a player ID and also send the current player's coordinates to the client. Therefore, two methods have been customized for the Player: Player.SyncPid() and Player.BroadCastStartPosition().

(1) SyncPid(): This method sends a message with MsgID 1 to the client, providing the generated player ID to the corresponding client. The code implementation of this method is as follows:

// Tell the client the player's ID, synchronizing the generated player ID to the client
func (p *Player) SyncPid() {
    // Construct proto data for SyncPid protocol
    data := &pb.SyncPid{
        Pid: p.Pid,
    }

    // Send data to the client
    p.SendMsg(1, data)
}
Enter fullscreen mode Exit fullscreen mode

(2) BroadCastStartPosition(): This method sends a broadcast position message with MsgID 200. Even though the first player logging in might not have other users online, the coordinates of the current player should still be communicated. The code implementation of this method is as follows:

// Broadcast the player's initial position
func (p *Player) BroadCastStartPosition() {
    // Construct proto data for BroadCast protocol
    msg := &pb.BroadCast{
        Pid: p.Pid,
        Tp:  2, // TP2 represents broadcasting coordinates
        Data: &pb.BroadCast_P{
            P: &pb.Position{
                X: p.X,
                Y: p.Y,
                Z: p.Z,
                V: p.V,
            },
        },
    }

    p.SendMsg(200, msg)
}
Enter fullscreen mode Exit fullscreen mode

In this case, Tp is set to 2, which, according to the protocol definition, represents broadcasting coordinates. The Data field is assigned the coordinate data format.

14.2.4 Test User Login Process

Now that the server-side code for the MMO login process has been developed, you can proceed to start the server program and test the functionality by running the client program. First, execute the following command to start the server program:

$ cd mmo_game/
$ go run server.go
Enter fullscreen mode Exit fullscreen mode

Next, open the client.exe(For addressing environment-related issues with the Windows client.exe client program, you can refer to the issues section of the Zinx source code repository. The relevant link is: https://github.com/aceld/zinx/issues/7) client program on a Windows operating system.

Note: Make sure that you can establish network connectivity between Windows and the Linux server where you're launching the server program. For ease of testing, it's recommended to either disable the firewall on the Linux server or ensure that the required port is open. This precaution will prevent any debugging delays or issues during testing.

After launching the client application, a login interface will appear, prompting users to enter the IP and Port information of the server, as shown in Figure 14.2.

Figure 14.2

Figure 14.2

Enter the server's IP address and port here. The port should match the port number configured in the server's zinx.json. Then click "Connect" to establish a connection with the server.

If the login is successful, you will enter the game demo interface as shown in Figure 14.3.

Figure 14.3

Figure 14.3

If the game interface successfully loads and displays the player ID as "Player_1," it indicates a successful login. Additionally, you may observe some debugging information on the server side. You can use the keyboard keys "W," "A," "S," and "D" to move the player. If the player ID is not displayed or appears as "TextView," it signifies a login failure, and developers need to further debug the protocol matching.


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)