// ---------------------------------------------------------------------------- // // PhotonNetwork Framework for Unity - Copyright (C) 2018 Exit Games GmbH // // // Component to synchronize Transforms via PUN PhotonView. // // developer@exitgames.com // ---------------------------------------------------------------------------- namespace Photon.Pun { using UnityEngine; using System.Collections.Generic; /// /// This class helps you to synchronize position, rotation and scale /// of a GameObject. It also gives you many different options to make /// the synchronized values appear smooth, even when the data is only /// send a couple of times per second. /// Simply add the component to your GameObject and make sure that /// the PhotonTransformViewClassic is added to the list of observed components /// [AddComponentMenu("Photon Networking/Photon Transform View Classic")] public class PhotonTransformViewClassic : MonoBehaviourPun, IPunObservable { //As this component is very complex, we separated it into multiple classes. //The PositionModel, RotationModel and ScaleMode store the data you are able to //configure in the inspector while the "control" objects below are actually moving //the object and calculating all the inter- and extrapolation [HideInInspector] public PhotonTransformViewPositionModel m_PositionModel = new PhotonTransformViewPositionModel(); [HideInInspector] public PhotonTransformViewRotationModel m_RotationModel = new PhotonTransformViewRotationModel(); [HideInInspector] public PhotonTransformViewScaleModel m_ScaleModel = new PhotonTransformViewScaleModel(); PhotonTransformViewPositionControl m_PositionControl; PhotonTransformViewRotationControl m_RotationControl; PhotonTransformViewScaleControl m_ScaleControl; PhotonView m_PhotonView; bool m_ReceivedNetworkUpdate = false; /// /// Flag to skip initial data when Object is instantiated and rely on the first deserialized data instead. /// bool m_firstTake = false; void Awake() { this.m_PhotonView = GetComponent(); this.m_PositionControl = new PhotonTransformViewPositionControl(this.m_PositionModel); this.m_RotationControl = new PhotonTransformViewRotationControl(this.m_RotationModel); this.m_ScaleControl = new PhotonTransformViewScaleControl(this.m_ScaleModel); } void OnEnable() { m_firstTake = true; } void Update() { if (this.m_PhotonView == null || this.m_PhotonView.IsMine == true || PhotonNetwork.IsConnectedAndReady == false) { return; } this.UpdatePosition(); this.UpdateRotation(); this.UpdateScale(); } void UpdatePosition() { if (this.m_PositionModel.SynchronizeEnabled == false || this.m_ReceivedNetworkUpdate == false) { return; } transform.localPosition = this.m_PositionControl.UpdatePosition(transform.localPosition); } void UpdateRotation() { if (this.m_RotationModel.SynchronizeEnabled == false || this.m_ReceivedNetworkUpdate == false) { return; } transform.localRotation = this.m_RotationControl.GetRotation(transform.localRotation); } void UpdateScale() { if (this.m_ScaleModel.SynchronizeEnabled == false || this.m_ReceivedNetworkUpdate == false) { return; } transform.localScale = this.m_ScaleControl.GetScale(transform.localScale); } /// /// These values are synchronized to the remote objects if the interpolation mode /// or the extrapolation mode SynchronizeValues is used. Your movement script should pass on /// the current speed (in units/second) and turning speed (in angles/second) so the remote /// object can use them to predict the objects movement. /// /// The current movement vector of the object in units/second. /// The current turn speed of the object in angles/second. public void SetSynchronizedValues(Vector3 speed, float turnSpeed) { this.m_PositionControl.SetSynchronizedValues(speed, turnSpeed); } public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { this.m_PositionControl.OnPhotonSerializeView(transform.localPosition, stream, info); this.m_RotationControl.OnPhotonSerializeView(transform.localRotation, stream, info); this.m_ScaleControl.OnPhotonSerializeView(transform.localScale, stream, info); if (stream.IsReading == true) { this.m_ReceivedNetworkUpdate = true; // force latest data to avoid initial drifts when player is instantiated. if (m_firstTake) { m_firstTake = false; if (this.m_PositionModel.SynchronizeEnabled) { this.transform.localPosition = this.m_PositionControl.GetNetworkPosition(); } if (this.m_RotationModel.SynchronizeEnabled) { this.transform.localRotation = this.m_RotationControl.GetNetworkRotation(); } if (this.m_ScaleModel.SynchronizeEnabled) { this.transform.localScale = this.m_ScaleControl.GetNetworkScale(); } } } } } [System.Serializable] public class PhotonTransformViewPositionModel { public enum InterpolateOptions { Disabled, FixedSpeed, EstimatedSpeed, SynchronizeValues, Lerp } public enum ExtrapolateOptions { Disabled, SynchronizeValues, EstimateSpeedAndTurn, FixedSpeed, } public bool SynchronizeEnabled; public bool TeleportEnabled = true; public float TeleportIfDistanceGreaterThan = 3f; public InterpolateOptions InterpolateOption = InterpolateOptions.EstimatedSpeed; public float InterpolateMoveTowardsSpeed = 1f; public float InterpolateLerpSpeed = 1f; public ExtrapolateOptions ExtrapolateOption = ExtrapolateOptions.Disabled; public float ExtrapolateSpeed = 1f; public bool ExtrapolateIncludingRoundTripTime = true; public int ExtrapolateNumberOfStoredPositions = 1; } public class PhotonTransformViewPositionControl { PhotonTransformViewPositionModel m_Model; float m_CurrentSpeed; double m_LastSerializeTime; Vector3 m_SynchronizedSpeed = Vector3.zero; float m_SynchronizedTurnSpeed = 0; Vector3 m_NetworkPosition; Queue m_OldNetworkPositions = new Queue(); bool m_UpdatedPositionAfterOnSerialize = true; public PhotonTransformViewPositionControl(PhotonTransformViewPositionModel model) { m_Model = model; } Vector3 GetOldestStoredNetworkPosition() { Vector3 oldPosition = m_NetworkPosition; if (m_OldNetworkPositions.Count > 0) { oldPosition = m_OldNetworkPositions.Peek(); } return oldPosition; } /// /// These values are synchronized to the remote objects if the interpolation mode /// or the extrapolation mode SynchronizeValues is used. Your movement script should pass on /// the current speed (in units/second) and turning speed (in angles/second) so the remote /// object can use them to predict the objects movement. /// /// The current movement vector of the object in units/second. /// The current turn speed of the object in angles/second. public void SetSynchronizedValues(Vector3 speed, float turnSpeed) { m_SynchronizedSpeed = speed; m_SynchronizedTurnSpeed = turnSpeed; } /// /// Calculates the new position based on the values setup in the inspector /// /// The current position. /// The new position. public Vector3 UpdatePosition(Vector3 currentPosition) { Vector3 targetPosition = GetNetworkPosition() + GetExtrapolatedPositionOffset(); switch (m_Model.InterpolateOption) { case PhotonTransformViewPositionModel.InterpolateOptions.Disabled: if (m_UpdatedPositionAfterOnSerialize == false) { currentPosition = targetPosition; m_UpdatedPositionAfterOnSerialize = true; } break; case PhotonTransformViewPositionModel.InterpolateOptions.FixedSpeed: currentPosition = Vector3.MoveTowards(currentPosition, targetPosition, Time.deltaTime * m_Model.InterpolateMoveTowardsSpeed); break; case PhotonTransformViewPositionModel.InterpolateOptions.EstimatedSpeed: if (m_OldNetworkPositions.Count == 0) { // special case: we have no previous updates in memory, so we can't guess a speed! break; } // knowing the last (incoming) position and the one before, we can guess a speed. // note that the speed is times sendRateOnSerialize! we send X updates/sec, so our estimate has to factor that in. float estimatedSpeed = (Vector3.Distance(m_NetworkPosition, GetOldestStoredNetworkPosition()) / m_OldNetworkPositions.Count) * PhotonNetwork.SerializationRate; // move towards the targetPosition (including estimates, if that's active) with the speed calculated from the last updates. currentPosition = Vector3.MoveTowards(currentPosition, targetPosition, Time.deltaTime * estimatedSpeed); break; case PhotonTransformViewPositionModel.InterpolateOptions.SynchronizeValues: if (m_SynchronizedSpeed.magnitude == 0) { currentPosition = targetPosition; } else { currentPosition = Vector3.MoveTowards(currentPosition, targetPosition, Time.deltaTime * m_SynchronizedSpeed.magnitude); } break; case PhotonTransformViewPositionModel.InterpolateOptions.Lerp: currentPosition = Vector3.Lerp(currentPosition, targetPosition, Time.deltaTime * m_Model.InterpolateLerpSpeed); break; } if (m_Model.TeleportEnabled == true) { if (Vector3.Distance(currentPosition, GetNetworkPosition()) > m_Model.TeleportIfDistanceGreaterThan) { currentPosition = GetNetworkPosition(); } } return currentPosition; } /// /// Gets the last position that was received through the network /// /// public Vector3 GetNetworkPosition() { return m_NetworkPosition; } /// /// Calculates an estimated position based on the last synchronized position, /// the time when the last position was received and the movement speed of the object /// /// Estimated position of the remote object public Vector3 GetExtrapolatedPositionOffset() { float timePassed = (float)(PhotonNetwork.Time - m_LastSerializeTime); if (m_Model.ExtrapolateIncludingRoundTripTime == true) { timePassed += (float)PhotonNetwork.GetPing() / 1000f; } Vector3 extrapolatePosition = Vector3.zero; switch (m_Model.ExtrapolateOption) { case PhotonTransformViewPositionModel.ExtrapolateOptions.SynchronizeValues: Quaternion turnRotation = Quaternion.Euler(0, m_SynchronizedTurnSpeed * timePassed, 0); extrapolatePosition = turnRotation * (m_SynchronizedSpeed * timePassed); break; case PhotonTransformViewPositionModel.ExtrapolateOptions.FixedSpeed: Vector3 moveDirection = (m_NetworkPosition - GetOldestStoredNetworkPosition()).normalized; extrapolatePosition = moveDirection * m_Model.ExtrapolateSpeed * timePassed; break; case PhotonTransformViewPositionModel.ExtrapolateOptions.EstimateSpeedAndTurn: Vector3 moveDelta = (m_NetworkPosition - GetOldestStoredNetworkPosition()) * PhotonNetwork.SerializationRate; extrapolatePosition = moveDelta * timePassed; break; } return extrapolatePosition; } public void OnPhotonSerializeView(Vector3 currentPosition, PhotonStream stream, PhotonMessageInfo info) { if (m_Model.SynchronizeEnabled == false) { return; } if (stream.IsWriting == true) { SerializeData(currentPosition, stream, info); } else { DeserializeData(stream, info); } m_LastSerializeTime = PhotonNetwork.Time; m_UpdatedPositionAfterOnSerialize = false; } void SerializeData(Vector3 currentPosition, PhotonStream stream, PhotonMessageInfo info) { stream.SendNext(currentPosition); m_NetworkPosition = currentPosition; if (m_Model.ExtrapolateOption == PhotonTransformViewPositionModel.ExtrapolateOptions.SynchronizeValues || m_Model.InterpolateOption == PhotonTransformViewPositionModel.InterpolateOptions.SynchronizeValues) { stream.SendNext(m_SynchronizedSpeed); stream.SendNext(m_SynchronizedTurnSpeed); } } void DeserializeData(PhotonStream stream, PhotonMessageInfo info) { Vector3 readPosition = (Vector3)stream.ReceiveNext(); if (m_Model.ExtrapolateOption == PhotonTransformViewPositionModel.ExtrapolateOptions.SynchronizeValues || m_Model.InterpolateOption == PhotonTransformViewPositionModel.InterpolateOptions.SynchronizeValues) { m_SynchronizedSpeed = (Vector3)stream.ReceiveNext(); m_SynchronizedTurnSpeed = (float)stream.ReceiveNext(); } if (m_OldNetworkPositions.Count == 0) { // if we don't have old positions yet, this is the very first update this client reads. let's use this as current AND old position. m_NetworkPosition = readPosition; } // the previously received position becomes the old(er) one and queued. the new one is the m_NetworkPosition m_OldNetworkPositions.Enqueue(m_NetworkPosition); m_NetworkPosition = readPosition; // reduce items in queue to defined number of stored positions. while (m_OldNetworkPositions.Count > m_Model.ExtrapolateNumberOfStoredPositions) { m_OldNetworkPositions.Dequeue(); } } } [System.Serializable] public class PhotonTransformViewRotationModel { public enum InterpolateOptions { Disabled, RotateTowards, Lerp, } public bool SynchronizeEnabled; public InterpolateOptions InterpolateOption = InterpolateOptions.RotateTowards; public float InterpolateRotateTowardsSpeed = 180; public float InterpolateLerpSpeed = 5; } public class PhotonTransformViewRotationControl { PhotonTransformViewRotationModel m_Model; Quaternion m_NetworkRotation; public PhotonTransformViewRotationControl(PhotonTransformViewRotationModel model) { m_Model = model; } /// /// Gets the last rotation that was received through the network /// /// public Quaternion GetNetworkRotation() { return m_NetworkRotation; } public Quaternion GetRotation(Quaternion currentRotation) { switch (m_Model.InterpolateOption) { default: case PhotonTransformViewRotationModel.InterpolateOptions.Disabled: return m_NetworkRotation; case PhotonTransformViewRotationModel.InterpolateOptions.RotateTowards: return Quaternion.RotateTowards(currentRotation, m_NetworkRotation, m_Model.InterpolateRotateTowardsSpeed * Time.deltaTime); case PhotonTransformViewRotationModel.InterpolateOptions.Lerp: return Quaternion.Lerp(currentRotation, m_NetworkRotation, m_Model.InterpolateLerpSpeed * Time.deltaTime); } } public void OnPhotonSerializeView(Quaternion currentRotation, PhotonStream stream, PhotonMessageInfo info) { if (m_Model.SynchronizeEnabled == false) { return; } if (stream.IsWriting == true) { stream.SendNext(currentRotation); m_NetworkRotation = currentRotation; } else { m_NetworkRotation = (Quaternion)stream.ReceiveNext(); } } } [System.Serializable] public class PhotonTransformViewScaleModel { public enum InterpolateOptions { Disabled, MoveTowards, Lerp, } public bool SynchronizeEnabled; public InterpolateOptions InterpolateOption = InterpolateOptions.Disabled; public float InterpolateMoveTowardsSpeed = 1f; public float InterpolateLerpSpeed; } public class PhotonTransformViewScaleControl { PhotonTransformViewScaleModel m_Model; Vector3 m_NetworkScale = Vector3.one; public PhotonTransformViewScaleControl(PhotonTransformViewScaleModel model) { m_Model = model; } /// /// Gets the last scale that was received through the network /// /// public Vector3 GetNetworkScale() { return m_NetworkScale; } public Vector3 GetScale(Vector3 currentScale) { switch (m_Model.InterpolateOption) { default: case PhotonTransformViewScaleModel.InterpolateOptions.Disabled: return m_NetworkScale; case PhotonTransformViewScaleModel.InterpolateOptions.MoveTowards: return Vector3.MoveTowards(currentScale, m_NetworkScale, m_Model.InterpolateMoveTowardsSpeed * Time.deltaTime); case PhotonTransformViewScaleModel.InterpolateOptions.Lerp: return Vector3.Lerp(currentScale, m_NetworkScale, m_Model.InterpolateLerpSpeed * Time.deltaTime); } } public void OnPhotonSerializeView(Vector3 currentScale, PhotonStream stream, PhotonMessageInfo info) { if (m_Model.SynchronizeEnabled == false) { return; } if (stream.IsWriting == true) { stream.SendNext(currentScale); m_NetworkScale = currentScale; } else { m_NetworkScale = (Vector3)stream.ReceiveNext(); } } } }