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);
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端的原始碼:
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
}
最後來測試遊戲看看吧,登入畫面就不截圖啦,直接登入兩個玩家。
移動其中一個玩家看看,太好了!其中一個玩家動起來了。
然後我從另外一個設備看,其他玩家的位置也有確實同步。
















留言
張貼留言