Related Posts Plugin for WordPress, Blogger...

6-27 Synchronizing Other Player Position

本章接下來要繼續製作『同步其他玩家位置』的功能啦!上一章已經做到當玩家登入遊戲後,會由伺服端發送Event通知其他玩家,並在其他玩家的設備上產生遊戲角色。所以,本章要讓這些遊戲角色動起來。

首先,要先解決上一章遇到的小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);

                List peerList = 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 Dictionary handlerDict =
            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端也必須註冊自定義類別,才能正常序列化與反序列化。註冊的方法用PhotonPeer的RegisterType,跟Server端的註冊方法不同。

以下提供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;
 Dictionary onlinePlayerList;

 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 Dictionary requestDict = 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
}



最後來測試遊戲看看吧,登入畫面就不截圖啦,直接登入兩個玩家。

移動其中一個玩家看看,太好了!其中一個玩家動起來了。

然後我從另外一個設備看,其他玩家的位置也有確實同步。

留言