Hi again, as promised here is the next instalment of my networking tutorial for PUN, albeit rather a long time coming!
Before we start I should point out a couple of things. Firstly I have moved on to a newer version of Unity (V2017.2.0f3) and also a new version of PUN (V1.87), and you may encounter some errors if you are still using an older version of Unity, so I would recommend you upgrade your Unity version to match before following this tutorial.
Secondly, I am changing the format of the tutorial slightly, and as such I will now provide the full project for you to download at the beginning of each part of the tutorial; I will then go on to provide explanations of all the new script changes and additions.
The reasoning behind this is that as the tutorial progresses, the game setup will become more complex and I will end up spending more time describing how to add and make changes to the game scene, HUD, game objects etc. than describing how the networking code works, and as this is above all a networking tutorial I didn’t think that was an ideal scenario.
You can download the project files for this part here.
(To run the game in the editor, load Assets/Main Scene and press play.)
You can download the project .exe here.
New scripts added in this part 5.
PlayerShoot.cs
PlayerScore.cs
PlayerHealth.cs
Missile.cs
Part 5a – Player Shooting
The first thing we want to do is give our players the ability to fire their guns and to achieve this I have added a new script to our player prefab called PlayerShoot.cs.
You’ll notice that the PlayerShoot script is a PunBehaviour rather than a mono behaviour. We can do this because it is a component of a GameObject that has a PhotonView component, and by making the PlayerShoot script a PunBehaviour it automatically gets access to the cached photonView and all the Photon events and callbacks.
The PlayerShoot script starts off by setting up a reference to the missile prefab (set in the inspector) and setting a couple of variable values that will be used to control the rate of fire.
We only want this script to run on the PC of the local player, and the easiest way to achieve this is to disable it on all clients where photonView.isMine is false, which we can do in the Awake function as follows.
In the Update function, we check the fire timeout and fire button state to see if we should launch a missile and if both the timeout is up and the fire button is pressed, we can launch one using an RPC call, which I will explain below.
I decided not to give the missile a PhotonView and instead have it as a non-networked object as this will save on network traffic; however, that does mean we can’t use PhotonInstantiate to create a copy of it on every client at once. So if we implemented the missile launch as a normal function, it would only fire a missile on the local client and other players in the game wouldn’t see it.
To overcome this we will give the missile launch function the PunRPC attribute, which marks it as a Remote Procedure Call (RPC), which we invoke with the PhotoView.RPC() method. Essentially what this does is cause the invoked function to run on one or more remote clients as well as our own (depending on the arguments supplied) and we can use this to launch the missile on all clients.
The PhotonView.RPC function takes a minimum of two arguments, the first being the string name of the function we want to invoke and the second is a target player or an enumeration that determines which clients should run the function. In this instance, we give it the name of our missile launch function “RPC_FireMissile” and PhotonTargets.All for the second argument. This means that the function will run on all clients including the player that fired the missile on the GameObject with the corresponding photonView.
RPCs can also take additional parameters, and I will cover that later in the tutorial.
This is the missile launch function. I give all my RPC functions the RPC_ prefix, however, this is a personal preference and isn’t required.
You’ll notice that although we didn’t pass any parameters to the RPC, it has actually got one parameter (PhotonMessageInfo info), this is automatically available for all RPCs and contains a couple of often useful bits of information. In this instance, we will use it to obtain the photonView of the player that fired the missile. (the PhotonMessageInfo parameter can be omitted if you don’t need any of the information it provides).
So the first thing the fire missile script does is instantiate a new missile with the position and rotation of the player object (which this script is attached to) and cache a reference to it. Then it passes the photonView of the player that fired the missile to the missile script so we can know who owns this missile. This will allow us to easily identify who should get a score increase if the missile hits another player. Lastly, it sets the missile GameObject active.
As far as the PlayerShoot script is concerned that’s it for the missile, movement and collisions are handled elsewhere; so it can relax until the next time the fire button is pressed.
Part 5b – PlayerHealth and PlayerScore
Before we move onto the missile let’s take a look at the PlayerScore and PlayerHealth scripts as they will be referenced by the Missile script. For now, both these scripts are quite basic and do little more than output debug.log text. They will both be expanded upon in the next part of the tutorial series.
This is the PlayerScore script, which is attached to the Player prefab.
This simple script contains a variable for storing the current score and an RPC function for adding points to the score and logging the new value to the debug console.
This is the PlayerHealth script, which is attached to the Player prefab.
This simple script contains a constant and variable relating to max health and current health. It also has a function for applying damage (reducing hitPoints) based on the damage of the missile passed to the function and a function to display in the debug console, the health as a percentage of the maximum HP.
Part 5c – The Missile
So let’s now take a look at the missile.
The missile itself is just a red sphere with a rigidbody and a couple of child particle systems for the trail and explosion. The missile prefab is contained in the Player folder.
The missile script contains references to the rigidbody and particles systems (set in the inspector) and missileOwner, speed and damage variables.
As soon as it is enabled, the missile simply uses FixedUpdate to move forward by
speed * Time.fixedDeltaTime every physics update. As this script runs on every client all players will see the missile moving, even though it isn’t networked. This is workable because the missile trajectory is straight and the missile is relatively short-lived, so there isn’t much opportunity for the missiles to become out of synch with different clients.
Next, we have the SetOwner function, which we called from the PlayerShoot script, this simply caches the passed PhotonView for use later by the collision function.
Now comes the OnTriggerEnter function, which is where we will deal with collisions.
Firstly we check to see if we hit a player. If we did then cache a reference to the attached PlayerHealth script on the player that was hit.
We only want one client to take action in the event of a hit and in this case, we will have the player that is hit be responsible for dealing with it. So we put in a condition that checks hitPlayer.photonView, and if isMine is true then this script is running on the client that was hit.
Next, we want to ignore collisions between our own missile and our own tank (unlikely, but could possibly happen just after being fired), which we can easily do by comparing the hitPlayer’s photonView.viewID with the same as the local player photoView.viewID. If they match we just return from the function.
If we get this far it means we have registered that we have been hit by someone else’s missile, so we call the DoDamage() function on our own PlayerHealth script.
We also need to tell the player that hit us to increase their score, and as this needs to be executed on a remote client we need to make use of the RPC function again, but using a different set of arguments to when we used an RPC in the PlayerShoot script.
The first argument is the name of the RPC_AddScore function in the PlayerScore script.
For the second argument, we use missileOwner.photonView.owner to supply the PhotonPlayer object of the missile owner. This means that the RPC will only be executed on the client of the player that fired the missile; Which in turn means that scores will only be tracked by the local player, which I have done purely to demonstrate a second way to use an RPC. (Later in the series, this will be changed to allow everyone to see all players’ scores.)
Lastly, as the RPC_AddScore function takes an argument for the score, we add 25 as the last parameter to the RPC function, as this will be the amount we want to add to the score each time we score a hit.
That’s all we need to do in the event a player was hit, which only leaves one more thing to do in this function and that is to destroy the missile regardless of what it hit. The last line of the function does this by calling the DestroyMissile function. As this function is called outside of the photonView check above, it will be run for all clients, which is what we want, because all players should see the missile explode.
This function Destroys the missile GameObject. But to allow the explosion particle to play and the trail particle to slowly fade out they are both unparented from the missile first. Then the trail particle is set to stop emitting and the explosion is played. Both particles have been set in the inspector to automatically destroy when they have finished.
Now when you play the game you should be able to fire missiles and when you get hit (if playing in the editor) you will get a Debug.Log of your HP percent and when you hit someone else you will see a Debug.Log of your points score.
That’s it for this part of the tutorial series, in the next part we will expand the health and score system and provide better visuals for them.
Hope to see you then.
You can download the project files for this part here.
(To run the game in the editor, load Assets/Main Scene and press play.)
You can download the project .exe here.