This is a crash course/refresher, not a tutorial! My intent was to provide a super-simple Photon PUN project that works with either PUN classic or PUN 2 and can be used as a sandbox for our multiplayer experiments. As such, the objective is not to wow you with a host of features. To the contrary, the idea is to get you to the point where "Okay, I got this, I'll just go and make my own multiplayer game right now".
The source code for this article is available on Github. To follow along, clone the project or open the source in another window.
PUN in a nutshell
PUN provides peer-to-peer multiplayer as a service. This means that there is no 'master server' - therefore, instead of a unique source of truth for what is happening in the game, every client manages networked game objects (NGOs).
Each NGO synchronizes state (position, rotation, animation) and will message session-wise clones via RPC (remote procedure call).
PUN provides its own 'Instantiate' and 'Destroy' methods thereby a client creates/destroys NGOs, such as an avatar representing the player, or a projectile.
Install either PUN2 or PUN classic.
At the time of writing PUN2 (rel. August 2018) looks ready for production use.
If you really want the PUN classic version, checkout 'V1' after cloning the git repo.
Create an account or log into the Photon website then make a new app in the dashboard:
Copy your app id from the app's 'manage' section to your project's PhotonServerSettings. Can't see PhotonServerSettings? Reload your project and it should regenerate automatically.
TIP: add PhotonServerSettings to .gitignore - your app id is a secret and should not be shared.
Creating/Joining a match
This utility class exposes the flow of connecting to the Photon server and creating/joining a room; added to any project it will create or join an existing room. No UI makes it perfect for iterating your multiplayer prototype.
Setting up and configuring a player avatar.
To instantiate the player avatar across the network, after joining a room, call:
PhotonNetwork.Instantiate(avatar, Vector3.zero, Quaternion.identity, 0);
Note: Vector and Quaternion are used to init the position and rotation of the avatar.
The sample comes with an extremely basic character controller, MotionControl
You can adapt existing controllers for multiplayer but since all scripts are running on all clients, how do we know to control 'our' avatar vs everybody else's? PhotonView provides the IsMine property, often demonstrated like this:
// ... check input and move avatar
This pattern has disadvantages:
Instead, suggesting you leverage a utility component like Disown to remove the controller (and other functionality specific to the local avatar) from un-owned NGOs.
Interaction via RPC (remote procedure calls)
In this sample, Shooter implements firing at other avatars via ray-cast; when hit, an avatar's ShootingTarget component is messaged, and the avatar is destroyed.
Note: Shooter is also Disown'd, otherwise we'd be shooting from every Avatar whenever pressing the space bar (superpower?)
On success (ray-cast did detect another avatar), instead of calling Hit() on the ShootingTarget, the call is messaged as an RPC: that.photonView.RPC("Hit", RpcTarget.All, that.view.Owner.ActorNumber)
For this to work the target function must be tagged [PunRPC].
[PunRPC] public void Hit(int id)
So what's the difference between a function call (the usual) and a remote procedure call?
Photon has a straightforward 'ownership concept' - A client owns every NGO that it creates. Only the 'original' can destroy its avatar. In this case I didn't use Disown because if we removed ShootingTarget from other clones, the RPC couldn't be handled, which would cause a PUN error - we'd have to elaborate on the original pattern to make this work.
Where to go from here?
This walkthrough and sample project are intended as a quick, but functional introduction to PUN 2. If you want to learn more, take the time to check the official Photon resources: the overview, tutorial and API.