ARPG case of online game from 0: Diablo Warlord (Chapter 8: World Chat System and Resource Trading System)

Keywords: Session network Unity Database

Catalog

World Chat System and Resource Trading System

Development of World Chat System Interface

Development of World Chat System 1

World Chat System Development 2

World Chat System Development 3

World Chat System Development 4

World Chat System Development 5

World Chat System Development 6

World Chat System Development 7

Demonstration debugging and bug solving process

Making General Purchase Window

Development of Resource Trading System 1

Resource trading system development 2

Resource trading system development 3

Resource Trading System Development 4

Resource trading system development 5

Message Frequency Timing Control

Client Integration of PETimer Timing Plug-in

Server Integrates PETimer Timing Plug-in

Timing detection using independent threads

Development of Physical Recovery System 1

Development of Physical Recovery System 2

Development of Physical Recovery System 3

Development of Physical Recovery System 4

Development of Physical Recovery System 5

Development of Physical Recovery System 6

World Chat System and Resource Trading System

Development of World Chat System Interface

Development of World Chat System 1

Create a ChatWnd script

using UnityEngine;

public class ChatWnd : WindowRoot
{
    protected override void InitWnd()
    {
        base.InitWnd();
    }

    public void ClickCloseBtn()
    {
        audioSvc.PlayUIAudio(Constants.UIClickBtn);

        SetWndState(false);
    }
}

Add a method to open the chat system in MainCityWnd.

    public void ClickChatBtn()
    {
        audioSvc.PlayUIAudio(Constants.UIOpenPage);
        MainCitySys.Instance.OpenChatWnd();
    }

This method is implemented by calling the method in MainCitySys

    public void OpenChatWnd()
    {
        chatWnd.SetWndState();
    }

World Chat System Development 2

Add a display method to control the chat categorization button

    public Text txtChat;
    public Image imgWorld;
    public Image imgGuild;
    public Image imgFriend;

    private int chatType;
    private List<string> chatList = new List<string>();

    protected override void InitWnd()
    {
        base.InitWnd();

        chatType = 0;
        RefreshUI();

    }

    private void RefreshUI()
    {
        if(chatType == 0)//world
        {
            string chatMsg = "";
            for(int i=0;i<chatList.Count;i++)
            {
                chatMsg += chatList[i] + "\n";
            }
            SetText(txtChat, chatMsg);

            SetSprite(imgWorld, PathDefine.BtnType1);
            SetSprite(imgGuild, PathDefine.BtnType2);
            SetSprite(imgFriend, PathDefine.BtnType2);
        }
        else if(chatType == 1)//Labour Union
        {
            SetText(txtChat, "There is no trade union at present.");
            SetSprite(imgWorld, PathDefine.BtnType2);
            SetSprite(imgGuild, PathDefine.BtnType1);
            SetSprite(imgFriend, PathDefine.BtnType2);
        }
        else if(chatType == 2)//Good friend
        {
            SetText(txtChat, "No Friends Information");
            SetSprite(imgWorld, PathDefine.BtnType2);
            SetSprite(imgGuild, PathDefine.BtnType2);
            SetSprite(imgFriend, PathDefine.BtnType1);
        }
    }

    public void ClickCloseBtn()
    {
        audioSvc.PlayUIAudio(Constants.UIClickBtn);

        SetWndState(false);
    }

World Chat System Development 3

Complete the button click events, respectively, are the Send button, the World Chat button, the Guild Chat button and the Friends Chat button.

    public InputField iptChat;
    public void ClickSendBtn()
    {
        if(iptChat.text!=null && iptChat.text!= "" && iptChat.text!= " ")
        {
            if(iptChat.text.Length > 12)
            {
                GameRoot.AddTips("Input information should not exceed 12 words");
            }
            else
            {
                //Send network messages to servers
            }
        }
        else
        {
            GameRoot.AddTips("No chat information has been entered yet");
        }
    }

    public void ClickWorldBtn()
    {
        audioSvc.PlayUIAudio(Constants.UIClickBtn);
        chatType = 0;
        RefreshUI();
    }

    public void ClickGuildBtn()
    {
        audioSvc.PlayUIAudio(Constants.UIClickBtn);
        chatType = 1;
        RefreshUI();
    }

    public void ClickFriendBtn()
    {
        audioSvc.PlayUIAudio(Constants.UIClickBtn);
        chatType = 2;
        RefreshUI();
    }

World Chat System Development 4

Firstly, two communication message classes are defined in GameMsg on the server side. One is SndChat to send messages to the server, the other is PshChat to broadcast messages.

    [Serializable]
    public class SndChat
    {
        public string chat;
    }

    [Serializable]
    public class PshChat
    {
        public string name;
        public string chat;
    }

CMD codes are defined as follows:

        SndChat = 205,
        PshChat = 206,

Sending messages in the client's hatWnd class

                //Send network messages to servers
                GameMsg msg = new GameMsg
                {
                    cmd = (int)CMD.SndChat,
                    sndChat = new SndChat
                    {
                        chat = iptChat.text
                    }
                };
                iptChat.text = "";
                netSvc.SendMsg(msg);

World Chat System Development 5

Processing and broadcasting messages sent on the server side, which are processed in ChatSys

using PEProtocol;
using System.Collections.Generic;

public class ChatSys
{
    private static ChatSys instance = null;
    public static ChatSys Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new ChatSys();
            }
            return instance;
        }
    }
    private CacheSvc cacheSvc = null;

    public void Init()
    {
        cacheSvc = CacheSvc.Instance;
        PECommon.Log("ChatSys Init Done.");
    }

    public void SndChat(MsgPack pack)
    {
        //Get the role name
        SndChat data = pack.msg.sndChat;
        PlayerData pd = cacheSvc.GetPlayerDataBySession(pack.session);

        GameMsg msg = new GameMsg
        {
            cmd = (int)CMD.PshChat,
            pshChat = new PshChat
            {
                name = pd.name,
                chat = data.chat
            }
        };

        //Broadcast all online clients
        List<ServerSession> lst = cacheSvc.GetOnlineServerSessions();
        for(int i = 0;i<lst.Count;i++)
        {
            lst[i].SendMsg(msg);
        }
    }
}

Where GetOnline Server Sessions is a session to get all client online users

World Chat System Development 6

Method of broadcasting all clients before optimization

        //Broadcast all online clients
        List<ServerSession> lst = cacheSvc.GetOnlineServerSessions();
        byte[] bytes = PENet.PETool.PackNetMsg(msg);//Serialize messages into binary
        for(int i = 0;i<lst.Count;i++)
        {
            //lst[i].SendMsg(msg); can be optimized, preferably using serialized messages for sending
            lst[i].SendMsg(bytes);
        }

World Chat System Development 7

This section is responsible for receiving server-side messages and displaying them on the client side.

    public void PshChat(GameMsg msg)
    {
        chatWnd.AddChatMsg(msg.pshChat.name, msg.pshChat.chat);
    }
    public void AddChatMsg(string name,string chat)
    {
        chatList.Add(Constants.Color(name + ": ", TxtColor.Blue) + chat);
        if(chatList.Count>12)
        {
            chatList.RemoveAt(0);
        }
        RefreshUI();
    }

Demonstration debugging and bug solving process

There is a bug. When one client sends a message, another client clicks the chat button to report an error. This is because before the chat box comes out, the method on the chat box is called. At this point, we just need to activate the chat box and then call the method.

    public void AddChatMsg(string name,string chat)
    {
        chatList.Add(Constants.Color(name + ": ", TxtColor.Blue) + chat);
        if(chatList.Count>12)
        {
            chatList.RemoveAt(0);
        }
        if(GetWndState())//In the active state, refresh again.
            RefreshUI();
    }

Making General Purchase Window

Development of Resource Trading System 1

Create scripts for BuyWnd, then initialize and register button events.

using UnityEngine;

public class BuyWnd : WindowRoot
{
    protected override void InitWnd()
    {
        base.InitWnd();
    }
}
    public void ClickMKCoinBtn()
    {
        audioSvc.PlayUIAudio(Constants.UIOpenPage);
        MainCitySys.Instance.OpenBuyWnd();
    }

    public void ClickBuyPowerBtn()
    {
        audioSvc.PlayUIAudio(Constants.UIOpenPage);
        MainCitySys.Instance.OpenBuyWnd();
    }
    public void OpenBuyWnd()
    {
        buyWnd.SetWndState();
    }

Resource trading system development 2

Perfect the previous ChatWnd, and then open the ChatWnd to give its type, whether to buy physical strength or gold coins.

    public Text txtInfo;
    private int buyType;//0:Physical strength 1:Gold coin

    public void SetBuyInfoType(int type)
    {
        this.buyType = type;
    }

    protected override void InitWnd()
    {
        base.InitWnd();
        RefreshUI();
    }

    private void RefreshUI()
    {
        switch(buyType)
        {
            case 0:
                //physical strength
                txtInfo.text = "Whether to spend or not" + Constants.Color("10 masonry", TxtColor.Red) + "purchase" + Constants.Color("100 physical strength", TxtColor.Green) + "?";
                break;
            case 1:
                //Gold coin
                txtInfo.text = "Whether to spend or not" + Constants.Color("10 masonry", TxtColor.Red) + "purchase" + Constants.Color("1000 Gold coin", TxtColor.Green) + "?";
                break;
        }
    }

    public void ClickSureBtn()
    {
        //Send Network Purchase Information TODO
    }

    public void ClickCloseBtn()
    {
        audioSvc.PlayUIAudio(Constants.UIClickBtn);
        SetWndState(false);
    }
    public void OpenBuyWnd(int type)
    {        
        buyWnd.SetBuyInfoType(type);
        buyWnd.SetWndState();
    }

Registration Event Method with Two Buttons in MainCityWnd

    public void ClickMKCoinBtn()
    {
        audioSvc.PlayUIAudio(Constants.UIOpenPage);
        MainCitySys.Instance.OpenBuyWnd(1);
    }

    public void ClickBuyPowerBtn()
    {
        audioSvc.PlayUIAudio(Constants.UIOpenPage);
        MainCitySys.Instance.OpenBuyWnd(0);
    }

Resource trading system development 3

Add Client Click to Determine Button Event and Send Information to Client

    public void ClickSureBtn()
    {
        //Sending Network Purchase Information
        GameMsg msg = new GameMsg
        {
            cmd = (int)CMD.ReqBuy,
            reqBuy = new ReqBuy
            {
                type = buyType,
                cost =10,
            }
        };
        netSvc.SendMsg(msg);
    }

This section is mainly responsible for the content of the BuyWnd client. The values that need to be sent and returned are as follows

    [Serializable]
    public class ReqBuy
    {
        public int type;
        public int cost;
    }

    [Serializable]
    public class RspBuy
    {
        public int type;
        public int diamond;
        public int coin;
        public int power;
    }

The BuySys script is as follows:

using PEProtocol;

public class BuySys
{
    private static BuySys instance = null;
    public static BuySys Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new BuySys();
            }
            return instance;
        }
    }
    private CacheSvc cacheSvc = null;

    public void Init()
    {
        cacheSvc = CacheSvc.Instance;
        PECommon.Log("BuySys Init Done.");
    }

    public void ReqBuy(MsgPack pack)
    {
        ReqBuy data = pack.msg.reqBuy;
        GameMsg msg = new GameMsg
        {
            cmd = (int)CMD.RspBuy,
        };
        //TODO
    }
}

Resource Trading System Development 4

Perfect ReqBuy method and add TODO content

        //Getting Cache Information
        PlayerData pd = cacheSvc.GetPlayerDataBySession(pack.session);

        if(pd.diamond<data.cost)
        {
            msg.err = (int)ErrorCode.LackDiamond;
        }
        else
        {
            pd.diamond = pd.diamond - data.cost;
            switch(data.type)
            {//0 physical strength 1 gold coin
                case 0:
                    pd.power += 100;
                    break;
                case 1:
                    pd.coin += 1000;
                    break;
            }
        }
        if(!cacheSvc.UpdatePlayerData(pd.id,pd))
        {
            msg.err = (int)ErrorCode.UpdateDBError;
        }
        else
        {
            RspBuy rspBuy = new RspBuy
            {
                type = data.type,
               diamond = pd.diamond,
               coin = pd.coin,
               power = pd.power
            };
            msg.rspBuy = rspBuy;
        }
        pack.session.SendMsg(msg);

Resource trading system development 5

Add the client processing method RspBuy()

    public void RspBuy(GameMsg msg)
    {
        RspBuy data = msg.rspBuy;
        GameRoot.Instance.SetPlayerDataByBuy(data);
        GameRoot.AddTips("Successful Purchase");

        maincityWnd.RefreshUI();
        buyWnd.SetWndState(false);
    }

    public void SetPlayerDataByBuy(RspBuy data)
    {
        playerData.diamond = data.diamond;
        playerData.coin = data.coin;
        playerData.power = data.power;
    }

In order to prevent network latency from causing players to click on the confirmation multiple times leading to repeated purchases, add a non-interactive after clicking on the confirmation.

Every time a page pops up, the interaction is set to true

Message Frequency Timing Control

The first is a simple protocol, added in ChatWnd

    private bool canSend = true;
    public void ClickSendBtn()
    {
        if (!canSend)
        {
            GameRoot.AddTips("Chat messages are sent every five seconds");
            return;
        }

        if (iptChat.text!=null && iptChat.text!= "" && iptChat.text!= " ")
        {
            if(iptChat.text.Length > 12)
            {
                GameRoot.AddTips("Input information should not exceed 12 words");
            }
            else
            {
                //Send network messages to servers
                GameMsg msg = new GameMsg
                {
                    cmd = (int)CMD.SndChat,
                    sndChat = new SndChat
                    {
                        chat = iptChat.text
                    }
                };
                iptChat.text = "";
                netSvc.SendMsg(msg);
                canSend = false;

                //Open the coordinator and set canSend to true after 5 seconds
                StartCoroutine(MsgTimer());
            }
        }
        else
        {
            GameRoot.AddTips("No chat information has been entered yet");
        }
    }

    IEnumerator MsgTimer()
    {
        yield return new WaitForSeconds(5.0f);
        canSend = true;
    }

Client Integration of PETimer Timing Plug-in

Here we will use the PETimer timing component that we have learned before. It has the following functions:

1. Dual-end versatility: an efficient and convenient timer based on C # language can run in the server (. net core/.net framework) and Unity client environment.

2. Rich functions: PETimer supports frame number timing and time timing. Timing tasks are cyclic, replaceable and cancelable. You can use separate thread timing (self-setting detection intervals) or external driver timing, such as using the Update() function in MonoBehaviour.

3. Simple integration: Only one PETimer.cs file, just need to instantiate a PETimer class, docking the corresponding API, can be integrated into their game framework, to achieve convenient and efficient timing callback service.

Drag PETimer.cs into the Service folder and create a TimerService class

using System;
using UnityEngine;

public class TimerSvc : SystemRoot 
{
    public static TimerSvc Instance = null;

    private PETimer pt;

    public void InitSvc()
    {
        Instance = this;
        pt = new PETimer();

        //Setting Log Output
        pt.SetLog((string info) =>
        {
            PECommon.Log(info);
        });
        PECommon.Log("Init TimerSvc...");
    }

    public void Update()
    {
        pt.Update();
    }

    //                                  Event Time Unit Counting             
    public int AddTimeTask(Action<int> callback,double delay,PETimeUnit timeUnit=PETimeUnit.Millisecond,int count = 1)
    {
        return pt.AddTimeTask(callback, delay, timeUnit, count);
    }
}

Testing in GameRoot

        //PETimer test
        TimerSvc.Instance.AddTimeTask((int tid) => {
            PECommon.Log("Test");
        }, 1000);

Server Integrates PETimer Timing Plug-in

To create a TimerSvc script on the server side to control timing, you also need to drag the PETimer.cs file in.

using System;

public class TimerSvc
{
    private static TimerSvc instance = null;
    public static TimerSvc Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new TimerSvc();
            }
            return instance;
        }
    }

    PETimer pt = null;

    public void Init()
    {
        pt = new PETimer();

        //Setting Log Output
        pt.SetLog((string info) =>
        {
            PECommon.Log(info);
        });

        PECommon.Log("TimerSvc Init Done.");
    }

    public void Update()
    {
        pt.Update();
    }

    public int AddTimeTask(Action<int> callback, double delay, PETimeUnit timeUnit = PETimeUnit.Millisecond, int count = 1)
    {
        return pt.AddTimeTask(callback, delay, timeUnit, count);
    }
}

Timing detection using independent threads

This section is used to improve the previous TimerSvc script. If Update is used all the time, thread detection will always be done, which may result in waste of resources. Here we use an independent detection mechanism.

pt = new PETimer(100);

The timer runs independently and detects every 100 milliseconds. Modify the Writing of TimeSvc

using System;
using System.Collections.Generic;

public class TimerSvc
{

    class TaskPack
    {
        public int tid;
        public Action<int> cb;
        public TaskPack(int tid, Action<int> cb)
        {
            this.tid = tid;
            this.cb = cb;
        }
    }

    private static TimerSvc instance = null;
    public static TimerSvc Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new TimerSvc();
            }
            return instance;
        }
    }

    PETimer pt = null;
    Queue<TaskPack> tpQue = new Queue<TaskPack>();
    private static readonly string tpQueLock = "tpQueLock";

    public void Init()
    {

        pt = new PETimer(100);


        //Setting Log Output
        pt.SetLog((string info) =>
        {
            PECommon.Log(info);
        });

        pt.SetHandle((Action<int> cb, int tid) =>
        {
            if (cb != null)
            {
                lock (tpQueLock)
                {
                    tpQue.Enqueue(new TaskPack(tid, cb));
                }
            }
        });
        PECommon.Log("Init TimerSvc...");
    }

    public void Update()
    {
        while (tpQue.Count > 0)
        {
            TaskPack tp = null;
            lock (tpQueLock)
            {
                tp = tpQue.Dequeue();
            }

            if (tp != null)
            {
                tp.cb(tp.tid);
            }
        }
    }

    public int AddTimeTask(Action<int> callback, double delay, PETimeUnit timeUnit = PETimeUnit.Millisecond, int count = 1)
    {
        return pt.AddTimeTask(callback, delay, timeUnit, count);
    }
}

Do a test

        TimerSvc.Instance.AddTimeTask((int tid) =>
        {
            PECommon.Log("xxx");
        },1000,PETimeUnit.Millisecond,0);

Development of Physical Recovery System 1

Create a PowerSys to handle physical recovery

//Physical recovery system
public class PowerSys
{
    private static PowerSys instance = null;
    public static PowerSys Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new PowerSys();
            }
            return instance;
        }
    }
    private CacheSvc cacheSvc = null;

    public void Init()
    {
        cacheSvc = CacheSvc.Instance;

        TimerSvc.Instance.AddTimeTask(CalPowerAdd, PECommon.PowerAddSpace, PETimeUnit.Second, 0);
        PECommon.Log("PowerSys Init Done.");
    }

    private void CalPowerAdd(int tid)
    {
        //Computing Physical Strengthen TODO
        PECommon.Log("Add...Power.");
    }
}

    public const int PowerAddSpace = 5;//Minute
    public const int PowerAddCount = 2;//Physical strength

Development of Physical Recovery System 2

Continue to improve the code of the physical recovery system, which is used to push real-time physical recovery data to all online inquiries.

    public void Init()
    {
        cacheSvc = CacheSvc.Instance;

        TimerSvc.Instance.AddTimeTask(CalPowerAdd, PECommon.PowerAddSpace, PETimeUnit.Second, 0);
        PECommon.Log("PowerSys Init Done.");
    }

    private void CalPowerAdd(int tid)
    {
        //Computing Physical Strengthen TODO
        PECommon.Log("All Online Player Calc Power Incress...");
        GameMsg msg = new GameMsg
        {
            cmd = (int)CMD.PshPower
        };

        msg.pshPower = new PshPower();

        //All online players get real-time physical growth push data
        Dictionary<ServerSession, PlayerData> onlineDic = cacheSvc.GetOnlineCache();
        foreach(var item in onlineDic)
        {
            PlayerData pd = item.Value;
            ServerSession session = item.Key;

            int powerMax = PECommon.GetPowerLimit(pd.lv);
            if (pd.power >= powerMax)
                continue;
            else
            {
                pd.power += PECommon.PowerAddCount;
                if (pd.power >= powerMax)
                    pd.power = powerMax;
            }
            if(!cacheSvc.UpdatePlayerData(pd.id,pd))
            {
                msg.err = (int)ErrorCode.UpdateDBError;
            }
            else
            {
                msg.pshPower.power = pd.power;
                session.SendMsg(msg);
            }
        }
    }

Development of Physical Recovery System 3

Adding methods to process information and updating the display of physical strength on the client side

    public void PshPower(GameMsg msg)
    {
        PshPower data = msg.pshPower;
        GameRoot.Instance.SetPlayerDataByPower(data);
        maincityWnd.RefreshUI();
    }

Development of Physical Recovery System 4

Add a time to the database user information to calculate the time, so as to facilitate the recovery of physical strength for offline users.

Add a method to calculate the current number of milliseconds

    //Get the current time (milliseconds from 1970 to the present time of the computer)
    public long GetNowTime()
    {
        return (long)pt.GetMillisecondsTime();
    }

When creating an account, add this time information to the user.

Development of Physical Recovery System 5

Processing offline physical recovery requires only processing at login time. Here's the code to add to LoginSys

                //Calculating offline physical growth
                int power = pd.power;
                long now = timerSvc.GetNowTime();
                long milliseconds = now - pd.time;
                int addPower = (int)(milliseconds / (1000 * 60 * PECommon.PowerAddSpace)) * PECommon.PowerAddCount;
                if(addPower > 0)
                {
                    int powerMax = PECommon.GetPowerLimit(pd.lv);
                    if(pd.power < powerMax)
                    {
                        pd.power += addPower;
                        if (pd.power > powerMax)
                            pd.power = powerMax;
                    }
                }

                if(power!=pd.power)
                {
                    cacheSvc.UpdatePlayerData(pd.id, pd);
                }

Development of Physical Recovery System 6

The method of rewriting server-side offline, calculating the time of each offline, is used to calculate offline physical recovery.

    public void ClearOfflineData(ServerSession session) {
        //Write down time to handle physical strength
        PlayerData pd = cacheSvc.GetPlayerDataBySession(session);
        if(pd != null)
        {
            pd.time = timerSvc.GetNowTime();
            if(!cacheSvc.UpdatePlayerData(pd.id,pd))
            {
                PECommon.Log("Update offline time error", LogType.Error);
            }
            cacheSvc.AcctOffLine(session);
        }      
    }

Prevent accidents or update time when power is updated

Posted by messels on Tue, 03 Sep 2019 07:16:39 -0700