6-26 Sending Event To Instantiate Player GameObject When Player Login
上一章已經介紹如何上傳玩家位置的方法了,接下來,當其他玩家登錄遊戲的時候,也應該要在遊戲場景中實例化新的玩家物件。首先,介紹要實作的流程會讓大家感到比較清楚:
1. 當玩家登錄時,發送Request,要求取得其他玩家的資訊。
2. Server發送Response,將目前線上玩家的所有資訊回傳。
3. 玩家接收到Response後,依照線上玩家的資訊列表,產生玩家的GameObject物件。
4. Server發送Event給其他線上玩家,告知有新的玩家上線。
5. 其他線上玩家接收到Event,於場景中產生一個新的玩家的GameObject。
上述便是此次要實作的流程。
首先,在PhotonServer端的MyClientPeer,新增變數,用來記錄Account與x, y, z位置資訊。當玩家與PhotonServer連線時,便會產生一個ClientPeer物件,所以直接將該名玩家的資訊記錄在ClientPeer物件中。
接著,在SyncPositionHandler.cs中的OnOperationRequest,把來自Client端的位置資訊儲存到ClientPeer的變數中。
同時,也要在LoginHandler中把來自Client端的Account,儲存到ClientPeer的變數。
接著我們回到Unity端來實作SyncOtherPlayerRequest.cs,這個類別的作用是初始化場景時,負責發送Request,要求取得其他玩家的資訊。
所以在OnOperationResponse中,會從Server端取得string陣列,裡面儲存已登入玩家的帳號。這邊,我呼叫了SyncPosition類別的AddNewPlayer方法,待會會告訴大家如何撰寫。
另外提醒,PhotonServer預設支援可自動序列化的類型,或者如何實作自定義類別的序列化,請參考下列網址:
https://doc.photonengine.com/en-us/onpremise/current/reference/serialization-in-photon
現在來到SyncPosition.cs,我新增了一個Dictionary管理登入的線上玩家(不包含自己)。
大家可以看到AddNewPlayer中,先從onlinePlayerList確定這個帳號不在列表中,然後使用Instantiate產生GameObject,並將PlayerMovement類別存入onlinePlayerList中,以後同步位置時需要使用。
接著我們替這次的Request增加一個OperationCode,名為SyncOtherPlayerPosition。
來到場景中的SyncPosition物件,將SyncOtherPlayerRequest.cs加進去,並記得選擇正確的OperationCode。
接下來,回到PhotonServer端,讓我們來處理SyncOtherPlayerHandler.cs。
再次說明,本次的Request需要將線上玩家的清單傳回給Client。所以,在OnOperationRequest方法中,我們需要從PeerList中剔除目前正在登錄的自己,然後將其他線上玩家的清單傳回去。
處理完Request後,Server還必須發送Event給其他線上玩家,告知有新的玩家上線。
於是,我們要再回到Unity端,處理來自Server發送的Event。同Request的做法一樣,我們使用Dictionary統一管理不同功用的Event,所有Event的處理都會繼承我自定義的BaseEvent類別。
在PhotonEngine.cs中提供AddEvent跟RemoveEvent,將事件加入到Dictionary中。
然後在OnEvent方法中,藉由EventCode從Dictionary中取得正確的Event物件,並處理OnEventData方法。
BaseEvent.cs的寫法與Request.cs相似。若不懂我用的這個架構,請參考之前的文章:
6-14 Using Dictionary To Manage All Request
https://3dactionrpg.blogspot.com/2018/05/6-14-using-dictionary-to-manage-all.html
接著新增NewPlayerEvent.cs,繼承BaseEvent,在OnEventData中處理來自Server的訊息。在流程中,要處理其他線上玩家登入遊戲的情況,所以需要於場景中產生一個新的玩家的GameObject
撰寫完成後,記得將NewPlayerEvent.cs放進場景中的SyncPosition。
此時開啟遊戲測試看看,要從登入畫面進入遊戲。首先輸入帳號aaa。
接著使用其他的設備進行登入,使用帳號bbb。
場景中確實產生了第二個玩家,但是不知道為什麼腳都陷進地面底下了。
看了一下Console,發現有錯誤訊息。
經查詢後,似乎是於Awake階段,透過AddComponent新增NavMeshAgent,會出現這個問題,而且還僅限於Instatiate的時候。我原本放在場景中的Player就一點事情都沒有。為了解決這個問題,只好修改我的Character.cs,將AddComponent<NavMeshAgent>放到Start方法中。
另外,我也有調查網路上針對此問題的其他解決方案,但對我來說都沒有效果,大家參考看看。
https://forum.unity.com/threads/failed-to-create-agent-because-it-is-not-close-enough-to-the-navmesh.125593/
https://forum.unity.com/threads/failed-to-create-agent-because-it-is-not-close-enough-to-the-navmesh.500553/
以下提供本次修改的程式碼:
以下提供Client端原始碼:
PhotonEngine.cs:
SyncPosition.cs:
OperationCode.cs:
ParameterCode.cs:
EventCode.cs:
BaseEvent.cs:
NewPlayerEvent.cs:
Character.cs:
以下提供Server端原始碼:
SyncOtherPlayerHandler.cs:
SyncPositionHandler.cs:
LoginHandler.cs:
NoliahFantasyServer.cs:
最後來測試遊戲看看吧,這次就成功啦!
可是發現了一個很好笑的Bug,移動自己的時候,另一個複製的玩家也會跟著移動。這個Bug會於下一章介紹位置資訊同步的時候一併解決。
1. 當玩家登錄時,發送Request,要求取得其他玩家的資訊。
2. Server發送Response,將目前線上玩家的所有資訊回傳。
3. 玩家接收到Response後,依照線上玩家的資訊列表,產生玩家的GameObject物件。
4. Server發送Event給其他線上玩家,告知有新的玩家上線。
5. 其他線上玩家接收到Event,於場景中產生一個新的玩家的GameObject。
上述便是此次要實作的流程。
首先,在PhotonServer端的MyClientPeer,新增變數,用來記錄Account與x, y, z位置資訊。當玩家與PhotonServer連線時,便會產生一個ClientPeer物件,所以直接將該名玩家的資訊記錄在ClientPeer物件中。
接著,在SyncPositionHandler.cs中的OnOperationRequest,把來自Client端的位置資訊儲存到ClientPeer的變數中。
同時,也要在LoginHandler中把來自Client端的Account,儲存到ClientPeer的變數。
接著我們回到Unity端來實作SyncOtherPlayerRequest.cs,這個類別的作用是初始化場景時,負責發送Request,要求取得其他玩家的資訊。
所以在OnOperationResponse中,會從Server端取得string陣列,裡面儲存已登入玩家的帳號。這邊,我呼叫了SyncPosition類別的AddNewPlayer方法,待會會告訴大家如何撰寫。
另外提醒,PhotonServer預設支援可自動序列化的類型,或者如何實作自定義類別的序列化,請參考下列網址:
https://doc.photonengine.com/en-us/onpremise/current/reference/serialization-in-photon
現在來到SyncPosition.cs,我新增了一個Dictionary管理登入的線上玩家(不包含自己)。
大家可以看到AddNewPlayer中,先從onlinePlayerList確定這個帳號不在列表中,然後使用Instantiate產生GameObject,並將PlayerMovement類別存入onlinePlayerList中,以後同步位置時需要使用。
接著我們替這次的Request增加一個OperationCode,名為SyncOtherPlayerPosition。
來到場景中的SyncPosition物件,將SyncOtherPlayerRequest.cs加進去,並記得選擇正確的OperationCode。
接下來,回到PhotonServer端,讓我們來處理SyncOtherPlayerHandler.cs。
再次說明,本次的Request需要將線上玩家的清單傳回給Client。所以,在OnOperationRequest方法中,我們需要從PeerList中剔除目前正在登錄的自己,然後將其他線上玩家的清單傳回去。
處理完Request後,Server還必須發送Event給其他線上玩家,告知有新的玩家上線。
於是,我們要再回到Unity端,處理來自Server發送的Event。同Request的做法一樣,我們使用Dictionary統一管理不同功用的Event,所有Event的處理都會繼承我自定義的BaseEvent類別。
在PhotonEngine.cs中提供AddEvent跟RemoveEvent,將事件加入到Dictionary中。
然後在OnEvent方法中,藉由EventCode從Dictionary中取得正確的Event物件,並處理OnEventData方法。
BaseEvent.cs的寫法與Request.cs相似。若不懂我用的這個架構,請參考之前的文章:
6-14 Using Dictionary To Manage All Request
https://3dactionrpg.blogspot.com/2018/05/6-14-using-dictionary-to-manage-all.html
接著新增NewPlayerEvent.cs,繼承BaseEvent,在OnEventData中處理來自Server的訊息。在流程中,要處理其他線上玩家登入遊戲的情況,所以需要於場景中產生一個新的玩家的GameObject
撰寫完成後,記得將NewPlayerEvent.cs放進場景中的SyncPosition。
此時開啟遊戲測試看看,要從登入畫面進入遊戲。首先輸入帳號aaa。
接著使用其他的設備進行登入,使用帳號bbb。
場景中確實產生了第二個玩家,但是不知道為什麼腳都陷進地面底下了。
看了一下Console,發現有錯誤訊息。
經查詢後,似乎是於Awake階段,透過AddComponent新增NavMeshAgent,會出現這個問題,而且還僅限於Instatiate的時候。我原本放在場景中的Player就一點事情都沒有。為了解決這個問題,只好修改我的Character.cs,將AddComponent<NavMeshAgent>放到Start方法中。
另外,我也有調查網路上針對此問題的其他解決方案,但對我來說都沒有效果,大家參考看看。
https://forum.unity.com/threads/failed-to-create-agent-because-it-is-not-close-enough-to-the-navmesh.125593/
https://forum.unity.com/threads/failed-to-create-agent-because-it-is-not-close-enough-to-the-navmesh.500553/
以下提供本次修改的程式碼:
以下提供Client端原始碼:
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"); } 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 }
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 (); } }
OperationCode.cs:
public enum OperationCode : byte { Login, Signup, SyncPosition, SyncOtherPlayerPosition }
ParameterCode.cs:
public enum ParameterCode : byte { Acccount, Password, Position, x, y, z, AccountList }
EventCode.cs:
public enum EventCode : byte { NewPlayer }
BaseEvent.cs:
using System; using UnityEngine; using ExitGames.Client.Photon; public abstract class BaseEvent : MonoBehaviour { public EventCode eventCode; public abstract void OnEventData(EventData eventData); public virtual void Start(){ PhotonEngine.Instance.AddEvent (this); } public void Destory(){ PhotonEngine.Instance.RemoveEvent (this); } }
NewPlayerEvent.cs:
using System; using ExitGames.Client.Photon; ////// 當Server傳來Event告知有其他Player登入場景,需要在場景添加Player物件的Event /// public class NewPlayerEvent : BaseEvent { SyncPosition syncPosition; void Awake(){ syncPosition = GetComponent(); } public override void OnEventData (EventData eventData) { print ("處理來自Server的NewPlayerEvent"); object account; eventData.Parameters.TryGetValue ((byte)ParameterCode.Acccount, out account); // 通知SyncPosition產生其他的Player物件 syncPosition.AddNewPlayer(account.ToString()); } }
Character.cs:
using System; using UnityEngine; using UnityEngine.AI; using RPG.CameraUI; namespace RPG.Character{ [SelectionBase] public class Character : MonoBehaviour { [Header("Audio")] [Range(0.0f, 1.0f)][SerializeField] float volume = 1f; [Header("Capsule Collider")] [SerializeField] Vector3 colliderCenter = new Vector3(0, 1, 0); [SerializeField] float colliderRadius = 0.3f; [SerializeField] float colliderHeight = 1.5f; [Header("Animator")] [SerializeField] RuntimeAnimatorController animatorController; [SerializeField] AnimatorOverrideController animatorOverrideController; [SerializeField] Avatar avatar; [Range(0.0f, 1.0f)][SerializeField] float animatorForwardCap = 1f; [Header("Movement")] [SerializeField] float moveSpeedMultiplier = 1f; [SerializeField] float animationSpeedMultiplier = 1f; [SerializeField] float movingTurnSpeed = 1800; [SerializeField] float stationaryTurnSpeed = 1800; [SerializeField] float moveThreshold = 1f; [Header("Nav Mesh Agent")] [SerializeField] float steeringSpeed = 1.0f; NavMeshAgent navMeshAgent; Animator animator; Rigidbody myRigidbody; CapsuleCollider capsuleCollider; float turnAmount; float forwardAmount; bool isAlive = true; void Awake(){ AddRequiredComponents (); } private void AddRequiredComponents(){ capsuleCollider = gameObject.AddComponent(); capsuleCollider.center = colliderCenter; capsuleCollider.radius = colliderRadius; capsuleCollider.height = colliderHeight; myRigidbody = gameObject.AddComponent (); myRigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous; myRigidbody.constraints = RigidbodyConstraints.FreezeRotation; AudioSource audioSource = gameObject.AddComponent (); audioSource.volume = volume; animator = gameObject.AddComponent (); animator.runtimeAnimatorController = animatorController; animator.updateMode = AnimatorUpdateMode.AnimatePhysics; animator.avatar = avatar; } void Start(){ // 當Instatiate時,NavMeshAgent在Awake階段進行AddComponent // 會產生下列Bug,故移動到Start內 // Failed to create agent because it is not close enough to the NavMesh navMeshAgent = gameObject.AddComponent (); navMeshAgent.updateRotation = false; navMeshAgent.updatePosition = true; navMeshAgent.speed = steeringSpeed; } void Update(){ if (isAlive && navMeshAgent.remainingDistance > navMeshAgent.stoppingDistance) { Move(navMeshAgent.desiredVelocity); } else { Move (Vector3.zero); } } void Move(Vector3 movement) { SetForwardAndTurn (movement); ApplyExtraTurnRotation(); UpdateAnimator(); } public float GetAnimSpeedMultiplier(){ return animator.speed; } public void Kill(){ isAlive = false; } public void SetDestination(Vector3 worldPos){ if (isAlive) { navMeshAgent.SetDestination (worldPos); } } public void SetStopDistance(float stoppingDistance){ if (isAlive) { navMeshAgent.stoppingDistance = stoppingDistance; } } public AnimatorOverrideController GetOverrideController(){ return animatorOverrideController; } private void SetForwardAndTurn(Vector3 movement){ // 將Wolrd相對的向量轉換成Local相對的向量 if (movement.magnitude > moveThreshold) { movement.Normalize (); } // 將Worldspace的方向轉換成LocalSpace Vector3 localMovement = transform.InverseTransformDirection(movement); turnAmount = Mathf.Atan2(localMovement.x, localMovement.z); forwardAmount = localMovement.z; } void UpdateAnimator() { // update the animator parameters animator.SetFloat("Forward", forwardAmount * animatorForwardCap, 0.1f, Time.deltaTime); animator.SetFloat("Turn", turnAmount, 0.1f, Time.deltaTime); animator.speed = animationSpeedMultiplier; } void ApplyExtraTurnRotation() { // help the character turn faster (this is in addition to root rotation in the animation) float turnSpeed = Mathf.Lerp(stationaryTurnSpeed, movingTurnSpeed, forwardAmount); transform.Rotate(0, turnAmount * turnSpeed * Time.deltaTime, 0); } void OnAnimatorMove(){ if (Time.deltaTime > 0) { Vector3 velocity = (animator.deltaPosition * moveSpeedMultiplier) / Time.deltaTime; // 保持y軸的速度不變 velocity.y = myRigidbody.velocity.y; myRigidbody.velocity = velocity; } } } }
以下提供Server端原始碼:
SyncOtherPlayerHandler.cs:
using System.Collections.Generic; using Photon.SocketServer; using NoliahFantasyServer.Constant; namespace NoliahFantasyServer.Handler { public class SyncOtherPlayerHandler : BaseHandler { public SyncOtherPlayerHandler() { operationCode = OperationCode.SyncOtherPlayerPosition; } public override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters, MyClientPeer myClientPeer) { ListuserAccountList = new List (); foreach(MyClientPeer otherPeer in NoliahFantasyServer.peerList){ if(OtherPeerHasUserAccount(otherPeer, myClientPeer)){ // 紀錄其他玩家的帳號清單 userAccountList.Add(otherPeer.userAccount); NoliahFantasyServer.logger.Info("其他線上玩家的帳號:" + otherPeer.userAccount); } } // 設定回傳Data Dictionary responseData = new Dictionary (); responseData.Add((byte)ParameterCode.AccountList, userAccountList.ToArray()); OperationResponse operationResponse = new OperationResponse( (byte)OperationCode.SyncOtherPlayerPosition, responseData); // 發送Response myClientPeer.SendOperationResponse(operationResponse, sendParameters); // 通知其他Peer有玩家登入場景 foreach (MyClientPeer otherPeer in NoliahFantasyServer.peerList) { NoliahFantasyServer.logger.Info("發送Event給其他線上玩家的帳號:" + otherPeer.userAccount); if (OtherPeerHasUserAccount(otherPeer, myClientPeer)) { Dictionary eventDataDict = new Dictionary (); eventDataDict.Add((byte)ParameterCode.Acccount, myClientPeer.userAccount); EventData eventData = new EventData((byte)EventCode.NewPlayer); eventData.Parameters = eventDataDict; otherPeer.SendEvent(eventData, sendParameters); } } } bool OtherPeerHasUserAccount(MyClientPeer otherPeer, MyClientPeer me){ return string.IsNullOrEmpty(otherPeer.userAccount) == false && otherPeer != me; } } }
SyncPositionHandler.cs:
using System; using System.Collections.Generic; using Photon.SocketServer; using NoliahFantasyServer.Constant; namespace NoliahFantasyServer.Handler { public class SyncPositionHandler : BaseHandler { public SyncPositionHandler() { operationCode = OperationCode.SyncPosition; } public override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters, MyClientPeer myClientPeer) { object xPosition; operationRequest.Parameters.TryGetValue((byte)ParameterCode.x, out xPosition); object yPosition; operationRequest.Parameters.TryGetValue((byte)ParameterCode.y, out yPosition); object zPosition; operationRequest.Parameters.TryGetValue((byte)ParameterCode.z, out zPosition); myClientPeer.xPosition = (float)xPosition; myClientPeer.yPosition = (float)yPosition; myClientPeer.zPosition = (float)zPosition; } } }
LoginHandler.cs:
using Photon.SocketServer; using NoliahFantasyServer.Constant; using NoliahFantasyServer.Manager; namespace NoliahFantasyServer.Handler { public class LoginHandler : BaseHandler { public LoginHandler(){ operationCode = OperationCode.Login; } public override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters, MyClientPeer myClientPeer) { // 從OperationRequest取得帳號密碼資訊 object account; operationRequest.Parameters.TryGetValue((byte)ParameterCode.Acccount, out account); object password; operationRequest.Parameters.TryGetValue((byte)ParameterCode.Password, out password); // 使用UserManager向資料庫驗證使用者的帳號密碼 UserManager userManager = new UserManager(); bool isVerify = userManager.VerifyUser(account as string, password as string); // 使用short類型的ReturnCode簡單回傳結果 OperationResponse operationResponse = new OperationResponse((byte)OperationCode.Login); if(isVerify){ operationResponse.ReturnCode = (short)ReturnCode.Success; }else{ operationResponse.ReturnCode = (short)ReturnCode.Failed; } // 發送Response myClientPeer.SendOperationResponse(operationResponse, sendParameters); // 紀錄User Account NoliahFantasyServer.logger.Info("玩家登入:" + account.ToString()); myClientPeer.userAccount = account.ToString(); } } }
NoliahFantasyServer.cs:
using System.IO; using System.Collections.Generic; using Photon.SocketServer; using ExitGames.Logging; using log4net.Config; using NoliahFantasyServer.Constant; using NoliahFantasyServer.Handler; 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 (); // 當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(); } // Server端關閉的時候 protected override void TearDown() { } 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); } } }
最後來測試遊戲看看吧,這次就成功啦!
可是發現了一個很好笑的Bug,移動自己的時候,另一個複製的玩家也會跟著移動。這個Bug會於下一章介紹位置資訊同步的時候一併解決。
留言
張貼留言