This document is about: PUN 1
SWITCH TO

PUN Classic (v1)、PUN 2 和 Bolt 處於維護模式。 PUN 2 將支援 Unity 2019 至 2022,但不會添加新功能。 當然,您所有的 PUN & Bolt 專案可以用已知性能繼續運行使用。 對於任何即將開始或新的專案:請切換到 Photon Fusion 或 Quantum。

Lag Compensation

Lag Compensation for Physic Objects

When you have physic objects in your game, you may have noticed that those objects might run slightly out of synchronization - especially when you have two or more game windows next to each other. This can lead to some serious problems with the game and might also lower the players’ experience in the end.

This kind of synchronization issues are caused by the time it takes for a message to ‘travel’ from one client to another. An example: Client A moves his character forward and sends his current position. This message is received by Client B just 100ms after it has been sent by Client A. Client B uses this information, to place Client A’s character on the correct position in order to keep his game up to date. Since Client A didn’t stop moving his character forward for the last 100ms, his character reached a new position in the world. In this moment the object isn’t fully synchronized any longer because it has a different position in the game of Client A and Client B. Based on the character’s movement speed, the difference between both positions varies: if the movement is rather slow the difference might not be noticeable at all - however if the movement speed is very high the difference is clearly visible on both game windows. Since we can’t get fully rid of this problem (unless we are using another technology like Photon Quantum), we’re trying to reduce the appearance of this problem as much as possible and introducing a technique we call ‘Lag Compensation’.

What does Lag Compensation mean and how does it work?

When applying Lag Compensation to our physic object we are asking the owner of the object to send additional data besides the object’s position and rotation: we are looking for the object’s velocity in this case. Instead of simply applying the received information to the object on the remote client we are using them in order to calculate a more up to date and a more accurate behaviour of the object. Therefore we additionally need the exact time that has passed between sending and receiving the message. Since our example uses a custom OnPhotonSerializeView solution (see below) we are showing how to calculate the passed time based on this function.

Firstly we need an empty OnPhotonSerializeView implementation:

C#

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { }

Since the sending side of this function’s implementation is relatively straightforward and will be shown later, we will take a look at the receiving side first as most of the work will be done here. One thing a receiving client has to do is to calculate the previously mentioned time which has passed between sending and receiving the current message. Therefore we are using the PhotonMessageInfo which contains a timestamp which describes the moment the message has been sent. Additionally we are using PhotonNetwork.time to calculate the difference between the current time and the previously mentioned timestamp. The result is the time that has passed in between.

C#

    float lag = Mathf.Abs((float) (PhotonNetwork.time - info.timestamp));

Having this value we can calculate how the object might have been moved based on the information we received from the owner. To do so we have two different options which are described below.

Using OnPhotonSerializeView to update the object

The first option simply uses the OnPhotonSerializeView function to update the object. Based on our empty OnPhotonSerializeView function the sender shares all necessary information with other clients. In this case we are sending the Rigidbody’s position, rotation as well as its velocity. The receiver stores the received information directly in the Rigidbody component of the object before he calculates the time which has passed as already described above. Afterwards he multiplies the velocity by the passed time he has calculated before. The result of this calculation is then added up to the position of the Rigidbody component. We now have a more accurate described object on our remote client. For a better understanding the entire implementation of the OnPhotonSerializeView function is shown below.

C#

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.isWriting)
        {
            stream.SendNext(rigidbody.position);
            stream.SendNext(rigidbody.rotation);
            stream.SendNext(rigidbody.velocity);
        }
        else
        {
            rigidbody.position = (Vector3) stream.ReceiveNext();
            rigidbody.rotation = (Quaternion) stream.ReceiveNext();
            rigidbody.velocity = (Vector3) stream.ReceiveNext();

            float lag = Mathf.Abs((float) (PhotonNetwork.time - info.timestamp));
            rigidbody.position += rigidbody.velocity * lag;
        }
    }

Using OnPhotonSerializeView and FixedUpdate to update the object

The second option uses Unity’s FixedUpdate function besides an OnPhotonSerializeView implementation. We are starting with our empty OnPhotonSerializeView function again. In this approach the sender has the same task: sharing information about the Rigidbody’s position, rotation as well as its velocity. The tasks of the receiver are different compared to the previous approach. This time he only stores the received velocity information to the Rigidbody component of the object before calculating the time which has passed between sending and receiving the current message. The other information - position and rotation - are stored in local variables at this point. For this example the local variables are called networkPosition (type Vector3) and networkRotation (type Quaternion). Afterwards the receiver multiplies the Rigidbody’s velocity by the passed time and adds up the result of this calculation to the locally stored networkPosition variable. The entire implementation of the OnPhotonSerializeView function is shown below.

C#

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.isWriting)
        {
            stream.SendNext(this.m_Body.position);
            stream.SendNext(this.m_Body.rotation);
            stream.SendNext(this.m_Body.velocity);
        }
        else
        {
            networkPosition = (Vector3) stream.ReceiveNext();
            networkRotation = (Quaternion) stream.ReceiveNext();
            rigidbody.velocity = (Vector3) stream.ReceiveNext();

            float lag = Mathf.Abs((float) (PhotonNetwork.time - info.timestamp));
            networkPosition += (this.m_Body.velocity * lag);
        }
    }

You have definitely noticed that we haven’t applied any position or rotation updates to the object so far. This will be done in the next step when we are moving the object to its target position and rotating it to its target rotation. We are doing this stepwise inside Unity’s FixedUpdate function which is shown below.

C#

    public void FixedUpdate()
    {
        if (!photonView.IsMine)
        {
            rigidbody.position = Vector3.MoveTowards(rigidbody.position, networkPosition, Time.fixedDeltaTime);
            rigidbody.rotation = Quaternion.RotateTowards(rigidbody.rotation, networkRotation, Time.fixedDeltaTime * 100.0f);
        }
    }

Lag Compensation for non-Physic Objects

Sometimes you have objects in your game, that don’t have an attached Rigidbody component because it’s not necessary. In this case you are synchronizing them using their Transform component. At this point you may have also noticed that there is some delay and a slightly offset between the same object on two (or more) different screens, especially when having the game windows next to each other. The good news is that we can use what we have learned before also for those objects with some adjustments.

At first we need an option to describe the movement of the object. Since we don’t have the velocity property of the Rigidbody component, we have to use a custom solution. An easy approach here is to use the difference between the last two positions of the object.

C#

    Vector3 oldPosition = transform.position;

    // Handling position updates related to the given input

    movement = transform.position - oldPosition;

Firstly we are storing the current position in a temporary variable called oldPosition. Afterwards we are processing all of our input and update the position of the object according to this. In the end we are calculating the difference between the locally stored and the updated position, which describes the movement (type Vector3) of our object and is furthermore our ‘replacement’ for the velocity property of the Rigidbody component. This code snippet is part of the Update function.

The rest is basically the same as shown in the previous approach, but instead of using the FixedUpdate function, we are using the Update function this time where we can add the following code snippet at the beginning.

C#

    if (!pView.isMine)
    {
        transform.position = Vector3.MoveTowards(transform.position, networkPosition, Time.deltaTime * movementSpeed);
        transform.rotation = Quaternion.RotateTowards(transform.rotation, networkRotation, Time.deltaTime * 100);
        return;
    }

This way we can make use of Lag Compensation even when our objects don’t have a Rigidbody component.

Conclusion

Lag Compensation won’t help you to get rid of all kind of synchronization issues you might have in your game, but it will help you to get a more stable game and a more stable simulation with much less synchronization issues overall, which would badly affect players’ experience.

Back to top