目录

第1章: 初始场景与UI界面制作

登录场景制作

制作登录界面UI

UI自适应原理

制作角色创建界面​

制作Tips动态提示界面

制作Loading进度界面

第2章: UI逻辑框架与配置文件

客户端开发环境配置

UI逻辑框架介绍

游戏启动逻辑

异步场景加载

更新场景加载进度

登录注册界面逻辑

UI窗口基类

音效播放服务

业务系统层基类

Tips弹窗显示

设置Tips显示队列

角色创建界面逻辑

生成随机名字配置文件

解析随机名字配置文件

完成随机名字生成

第3章: 网络通信与服务器环境配置

服务器开发环境配置

介绍PESocket开源网络库

服务器使用PESocket

Unity使用PESocket

设置日志接口

服务器框架介绍

服务器启动逻辑

服务器网络服务

客户端网络服务

封装通用工具

登录协议定制

登录消息分发处理

附加会话信息到消息包

驱动逻辑处理

增加数据缓存层

缓存上线玩家数据

客户端消息分发

第4章:数据库与服务器缓存层

数据库环境配置

数据库增删改查

增加数据库管理器

查询玩家数据

插入默认玩家数据

重命名功能

缓存更新逻辑

数据表备份与错误码处理


第1章: 初始场景与UI界面制作

登录场景制作

制作登录界面UI

UI自适应原理

首先设置Canvas根据窗口大小匹配匹配,然后以高度为匹配对象

给UI上的各个物体添加锚点,进行自适应

制作角色创建界面

制作Tips动态提示界面

制作Loading进度界面


第2章: UI逻辑框架与配置文件

客户端开发环境配置

使用VS2017加VA助手进行代码开发。

UI逻辑框架介绍

游戏启动逻辑

根据设计的UI逻辑框架,首先创建三个脚本GameRoot,ResSvc和LoginSys分别负责游戏启动入口,资源加载服务和游戏登录服务。

public class GameRoot : MonoBehaviour
{private void Start(){Debug.Log("Game Start...");Init();}private void Init(){//服务模块初始化ResSvc res = GetComponent<ResSvc>();res.InitSvc();//业务系统初始化LoginSys login = GetComponent<LoginSys>();login.InitSys();//进入登录场景并加载相应UIlogin.EnterLogin();}
}
public class ResSvc : MonoBehaviour
{public void InitSvc(){Debug.Log("Init ResSvg...");}
}
public class LoginSys : MonoBehaviour
{public void InitSys(){Debug.Log("Init LoginSys...");}/// <summary>/// 进入登录场景/// </summary>public void EnterLogin(){//TODO//异步加载登录场景//并显示加载的进度条//加载完成以后并显示注册登录界面}
}

异步场景加载

异步加载登录场景

    public void AsyncLoadScene(string sceneName){SceneManager.LoadSceneAsync(sceneName);}

给加载界面添加一个LoadingWnd脚本,进行管理。这个脚本有GameRoot直接负责进行管理,因为游戏中很多其他地方也会多次加载页面。

public LoadingWnd loadingWnd;

在LoginSys脚本的EnterLogin中首先加载这个页面。

GameRoot.Instance.loadingWnd.gameObject.SetActive(true);

更新场景加载进度

在loadingWnd中添加初始化场景加载和修改读取条的方法

    public Text txtTips;public Image imgFG;public Image imgPoint;public Text txtPrg;private float fgWidth;//初始化窗口public void InitWnd(){fgWidth = imgFG.GetComponent<RectTransform>().sizeDelta.x;txtTips.text = "这是一条游戏Tips";txtPrg.text = "0%";imgFG.fillAmount = 0;imgPoint.transform.localPosition = new Vector3(-400f, 0, 0);}//修改进度条public void SetProgress(float prg){txtPrg.text = (int)(prg * 100) + "%";imgFG.fillAmount = prg;float posX = prg * fgWidth - 400;imgPoint.GetComponent<RectTransform>().anchoredPosition = new Vector2(posX, 0);}

ResSvc中添加异步加载场景的动画并且实时更新

    private Action prgCB = null;public void AsyncLoadScene(string sceneName){AsyncOperation sceneAsync = SceneManager.LoadSceneAsync(sceneName);prgCB = () => {float val = sceneAsync.progress;GameRoot.Instance.loadingWnd.SetProgress(val);if (val == 1){prgCB = null;sceneAsync = null;GameRoot.Instance.loadingWnd.gameObject.SetActive(false);}};}private void Update(){if (prgCB != null)prgCB();}

LoginSys的的EnterLogin方法中进行调用

    /// <summary>/// 进入登录场景/// </summary>public void EnterLogin(){//异步加载登录场景//并显示加载的进度条GameRoot.Instance.loadingWnd.gameObject.SetActive(true);GameRoot.Instance.loadingWnd.InitWnd();//加载完成以后并显示注册登录界面ResSvc.Instance.AsyncLoadScene(Constants.SceneLogin);}

登录注册界面逻辑

给LoginWnd添加一个LoginWnd脚本,里面进行初始化和更新

public class LoginWnd : MonoBehaviour
{public InputField iptAcct;public InputField iptPass;public Button btnEnter;public Button btnNotice;public void InitWnd(){//获取本地存储的账号密码if(PlayerPrefs.HasKey("Acct")&&PlayerPrefs.HasKey("Pass")){iptAcct.text = PlayerPrefs.GetString("Acct");iptPass.text = PlayerPrefs.GetString("Pass");}else{iptAcct.text = "";iptPass.text = "";}}//TODO 更新本地存储的账号密码

更新上节课异步加载的方法,让它能够公用

    private Action prgCB = null;public void AsyncLoadScene(string sceneName, Action loaded){//异步加载登录场景//并显示加载的进度条GameRoot.Instance.loadingWnd.gameObject.SetActive(true);GameRoot.Instance.loadingWnd.InitWnd();AsyncOperation sceneAsync = SceneManager.LoadSceneAsync(sceneName);prgCB = () => {float val = sceneAsync.progress;GameRoot.Instance.loadingWnd.SetProgress(val);if (val == 1){if (loaded != null)loaded();//LoginSys.Instance.OpenLoginWnd();prgCB = null;sceneAsync = null;GameRoot.Instance.loadingWnd.gameObject.SetActive(false);}};}private void Update(){if (prgCB != null)prgCB();}

修改LoginSys中的进入登录界面的和打开登录界面的方法

    /// <summary>/// 进入登录场景/// </summary>public void EnterLogin(){//异步加载登录场景//并显示加载的进度条//加载完成以后并显示注册登录界面ResSvc.Instance.AsyncLoadScene(Constants.SceneLogin,OpenLoginWnd);}/// <summary>/// 打开登录界面/// </summary>public void OpenLoginWnd(){loginWnd.gameObject.SetActive(true);loginWnd.InitWnd();}

UI窗口基类

建立一个WindowRoot基类,让LoadingWnd和LoginWnd都继承它,方便管理

public class WindowRoot : MonoBehaviour
{public ResSvc resSvc = null;public void SetWndState(bool isActive = true){if (gameObject.activeSelf != isActive)SetActive(gameObject, isActive);if (isActive)InitWnd();elseClearWnd();}protected virtual void InitWnd(){resSvc = ResSvc.Instance;}protected virtual void ClearWnd(){resSvc = null;}#region Tool Functionsprotected void SetActive(GameObject go, bool isActive = true){go.SetActive(isActive);}protected void SetActive(Transform trans, bool state = true){trans.gameObject.SetActive(state);}protected void SetActive(RectTransform rectTrans, bool state = true){rectTrans.gameObject.SetActive(state);}protected void SetActive(Image img, bool state = true){img.transform.gameObject.SetActive(state);}protected void SetActive(Text txt, bool state = true){txt.transform.gameObject.SetActive(state);}protected void SetText(Text txt, string context = ""){txt.text = context;}protected void SetText(Transform trans, int num = 0){SetText(trans.GetComponent<Text>(), num);}protected void SetText(Transform trans, string context = ""){SetText(trans.GetComponent<Text>(), context);}protected void SetText(Text txt, int num = 0){SetText(txt, num.ToString());}#endregion
}

音效播放服务

创建两个空物体BGAudio和UIAudio分别挂上AudioSource组件,新建一个AudioSvc负责声音资源

public class AudioSvc : MonoBehaviour
{public static AudioSvc Instance = null;public AudioSource bgAudio;public AudioSource uiAudio;public void InitSvc(){Instance = this;Debug.Log("Init AudioSvc...");}public void PlayBGMusic(string name,bool isLoop = true){AudioClip audio = ResSvc.Instance.LoadAudio("ResAudio/"+name,true);if(bgAudio.clip == null || bgAudio.clip.name !=audio.name){bgAudio.clip = audio;bgAudio.loop = isLoop;bgAudio.Play();}}public void PlayUIMusic(string name){AudioClip audio = ResSvc.Instance.LoadAudio("ResAudio/" + name, true);uiAudio.clip = audio;uiAudio.Play();}
}

ResSvc类中添加一个加载声音的方法

    private Dictionary<string, AudioClip> adDic = new Dictionary<string, AudioClip>(); public AudioClip LoadAudio(string path,bool cache = false){AudioClip au = null;if(!adDic.TryGetValue(path,out au)){au = Resources.Load<AudioClip>(path);if (cache)adDic.Add(path, au);}       return au;}

添加常量到Constants中

    //音效public const string BGlogin = "bgLogin";

在打开登录界面的时候进行声音播放

AudioSvc.Instance.PlayBGMusic(Constants.BGlogin);

业务系统层基类

创建一个业务系统基类SystemRoot,让所有以Sys结尾的文件继承它

public class SystemRoot : MonoBehaviour
{protected ResSvc resSvc;protected AudioSvc audioSvc;public virtual void InitSys(){resSvc = ResSvc.Instance;audioSvc = AudioSvc.Instance;}
}

让登录界面的飞龙可以一直在界面上,需要循环这个动画,在它身上添加一个脚本

public class LoopDragonAni : MonoBehaviour
{private Animation ani;private void Awake(){ani = transform.GetComponent<Animation>();}private void Start(){if (ani != null){InvokeRepeating("PlayDragonAni", 0, 20);}}private void PlayDragonAni(){if (ani != null)ani.Play();}
}

Tips弹窗显示

给DynamicWnd创建一个同名脚本,用来负责弹窗的显示。另外还支持弹窗动画播放完毕后,关闭显示。

public class DynamicWnd : WindowRoot
{public Animation tipsAni;public Text txtTips;protected override void InitWnd(){base.InitWnd();SetActive(txtTips, false);//初始关闭text的显示}public void SetTips(string tips){SetActive(txtTips);SetText(txtTips, tips);AnimationClip clip = tipsAni.GetClip("TipsShowAni");tipsAni.Play();//延时关闭激活状态StartCoroutine(AniPlayDone(clip.length, () =>{SetActive(txtTips, false);}));}private IEnumerator AniPlayDone(float sec,Action cb){yield return new WaitForSeconds(sec);if(cb!=null){cb();}}
}

但存在Bug,如果在GameRoot里连续调用SetTips函数,旧的tips会被新的覆盖。

设置Tips显示队列

新增一个队列来进行Tips的显示,修改之前的DynamicWnd里面的方法

public class DynamicWnd : WindowRoot
{public Animation tipsAni;public Text txtTips;//定义一个队列来进行tips的队列显示private Queue<string> tipsQue = new Queue<string>();private bool isTipsShow = false;protected override void InitWnd(){base.InitWnd();SetActive(txtTips, false);//初始关闭text的显示}public void AddTips(string tips){lock (tipsQue){tipsQue.Enqueue(tips);}}private void Update(){if(tipsQue.Count>0 && isTipsShow == false){lock(tipsQue){string tips = tipsQue.Dequeue();isTipsShow = true;SetTips(tips);}}}public void SetTips(string tips){SetActive(txtTips);SetText(txtTips, tips);AnimationClip clip = tipsAni.GetClip("TipsShowAni");tipsAni.Play();//延时关闭激活状态StartCoroutine(AniPlayDone(clip.length, () =>{SetActive(txtTips, false);isTipsShow = false;}));}private IEnumerator AniPlayDone(float sec,Action cb){yield return new WaitForSeconds(sec);if(cb!=null){cb();}}
}

在GameRoot里面新增加两个方法,一个是AddTips用来实现tips的添加,一个是ClearUIRoot用来把所有的Wnd都隐藏

    private void ClearUIRoot(){Transform canvas = transform.Find("Canvas");for(int i=0;i<canvas.childCount;i++){canvas.GetChild(i).gameObject.SetActive(false);}dynamicWnd.SetWndState();}public static void AddTips(string tips){Instance.dynamicWnd.AddTips(tips);}

角色创建界面逻辑

给CreateWnd创建一个同名脚本,用来角色创建。

在LoginSys中添加登录成功的方法

    public void RspLogin(){GameRoot.AddTips("登录成功");//打开角色创建页面createWnd.SetWndState();//关闭登录页面loginWnd.SetWndState(false);}

在LoginWnd中更新进入游戏的方法

    /// <summary>/// 点击进入游戏/// </summary>public void ClickEnterBtn(){audioSvc.PlayUIMusic(Constants.UILoginBtn);string acct = iptAcct.text;string pass = iptPass.text;if(acct!=""&&pass!=""){//更新本地存储的账号密码PlayerPrefs.SetString("Acct", acct);PlayerPrefs.SetString("Pass", pass);//TODO 发送网络消息,请求登录//TO RemoveLoginSys.Instance.RspLogin();}else{GameRoot.AddTips("账号或密码为空");}}public void ClickNoticeBtn(){audioSvc.PlayUIMusic(Constants.UIClickBtn);GameRoot.AddTips("功能正在开发中...");}

生成随机名字配置文件

首先需要一个xml的模板,如下所示

<?xml version="1.0" encoding="UTF-8"?>
<root><item ID="">       <surname></surname><man></man><woman></woman></item><item ID="">     <surname></surname><man></man><woman></woman></item>
</root>

然后在excel里面讲xml导入进去,然后自己添加上合适的信息

然后进行导出

打开导出的文件,文件中的内容为

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><item ID="1"><surname>a</surname><man>b</man><woman>c</woman></item><item ID="2"><surname>d</surname><man>e</man><woman>f</woman></item>
</root>

解析随机名字配置文件

首先建立一个PathDefine的脚本,用来存储xml文件的地址

public class PathDefine
{#region Configspublic const string RDNameCfg = "ResCfgs/rdname";#endregion
}

然后在ResSvc中写入一个新的方法来解析xml文件

    private List<string> surnameLst = new List<string>();private List<string> manLst = new List<string>();private List<string> womanLst = new List<string>();//解析随机名字的xml文件private void InitRDNameCfg(){TextAsset xml = Resources.Load<TextAsset>(PathDefine.RDNameCfg);if (!xml)Debug.LogError("xml file:" + PathDefine.RDNameCfg + "not exist");else{XmlDocument doc = new XmlDocument();doc.LoadXml(xml.text);XmlNodeList nodLst = doc.SelectSingleNode("root").ChildNodes;for(int i =0;i<nodLst.Count;i++){XmlElement ele = nodLst[i] as XmlElement;if (ele.GetAttributeNode("ID") == null)continue;int ID = Convert.ToInt32(ele.GetAttributeNode("ID").InnerText);foreach(XmlElement e in nodLst[i].ChildNodes){switch(e.Name){case "surname":surnameLst.Add(e.InnerText);break;case "man":manLst.Add(e.InnerText);break;case "woman":womanLst.Add(e.InnerText);break;}}}}}

完成随机名字生成

首先创建一个PETools工具脚本,这里用来生成一个随机数,用来随机名字

public class PETools
{//产生一个随机数public static int RDInt(int min,int max,System.Random rd = null){if(rd == null)rd = new System.Random();int val = rd.Next(min, max + 1);return val;}
}

在ResSvc中添加一个新方法,用来获取一个随机名字

    public string GetRDNameData(bool man = true){System.Random rd = new System.Random();string rdName = surnameLst[PETools.RDInt(0, surnameLst.Count - 1)];if (man){rdName += manLst[PETools.RDInt(0, manLst.Count - 1)];}elserdName += womanLst[PETools.RDInt(0, womanLst.Count - 1)];return rdName;}

在CreateWnd中调用产生随机名字的方法,并且添加按钮点击的方法

    public InputField iptName;protected override void InitWnd(){base.InitWnd();//TODO//显示一个随机名字iptName.text = resSvc.GetRDNameData(false);}public void ClickRandBtn(){audioSvc.PlayUIMusic(Constants.UIClickBtn);iptName.text = resSvc.GetRDNameData();}public void ClickEnterBtn(){audioSvc.PlayUIMusic(Constants.UIClickBtn);if (iptName.text != ""){//TODO//发送名字数据到服务器,登录主程}elseGameRoot.AddTips("当前名字不符合规范");}


第3章: 网络通信与服务器环境配置

服务器开发环境配置

VS2017中需要安装C#平台的支持,我们在windos上使用服务器。在这个项目中,我们使用PESocket开源网络库来实现网络通信。

介绍PESocket开源网络库

下载PESocket:
GitHub地址:https://github.com/PlaneZhong/PESocket
介绍网络库(防止学校访问外网波动)
博客园地址:
https://www.cnblogs.com/planezhong/p/10074676.html

服务器使用PESocket

需要创建和引用的文件如下

NetMsg.cs一个类库,用来存储发送的信息以及服务器ip地址和接口号


using PENet;
using System;namespace Protocal
{[Serializable]public class NetMsg:PEMsg{public string text;}public class IPCfg{public const string srvIP = "127.0.0.1";public const int srvPort = 17666;}
}

ServerSession.cs用来进行服务器会话控制

using System;
using Protocal;
using PENet;public class ServerSession:PESession<NetMsg>
{protected override void OnConnected(){PETool.LogMsg("Client Connect");}protected override void OnReciveMsg(NetMsg msg){PETool.LogMsg("Client req" + msg.text);}protected override void OnDisConnected(){PETool.LogMsg("Client DisConnect");}
}

ServerStart.cs用来启动服务器

using Protocal;
using PENet;namespace Server
{class ServerStart{static void Main(string[] args){PESocket<ServerSession, NetMsg> server = new PESocket<ServerSession, NetMsg>();server.StartAsServer(IPCfg.srvIP, IPCfg.srvPort);while(true){}}}
}

运行后结果如下:

Unity使用PESocket

Unity创建一个新的项目,然后将PENet.dll和Protocal.dll添加到这个项目中

ClientSession.cs用来进行客户端会话控制,与服务器端一致

using PENet;
using Protocal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;public class ClientSession:PENet.PESession<NetMsg>
{protected override void OnConnected(){PETool.LogMsg("Server Connect");}protected override void OnReciveMsg(NetMsg msg){PETool.LogMsg("Server req" + msg.text);}protected override void OnDisConnected(){PETool.LogMsg("Server DisConnect");}
}

GameStart.cs用来启动客户端,并进行测试

using Protocal;
using UnityEngine;public class GameStart : MonoBehaviour
{PENet.PESocket<ClientSession, NetMsg> client = null;private void Start(){client = new PENet.PESocket<ClientSession, NetMsg>();client.StartAsClient(IPCfg.srvIP, IPCfg.srvPort);}private void Update(){if(Input.GetKeyDown(KeyCode.Space)){client.session.SendMsg(new NetMsg{text = "hello unity"});}}
}

设置日志接口

        //指定一个日志接口,让服务器的错误在Unity的控制台打印出来client.SetLog(true,(string msg,int lv) =>{switch(lv){case 0:msg = "Log:" + msg;Debug.Log(msg);break;case 1:msg = "Warn:" + msg;Debug.LogWarning(msg);break;case 2:msg = "Error:" + msg;Debug.LogError(msg);break;case 3:msg = "Info:" + msg;Debug.Log(msg);break;}});

服务器框架介绍

服务器启动逻辑

创建一个服务端的C#项目,暂时需要的文件如下

ServeRoot.cs

/// <summary>
/// 服务器初始化模板
/// </summary>public class ServerRoot
{private static ServerRoot instance = null;public static ServerRoot Instance{get{if (instance == null)instance = new ServerRoot();return instance;}}public void Init(){//数据库TODO//服务处NetSvc.Instance.Init();//业务系统层LoginSys.Instance.Init();}
}

ServeStart.cs

/// <summary>
/// 服务器入口
/// </summary>public class ServerStart
{static void Main(string[] args){ServerRoot.Instance.Init();while (true){}}
}

服务器网络服务

与之前的案例相似,需要创建ServerSession和GameMsg文件。

下面修改NetSvc和LoginSys中的部分方法。

/// <summary>
/// 网络服务
/// </summary>
///
using PENet;
using PEProtocol;public class NetSvc
{private static NetSvc instance = null;public static NetSvc Instance{get{if (instance == null)instance = new NetSvc();return instance;}}public void Init(){PESocket<ServerSession, GameMsg> server = new PESocket<ServerSession, GameMsg>();server.StartAsServer(SrvCfg.srvIP, SrvCfg.srvPort);PETool.LogMsg("NetSvc Init Done.");}
}
using PENet;
/// <summary>
/// 登录业务系统
/// </summary>
public class LoginSys
{private static LoginSys instance = null;public static LoginSys Instance{get{if (instance == null)instance = new LoginSys();return instance;}}public void Init(){PETool.LogMsg("LoginSys Init Done.");}
}

客户端网络服务

按照之前的方法添加客户端连接服务,然后在NetSvc中进行初始化

using UnityEngine;
using PENet;
using PEProtocol;public class NetSvc : MonoBehaviour
{public static NetSvc Instance = null;PESocket<ClientSession, GameMsg> client = null;public void InitSvc(){Instance = this;client = new PESocket<ClientSession, GameMsg>();client.SetLog(true, (string msg, int lv) =>{switch (lv){case 0:msg = "Log:" + msg;Debug.Log(msg);break;case 1:msg = "Warn:" + msg;Debug.LogWarning(msg);break;case 2:msg = "Error:" + msg;Debug.LogError(msg);break;case 3:msg = "Info:" + msg;Debug.Log(msg);break;}});client.StartAsClient(SrvCfg.srvIP, SrvCfg.srvPort);Debug.Log("Init NetSvc...");}
}

封装通用工具

using PENet;public enum LogType
{Log=0,Warn=1,Error=2,Info=3
}public class PECommon
{public static void Log(string msg="",LogType tp = LogType.Log){LogLevel lv = (LogLevel)tp;PETool.LogMsg(msg, lv);}
}

使用PECommon.Log可以让其他文件不需要另外调用命名空间。

登录协议定制

首先修改服务器的GmaeMsg.cs,让它知道发送过来的消息是什么

    [Serializable]public class GameMsg:PEMsg{public ReqLogin reqLogin;}[Serializable]public class ReqLogin{public string acct;public string pass;}public enum CMD{None=0,//登录相关 100ReqLogin = 101,RspLogin = 102,}

在客户端的LoginWnd.cs中给服务器发送登录的账号密码

            GameMsg msg = new GameMsg {cmd = (int)CMD.ReqLogin,reqLogin = new ReqLogin{acct = _acct,pass = _pass}};netSvc.SendMsg(msg);

登录消息分发处理

在服务器端LoginSys.cs中添加回复登录请求的函数

    //对登消息进行回复public void ReqLogin(GameMsg msg){}

在NetSvc.cs中增加消息队列,并且添加各种操作方法

    public static readonly string obj = "lock";private Queue<GameMsg> msgPackQue = new Queue<GameMsg>();//将收到的消息存到队列中public void AddMsgQue(GameMsg msg){   lock(obj){msgPackQue.Enqueue(msg);}      }//取出消息队列中的消息进行处理public void Update(){if (msgPackQue.Count > 0){PECommon.Log("PackCount:" + msgPackQue.Count);lock(obj){GameMsg msg = msgPackQue.Dequeue();}}}private void HandOutMsg(GameMsg msg){switch ((CMD)msg.cmd){case CMD.ReqLogin:LoginSys.Instance.ReqLogin(msg);break;}}

附加会话信息到消息包

为了方便回复消息,把之前的消息队列打包成<ServerSession,GameMsg>这两个。

//建立一个消息包,包含msg和session
public class MsgPack
{public ServerSession session;public GameMsg msg;public MsgPack(ServerSession session,GameMsg msg){this.session = session;this.msg = msg;}
}

在LoginSys里面,分析登录验证逻辑和回应客户端

    //对登消息进行回复public void ReqLogin(MsgPack pack){//当前账号是否已经上线//已上线:返回错误信息//未上线://账号是否存在//存在,检测密码//不存在,创建默认的账号密码//回应客户端GameMsg msg = new GameMsg{cmd = (int)CMD.ReqLogin,rspLogin = new RspLogin { }};pack.session.SendMsg(msg);}

驱动逻辑处理

将NetSvc中的消息处理方法添加到服务器入口ServerStart中的主函数中去,让它一直执行

public class ServerStart
{static void Main(string[] args){ServerRoot.Instance.Init();while (true){ServerRoot.Instance.Update();}}
}

增加数据缓存层

新建一个缓存层,里面主要负责服务器端数据检查等功能

using System.Collections.Generic;
/// <summary>
/// 缓存层
/// </summary>
class CacheSvc
{private static CacheSvc instance = null;public static CacheSvc Instance{get{if (instance == null)instance = new CacheSvc();return instance;}}private Dictionary<string, ServerSession> onLineAcctDic = new Dictionary<string, ServerSession>();public void Init(){PECommon.Log("CacheSvc Init Done.");}//判断账号是否上线public bool IsAcctOnline(string acct){return onLineAcctDic.ContainsKey(acct);}
}

在LoginSys中继续完成登录验证的方法

    private CacheSvc cacheSvc = null;public void Init(){cacheSvc = CacheSvc.Instance;PECommon.Log("LoginSys Init Done.");}//对登消息进行回复public void ReqLogin(MsgPack pack){ReqLogin data = pack.msg.reqLogin;GameMsg msg = new GameMsg{cmd = (int)CMD.RspLogin,rspLogin = new RspLogin { }};//当前账号是否已经上线if(cacheSvc.IsAcctOnline(data.acct)){//已上线:返回错误信息msg.err = (int)ErrorCode.AcctIsOnline;}else//未上线:{//账号是否存在//存在,检测密码//不存在,创建默认的账号密码}//回应客户端pack.session.SendMsg(msg);}

在GameMsg中,设置好错误代码和登录信息

    [Serializable]public class RspLogin{public PlayerData playerData;}[Serializable]public class PlayerData{public int id;public string name;public int lv;public int exp;public int power;public int coin;public int diamond;}public enum ErrorCode{None=0,//没有错误AcctIsOnline,//账号已经上线}

缓存上线玩家数据

缓冲层CacheSvc中添加两个方法

    //根据账号密码返回对应账号数据,密码错误返回null,账号不存在则默认创建新账号public PlayerData GetPlayerData(string acct,string pass){//TODO:从数据库中查找账号数据return null;}/// <summary>/// 账号上线,缓存数据/// </summary>public void AcctOnline(string acct,ServerSession serverSession,PlayerData playerData){onLineAcctDic.Add(acct, serverSession);onLineSessionDic.Add(serverSession, playerData);}

完善LoginSys中的登录方法

    //对登消息进行回复public void ReqLogin(MsgPack pack){ReqLogin data = pack.msg.reqLogin;GameMsg msg = new GameMsg{cmd = (int)CMD.RspLogin};//当前账号是否已经上线if(cacheSvc.IsAcctOnline(data.acct)){//已上线:返回错误信息msg.err = (int)ErrorCode.AcctIsOnline;}else//未上线:{//账号是否存在PlayerData playerData = cacheSvc.GetPlayerData(data.acct, data.pass);if (playerData == null){//存在,密码错误msg.err = (int)ErrorCode.WrongPass;}else{//不存在,创建默认的账号密码msg.rspLogin = new RspLogin{playerData = playerData};//将创建的账号密码缓存cacheSvc.AcctOnline(data.acct, pack.session, playerData);}}//回应客户端pack.session.SendMsg(msg);}

客户端消息分发

同服务器端一样,在客户端中处理消息,首先是在NetSvc中

    public static readonly string obj = "lock";private Queue<GameMsg> msgQue = new Queue<GameMsg>();   public void AddNetPkg(GameMsg msg){lock(obj){msgQue.Enqueue(msg);}}private void Update(){if(msgQue.Count>0){lock (obj){GameMsg msg = msgQue.Dequeue();ProcessMsg(msg);}}}private void ProcessMsg(GameMsg msg){if (msg.err != (int)ErrorCode.None){switch ((ErrorCode)msg.err){case ErrorCode.AcctIsOnline:GameRoot.AddTips("当前账号已经上线");break;case ErrorCode.WrongPass:GameRoot.AddTips("密码错误");break;}return;}switch ((CMD)msg.cmd){case CMD.RspLogin:LoginSys.Instance.RspLogin(msg);break;}}

修改LoginSys中登录成功的方法

    public void RspLogin(GameMsg msg){GameRoot.AddTips("登录成功");if(msg.rspLogin.playerData.name == ""){//打开角色创建页面createWnd.SetWndState();}else{//进入主城}//关闭登录页面loginWnd.SetWndState(false);}

第4章:数据库与服务器缓存层

数据库环境配置

使用MySQL5.6和Navicat for MySQL进行数据库开发和设计。

数据库增删改查

使用C#创建一个控制台应用SqlTest,并且添加引用MySql.Data.dll

使用下面两条语句进行连接数据库

static MySqlConnection conn = null;conn = new MySqlConnection("server=localhost;port=3306;database=studymysql;user=root;password=;charset = utf8;");
conn.Open();

增加数据的方法

    static void Add(){MySqlCommand cmd = new MySqlCommand("insert into userinfo set name ='Mai',age='25'", conn);cmd.ExecuteNonQuery();int id = (int)cmd.LastInsertedId;//获取主键的id号Console.WriteLine("Sql Insert Key:{0}",id);}

查找数据的方法

    static void Query(){//MySqlCommand cmd = new MySqlCommand("select * from userinfo", conn);MySqlCommand cmd = new MySqlCommand("select * from userinfo where name = 'sou'", conn);MySqlDataReader reader = cmd.ExecuteReader();while(reader.Read()){int id = reader.GetInt32("id");string name = reader.GetString("name");int age = reader.GetInt32("age");Console.WriteLine(string.Format("sql result:id:{0} name:{1} age:{2}", id, name, age));}}

更新和删除数据的方法

    static void Del(){MySqlCommand cmd = new MySqlCommand("delete from userinfo where id = 1", conn);cmd.ExecuteNonQuery();Console.WriteLine("delete done.");}static void Update(){//MySqlCommand cmd = new MySqlCommand("update userinfo set name = 'Akimoto',age=25 where id = 3", conn);MySqlCommand cmd = new MySqlCommand("update userinfo set name = @name,age=@age where id = @id", conn);cmd.Parameters.AddWithValue("name", "Hanabi");cmd.Parameters.AddWithValue("age", "123");cmd.Parameters.AddWithValue("id", "3");cmd.ExecuteNonQuery();Console.WriteLine("update done.");}

增加数据库管理器

首先在数据库中创建一张表account,该表有下列属性

然后在Server端创建一个DBMgr.cs类,用来进行数据库管理。首先要初始化该管理类,让它链接数据库。添加新的方法,用来验证账号密码是否正确。

/// <summary>
/// 数据库管理类
/// </summary>using MySql;
using MySql.Data.MySqlClient;
using PEProtocol;public class DBMgr
{private static DBMgr instance = null;public static DBMgr Instance{get{if (instance == null)instance = new DBMgr();return instance;}}private MySqlConnection conn = null;public void Init(){conn = new MySqlConnection("server=localhost;port=3306;database=darkgod;user=root;password=;charset = utf8;");PECommon.Log("DBMgr Init Done.");}public PlayerData QueryPlayerData(string acct,string pass){PlayerData playerData = null;//TODOreturn playerData;}
}

在CacheSvc中进行调用验证账号密码的方法。

    //根据账号密码返回对应账号数据,密码错误返回null,账号不存在则默认创建新账号public PlayerData GetPlayerData(string acct,string pass){//从数据库中查找账号数据return dBMgr.QueryPlayerData(acct,pass);}

查询玩家数据

完善查询玩家的方法,如果没有该账号密码,系统会自动创建新的账号密码

    public PlayerData QueryPlayerData(string acct,string pass){PlayerData playerData = null;MySqlDataReader reader = null;bool isNew = true;//默认是一个新的账号try{MySqlCommand cmd = new MySqlCommand("select * from account whre acct = @acct", conn);cmd.Parameters.AddWithValue("acct", acct);reader = cmd.ExecuteReader();if (reader.Read()){isNew = false;string _pass = reader.GetString("pass");if (_pass.Equals(pass)){//密码正确,返回玩家数据playerData = new PlayerData{id = reader.GetInt32("id"),name = reader.GetString("name"),lv = reader.GetInt32("level"),exp = reader.GetInt32("exp"),power = reader.GetInt32("power"),coin = reader.GetInt32("coin"),diamond = reader.GetInt32("diamond")};}}}catch(Exception e){PECommon.Log("Query PlayerData By Acct&Pass Error:" + e, LogType.Error);}finally{if(isNew){//不存在账号数据,创建新的默认账号数据,并返回playerData = new PlayerData{id = -1,name = "",lv = 1,exp=0,power=150,coin = 5000,diamond = 500};playerData.id = InsertNewAccData(acct,pass, playerData);}}return playerData;}//插入数据到数据库,并返回idprivate int InsertNewAccData(string acct,string pass,PlayerData pd){return 0;}

插入默认玩家数据

完善插入数据到数据库并返回id的方法

    //插入数据到数据库,并返回idprivate int InsertNewAccData(string acct,string pass,PlayerData pd){int id = -1;try{MySqlCommand cmd = new MySqlCommand("insert into account set acct=@acct,pass =@pass,name=@name,level=@level,exp=@exp,power=@power,coin=@coin,diamond=@diamond", conn);cmd.Parameters.AddWithValue("acct", acct);cmd.Parameters.AddWithValue("pass", pass);cmd.Parameters.AddWithValue("name", pd.name);cmd.Parameters.AddWithValue("level", pd.lv);cmd.Parameters.AddWithValue("exp", pd.exp);cmd.Parameters.AddWithValue("power", pd.power);cmd.Parameters.AddWithValue("coin", pd.coin);cmd.Parameters.AddWithValue("diamond", pd.diamond);cmd.ExecuteNonQuery();id = (int)cmd.LastInsertedId;}catch (Exception e){PECommon.Log("Insert PlayerData Error:" + e, LogType.Error);}return id;}

使用一条语句进行测试

QueryPlayerData("123", "123");

重命名功能

首先在GameMsg中添加重命名相关的请求和属性

    [Serializable]public class GameMsg:PEMsg{public ReqLogin reqLogin;public RspLogin rspLogin;public ReqRename reqRename;public ReqRename rspRename;}public class ReqRename{public string name;}public class RspRename{public string name;}public enum ErrorCode{None=0,//没有错误AcctIsOnline,//账号已经上线WrongPass,//密码错误NameIsExist,//名字已经存在}public enum CMD{None=0,//登录相关 100ReqLogin = 101,RspLogin = 102,ReqRename=103,RspRename=104,}

然后在客户端将名字发送给服务器端

        if (iptName.text != ""){//发送名字数据到服务器,登录主程GameMsg msg = new GameMsg{cmd = (int)CMD.ReqRename,reqRename = new ReqRename{name = iptName.text}};netSvc.SendMsg(msg);}

服务器端首先接收到消息,并识别是什么类型

            case CMD.ReqRename:LoginSys.Instance.ReqName(pack);break;

然后在调用LoginSys中的办法进行处理

    //对姓名进行处理public void ReqName(MsgPack pack){}

缓存更新逻辑

完善对姓名进行处理的办法

    //对姓名进行处理public void ReqName(MsgPack pack){ReqRename data = pack.msg.reqRename;GameMsg msg = new GameMsg{cmd = (int)CMD.RspRename};//判断名字是否已经存在           if(cacheSvc.IsNameExist(data.name)){//存在:返回错误码msg.err = (int)ErrorCode.NameIsExist;}else{//不存在:更新缓存,以及数据库,再返回给客户端PlayerData playerData = cacheSvc.GetPlayerDataBySession(pack.session);playerData.name = data.name;if(!cacheSvc.UpdatePlayerData(playerData.id,playerData)){msg.err = (int)ErrorCode.UpdateDBError;}else{msg.rspRename = new RspRename{name = data.name};}}pack.session.SendMsg(msg);}

其中IsNameExist(string name)的方法如下

    //判断名字是否在数据库中存在public bool IsNameExist(string name){return dBMgr.QueryNameData(name);}
    public bool QueryNameData(string name){bool exist = false;MySqlDataReader reader = null;try{MySqlCommand cmd = new MySqlCommand("select * from account where name = @name", conn);cmd.Parameters.AddWithValue("name", name);reader = cmd.ExecuteReader();if (reader.Read())exist = true;}catch(Exception e){PECommon.Log("Query Name State Error:" + e, LogType.Error);}finally{if (reader != null)reader.Close();}return exist;}

还需要对数据库的名字进行更新,函数是UpdatePlayerData(int id, PlayerData playerData)

    public bool UpdatePlayerData(int id,PlayerData playerData){     return dBMgr.UpdatePlayerData(id,playerData);}
    //更新数据库public bool UpdatePlayerData(int id,PlayerData playerData){try{MySqlCommand cmd = new MySqlCommand("update account set name=@name,level=@level,exp=@exp,power=@power,coin=@coin,diamond=@diamond where id =@id", conn);cmd.Parameters.AddWithValue("id", id);cmd.Parameters.AddWithValue("name", playerData.name);cmd.Parameters.AddWithValue("level", playerData.lv);cmd.Parameters.AddWithValue("exp", playerData.exp);cmd.Parameters.AddWithValue("power", playerData.power);cmd.Parameters.AddWithValue("coin", playerData.coin);cmd.Parameters.AddWithValue("diamond", playerData.diamond);//TOADD Otherscmd.ExecuteNonQuery();}catch (Exception e){PECommon.Log("Update PlayerData Error:" + e, LogType.Error);return false;}return true;}

然后在客户端进行操作,首先要接收来自服务器端的消息

            case CMD.RspRename:LoginSys.Instance.RspRename(msg);break;

GameRoot里面添加一个设置名字的方法

    public void SetPlayerName(string name){playerData.name = name;}

LoginSys.cs中添加RspRename()处理更换名字消息的方法

public void RspRename(GameMsg msg){GameRoot.Instance.SetPlayerName(msg.rspRename.name);//TODO//跳转场景进入主城//打开主城的界面//关闭创建页面createWnd.SetWndState(false);}

运行服务器端和客户端

数据库中的显示如下

数据表备份与错误码处理

                case ErrorCode.UpdateDBError:PECommon.Log("数据库更新异常", LogType.Error);//不能将数据库异常显示给用户GameRoot.AddTips("网络不稳定");break;

从0开始的网游ARPG实战案例:暗黑战神(第一章至第四章:设计登陆和创建角色功能实现)相关推荐

  1. Unity3D暗黑战神 网游ARPG实战案例(第二季)

    前面开发了几个单机小游戏,该是时候挑战一下网络游戏方面的开发了! 3D网游ARPG实战案例(第二季),使用Unity2017.3版本制作 内容包括 服务端部分 1.网络通信编码,协议及传输 2.数据驱 ...

  2. UG12.0四五多轴编程加工实战案例视频教程

    UG12.0四五多轴编程加工实战案例视频教程 链接:https://pan.baidu.com/s/1kCJrFKbbS_rQy0nytT40LQ 提取码:x5xa

  3. jQuery实战读书笔记(第一章至第四章)

    2019独角兽企业重金招聘Python工程师标准>>> 第一章 jQuery 基础 1. 包装器 jQuery对包装器(Wrapper)或包装集(wrapped set)进行操作,即 ...

  4. 软件测试实战(微软技术专家经验总结)--第四章(测试建模)读书笔记

    测试建模,以测试为目的建立产品模型.实际上,所有的测试都基于模型. 4.1从组合测试看建模的重要性 4.1.1组合测试简介 组合测试是一种测试用例生成方法.测试人员将被测对象抽象为一个受到多个变量影响 ...

  5. 手机网游制造之请求处理篇(已发《电脑报》)

    目标手机软件 手机网游制造之 请求处理篇 开发程序:疯狂的炸弹 开发进度:第四期 本期要点:设计服务器端请求处理功能 开发平台:Java平台 经过前面3期的讲解,我们的手机网游<疯狂的炸弹> ...

  6. 2D网游短期爆发难掩颓势

    新浪游戏讯 10月26日消息,今日共五款网游进行测试,分别是润趣科技的2D武侠网游<武林秘籍>.禹硕游戏的2DQ版网游<山海奇缘>.九城的2DQ版网游<仙境冒险>以 ...

  7. 传暴雪新网游源自暗黑

    原文出处:驱动之家 原文链接:http://news.mydrivers.com/1/96/96623.htm 久经考验的Surfer Girl又有新语录发表了: 你对EA印象如何? 答:业界领导者, ...

  8. Py自动化办公—Word文档替换、Excel表格读取、Pdf文件生成和Email自动邮件发送实战案例...

    点击上方"Python爬虫与数据挖掘",进行关注 回复"书籍"即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 平阳歌舞新承宠,帘外春寒赐锦袍. ...

  9. 高可用架构设计之道,实战案例直面流量洪峰

    流量洪峰所带来的一系列挑战,足以激发每位程序员的斗志:高并发.大吞吐.紧急扩容.降级保护--那么,作为程序员,应该如何应对? 由腾讯云官方社区-云加社区举办的线上直播活动,主题聚焦在「高可用架构之流量 ...

最新文章

  1. 在Ubuntu 12.04 64bit上搭建Crtmpserver视频直播服务
  2. egg 自学入门demo分享
  3. HashMap解决hash冲突的方法
  4. android 输入光标修改颜色_2.2 输入数值与文本
  5. Toping Kagglers:Bestfitting,目前世界排名第一
  6. PhpWord的autoload.php文件及目录的生成方式
  7. 如何评判在线直播源码优劣?视频直播软件开发经验之谈
  8. 抽取类的#技巧#成员变量最可能
  9. 二、噪音大小对使用的影响
  10. 阅读作业二-----读Lost in CatB有感 by 李栋
  11. Opencv2.X以上Mat类型与IplImage*的转换
  12. tp3.2 模型page和limit方法区别
  13. python带我起飞 百度云_CentOS/Debian安装人人影视客户端,下载资源并自动上传到OneDrive网盘...
  14. linux刷机软件,MTK平台刷机工具——SP_Flash_Tool
  15. Self-Supervised Gait Encoding with Locality-Aware Attention for Person Re-Identification阅读
  16. 用户计算机名更改为英文,win10将用户名改为英文怎么改_win10如何更改用户名为英文图文教程-系统城...
  17. 良树机器人_fate系列在国内是否有过气的趋势?
  18. Linux 使用 sed 整行(列)刪除
  19. 微信公众号测试账号总结
  20. typora 分割线_最全Typora语法大全(含详细数学表达式及流程图)

热门文章

  1. 初学SLAM二之BA当中的数学知识点
  2. c++读取tsv类型文件
  3. 右键txt打开html,文件解压不了怎么办 右键菜单中选择解压文件
  4. Android增强现实(三)-3D模型展示器
  5. MSDC 4.3 接口规范(24)
  6. JQuery学习之路Part8:家族树操作(查找祖先、后代、兄弟同胞、绝对查找)【完结】
  7. andoird 设置锁屏上不显示通知
  8. 解决小熊无叶电风扇摇头嘎嘎响的问题
  9. 百度地图 公交线路查询
  10. 【小y设计】二维码条形码打印编辑器