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.
- Add a PhotonTransformView to 'My Robot Kyle' Prefab
- Drag the PhotonTransformView from its header title onto the first observable component entry on the PhotonView component
- Now, check
Synchronize Position
in PhotonTransformView - 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.
- Add a PhotonAnimatorView to
My Robot Kyle
Prefab - Drag the PhotonAnimatorView from its header title onto a new observable component entry in the PhotonView component
- Now, in the Synchronized Parameters, set
Speed
toDiscrete
- Set
Direction
toDiscrete
- Set
Jump
toDiscrete
- Set
Hi
toDisabled
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.
Open the script
PlayerAnimatorManager
Turn the
PlayerAnimatorManager
class from a MonoBehaviour to a MonoBehaviourPun which conveniently exposes thePhotonView
component.In the
Update()
call, insert at the very beginningC#
if (photonView.IsMine == false && PhotonNetwork.IsConnected == true) { return; }
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.
Open the
PlayerManager
script.Insert the code below in between
Awake()
andUpdate()
methodsC#
/// <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); } }
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:
- Disable
Follow on Start
on theCameraWork
component on the prefabMy Robot Kyle
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
Open the Script
PlayerManager
Surround the input processing call with an if statement.
C#
if (photonView.IsMine) { ProcessInputs (); }
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.
Open the Script
PlayerManager
Implement
IPunObservable
C#
public class PlayerManager : MonoBehaviourPunCallbacks, IPunObservable { #region IPunObservable implementation public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info) { } #endregion
Inside
IPunObservable.OnPhotonSerializeView
add the following codeC#
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(); }
Save the Script
PlayerManager
Back to Unity editor, Select the
My Robot Kyle
prefab in your assets, and add an observe entry on the PhotonView component and drag thePlayerManager
component to it
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.
Open the Script
PlayerManager
Inside
IPunObservable.OnPhotonSerializeView
after youSendNext
andReceiveNext
theIsFiring
variable, do the same forHealth
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(); }
Save the Script
PlayerManager
That's all it takes in this scenario for synchronizing the Health
variable.