This document is about: PUN 1
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, its representation on other computers move and rotation in a similar way.

You could observe directly the Transform component in your own Script, but you would run into a lot of troubles, due to network latency and effectiveness of data being synchronized.
Luckily and to make this common task easier, we are going to use a [Photon Transform View] component, acting as a "middleman" between the Transform component and the PhotonView.
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 in the PhotonView component
    Drag & Drop PhotonTransformView
    Drag & Drop PhotonTransformView
  3. Now, check Synchronize Position in PhotonTransformView
  4. Within Synchronize Position, Choose "Lerp Value for Interpolation Option
  5. Set the Lerp Speed to 10 (the more the quicker it will catch up)
  6. Check SynchronizeRotation
PhotonTransformView Prefab
PhotonTransformView Prefab
Tip: notice the blue book help link within the sub sections. click on it to reveal information and learn about all the various settings and their effects
PhotonTransformView Help
PhotonTransformView Help

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 may 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 the 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).
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), 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 represent 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 Photon.MonoBehaviour which expose conveniently the photonView component.

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

    C#

    if (photonView.isMine == false && PhotonNetwork.connected == 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 set up earlier.

But, why having then to enforce PhotonNetwork.connected == 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 say.
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 effectively makes the camera follow that 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:

Disable Follow on Start on the CameraWork component on the prefab My Robot Kyle

CameraWork FollowOnStart Off
CameraWork FollowOnStart Off

This effectively now hand 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 is 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 other instance fires! 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

    IPunObservable Implementation
    IPunObservable Implementation
  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 in 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 are passed a variable stream, this is what is going to be sent over the network and this call if our chance to read and write data.
We can only write when we are the localPlayer (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 to the stream of data our IsFiring value, 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