第1章: 初始场景与UI界面制作
登录场景制作
制作登录界面UI
UI自适应原理
制作角色创建界面
制作Tips动态提示界面
制作Loading进度界面
第2章: UI逻辑框架与配置文件
客户端开发环境配置
UI逻辑框架介绍
游戏启动逻辑
异步场景加载
更新场景加载进度
登录注册界面逻辑
UI窗口基类
音效播放服务
业务系统层基类
Tips弹窗显示
设置Tips显示队列
角色创建界面逻辑
生成随机名字配置文件
解析随机名字配置文件
完成随机名字生成
第3章: 网络通信与服务器环境配置
服务器开发环境配置
介绍PESocket开源网络库
服务器使用PESocket
Unity使用PESocket
设置日志接口
服务器框架介绍
服务器启动逻辑
服务器网络服务
客户端网络服务
封装通用工具
登录协议定制
登录消息分发处理
附加会话信息到消息包
驱动逻辑处理
增加数据缓存层
缓存上线玩家数据
客户端消息分发
第4章:数据库与服务器缓存层
数据库环境配置
数据库增删改查
增加数据库管理器
查询玩家数据
插入默认玩家数据
重命名功能
缓存更新逻辑
数据表备份与错误码处理
首先设置Canvas根据窗口大小匹配匹配,然后以高度为匹配对象
给UI上的各个物体添加锚点,进行自适应
使用VS2017加VA助手进行代码开发。
根据设计的UI逻辑框架,首先创建三个脚本GameRoot,ResSvc和LoginSys分别负责游戏启动入口,资源加载服务和游戏登录服务。
public class GameRoot : MonoBehaviour
{
private void Start()
{
Debug.Log("Game Start...");
Init();
}
private void Init()
{
//服务模块初始化
ResSvc res = GetComponent();
res.InitSvc();
//业务系统初始化
LoginSys login = GetComponent();
login.InitSys();
//进入登录场景并加载相应UI
login.EnterLogin();
}
}
public class ResSvc : MonoBehaviour
{
public void InitSvc()
{
Debug.Log("Init ResSvg...");
}
}
public class LoginSys : MonoBehaviour
{
public void InitSys()
{
Debug.Log("Init LoginSys...");
}
///
/// 进入登录场景
///
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().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().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方法中进行调用
///
/// 进入登录场景
///
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中的进入登录界面的和打开登录界面的方法
///
/// 进入登录场景
///
public void EnterLogin()
{
//异步加载登录场景
//并显示加载的进度条
//加载完成以后并显示注册登录界面
ResSvc.Instance.AsyncLoadScene(Constants.SceneLogin,OpenLoginWnd);
}
///
/// 打开登录界面
///
public void OpenLoginWnd()
{
loginWnd.gameObject.SetActive(true);
loginWnd.InitWnd();
}
建立一个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();
else
ClearWnd();
}
protected virtual void InitWnd()
{
resSvc = ResSvc.Instance;
}
protected virtual void ClearWnd()
{
resSvc = null;
}
#region Tool Functions
protected 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(), num);
}
protected void SetText(Transform trans, string context = "")
{
SetText(trans.GetComponent(), 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 adDic = new Dictionary();
public AudioClip LoadAudio(string path,bool cache = false)
{
AudioClip au = null;
if(!adDic.TryGetValue(path,out au))
{
au = Resources.Load(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();
}
private void Start()
{
if (ani != null)
{
InvokeRepeating("PlayDragonAni", 0, 20);
}
}
private void PlayDragonAni()
{
if (ani != null)
ani.Play();
}
}
给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的显示,修改之前的DynamicWnd里面的方法
public class DynamicWnd : WindowRoot
{
public Animation tipsAni;
public Text txtTips;
//定义一个队列来进行tips的队列显示
private Queue tipsQue = new Queue();
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
给CreateWnd创建一个同名脚本,用来角色创建。
在LoginSys中添加登录成功的方法
public void RspLogin()
{
GameRoot.AddTips("登录成功");
//打开角色创建页面
createWnd.SetWndState();
//关闭登录页面
loginWnd.SetWndState(false);
}
在LoginWnd中更新进入游戏的方法
///
/// 点击进入游戏
///
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 Remove
LoginSys.Instance.RspLogin();
}
else
{
GameRoot.AddTips("账号或密码为空");
}
}
public void ClickNoticeBtn()
{
audioSvc.PlayUIMusic(Constants.UIClickBtn);
GameRoot.AddTips("功能正在开发中...");
}
首先需要一个xml的模板,如下所示
-
-
然后在excel里面讲xml导入进去,然后自己添加上合适的信息
然后进行导出
打开导出的文件,文件中的内容为
-
a
b
c
-
d
e
f
首先建立一个PathDefine的脚本,用来存储xml文件的地址
public class PathDefine
{
#region Configs
public const string RDNameCfg = "ResCfgs/rdname";
#endregion
}
然后在ResSvc中写入一个新的方法来解析xml文件
private List surnameLst = new List();
private List manLst = new List();
private List womanLst = new List();
//解析随机名字的xml文件
private void InitRDNameCfg()
{
TextAsset xml = Resources.Load(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
首先创建一个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)];
}
else
rdName += 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
//发送名字数据到服务器,登录主程
}
else
GameRoot.AddTips("当前名字不符合规范");
}
VS2017中需要安装C#平台的支持,我们在windos上使用服务器。在这个项目中,我们使用PESocket开源网络库来实现网络通信。
下载PESocket:
GitHub地址:https://github.com/PlaneZhong/PESocket
介绍网络库(防止学校访问外网波动)
博客园地址:
https://www.cnblogs.com/planezhong/p/10074676.html
需要创建和引用的文件如下
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
{
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 server = new PESocket();
server.StartAsServer(IPCfg.srvIP, IPCfg.srvPort);
while(true)
{
}
}
}
}
运行后结果如下:
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
{
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 client = null;
private void Start()
{
client = new PENet.PESocket();
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
///
/// 服务器初始化模板
///
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
///
/// 服务器入口
///
public class ServerStart
{
static void Main(string[] args)
{
ServerRoot.Instance.Init();
while (true)
{
}
}
}
与之前的案例相似,需要创建ServerSession和GameMsg文件。
下面修改NetSvc和LoginSys中的部分方法。
///
/// 网络服务
///
///
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 server = new PESocket();
server.StartAsServer(SrvCfg.srvIP, SrvCfg.srvPort);
PETool.LogMsg("NetSvc Init Done.");
}
}
using PENet;
///
/// 登录业务系统
///
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 client = null;
public void InitSvc()
{
Instance = this;
client = new PESocket();
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,
//登录相关 100
ReqLogin = 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 msgPackQue = new Queue();
//将收到的消息存到队列中
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;
}
}
为了方便回复消息,把之前的消息队列打包成
//建立一个消息包,包含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;
///
/// 缓存层
///
class CacheSvc
{
private static CacheSvc instance = null;
public static CacheSvc Instance
{
get
{
if (instance == null)
instance = new CacheSvc();
return instance;
}
}
private Dictionary onLineAcctDic = new Dictionary();
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;
}
///
/// 账号上线,缓存数据
///
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 msgQue = new Queue();
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);
}
使用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类,用来进行数据库管理。首先要初始化该管理类,让它链接数据库。添加新的方法,用来验证账号密码是否正确。
///
/// 数据库管理类
///
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;
//TODO
return 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;
}
//插入数据到数据库,并返回id
private int InsertNewAccData(string acct,string pass,PlayerData pd)
{
return 0;
}
完善插入数据到数据库并返回id的方法
//插入数据到数据库,并返回id
private 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,
//登录相关 100
ReqLogin = 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 Others
cmd.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;