This document is about: PUN 2
SWITCH TO

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

7 - Player Networking

This section will guide you to modify the "Player" prefab.
We've first created a player that works as is, but now we are going to modify it so that it works and complies when we use it within PUN environment.
The modifications are very light, however the concepts are critical.
So this section is very important indeed.

Transform Synchronization

The obvious feature we want to synchronize is the Position and Rotation of the character so that when a player is moving around, the character behave in a similar way on other players' instances of the game.

You could manually observe the Transform component in your own Script, but you would run into a lot of trouble, due to network latency, and effectiveness of data being synchronized.
Luckily, and to make this common task easier, we are going to use a PhotonTransformView component.
Basically, all the hard work has been done for you with this Component.

  1. Add a PhotonTransformView to 'My Robot Kyle' Prefab
  2. Drag the PhotonTransformView from its header title onto the first observable component entry on the PhotonView component
  3. Now, check Synchronize Position in PhotonTransformView
  4. Check Synchronize Rotation

Animator Synchronization

The PhotonAnimatorView is also making networking setup a breeze and will save you a lot of time and trouble.
It allows you to define which layer weights and which parameters you want to synchronize.
Layer weights only need to be synchronized if they change during the game and it might be possible to get away with not synchronizing them at all.
The same is true for parameters.
Sometimes it is possible to derive animator values from other factors.
A speed value is a good example for this, you don't necessarily need to have this value synchronized exactly but you can use the synchronized position updates to estimate its value.
If possible, try to synchronize as little parameters as you can get away with.

  1. Add a PhotonAnimatorView to My Robot Kyle Prefab
  2. Drag the PhotonAnimatorView from its header title onto a new observable component entry in the PhotonView component
  3. Now, in the Synchronized Parameters, set Speed to Discrete
  4. Set Direction to Discrete
  5. Set Jump to Discrete
  6. Set Hi to Disabled
Observe PhotonAnimatorView
Observe PhotonAnimatorView

Each value can be disabled, or synchronized either discretely or continuously.
In our case since we are not using the Hi parameter, we'll disable it and save traffic.

Discrete synchronization means that a value gets sent 10 times a second (in OnPhotonSerializeView) by default.
The receiving clients pass the value on to their local Animator.

Continuous synchronization means that the PhotonAnimatorView runs every frame.
When OnPhotonSerializeView is called (10 times per second by default), the values recorded since the last call are sent together.
The receiving client then applies the values in sequence to retain smooth transitions.
While this mode is smoother, it also sends more data to achieve this effect.

User Input Management

A critical aspect of user control over the network is that the same prefab will be instantiated for all players, but only one of them represents the user actually playing in front of the computer,
all other instances represents other users, playing on other computers.
So the first hurdle with this in mind is "Input Management".
How can we enable input on one instance and not on others and how to know which one is the right one?
Enter the IsMine concept.

Let's edit the PlayerAnimatorManager script we created earlier.
In its current form, this script doesn't know about this distinction, let's implement this.

  1. Open the script PlayerAnimatorManager

  2. Turn the PlayerAnimatorManager class from a MonoBehaviour to a MonoBehaviourPun which conveniently exposes the PhotonView component.

  3. In the Update() call, insert at the very beginning

    C#

    if (photonView.IsMine == false && PhotonNetwork.IsConnected == true)
    {
        return;
    }
    
  4. Save the Script PlayerAnimatorManager

Ok, photonView.IsMine will be true if the instance is controlled by the 'client' application, meaning this instance represents the physical person playing on this computer within this application.
So if it is false, we don't want to do anything and solely rely on the PhotonView component to synchronize the transform and animator components we've setup earlier.

But, why having then to enforce PhotonNetwork.IsConnected == true in our if statement? eh eh :) because during development, we may want to test this prefab without being connected.
In a dummy scene for example, just to create and validate code that is not related to networking features per se.
And so with this additional expression, we will allow input to be used if we are not connected. It's a very simple trick and will greatly improve your workflow during development.

Camera Control

It's the same as input, the player only has one view of the game, and so we need the CameraWork script to only follow the local player, not the other players.
That's why the CameraWork script has this ability to define when to follow.

Let's modify the PlayerManager script to control the CameraWork component.

  1. Open the PlayerManager script.

  2. Insert the code below in between Awake() and Update() methods

    C#

            /// <summary>
            /// MonoBehaviour method called on GameObject by Unity during initialization phase.
            /// </summary>
            void Start()
            {
                CameraWork _cameraWork = this.gameObject.GetComponent<CameraWork>();
    
                if (_cameraWork != null)
                {
                    if (photonView.IsMine)
                    {
                        _cameraWork.OnStartFollowing();
                    }
                }
                else
                {
                    Debug.LogError("<Color=Red><a>Missing</a></Color> CameraWork Component on playerPrefab.", this);
                }
            }
    
  3. Save the Script PlayerManager

First, it gets the CameraWork component, we expect this, so if we don't find it, we log an error.
Then, if photonView.IsMine is true, it means we need to follow this instance, and so we call _cameraWork.OnStartFollowing() which effectivly makes the camera follow that very instance in the scene.

All other player instances will have their photonView.IsMine set as false, and so their respective _cameraWork will do nothing.

One last change to make this work:

  1. Disable Follow on Start on the CameraWork component on the prefab My Robot Kyle
CameraWork FollowOnStart Off
CameraWork FollowOnStart Off

This effectively now hands over the logic to follow the player to the script PlayerManager that will call _cameraWork.OnStartFollowing() as described above.

Beams Fire Control

Firing also follow the input principle exposed above, it needs to only be working if photonView.IsMine is true

  1. Open the Script PlayerManager

  2. Surround the input processing call with an if statement.

    C#

    if (photonView.IsMine) 
    {
        ProcessInputs ();
    }
    
  3. Save the Script PlayerManager

However, when testing this, we only see the local player firing.
We need to see when another instance fires, too. We need a mechanism for synchronizing the firing across the network.
To do this, we are going to manually synchronize the IsFiring boolean value, until now, we got away with PhotonTransformView and PhotonAnimatorView to do all the internal synchronization of
variables for us, we only had to tweak what was conveniently exposed to us via the Unity Inspector, but here what we need is very specific to your game, and so we'll need to do this manually.

  1. Open the Script PlayerManager

  2. Implement IPunObservable

    C#

    public class PlayerManager : MonoBehaviourPunCallbacks, IPunObservable
    {
        #region IPunObservable implementation
    
        public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
        {
        }
    
        #endregion
    
  3. Inside IPunObservable.OnPhotonSerializeView add the following code

    C#

    if (stream.IsWriting)
    {
        // We own this player: send the others our data
        stream.SendNext(IsFiring);
    }
    else
    {
        // Network player, receive data
        this.IsFiring = (bool)stream.ReceiveNext();
    }
    
  4. Save the Script PlayerManager

  5. Back to Unity editor, Select the My Robot Kyle prefab in your assets, and add an observe entry on the PhotonView component and drag the PlayerManager component to it

Observe PhotonAnimatorView
Observe PlayerManager

Without this last step, IPunObservable.OnPhotonSerializeView is never called because it is not observed by a PhotonView.

In this IPunObservable.OnPhotonSerializeView method, we get a variable stream, this is what is going to be send over the network, and this call is our chance to read and write data.
We can only write when we are the local player (photonView.IsMine == true), else we read.

Since the stream class has helpers to know what to do, we simply rely on stream.isWriting to know what is expected in the current instance case.

If we are expected to write data, we append our IsFiring value to the stream of data, using stream.SendNext(), a very convenient method that hides away all the hard work of data serialization.
If we are expected to read, we use stream.ReceiveNext().

Health Synchronization

Ok, to finish with updating player features for networking, we'll synchronize the health value so that each instance of the player will have the right health value.
This is exactly the same principle as the IsFiring value we just covered above.

  1. Open the Script PlayerManager

  2. Inside IPunObservable.OnPhotonSerializeView after you SendNext and ReceiveNext the IsFiring variable, do the same for Health

    C#

    if (stream.IsWriting)
    {
        // We own this player: send the others our data
        stream.SendNext(IsFiring);
        stream.SendNext(Health);
    }
    else
    {
        // Network player, receive data
        this.IsFiring = (bool)stream.ReceiveNext();
        this.Health = (float)stream.ReceiveNext();
    }
    
  3. Save the Script PlayerManager

That's all it takes in this scenario for synchronizing the Health variable.

Next Part.
Previous Part.

Back to top