从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)

目录

世界聊天系统与资源交易系统

世界聊天系统界面开发

世界聊天系统开发1

世界聊天系统开发2

世界聊天系统开发3

世界聊天系统开发4

世界聊天系统开发5

世界聊天系统开发6

世界聊天系统开发7

示范调试解决bug流程

制作通用购买窗口

资源交易系统开发1

资源交易系统开发2

资源交易系统开发3

资源交易系统开发4

资源交易系统开发5

消息频率定时控制

客户端整合PETimer定时插件

服务端整合PETimer定时插件

使用独立线程检测定时

体力恢复系统开发1

体力恢复系统开发2

体力恢复系统开发3

体力恢复系统开发4

体力恢复系统开发5

体力恢复系统开发6


世界聊天系统与资源交易系统

世界聊天系统界面开发

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第1张图片

世界聊天系统开发1

创建一个ChatWnd脚本

using UnityEngine;

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

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

        SetWndState(false);
    }
}

在MainCityWnd中添加打开聊天系统的方法。

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

该方法通过调用MainCitySys中的方法实现

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

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第2张图片

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第3张图片

世界聊天系统开发2

添加用来控制聊天分类按钮的显示方法

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

    private int chatType;
    private List chatList = new List();

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

        chatType = 0;
        RefreshUI();

    }

    private void RefreshUI()
    {
        if(chatType == 0)//世界
        {
            string chatMsg = "";
            for(int i=0;i

世界聊天系统开发3

完成按钮点击的事件,分别是发送按钮,世界聊天按钮,公会聊天按钮和好友聊天按钮。

    public InputField iptChat;
    public void ClickSendBtn()
    {
        if(iptChat.text!=null && iptChat.text!= "" && iptChat.text!= " ")
        {
            if(iptChat.text.Length > 12)
            {
                GameRoot.AddTips("输入信息不能超过12个字");
            }
            else
            {
                //发送网络消息到服务器
            }
        }
        else
        {
            GameRoot.AddTips("尚未输入聊天信息");
        }
    }

    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();
    }

世界聊天系统开发4

首先在服务器端的GameMsg中定义好两个通信的消息类,一个是SndChat用来向服务器发送消息,一个是PshChat是将消息广播。

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

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

CMD码定义如下:

        SndChat = 205,
        PshChat = 206,

在客户端的ChatWnd类中进行消息发送

                //发送网络消息到服务器
                GameMsg msg = new GameMsg
                {
                    cmd = (int)CMD.SndChat,
                    sndChat = new SndChat
                    {
                        chat = iptChat.text
                    }
                };
                iptChat.text = "";
                netSvc.SendMsg(msg);

世界聊天系统开发5

在服务器端对发送的消息进行处理并广播,这些都在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)
    {
        //获取角色名称
        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
            }
        };

        //广播所有在线客户端
        List lst = cacheSvc.GetOnlineServerSessions();
        for(int i = 0;i

其中GetOnlineServerSessions是获取所有客户端在线用户的session

世界聊天系统开发6

优化之前广播所有客户端的方法

        //广播所有在线客户端
        List lst = cacheSvc.GetOnlineServerSessions();
        byte[] bytes = PENet.PETool.PackNetMsg(msg);//将消息序列化成二进制
        for(int i = 0;i

世界聊天系统开发7

这节负责客户端接收服务器端消息以及显示

    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();
    }

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第4张图片

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第5张图片

示范调试解决bug流程

存在一个bug,一个客户端发送消息时,另外一个客户端点击聊天按钮会报错。这是因为聊天框还没出来,就调用了聊天框上面的方法。此时,只需要我们让聊天框激活后再调用该方法即可。

    public void AddChatMsg(string name,string chat)
    {
        chatList.Add(Constants.Color(name + ":", TxtColor.Blue) + chat);
        if(chatList.Count>12)
        {
            chatList.RemoveAt(0);
        }
        if(GetWndState())//处于激活状态再进行刷新。
            RefreshUI();
    }

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第6张图片

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第7张图片

制作通用购买窗口

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第8张图片

资源交易系统开发1

给BuyWnd创建脚本,然后进行初始化和按钮事件注册。

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();
    }

资源交易系统开发2

完善之前的ChatWnd,然后打开ChatWnd时需要给其传类型,是购买体力还是购买金币。

    public Text txtInfo;
    private int buyType;//0:体力    1:金币

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

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

    private void RefreshUI()
    {
        switch(buyType)
        {
            case 0:
                //体力
                txtInfo.text = "是否花费" + Constants.Color("10砖石", TxtColor.Red) + "购买" + Constants.Color("100体力", TxtColor.Green) + "?";
                break;
            case 1:
                //金币
                txtInfo.text = "是否花费" + Constants.Color("10砖石", TxtColor.Red) + "购买" + Constants.Color("1000金币", TxtColor.Green) + "?";
                break;
        }
    }

    public void ClickSureBtn()
    {
        //发送网络购买信息 TODO
    }

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

MainCityWnd中添加两个按钮的注册事件方法

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

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

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第9张图片

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第10张图片

资源交易系统开发3

添加客户端点击确定按钮事件并向客户端发送信息

    public void ClickSureBtn()
    {
        //发送网络购买信息
        GameMsg msg = new GameMsg
        {
            cmd = (int)CMD.ReqBuy,
            reqBuy = new ReqBuy
            {
                type = buyType,
                cost =10,
            }
        };
        netSvc.SendMsg(msg);
    }

这节主要负责BuyWnd客户端的内容,需要发送和返回的值如下

    [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;
    }

BuySys脚本如下:


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

资源交易系统开发4

完善ReqBuy方法,添加TODO的内容

        //取得缓存信息
        PlayerData pd = cacheSvc.GetPlayerDataBySession(pack.session);

        if(pd.diamond

资源交易系统开发5

添加客户端处理的方法RspBuy()

    public void RspBuy(GameMsg msg)
    {
        RspBuy data = msg.rspBuy;
        GameRoot.Instance.SetPlayerDataByBuy(data);
        GameRoot.AddTips("购买成功");

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

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

为了防止网络延迟导致玩家多次点击确定导致重复购买,给点击确定之后添加一个不可交互

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第11张图片

每次弹出页面的是交互设置为true

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第12张图片

消息频率定时控制

首先是一个简单的协程,在ChatWnd中进行添加

    private bool canSend = true;
    public void ClickSendBtn()
    {
        if (!canSend)
        {
            GameRoot.AddTips("聊天消息每5秒钟才能发送一条");
            return;
        }

        if (iptChat.text!=null && iptChat.text!= "" && iptChat.text!= " ")
        {
            if(iptChat.text.Length > 12)
            {
                GameRoot.AddTips("输入信息不能超过12个字");
            }
            else
            {
                //发送网络消息到服务器
                GameMsg msg = new GameMsg
                {
                    cmd = (int)CMD.SndChat,
                    sndChat = new SndChat
                    {
                        chat = iptChat.text
                    }
                };
                iptChat.text = "";
                netSvc.SendMsg(msg);
                canSend = false;

                //开启协程,5秒之后把canSend设置为true
                StartCoroutine(MsgTimer());
            }
        }
        else
        {
            GameRoot.AddTips("尚未输入聊天信息");
        }
    }

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

客户端整合PETimer定时插件

这里会用到之前学习过的PETimer定时组件,它有以下的功能:

1.双端通用:基于C#语言实现的高效便捷计时器,可运行在服务器(.net core/.net framework)以及Unity客户端环境中。

2.功能丰富:PETimer支持帧数定时以及时间定时。定时任务可循环、可替换、可取消。可使用独立线程计时(自行设定检测间隔),也可以使用外部驱动计时,比如使用MonoBehaviour中的Update()函数来驱动。

3.集成简单:只有一个PETimer.cs文件,只需实例化一个PETimer类,对接相应的API,便能整合进自己的游戏框架,实现便捷高效的定时回调服务。

把PETimer.cs拖入到Service文件夹中,然后创建一个计时服务类TimerService

using System;
using UnityEngine;

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

    private PETimer pt;

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

        //设置日志输出
        pt.SetLog((string info) =>
        {
            PECommon.Log(info);
        });
        PECommon.Log("Init TimerSvc...");
    }

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

    //                                  事件        时间              单位                                  计数             
    public int AddTimeTask(Action callback,double delay,PETimeUnit timeUnit=PETimeUnit.Millisecond,int count = 1)
    {
        return pt.AddTimeTask(callback, delay, timeUnit, count);
    }
}

GameRoot中进行测试

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

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第13张图片

服务端整合PETimer定时插件

在服务器端创建一个TimerSvc脚本,用来控制定时,也需要把PETimer.cs文件拖进去。

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();

        //设置日志输出
        pt.SetLog((string info) =>
        {
            PECommon.Log(info);
        });

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

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

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

使用独立线程检测定时

这节用来改进之前的TimerSvc脚本,如果一直使用Update的话,就一直进行线程检测,可能造成资源浪费。这里我们使用独立的检测机制。

pt = new PETimer(100);

定时器独立运行,每隔100毫秒进行一次检测。修改TimeSvc的写法


using System;
using System.Collections.Generic;

public class TimerSvc
{

    class TaskPack
    {
        public int tid;
        public Action cb;
        public TaskPack(int tid, Action 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 tpQue = new Queue();
    private static readonly string tpQueLock = "tpQueLock";

    public void Init()
    {

        pt = new PETimer(100);


        //设置日志输出
        pt.SetLog((string info) =>
        {
            PECommon.Log(info);
        });

        pt.SetHandle((Action 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 callback, double delay, PETimeUnit timeUnit = PETimeUnit.Millisecond, int count = 1)
    {
        return pt.AddTimeTask(callback, delay, timeUnit, count);
    }
}

进行一个测试

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

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第14张图片

体力恢复系统开发1

创建一个PowerSys用来处理体力恢复

//体力恢复系统
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)
    {
        //计算体力增强 TODO
        PECommon.Log("Add...Power.");
    }
}

    public const int PowerAddSpace = 5;//分钟
    public const int PowerAddCount = 2;//体力值

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第15张图片

体力恢复系统开发2

继续完善体力恢复系统的代码,这里用来向所有在线问价实时推送体力恢复数据。

    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)
    {
        //计算体力增强 TODO
        PECommon.Log("All Online Player Calc Power Incress...");
        GameMsg msg = new GameMsg
        {
            cmd = (int)CMD.PshPower
        };

        msg.pshPower = new PshPower();

        //所有在线玩家获得实时的体力增长推送数据
        Dictionary 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);
            }
        }
    }

体力恢复系统开发3

在客户端添加处理信息的方法,并更新体力值显示

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

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第16张图片

体力恢复系统开发4

给数据库用户信息添加一个time,用来计算时间,从而方便给离线用户恢复体力

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第17张图片

添加一个方法,计算当前的毫秒数

    //获取当前时间(从计算机元年1970年到现在时间的毫秒数)
    public long GetNowTime()
    {
        return (long)pt.GetMillisecondsTime();
    }

当创建账号时,给用户添加这个时间信息。

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第18张图片

体力恢复系统开发5

处理离线体力恢复,只需要在登录的时候进行处理就行。这里在LoginSys中添加一下的代码

                //计算离线体力增长
                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);
                }

体力恢复系统开发6

重新改写服务器端下线的方法,计算每次下线的时间,用来计算离线体力恢复

    public void ClearOfflineData(ServerSession session) {
        //写入下线的时间,为了处理体力值
        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);
        }      
    }

防止意外,也可以在更新power时进行更新时间

从0开始的网游ARPG实战案例:暗黑战神(第八章:世界聊天系统与资源交易系统)_第19张图片

你可能感兴趣的:(ARPG实战)