PhotonServer伺服器利用NHibernate操作資料庫與客戶端互動(登入、註冊、多人位置同步)
1. 伺服器端
1.1 伺服器端工程準備
此次專案內容是對上兩次的整和,所以前兩篇博文是單個功能的基礎,分別是①NHibernate和MySQL互動,②PhotonServer的使用。這次專案也是在這兩個基礎之上進行的,很多直接拷貝過來進行修改;資料庫還是用mygamedb,所以對PhotonServer目錄下deploy\bin_Win64中PhotonControl.exe.config的配置檔案不會更改,只用到了其中的Users表,Users表包含userID和username、password。
NHibernate和MySQL互動工程如下:
其中,
①hibernate.cfg.xml為NHibernate連線資料庫的配置檔案,名稱不可更改,且屬性為“始終複製到輸出路徑”;
②User.hbm.xml為資料庫中Users表與定義的User類對應的配置檔案,屬性為“不復制,嵌入的資源”;
③User為定義的與Users表對應的欄位;NHibernateHelper是對NHibernate建立會話的一個封裝; ④IUserManager定義了操作Users表的方法(例如:對Users表新增、刪除、修改、通過UserID查詢、通過username查詢、賬號匹配等方法);
⑤而UserManager就是繼承自IUserManager用NHibernate的會話實現這些方法。
將所提到的類及配置檔案(如有目錄就包擴目錄)全部複製到MyGameServer工程下,如圖,本來的屬性設定不變,會將程式集名和名稱空間設定為MyGameServer的;!!!!!!!!重要:要將NHibernate和MySQL以及Log4Net所需引用進來的,Common是Common建後引用的。
下面介紹詳細複製來檔案的更改:
1.1.1 IUserManager.cs
將名稱空間修改為MyGameServer,應用所在的User類也改為using MyGameServer.Model。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using MyGameServer.Model; namespace MyGameServer.Manager { interface IUserManager { void Add(User user); void Update(User user); void Remove(User user); User GetById(int id); User GetByUsername(string username); ICollection<User> GetAllUsers(); bool VerifyUser(string username, string password);//驗證使用者名稱和密碼 } }
1.1.2 UserManager.cs
本來的引用名稱空間變為MyGameServer.Model。
using System;
using System.Collections.Generic;
using MyGameServer.Manager;
using NHibernate;
using NHibernate.Criterion;
using MyGameServer.Model;
namespace MyGameServer.Manager
{
class UserManager :IUserManager
{
public void Add(Model.User user)
{
/*第一種
ISession session = NHibernateHelper.OpenSession();
session.Save(user);
session.Close();
*/
using (ISession session = NHibernateHelper.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
session.Save(user);
transaction.Commit();
}
}
}
/// <summary>
/// 得到表中所有內容
/// </summary>
/// <returns></returns>
public ICollection<User> GetAllUsers()
{
using (ISession session = NHibernateHelper.OpenSession())
{
IList<User> users = session.CreateCriteria(typeof(User)).List<User>();
return users;
}
}
public User GetById(int id)
{
using (ISession session = NHibernateHelper.OpenSession())
{
//事務(事務中的一系列事件,只要有一個不成功,之前成功的也會回滾,即插入成功的又被刪除,修改成功的又恢復.....)
// transaction = session.BeginTransaction();//開啟事務
using (ITransaction transaction = session.BeginTransaction())
{
User user= session.Get<User>(id);
transaction.Commit();
return user;
}
}
}
public User GetByUsername(string username)
{
using (ISession session = NHibernateHelper.OpenSession())
{
/*
ICriteria criteria= session.CreateCriteria(typeof(User));
criteria.Add(Restrictions.Eq("Username", username));//新增一個查詢條件,第一個引數表示對哪個屬性(欄位)做限制,第二個表示值為多少
User user = criteria.UniqueResult<User>();
*/
User user = session.CreateCriteria(typeof(User)).Add(Restrictions.Eq("Username", username)).UniqueResult<User>();
return user;
}
}
/// <summary>
/// NHibernate刪除時根據主鍵更新,所以傳來的物件user中得有主鍵
/// </summary>
/// <param name="user"></param>
public void Remove(User user)
{
using (ISession session = NHibernateHelper.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
session.Delete(user);
transaction.Commit();
}
}
}
/// <summary>
/// NHibernate更新時根據主鍵更新,所以傳來的物件user中得有主鍵
/// </summary>
/// <param name="user"></param>
public void Update(User user)
{
using (ISession session = NHibernateHelper.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
session.Update(user);
transaction.Commit();
}
}
}
public bool VerifyUser(string username, string password)
{
using (ISession session = NHibernateHelper.OpenSession())
{
User user = session
.CreateCriteria(typeof(User))
.Add(Restrictions.Eq("Username", username))
.Add(Restrictions.Eq("Password", password))
.UniqueResult<User>();
if (user == null) return false;
return true;
}
}
}
}
1.1.3 User.hmb.xml
只需將程式集名和User類所在位置更改。
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
assembly="MyGameServer"
namespace="MyGameServer.Model"> <!--assembly表示程式集,namespace所在資料夾Model-->
<class name="User" table="users"><!--name為類名,table為表名-->
<id name="ID" column="id" type="Int32"><!--id name對應為類中變數名,type為nhibernate的型別-->
<generator class="native"></generator>
</id><!--主鍵配置完成-->
<property name="Username" column="username" type="String"></property>
<property name="Password" column="password" type="String"></property>
<property name="Registerdate" column="registerdate" type="Date"></property>
</class>
</hibernate-mapping>
1.1.4 User.cs
更改名稱空間為MyGameServer。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MyGameServer.Model
{
public class User
{
public virtual int ID { get; set; }
public virtual string Username { get; set; }
public virtual string Password { get; set; }
public virtual DateTime Registerdate { get; set; }
}
}
1.1.5 hibernate.cfg.xml
連線資料庫的配置檔案不需要更改。
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
<session-factory>
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
<property name="dialect">NHibernate.Dialect.MySQL5Dialect</property><!--版本-->
<property name="connection.driver_class">NHibernate.Driver.MySqlDataDriver</property><!--使用什麼資料庫-->
<property name="connection.connection_string">Server=localhost;Database=mygamedb;User ID=root;Password=root;</property>
<property name="show_sql">true</property>
</session-factory>
</hibernate-configuration>
1.1.6 NHibernateHelper.cs
也只需要更改名稱空間。
using NHibernate;
using NHibernate.Cfg;
namespace MyGameServer
{
class NHibernateHelper
{
private static ISessionFactory _sessionFactory;
private static ISessionFactory SessionFactory
{
get
{
if(_sessionFactory==null)
{
var configuration = new Configuration();
configuration.Configure();//解析hibernate.cfg.xml
configuration.AddAssembly("MyGameServer");//解析對映檔案User.hbm.xml
_sessionFactory = configuration.BuildSessionFactory();
}
return _sessionFactory;
}
}
public static ISession OpenSession()
{
return SessionFactory.OpenSession();//開啟一個跟資料庫的會話
}
}
}
1.2 伺服器端和客戶端公共所需
伺服器端和客戶端都所需要的內容定義在此工程中,例如將客戶端傳送來的請求型別用列舉型別進行區分,將伺服器傳送的事件型別用列舉型別區分,將伺服器傳送給客戶端的引數進行列舉型別區分,將返回給客戶端的單個值用列舉型別區分等等。下面詳細介紹。
1.2.1 工程建立
因為要在客戶端和伺服器都需要用,所以將工程建立為類庫,同伺服器端工程一樣,如下圖,最後生成dll檔案在伺服器端和客戶端(Unity)進行使用,因為unity工程所使用的dll檔案.Net版本不能超過3.5,所以將Common工程屬性的.NET版本設定為3.5。
最終所有內容如下:
1.2.2 Common工程內容
①Tools/DictTool.cs
工具類,DictTools是為了通過傳入的任何型別的字典和任何型別的key都可以獲取各種型別value的一個工具類。
using System;
using System.Collections.Generic;
namespace Common.Tools
{
public class DictTool
{
public static T2 GetValue<T1,T2>(Dictionary<T1,T2> dict,T1 key)
{
T2 value;
bool isSuccess = dict.TryGetValue(key, out value);
if(isSuccess)
{
return value;
}
else
{
return default(T2);
}
}
}
}
② EventCode.cs
定義的伺服器傳送給客戶端事件的列舉型別。
namespace Common
{
public enum EventCode:byte//區分伺服器向客戶端傳送事件型別
{
NewPlayer,//建立其他客戶端player的事件
SyncPosition//同步位置的事件
}
}
③ OperationCode.cs
定義的客戶端發來的請求的列舉型別。
namespace Common
{
public enum OperationCode:byte//區分客戶端發來請求的型別
{
Login,//客戶端發來登入的請求
Register,//客戶端發來註冊的請求
Default,//預設請求
SyncPosition,//客戶端發來自身位置的請求
SyncPlayer//同步其他玩家資訊的請求
}
}
④ ParameterCode.cs
定義了伺服器傳送給客戶端引數的列舉型別。
namespace Common
{
public enum ParameterCode:byte//區分傳送資料的時候,引數的型別
{
Username,
Password,
Position,
X,Y,Z,//玩家的座標xyz
UsernameList,//所有的線上客戶端使用者名稱
PlayerDataList//所有的線上客戶端使用者名稱以及座標位置
}
}
⑤ ReturnCode.cs
定義了伺服器傳送給客戶端返回的引數(單個確認是否成功)列舉型別。
namespace Common
{
public enum ReturnCode:short
{
Success,
Failed
}
}
⑥ PlayerData.cs
定義了伺服器傳送給所有客戶端的其他客戶端的名字和位置資訊,注意:非列舉。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
/// <summary>
/// 伺服器傳送給所有線上的客戶端其他客戶端的名字和位置資訊
/// 封裝到此類中,後續再序列化傳送出去
/// </summary>
namespace Common
{
[Serializable]
public class PlayerData
{
public Vector3Data Pos { get; set; }
public string Username { get; set; }
}
}
⑦ Vector3Data.cs
伺服器端沒有Vector3類,自定義的此類。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Common
{
/// <summary>
/// 客戶端將自己的位置資訊傳送給伺服器端,伺服器端沒有Vector3類,所以自己建立一個
/// </summary>
[Serializable]
public class Vector3Data
{
public float x { get; set; }
public float y { get; set; }
public float z { get; set; }
}
}
1.3 伺服器端設計
伺服器端需要用到Common工程中內容,所以需要引入Common,方法:右鍵引用,選擇新增引用後選擇專案,勾選Common。然後每次客戶端生成後在同一個bin目錄會有Common的dll檔案。
1.3.1 客戶端發來的請求處理(Handler資料夾下)
1.3.1.1 請求處理的基類(BaseHandler.cs)
定義一個基類,之後新增的請求處理都繼承此類;包含了客戶端發來的請求列舉型別,和響應請求方法OnOperationRequest,引數中operationRequest為發來的請求,sendParameters為響應回去的引數,peer用於區分哪個客戶端。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Common;
using Photon.SocketServer;
namespace MyGameServer.Handler
{
public abstract class BaseHandler
{
public OperationCode OpCode;
//引數中operationRequest為發來的請求,sendParameters為響應回去的引數,peer用於區分哪個客戶端
public abstract void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters,ClientPeer peer);
}
}
1.3.1.2 預設請求處理(DefaultHandler.cs)
當傳來的請求伺服器端沒有對應的處理時,呼叫此預設請求進行處理,但此時只設置了請求的型別。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Photon.SocketServer;
namespace MyGameServer.Handler
{
class DefaultHandler : BaseHandler
{
public DefaultHandler()
{
OpCode = Common.OperationCode.Default;
}
public override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters, ClientPeer peer)
{
}
}
}
1.3.1.3 登入請求的響應(LoginHandler.cs)
直接在構造方法中將請求的型別設定為Login。再在OnOperationRequest中響應客戶端的請求,因為只是迴應是否登入成功,所以用響應中的ReturnCode進行返回,ReturnCode為short型別。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Common;
using Photon.SocketServer;
using Common.Tools;
using MyGameServer.Manager;
namespace MyGameServer.Handler
{
class LoginHandler : BaseHandler
{
public LoginHandler()
{
OpCode = OperationCode.Login;//將自身的請求設定
}
public override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters, ClientPeer peer)
{
//從客戶端傳來的請求中用工具類得到使用者名稱和密碼
string username = DictTool.GetValue<byte,object>(operationRequest.Parameters,(byte)ParameterCode.Username) as string;
string password = DictTool.GetValue<byte, object>(operationRequest.Parameters, (byte)ParameterCode.Password) as string;
UserManager userManager = new UserManager();//UserManager中用NHibernate對資料庫中Users表進行操作
bool iSsuccess= userManager.VerifyUser(username, password);//驗證賬戶在資料庫中是否存在
OperationResponse operationResponse = new OperationResponse(operationRequest.OperationCode);//建立迴應
if(iSsuccess)//賬號密碼在資料庫中匹配成功
{
operationResponse.ReturnCode=(short)Common.ReturnCode.Success;//將傳給客戶端的引數設定為定義的列舉型別Success
peer.username = username;
}
else
{
operationResponse.ReturnCode = (short)Common.ReturnCode.Failed;//將傳給客戶端的引數設定為定義的列舉型別Failed
}
peer.SendOperationResponse(operationResponse,sendParameters);//傳送響應
}
}
}
1.3.1.4 註冊請求的響應(RegisterHandler.cs)
直接在構造方法中將請求的型別設定為Register。再在OnOperationRequest中響應客戶端的請求,因為只是迴應是否註冊成功,所以用響應中的ReturnCode進行返回,ReturnCode為short型別。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Common;
using Common.Tools;
using MyGameServer.Manager;
using Photon.SocketServer;
using MyGameServer.Model;
namespace MyGameServer.Handler
{
class RegisterHandler : BaseHandler
{
public RegisterHandler()
{
OpCode = OperationCode.Register;
}
public override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters, ClientPeer peer)
{
//從客戶端傳來的請求中用工具類得到使用者名稱和密碼
string username = DictTool.GetValue<byte, object>(operationRequest.Parameters, (byte)ParameterCode.Username) as string;
string password = DictTool.GetValue<byte, object>(operationRequest.Parameters, (byte)ParameterCode.Password) as string;
UserManager userManager = new UserManager();//UserManager中用NHibernate對資料庫中Users表進行操作
User user = userManager.GetByUsername(username);//通過username得到User
OperationResponse response = new OperationResponse(operationRequest.OperationCode);
if(user==null)//檢視資料庫中是否已經有要新增的使用者
{
user = new User() { Username = username, Password = password };
userManager.Add(user);//符合要求,向資料庫中新增賬戶和密碼
response.ReturnCode = (short)ReturnCode.Success;//設定傳遞給客戶端的列舉型別
}
else
{
response.ReturnCode = (short)ReturnCode.Failed;
}
peer.SendOperationResponse(response, sendParameters);
}
}
}
1.3.1.5 傳送同步位置的事件(SyncPlayerHandler.cs)
向所有線上客戶端傳送同步位置的事件。主動向客戶端傳送,所以用事件傳送。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Common;
using Photon.SocketServer;
using System.Xml.Serialization;
using System.IO;
namespace MyGameServer.Handler
{
class SyncPlayerHandler : BaseHandler
{
public SyncPlayerHandler()
{
OpCode= OperationCode.SyncPlayer;
}
public override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters, ClientPeer peer)
{
//取得所有已經登入(線上)的使用者名稱
List<string> usernameList = new List<string>();//儲存所有已經登入(線上)的使用者名稱
foreach (ClientPeer tempPeer in MyGameServer.Instance.peerList)//遍歷所有已經登入的使用者
{
//除開沒有登入的和自身不儲存到usernameList中
if(string.IsNullOrEmpty(tempPeer.username)==false&&tempPeer!=peer)
{
usernameList.Add(tempPeer.username);
}
}
//**************將usernameList序列化
StringWriter sw = new StringWriter();
XmlSerializer serializer = new XmlSerializer(typeof(List<string>));
serializer.Serialize(sw, usernameList);
sw.Close();
string usernameListString = sw.ToString();
Dictionary<byte, object> data = new Dictionary<byte, object>();
data.Add((byte)ParameterCode.UsernameList, usernameListString);
OperationResponse response = new OperationResponse(operationRequest.OperationCode);
response.Parameters = data;
peer.SendOperationResponse(response, sendParameters);
//告訴其他客戶端有新的客戶端加入
//用事件傳送
foreach (ClientPeer tempPeer in MyGameServer.Instance.peerList)
{
if (string.IsNullOrEmpty(tempPeer.username) == false && tempPeer != peer)
{
EventData ed = new EventData((byte)EventCode.NewPlayer);
Dictionary<byte, object> dataToAll = new Dictionary<byte, object>();//儲存向所有客戶端傳送的資料
dataToAll.Add((byte)ParameterCode.Username, peer.username);
ed.Parameters = dataToAll;//將要傳送事件的引數設為要給所有客戶端傳送的資料
tempPeer.SendEvent(ed, sendParameters);//傳送事件
}
}
}
}
}
1.3.1.6 客戶端將自身位置傳送到伺服器端,伺服器端的處理(SyncPositionHandler.cs)
伺服器端收到客戶端發來的自身位置請求後,伺服器端先得到三個座標,然後儲存到對應客戶端的xyz中。
using System;
using System.Collections.Generic;
using System.Linq;
using Common.Tools;
using Common;
using Photon.SocketServer;
namespace MyGameServer.Handler
{
class SyncPositionHandler : BaseHandler
{
public SyncPositionHandler()
{
OpCode = OperationCode.SyncPosition;
}
public override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters, ClientPeer peer)
{
Vector3Data pos=(Vector3Data)DictTool.GetValue<byte,object>(operationRequest.Parameters,(byte)ParameterCode.Position);
float x=(float)DictTool.GetValue<byte, object>(operationRequest.Parameters, (byte)ParameterCode.X);
float y = (float)DictTool.GetValue<byte, object>(operationRequest.Parameters, (byte)ParameterCode.Y);
float z = (float)DictTool.GetValue<byte, object>(operationRequest.Parameters, (byte)ParameterCode.Z);
peer.x = x;peer.y = y;peer.z = z;
}
}
}
1.3.2 伺服器向所有客戶端傳送其他所有客戶端位置的執行緒(Threads資料夾下)
在Threads資料夾下定義了SyncPositionThread類,用來建立一個執行緒,線上程中通過傳送事件將客戶端的位置傳送到所有線上以登入的客戶端中。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Common;
using System.Xml.Serialization;
using System.IO;
using Photon.SocketServer;
namespace MyGameServer.Threads
{
public class SyncPositionThread
{
private Thread t;
/// <summary>
/// 開啟執行緒
/// </summary>
public void Run()
{
MyGameServer.log.Info("執行緒執行中");
t = new Thread(UpdatePosition);
t.IsBackground = true;
t.Start();
}
/// <summary>
/// 結束執行緒
/// </summary>
public void StopThread()
{
t.Abort();
}
private void UpdatePosition()
{
Thread.Sleep(2000);//停止2秒
while(true)
{
Thread.Sleep(200);
//進行位置同步
//收集當前所有的客戶端傳送其他客戶端位置
SendPosition();
}
}
private void SendPosition()
{
List<PlayerData> playerDataList = new List<PlayerData>();
foreach(ClientPeer peer in MyGameServer.Instance.peerList)//遍歷所有客戶端
{
if(string.IsNullOrEmpty(peer.username)==false) //客戶端線上
{
PlayerData playerData = new PlayerData();
playerData.Username = peer.username;//將此線上客戶端的名字儲存
playerData.Pos = new Vector3Data() { x = peer.x, y = peer.y, z = peer.z };//將此線上客戶端的位置資訊儲存
playerDataList.Add(playerData);
}
}
////序列化所有player的名字和位置資訊(PlayerData)
StringWriter sw = new StringWriter();
XmlSerializer serializer = new XmlSerializer(typeof(List<PlayerData>));
serializer.Serialize(sw, playerDataList);//序列化到sw中
sw.Close();
string playerDataListString = sw.ToString();
Dictionary<byte, object> data = new Dictionary<byte, object>();
data.Add((byte)ParameterCode.PlayerDataList, playerDataListString);
//向所有線上客戶端傳送其他客戶端的名字和位置資訊(PlayerData)
foreach(ClientPeer peer in MyGameServer.Instance.peerList)
{
if(string.IsNullOrEmpty(peer.username)==false)
{
EventData ed = new EventData((byte)EventCode.SyncPosition);
ed.Parameters = data;
peer.SendEvent(ed, new SendParameters());
}
}
}
}
}
1.3. 伺服器接收請求並處理(ClientPeer.cs)
繼承自Photon.SocketServer.ClientPeer,當客戶端連線到伺服器、斷開於伺服器和發來請求時相應的處理,並定義了用來儲存每個客戶端peer的座標資訊,和名字。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Photon.SocketServer;
using PhotonHostRuntimeInterfaces;
using Common.Tools;
using Common;
using MyGameServer.Handler;
namespace MyGameServer
{
public class ClientPeer : Photon.SocketServer.ClientPeer
{
public float x, y, z;//儲存客戶端中Player座標
public string username;//儲存客戶端登入的使用者名稱
public ClientPeer(InitRequest initRequest) : base(initRequest)
{
}
/// <summary>
/// 處理斷開連線的處理工作
/// </summary>
/// <param name="reasonCode"></param>
/// <param name="reasonDetail"></param>
protected override void OnDisconnect(DisconnectReason reasonCode, string reasonDetail)
{
MyGameServer.Instance.peerList.Remove(this);//每一個客戶端斷開連線後,將此客戶端從peerList中移除
}
/// <summary>
/// 處理客戶端發來的請求
/// </summary>
/// <param name="operationRequest"></param>
/// <param name="sendParameters"></param>
protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
{
//通過請求的OperationCode找到對應的處理handler
BaseHandler handler = DictTool.GetValue<OperationCode, BaseHandler>(MyGameServer.Instance.handlerDict, (OperationCode)operationRequest.OperationCode);
if(handler!=null)
{
handler.OnOperationRequest(operationRequest, sendParameters, this);//用對應的handler進行處理
}
else
{
BaseHandler defaultHandler = DictTool.GetValue<OperationCode, BaseHandler>(MyGameServer.Instance.handlerDict, OperationCode.Default);
defaultHandler.OnOperationRequest(operationRequest, sendParameters, this);
}
}
}
}
1.4 伺服器主類(伺服器入口)
定義的MyGameServer繼承了ApplicationBase,為伺服器的主類,即入口類,所有伺服器端的主類都必須繼承自ApplicationBase,處理伺服器的初始化,客戶端連線、伺服器關閉,並自己定義了handlerDict字典,儲存所有的請求,方便當有請求發來時伺服器可通過operationRequest.OperationCode來區分是何種請求;定義了peerList集合,每個客戶端連線伺服器後都被新增進來,用於後續向他們傳送資料。傳送同步位置的執行緒也是在伺服器啟動後開啟。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Common;
using ExitGames.Logging;
using ExitGames.Logging.Log4Net;
using log4net.Config;
using MyGameServer.Handler;
using MyGameServer.Manager;
using Photon.SocketServer;
using MyGameServer.Threads;
namespace MyGameServer
{
//所有的server端,主類都要繼承自ApplicationBase
public class MyGameServer : ApplicationBase
{
public static readonly ILogger log = LogManager.GetCurrentClassLogger();//日誌
public static MyGameServer Instance
{
get;
private set;
}
public Dictionary<OperationCode, BaseHandler> handlerDict=new Dictionary<OperationCode, BaseHandler>();//儲存客戶端傳來的請求,方便當有請求發來時伺服器可通過operationRequest.OperationCode來區分是何種請求
public List<ClientPeer> peerList = new List<ClientPeer>();//存放所有的客戶端,通過這個集合可以訪問到所有客戶端的peer,用於向所有客戶端傳送資料(其他玩家的位置)
private SyncPositionThread syncPositionThread = new SyncPositionThread();
//當一個客戶端請求連線時呼叫
//使用peerbase表示和一個客戶端的連線
//Photon會自動將所有的客戶端管理起來
protected override PeerBase CreatePeer(InitRequest initRequest)
{
log.Info("一個客戶端連線進來了.................");
ClientPeer peer=new ClientPeer(initRequest);
peerList.Add(peer);
return peer;
}
/// <summary>
/// server啟動後就呼叫,初始化
/// </summary>
protected override void Setup()
{
Instance = this;
//日誌的初始化
log4net.GlobalContext.Properties["Photon:ApplicationLogPath"]=Path.Combine(
Path.Combine( this.ApplicationRootPath,"bin_Win64"),"log");//設定日誌輸出路徑
FileInfo configFileInfo = new FileInfo(Path.Combine(this.BinaryPath, "log4net.config")); //BinaryPath為工程的bin目錄
if (configFileInfo.Exists)
{
LogManager.SetLoggerFactory(Log4NetLoggerFactory.Instance);//讓photon知道使用哪個日誌外掛
XmlConfigurator.ConfigureAndWatch(configFileInfo);//讓log4net這個外掛讀取配置檔案
}
log.Info("Setup Completed!");
InitHandler();
syncPositionThread.Run();//開啟同步客戶端player位置的執行緒
}
/// <summary>
/// server端關閉時
/// </summary>
protected override void TearDown()
{
syncPositionThread.StopThread();//關閉執行緒
log.Info("伺服器應用關閉了");
}
/// <summary>
/// 初始化所有的Handler,每新增一種請求就有一種對應處理請求的Handler,
/// 並且需要新增到handlerDict中
/// </summary>
void InitHandler()
{
LoginHandler loginHandler = new LoginHandler();
DefaultHandler defaultHandler = new DefaultHandler();
RegisterHandler registerHandler = new RegisterHandler();
SyncPositionHandler syncPositionHandler = new SyncPositionHandler();
SyncPlayerHandler syncPlayerHandler = new SyncPlayerHandler();
handlerDict.Add(loginHandler.OpCode, loginHandler);
handlerDict.Add(defaultHandler.OpCode, defaultHandler);
handlerDict.Add(registerHandler.OpCode, registerHandler);
handlerDict.Add(syncPositionHandler.OpCode, syncPositionHandler);
handlerDict.Add(syncPlayerHandler.OpCode, syncPlayerHandler);
}
}
}
儲存好後,右鍵點選工程,生成,就會也將Common.dll也生成庫檔案了。
2. 客戶端
新建unity工程,再建資料夾Plugins將Photonserver的\lib資料夾的Photon3Unity3D放於unity工程的Plugins資料夾下,並將Common.dll檔案也放於此資料夾下。
2.1 場景的準備
2.2.1 登入場景
新建場景,MainGame,為登入場景,有輸出使用者名稱和密碼的InputFiled,和一個確定按鈕;一共有兩個UI面板,一個登入一個註冊。
如圖:
2.2.2 遊戲場景
新建場景GameScene,包含了一個主角player和地面,還有一個用於控制player的空遊戲物體。
如圖:
2.2 登入註冊
2.2.1 與伺服器“交流”(PhotonEngine.cs)
掛載在登入場景的PhotonEngine遊戲物體上,需要繼承IPhotonPeerListener,用單例並在場景中不會銷燬,用於連線伺服器,處理伺服器的響應、事件等;並定義了儲存所有請求的字典,和儲存所有事件的字典,及操作這兩個字典的方法。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using ExitGames.Client.Photon;
using Common;
using Common.Tools;
public class PhotonEngine : MonoBehaviour,IPhotonPeerListener {
public static PhotonEngine Instance;
private static PhotonPeer peer;
public static PhotonPeer Peer
{
get
{
return peer;
}
}
private Dictionary<OperationCode, Request> RequestDict = new Dictionary<OperationCode, Request>();//儲存所有的請求
private Dictionary<EventCode, BaseEvent> EventDict = new Dictionary<EventCode, BaseEvent>();//儲存所有的事件
public static string username;//儲存當前所登入的賬戶名
void Awake()
{
if(Instance==null)
{
Instance = this;
DontDestroyOnLoad(this.gameObject);
}
else if(Instance!=this)//若跳轉到其他場景,刪除多餘的PhotonEngine
{
Destroy(this.gameObject);
return;
}
}
// Use this for initialization
void Start () {
//通過Listender接收伺服器端的響應
peer = new PhotonPeer(this, ConnectionProtocol.Udp);
peer.Connect("127.0.0.1:5055", "MyGame1");//連線伺服器,指定IP的埠號,第二個引數為應用名
}
// Update is called once per frame
void Update()
{
peer.Service();//需要一直呼叫
}
/// <summary>
/// 遊戲停止或者關閉時,要斷開連線
/// </summary>
void OnDestroy()
{
if(peer!=null&&peer.PeerState==PeerStateValue.Disconnected)
{
peer.Disconnect();
}
}
public void DebugReturn(DebugLevel level, string message)
{
}
/// <summary>
/// 處理伺服器傳送的事件
/// </summary>
/// <param name="eventData"></param>
public void OnEvent(EventData eventData)
{
EventCode code = (EventCode)eventData.Code;
BaseEvent e = DictTool.GetValue<EventCode, BaseEvent>(EventDict, code);//通過EventCode的型別從字典中得到事件類
e.OnEvent(eventData);
}
/// <summary>
/// //接收到伺服器傳送回的響應
/// </summary>
/// <param name="operationResponse"></param>
public void OnOperationResponse(OperationResponse operationResponse)
{
//接收到伺服器傳送回的響應
OperationCode opCode = (OperationCode)operationResponse.OperationCode;
Request request = null;
bool temp = RequestDict.TryGetValue(opCode, out request);
if(temp)
{
request.OnOperationResponse(operationResponse);
}
else
{
Debug.Log("沒有找到對應的響應處理物件");
}
}
public void OnStatusChanged(StatusCode statusCode)
{
Debug.Log(statusCode);
}
/// <summary>
/// 向RequestDict中新增請求
/// </summary>
public void AddRequest(Request request)
{
RequestDict.Add(request.OpCode, request);
}
/// <summary>
/// 移除RequestDict中請求
/// </summary>
public void RemoveRequest(Request request)
{
RequestDict.Remove(request.OpCode);
}
/// <summary>
/// 向事件字典eventDict中新增事件
/// </summary>
/// <param name="e"></param>
public void AddEvent(BaseEvent e)
{
EventDict.Add(e.EventCode, e);
}
/// <summary>
/// 移除字典eventDict中的事件
/// </summary>
/// <param name="e"></param>
public void RemoveEvent(BaseEvent e)
{
EventDict.Remove(e.EventCode);
}
}
2.2.2 進行請求
2.2.2.1 請求的基類(Request.cs)
在Scripts/Request下新建指令碼Request,需要繼承MonoBehaviour,因為他的子類需要掛載到場景中;這是所有請求的基類,定義了請求的列舉型別,並在Start()方法中將自身(也就是Request或者其子類)新增到PhotonEngine所定義的請求字典RequestDict中,銷燬時從RequestDict中移除;定義了傳送請求的方法和接收響應的方法。
using Common;
using ExitGames.Client.Photon;
using UnityEngine;
public abstract class Request:MonoBehaviour {
public OperationCode OpCode;
public abstract void DefaultRequest();
public abstract void OnOperationResponse(OperationResponse operationResponse);
public virtual void Start()
{
PhotonEngine.Instance.AddRequest(this);
}
public void OnDestroy()
{
PhotonEngine.Instance.RemoveRequest(this);
}
}
2.2.2.2 登入請求(LoginRequest.cs)
處理髮送請求和伺服器傳來的響應,登入驗證成功後交給loginPanel進行UI的操作。
掛載到LoginPanel上,並設定opCode為login
using ExitGames.Client.Photon;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Common;
public class LoginRequest :Request {
[HideInInspector]
public string Username;
[HideInInspector]
public string Password;
private LoginPanel loginPanel;//得到伺服器的請求響應後,UI的處理(即跳轉場景)傳遞給LoginPanel處理
public override void Start()
{
base.Start();
loginPanel = GetComponent<LoginPanel>();
}
/// <summary>
/// //向伺服器傳送請求
/// </summary>
public override void DefaultRequest()
{
Dictionary<byte, object> data = new Dictionary<byte, object>();
data.Add((byte)ParameterCode.Username, Username);
data.Add((byte)ParameterCode.Password, Password);
PhotonEngine.Peer.OpCustom((byte)OpCode, data, true);
}
/// <summary>
/// 處理伺服器傳來的響應
/// </summary>
/// <param name="operationResponse"></param>
public override void OnOperationResponse(OperationResponse operationResponse)
{
Debug.Log(operationResponse.ReturnCode);//0為成功,1為失敗
ReturnCode returnCode = (ReturnCode)operationResponse.ReturnCode;
if(returnCode==ReturnCode.Success)
{
PhotonEngine.username = Username;//儲存當前登入的使用者名稱
}
loginPanel.OnLoginResponse(returnCode);
}
}
2.2.2.3 註冊請求(LoginRequest.cs)
處理髮送請求和伺服器傳來的響應,成功後交給RegisterPanel進行UI的操作。
掛載到RegisterPanel上,並將OpCode設定為register。
using System.Collections;
using System.Collections.Generic;
using Common;
using ExitGames.Client.Photon;
using UnityEngine;
public class RegisterRequest : Request {
[HideInInspector]
public string Username;
[HideInInspector]
public string Password;
private RegisterPanel registerPanel;//得到伺服器的請求響應後,UI的處理(即跳轉場景)傳遞給registerPanel處理
public override void Start()
{
base.Start();
registerPanel = GetComponent<RegisterPanel>();
}
/// <summary>
/// //向伺服器傳送請求
/// </summary>
public override void DefaultRequest()
{
Dictionary<byte, object> data = new Dictionary<byte, object>();
data.Add((byte)ParameterCode.Username, Username);
data.Add((byte)ParameterCode.Password, Password);
PhotonEngine.Peer.OpCustom((byte)OpCode, data, true);//向伺服器傳送請求
}
/// <summary>
/// 處理伺服器傳來的響應
/// </summary>
/// <param name="operationResponse"></param>
public override void OnOperationResponse(OperationResponse operationResponse)
{
ReturnCode returnCode = (ReturnCode)operationResponse.ReturnCode;
registerPanel.OnRegisterResponse(returnCode);
}
}
2.2.3 登入的啟動(LoginPanel.cs)
在Scripts/UI下新建指令碼LoginPanel,掛載到LoginPanel物體上,處理UI的事件(包括按鈕事件,需手動賦值),並進行各自請求的傳送。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Common;
using UnityEngine.SceneManagement;
public class LoginPanel : MonoBehaviour {
public GameObject registerPanel;
public InputField usernameIF;
public InputField passwordIF;
public Text hitMessage;//提示資訊
private LoginRequest loginRequest;
private void Start()
{
loginRequest = GetComponent<LoginRequest>();
}
/// <summary>
/// 登入按鈕點選
/// </summary>
public void OnLoginButton()
{
hitMessage.text = "";//每次點選登陸後將上一次提示資訊清空
loginRequest.Username = usernameIF.text;
loginRequest.Password = passwordIF.text;
loginRequest.DefaultRequest();
}
/// <summary>
/// 註冊按鈕點選
/// </summary>
public void OnRegisterButton()
{
registerPanel.SetActive(true);
gameObject.SetActive(false);
}
/// <summary>
/// 處理伺服器傳來的響應
/// </summary>
/// <param name="returnCode"></param>
public void OnLoginResponse(ReturnCode returnCode)
{
if(returnCode==ReturnCode.Success)
{
//跳轉到下一個場景
SceneManager.LoadScene("GameScene");
}
else
{
hitMessage.text = "賬號或密碼錯誤!";
}
}
}
設定如下:
2.2.4 註冊的啟動(registerPanel.cs)
在Scripts/UI下新建指令碼RegisterPanel,掛載到RegisterPanel物體上,處理UI的事件(包括按鈕事件,需手動賦值),並進行各自請求的傳送。
using Common;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class RegisterPanel : MonoBehaviour {
public GameObject loginPanel;
public InputField usernameIF;
public InputField passwordIF;
public Text hitMessage;
private RegisterRequest registerRequest;
private void Start()
{
registerRequest = GetComponent<RegisterRequest>();
}
public void OnRegisterButton()
{
hitMessage.text = "";//每次點選登陸後將上一次提示資訊清空
registerRequest.Username = usernameIF.text;
registerRequest.Password = passwordIF.text;
registerRequest.DefaultRequest();
}
public void OnBackButton()
{
loginPanel.SetActive(true);
gameObject.SetActive(false);
}
/// <summary>
/// 處理伺服器傳來的響應
/// </summary>
/// <param name="returnCode"></param>
public void OnRegisterResponse(ReturnCode returnCode)
{
if (returnCode == ReturnCode.Success)
{
hitMessage.text = "註冊成功,返回登入!";
}
else
{
hitMessage.text = "賬號或密碼已存在,更改後重新註冊!";
}
}
}
設定如下:
2.3 遊戲中角色同步
2.3.1 自身位置上傳請求(SyncPositionRequest.cs)
在Stripts/Request下新建指令碼SyncPositionRequest,需要掛載到PlayerController上;在面板上設定opCode為SyncPosition;將自身位置上傳的伺服器的請求。
using System.Collections;
using System.Collections.Generic;
using Common;
using ExitGames.Client.Photon;
using UnityEngine;
/// <summary>
/// 向伺服器傳送位置請求
/// </summary>
public class SyncPositionRequest : Request {
// Use this for initialization
[HideInInspector]
public Vector3 pos;
// Update is called once per frame
void Update()
{
}
public override void DefaultRequest()
{
Dictionary<byte, object> data = new Dictionary<byte, object>();
//data.Add((byte)ParameterCode.Position, new Vector3Data() { x=pos.x, y=pos.y, z=pos.z });//將位置資訊通過自定義的Vector3傳送給伺服器端
data.Add((byte)ParameterCode.X, pos.x);
data.Add((byte)ParameterCode.Y, pos.y);
data.Add((byte)ParameterCode.Z, pos.z);
PhotonEngine.Peer.OpCustom((byte)OpCode, data, true);
}
public override void OnOperationResponse(OperationResponse operationResponse)
{
throw new System.NotImplementedException();
}
}
2.3.2 同步其他客戶端player位置請求
在Stripts/Request下新建指令碼SyncPlayerDataRequest,需要掛載到PlayerController上;在面板上設定opCode為SyncPlayer;將伺服器的響應傳來的引數反序列化出PlayerData。
using System.Collections;
using System.Collections.Generic;
using ExitGames.Client.Photon;
using UnityEngine;
using Common;
using Common.Tools;
using System.Xml.Serialization;
using System.IO;
/// <summary>
/// 同步其他客戶端玩家位置資訊的請求
/// 包括建立其他客戶端
/// </summary>
public class SyncPlayerRequest : Request {
private Player player;
public override void Start()
{
base.Start();
player = GetComponent<Player>();
}
public override void DefaultRequest()
{
PhotonEngine.Peer.OpCustom((byte)OpCode, null, true);
}
public override void OnOperationResponse(OperationResponse operationResponse)
{
string usernameListString=(string)DictTool.GetValue<byte, object>(operationResponse.Parameters, (byte)ParameterCode.UsernameList);
//反序列化
using (StringReader reader = new StringReader(usernameListString))
{
XmlSerializer serializer = new XmlSerializer(typeof(List<string>));
List<string> usernameList=(List<string>)serializer.Deserialize(reader);
player.OnSyncPlayerResponse(usernameList);
}
}
}
2.3.2 伺服器發來的事件處理
2.3.2.1 事件基類(BaseEvent.cs)
在Stripts/Event下新建指令碼BaseEvent.cs,繼承自MonoBehaviour,因為其子類需要掛載到場景中,定義了傳來的事件型別的列舉型別,在Start()方法中處理新增本身到PhotonEngine的eventDict中,OnDestroy()中將自身移除。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Common;
using ExitGames.Client.Photon;
/// <summary>
/// 處理收到的伺服器的事件
/// </summary>
public abstract class BaseEvent : MonoBehaviour {
public EventCode EventCode;
public abstract void OnEvent(EventData eventData);
public virtual void Start()
{
PhotonEngine.Instance.AddEvent(this);
}
public void OnDestroy()
{
PhotonEngine.Instance.RemoveEvent(this);
}
}
2.3.2.2 例項化其他客戶端事件(NewPlayerEvent.cs) 在Stripts/Event下新建指令碼NewPlayerEvent.cs,需要掛載到PlayerController,在面板上設定EventCode為NewPlayer;;只需要得到名字,例項化的工作交給player完成。
using System.Collections;
using System.Collections.Generic;
using ExitGames.Client.Photon;
using UnityEngine;
using Common;
using Common.Tools;
public class NewPlayerEvent : BaseEvent {
private Player player;
public override void Start()
{
base.Start();
player = GetComponent<Player>();
}
public override void OnEvent(EventData eventData)
{
string username = (string)DictTool.GetValue<byte, object>(eventData.Parameters, (byte)ParameterCode.Username);
player.OnNewPlayerEvent(username);
}
}
2.3.2.3 同步其他客戶端player位置事件(SyncPositionEvent.cs)
在Stripts/Event下新建指令碼SyncPositionEvent.cs,需要掛載到PlayerController,在面板上設定EventCode為SyncPosition;只需要發序列化得到其他客戶端player的位置資訊和名字,同步位置的工作交給player完成。
using System.Collections;
using System.Collections.Generic;
using ExitGames.Client.Photon;
using UnityEngine;
using Common;
using Common.Tools;
using System.Xml.Serialization;
using System.IO;
public class SyncPositionEvent : BaseEvent
{
private Player player;
public override void Start()
{
base.Start();
player = GetComponent<Player>();
}
public override void OnEvent(EventData eventData)
{
string playDataListString =(string) DictTool.GetValue<byte, object>(eventData.Parameters,(byte)ParameterCode.PlayerDataList);
//////將傳來的被序列化的player名字和位置資訊進行反序列化解析
using (StringReader reader = new StringReader(playDataListString))
{
XmlSerializer serializer = new XmlSerializer(typeof(List<PlayerData>));
List<PlayerData> playerDataList =(List<PlayerData>) serializer.Deserialize(reader);
player.OnSyncPositionEvent(playerDataList);//交給player處理已經反序列化出的其他客戶端player位置資訊和名字(PlayerData)
}
}
}
2.3.3 控制player移動及同步其他客戶端(player.cs)
將Player物體作為預製體,在Scripts/AI 下新建指令碼Player,掛載到PlayerController物體上,然後為playerPrefab指定為剛剛的預製體,player指定為場景中的Player.其中控制了player的移動,和請求、事件的呼叫。
using Common;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Common.Tools;
public class Player : MonoBehaviour {
public string username;//用於區分不同的客戶端player
public GameObject playerPrefab;//用於例項化其他客戶端player的預製體
public GameObject player;//所控制的Player
private SyncPositionRequest syncPosRequest;
private SyncPlayerRequest syncPlayerRequest;
private Vector3 lastPosition = Vector3.zero;//得到上次的位置資訊,用於判斷位置是否變化,無變化就不向伺服器傳送同步位置請求,節約效能
private float moveOffest = 0.1f;//位置變化偏移量,大於此值就向伺服器同步位置
private Dictionary<string, GameObject> playerDict = new Dictionary<string, GameObject>();//儲存建立的其他客戶端player
// Use this for initialization
void Start () {
syncPosRequest = GetComponent<SyncPositionRequest>();
syncPlayerRequest = GetComponent<SyncPlayerRequest>();
player.GetComponent<Renderer>().material.color = Color.green;//將本機的player顏色設定為綠色,用於區分本機和其他客戶端player
syncPlayerRequest.DefaultRequest();//同步其他玩家的位置
InvokeRepeating("SyncPosition", 2, 0.05f);//同步位置,呼叫SyncPosition方法,2秒開始,每0.1秒呼叫一次
}
/// <summary>
/// 同步自己位置到伺服器
/// </summary>
void SyncPosition()
{
if(Vector3.Distance(lastPosition,player.transform.position)>moveOffest)
{
lastPosition= player.transform.position;
syncPosRequest.pos = player.transform.position;
syncPosRequest.DefaultRequest();
}
}
// Update is called once per frame
void Update () {
//控制player移動
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
player.transform.Translate(new Vector3(h, 0, v) * Time.deltaTime *15);
}
/// <summary>
/// 例項化出其他player(因為與OnNewPlayerEvent有重複了,直接呼叫OnNewPlayerEvent
/// </summary>
/// <param name="usernameList"></param>
public void OnSyncPlayerResponse(List<string> usernameList)
{
//建立其他客戶端的player角色
foreach(string username in usernameList)
{
OnNewPlayerEvent(username);
}
}
/// <summary>
/// 處理伺服器傳來的例項化其他客戶端事件
/// </summary>
public void OnNewPlayerEvent(string username)
{
GameObject go = GameObject.Instantiate(playerPrefab);
playerDict.Add(username, go);
}
/// <summary>
/// 處理伺服器發來的同步其他客戶端player位置和名字的事件
/// </summary>
/// <param name="playerDataList"></param>
public void OnSyncPositionEvent(List<PlayerData> playerDataList)
{
//根據使用者名稱遍歷所有線上的遊戲物體設定他們的位置
foreach(PlayerData pd in playerDataList)
{
GameObject go = DictTool.GetValue<string, GameObject>(playerDict, pd.Username);
if(go!=null)
go.transform.position = new Vector3() { x = pd.Pos.x, y = pd.Pos.y, z = pd.Pos.z };
}
}
}
設定如上圖。、
最後打包出客戶端exe。
3. 執行
開啟MySQL資料庫服務,開啟PhotonServer資料夾下deploy\bin_Win64的PhotonControl.exe,啟動自定義的應用——MyGame,再開啟log,執行多個客戶端。