6-27 Synchronizing Other Player Position
本章接下來要繼續製作『同步其他玩家位置』的功能啦!上一章已經做到當玩家登入遊戲後,會由伺服端發送Event通知其他玩家,並在其他玩家的設備上產生遊戲角色。所以,本章要讓這些遊戲角色動起來。
首先,要先解決上一章遇到的小Bug,玩家操作角色的時候會讓其他玩家的角色也動起來,這是因為我用的是同一個Player物件。
再另外創造一個名為OnlinePlayer的Prefab。
其中的PlayerMovement的dontRegisterMouseOnWalkable參數,設為True即可。
然後記得將OnlinePlayer放置在SyncPosition的參數中。
上述Bug解決以後,接著在PhotonServer端這邊建立一個Thread,該Thread會定時發送Event給在線上的玩家,給予每位玩家的即時位置資訊,讓Client端進行位置同步。
以下是針對SyncPositionThread內主要實作的內容,取得目前在線的MyClientPeerList後,將內部的Account, x, y, z等資訊存進自定義的類別PlayerSyncData,並組合成一個陣列。
接著使用SendEvent方法,將PlayerSyncData陣列傳送給每一個ClientPeer。特別要注意的是,前面我們都只傳送C#的value-type變數,如果要像這邊一樣傳送自定義類別,必須要有一些方法。
可以參考官方的說明文件:
https://doc.photonengine.com/en-us/onpremise/current/reference/serialization-in-photon
做法不難,如下圖,於PlayerSyncData內定義兩個static方法,Deserialize方法與Serialize方法。這邊我用了自己寫的XmlDataSerializer,將PlayerSyncData轉換為Xml格式後,再轉換成byte陣列。
我自己寫的XmlDataSerializer.cs程式碼如下:
然後,最重要的是記得在PhotonServer初始化時,使用Protocol的TryRegisterCustomType方法,註冊自定義類別。
以下提供Server端的程式碼:
SyncPositionThread.cs:
PlayerSyncData.cs:
NoliahFantasyServer.cs:
接著來撰寫Unity端,我們建立一個新的Event物件來處理,名為SyncOtherPlayerPositionEvent。在OnEventData中取得PlayerSyncData陣列,然後傳給SyncPosition類別,統一處理。
在SyncPosition的OnSyncPlayerPositionEvent方法中,將陣列內的資料取出,並從onlinePlayerList比對帳號,取得對應的PlayerMovement物件。 接著根據Server傳來的位置資訊建立一個新的Vector3,呼叫PlayerMovement內的WalkTo方法,讓物件在地圖上移動。
同樣在Client端也必須註冊自定義類別,才能正常序列化與反序列化。註冊的方法用PhotonPeer的RegisterType,跟Server端的註冊方法不同。
以下提供Client端的原始碼:
SyncOtherPlayerPositionEvent.cs:
SyncPosition.cs:
PhotonEngine.cs:
移動其中一個玩家看看,太好了!其中一個玩家動起來了。
然後我從另外一個設備看,其他玩家的位置也有確實同步。
首先,要先解決上一章遇到的小Bug,玩家操作角色的時候會讓其他玩家的角色也動起來,這是因為我用的是同一個Player物件。
我的作法是在PlayerMovement中加一個bool參數,名為dontRegisterMouseOnWalkable,判斷這個Player是否要回應點擊事件。
再另外創造一個名為OnlinePlayer的Prefab。
其中的PlayerMovement的dontRegisterMouseOnWalkable參數,設為True即可。
然後記得將OnlinePlayer放置在SyncPosition的參數中。
上述Bug解決以後,接著在PhotonServer端這邊建立一個Thread,該Thread會定時發送Event給在線上的玩家,給予每位玩家的即時位置資訊,讓Client端進行位置同步。
以下是針對SyncPositionThread內主要實作的內容,取得目前在線的MyClientPeerList後,將內部的Account, x, y, z等資訊存進自定義的類別PlayerSyncData,並組合成一個陣列。
接著使用SendEvent方法,將PlayerSyncData陣列傳送給每一個ClientPeer。特別要注意的是,前面我們都只傳送C#的value-type變數,如果要像這邊一樣傳送自定義類別,必須要有一些方法。
可以參考官方的說明文件:
https://doc.photonengine.com/en-us/onpremise/current/reference/serialization-in-photon
做法不難,如下圖,於PlayerSyncData內定義兩個static方法,Deserialize方法與Serialize方法。這邊我用了自己寫的XmlDataSerializer,將PlayerSyncData轉換為Xml格式後,再轉換成byte陣列。
我自己寫的XmlDataSerializer.cs程式碼如下:
using System; using System.Xml.Serialization; using System.IO; using System.Text; public class XmlDataSerializer { public static object Deserialize(byte[] data) { using (StringReader stringReader = new StringReader (Encoding.Default.GetString (data))) { XmlSerializer xmlSerializer = new XmlSerializer (typeof(T)); object result = xmlSerializer.Deserialize (stringReader); return result; } } public static byte[] Serialize (object customType) { StringWriter stringWriter = new StringWriter (); XmlSerializer xmlSerializer = new XmlSerializer (typeof(T)); T playerSyncData = (T)customType; xmlSerializer.Serialize (stringWriter, playerSyncData); stringWriter.Close (); return Encoding.Default.GetBytes (stringWriter.ToString ()); } }
然後,最重要的是記得在PhotonServer初始化時,使用Protocol的TryRegisterCustomType方法,註冊自定義類別。
以下提供Server端的程式碼:
SyncPositionThread.cs:
using System.Threading; using System.Collections.Generic; using Photon.SocketServer; using NoliahFantasyServer.Constant; namespace NoliahFantasyServer.Threads { public class SyncPositionThread { Thread thread; public void Run(){ thread = new Thread(UpdatePosition); thread.IsBackground = true; thread.Start(); } public void Stop(){ thread.Abort(); } void UpdatePosition(){ Thread.Sleep(5000); // 開始進行同步 while(true){ Thread.Sleep(200); ListpeerList = NoliahFantasyServer.peerList; PlayerSyncData[] playerSyncDatas = new PlayerSyncData[peerList.Count]; for (int i = 0; i < peerList.Count; i++) { if (string.IsNullOrEmpty(peerList[i].userAccount) == false) { // 儲存PlaerSyncData PlayerSyncData playerSyncData = new PlayerSyncData(); playerSyncData.userAccount = peerList[i].userAccount; playerSyncData.xPosition = peerList[i].xPosition; playerSyncData.yPosition = peerList[i].yPosition; playerSyncData.zPosition = peerList[i].zPosition; playerSyncDatas[i] = playerSyncData; } } foreach(MyClientPeer allPeer in peerList){ if (string.IsNullOrEmpty(allPeer.userAccount) == false) { Dictionary eventDataDict = new Dictionary (); eventDataDict.Add((byte)ParameterCode.PlayerSyncData, playerSyncDatas); EventData eventData = new EventData((byte)EventCode.SyncOtherPlayerPosition); eventData.Parameters = eventDataDict; allPeer.SendEvent(eventData, new SendParameters()); } } } } } }
PlayerSyncData.cs:
public class PlayerSyncData { public string userAccount { get; set; } public float xPosition { get; set; } public float yPosition { get; set; } public float zPosition { get; set; } public static object Deserialize (byte[] data) { return XmlDataSerializer.Deserialize(data); } // For a SerializeMethod, we need a byte-array as result. public static byte[] Serialize (object customType) { return XmlDataSerializer.Serialize (customType); } }
NoliahFantasyServer.cs:
using System.IO; using System.Collections.Generic; using Photon.SocketServer; using ExitGames.Logging; using log4net.Config; using NoliahFantasyServer.Constant; using NoliahFantasyServer.Handler; using NoliahFantasyServer.Threads; namespace NoliahFantasyServer { public class NoliahFantasyServer : ApplicationBase { public static readonly ILogger logger = LogManager.GetCurrentClassLogger(); public static DictionaryhandlerDict = new Dictionary (); // 透過PeerList,可向任何一個客戶端發送數據 public static List peerList = new List (); SyncPositionThread syncPositionThread = new SyncPositionThread(); // 當Client端發出Request的時候 protected override PeerBase CreatePeer(InitRequest initRequest) { MyClientPeer peer = new MyClientPeer(initRequest); peerList.Add(peer); return peer; } // Server端啟動的時候初始化 protected override void Setup() { InitLogger(); InitHandler(); InitCutomType(); InitThreads(); } // Server端關閉的時候 protected override void TearDown() { StopThreads(); } void InitLogger() { // 日誌初始化 log4net.GlobalContext.Properties["Photon:ApplicationLogPath"] = Path.Combine(this.ApplicationRootPath, "bin_Win64", "log"); FileInfo loggerConfig = new FileInfo(Path.Combine(this.BinaryPath, "log4net.config")); if (loggerConfig.Exists) { // 設置使用log4net的Log功能 LogManager.SetLoggerFactory(ExitGames.Logging.Log4Net.Log4NetLoggerFactory.Instance); // 讓log4net讀取config XmlConfigurator.ConfigureAndWatch(loggerConfig); } logger.Info("Setup Log4Net Compeleted!"); } void InitHandler(){ LoginHandler loginHandler = new LoginHandler(); handlerDict.Add(loginHandler.operationCode, loginHandler); SignupHandler signupHandler = new SignupHandler(); handlerDict.Add(signupHandler.operationCode, signupHandler); SyncPositionHandler syncPositionHandler = new SyncPositionHandler(); handlerDict.Add(syncPositionHandler.operationCode, syncPositionHandler); SyncOtherPlayerHandler syncOtherPlayerHandler = new SyncOtherPlayerHandler(); handlerDict.Add(syncOtherPlayerHandler.operationCode, syncOtherPlayerHandler); } void InitCutomType(){ Protocol.TryRegisterCustomType( typeof(PlayerSyncData), (byte)'w', PlayerSyncData.Serialize, PlayerSyncData.Deserialize); } void InitThreads(){ syncPositionThread.Run(); } void StopThreads(){ syncPositionThread.Stop(); } } }
接著來撰寫Unity端,我們建立一個新的Event物件來處理,名為SyncOtherPlayerPositionEvent。在OnEventData中取得PlayerSyncData陣列,然後傳給SyncPosition類別,統一處理。
在SyncPosition的OnSyncPlayerPositionEvent方法中,將陣列內的資料取出,並從onlinePlayerList比對帳號,取得對應的PlayerMovement物件。 接著根據Server傳來的位置資訊建立一個新的Vector3,呼叫PlayerMovement內的WalkTo方法,讓物件在地圖上移動。
以下提供Client端的原始碼:
SyncOtherPlayerPositionEvent.cs:
using System.Collections; using System.Collections.Generic; using UnityEngine; using ExitGames.Client.Photon; public class SyncOtherPlayerPositionEvent : BaseEvent { SyncPosition syncPosition; void Awake(){ syncPosition = GetComponent(); } public override void OnEventData (EventData eventData) { object data; eventData.Parameters.TryGetValue ((byte)ParameterCode.PlayerSyncData, out data); PlayerSyncData[] playerSyncDatas = (PlayerSyncData[])data; syncPosition.OnSyncPlayerPositionEvent (playerSyncDatas); } }
SyncPosition.cs:
using System.Collections; using System.Collections.Generic; using UnityEngine; using RPG.Character; using RPG.Core; public class SyncPosition : MonoBehaviour { [SerializeField] GameObject playerPrefab; GameObject player; SyncPositionRequest syncPosiRequest; SyncOtherPlayerRequest syncOtherPlayerRequest; Vector3 lastPosition; DictionaryonlinePlayerList; void Awake(){ onlinePlayerList = new Dictionary (); } void Start () { syncPosiRequest = GetComponent (); syncOtherPlayerRequest = GetComponent (); player = Game.Instance.playerMovement.gameObject; // 初始化Player位置 lastPosition = player.transform.position; // 每段時間同步玩家位置 StartCoroutine (UploadPosition ()); // 同步其他玩家位置 StartCoroutine (SyncOtherPlayerPosition ()); } IEnumerator UploadPosition(){ while (true) { // 每秒同步五次 yield return new WaitForSeconds (0.2f); // 判斷是否有移動,若Player沒有在移動便不需要同步 Vector3 nowPosition = player.transform.position; if(Vector3.Distance(nowPosition, lastPosition) > 0.1f){ lastPosition = nowPosition; syncPosiRequest.position = nowPosition; // 發送位置更新訊息 syncPosiRequest.OnDefaultRequest (); } } } public void AddNewPlayer(string account){ if (onlinePlayerList.ContainsKey (account) == false) { // TODO 製作Player Factory改善這個問題 // Position使用此遊戲物件的位置 GameObject player = Instantiate (playerPrefab, transform.position, Quaternion.identity); onlinePlayerList.Add (account, player.GetComponent ()); } } IEnumerator SyncOtherPlayerPosition(){ yield return new WaitForSeconds (0.2f); syncOtherPlayerRequest.OnDefaultRequest (); } public void OnSyncPlayerPositionEvent(PlayerSyncData[] playerSyncDatas){ foreach (PlayerSyncData playerSyncData in playerSyncDatas) { PlayerMovement player; onlinePlayerList.TryGetValue (playerSyncData.userAccount, out player); // 因為Server回傳的是全部Player的資料(包含遊戲的自己) // 但onlinePlayerList中不包含自己,所以要檢查不等於null if (player != null) { Vector3 destination = new Vector3 ( playerSyncData.xPosition, playerSyncData.yPosition, playerSyncData.zPosition); player.WalkTo (destination); } } } }
PhotonEngine.cs:
using System.Collections; using System.Collections.Generic; using UnityEngine; using ExitGames.Client.Photon; public class PhotonEngine : MonoBehaviour, IPhotonPeerListener { private DictionaryrequestDict = new Dictionary (); private Dictionary eventDict = new Dictionary (); PhotonPeer peer; public static PhotonEngine Instance; public static string userAccount; void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad (this); } else if (Instance != this) { Destroy (gameObject); return; } } void Start () { // 透過Listener回應伺服器的Response peer = new PhotonPeer (this, ConnectionProtocol.Udp); peer.Connect ("anoneko.cloudapp.net:5055", "NoliahFantasyServer"); // 註冊自定義類別 bool success = PhotonPeer.RegisterType (typeof(PlayerSyncData), (byte)'w', PlayerSyncData.Serialize, PlayerSyncData.Deserialize); Debug.Log ("Custom Type Register:" + success); } void Update () { // 必須在Update一直呼叫Service方法才能持續連線到Photon peer.Service (); } void OnDestroy(){ if (peer != null && peer.PeerState == PeerStateValue.Connected) { peer.Disconnect (); } } public PhotonPeer GetPeer(){ return peer; } public void AddRequest(Request request){ requestDict.Add (request.operationCode, request); } public void RemoveRequest(Request request){ requestDict.Remove (request.operationCode); } public void AddEvent(BaseEvent baseEvent){ eventDict.Add (baseEvent.eventCode, baseEvent); } public void RemoveEvent(BaseEvent baseEvent){ eventDict.Remove(baseEvent.eventCode); } #region IPhotonPeerListener implementation public void DebugReturn (DebugLevel level, string message) { } // 接收由Server端傳回的Response public void OnOperationResponse (OperationResponse operationResponse) { OperationCode opCode = (OperationCode)operationResponse.OperationCode; Request request = null; requestDict.TryGetValue (opCode, out request); request.OnOperationResponse (operationResponse); } public void OnStatusChanged (StatusCode statusCode) { } // 接收由Server端發送的Event事件 public void OnEvent (EventData eventData) { EventCode eventCode = (EventCode)eventData.Code; BaseEvent baseEvent = null; eventDict.TryGetValue (eventCode, out baseEvent); baseEvent.OnEventData (eventData); } #endregion }
ParameterCode.cs:
public enum ParameterCode : byte { Acccount, Password, Position, x, y, z, AccountList, PlayerSyncData }
EventCode.cs:
public enum EventCode : byte { NewPlayer, SyncOtherPlayerPosition }
最後來測試遊戲看看吧,登入畫面就不截圖啦,直接登入兩個玩家。
移動其中一個玩家看看,太好了!其中一個玩家動起來了。
然後我從另外一個設備看,其他玩家的位置也有確實同步。
留言
張貼留言