目录
世界聊天系统与资源交易系统
世界聊天系统界面开发
世界聊天系统开发1
世界聊天系统开发2
世界聊天系统开发3
世界聊天系统开发4
世界聊天系统开发5
世界聊天系统开发6
世界聊天系统开发7
示范调试解决bug流程
制作通用购买窗口
资源交易系统开发1
资源交易系统开发2
资源交易系统开发3
资源交易系统开发4
资源交易系统开发5
消息频率定时控制
客户端整合PETimer定时插件
服务端整合PETimer定时插件
使用独立线程检测定时
体力恢复系统开发1
体力恢复系统开发2
体力恢复系统开发3
体力恢复系统开发4
体力恢复系统开发5
体力恢复系统开发6
创建一个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();
}
添加用来控制聊天分类按钮的显示方法
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
完成按钮点击的事件,分别是发送按钮,世界聊天按钮,公会聊天按钮和好友聊天按钮。
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();
}
首先在服务器端的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);
在服务器端对发送的消息进行处理并广播,这些都在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
优化之前广播所有客户端的方法
//广播所有在线客户端
List lst = cacheSvc.GetOnlineServerSessions();
byte[] bytes = PENet.PETool.PackNetMsg(msg);//将消息序列化成二进制
for(int i = 0;i
这节负责客户端接收服务器端消息以及显示
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();
}
存在一个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();
}
给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();
}
完善之前的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);
}
添加客户端点击确定按钮事件并向客户端发送信息
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
}
}
完善ReqBuy方法,添加TODO的内容
//取得缓存信息
PlayerData pd = cacheSvc.GetPlayerDataBySession(pack.session);
if(pd.diamond
添加客户端处理的方法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;
}
为了防止网络延迟导致玩家多次点击确定导致重复购买,给点击确定之后添加一个不可交互
每次弹出页面的是交互设置为true
首先是一个简单的协程,在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定时组件,它有以下的功能:
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);
在服务器端创建一个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);
创建一个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;//体力值
继续完善体力恢复系统的代码,这里用来向所有在线问价实时推送体力恢复数据。
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);
}
}
}
在客户端添加处理信息的方法,并更新体力值显示
public void PshPower(GameMsg msg)
{
PshPower data = msg.pshPower;
GameRoot.Instance.SetPlayerDataByPower(data);
maincityWnd.RefreshUI();
}
给数据库用户信息添加一个time,用来计算时间,从而方便给离线用户恢复体力
添加一个方法,计算当前的毫秒数
//获取当前时间(从计算机元年1970年到现在时间的毫秒数)
public long GetNowTime()
{
return (long)pt.GetMillisecondsTime();
}
当创建账号时,给用户添加这个时间信息。
处理离线体力恢复,只需要在登录的时候进行处理就行。这里在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);
}
重新改写服务器端下线的方法,计算每次下线的时间,用来计算离线体力恢复
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时进行更新时间