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".
You already know how to program games using Unity 3D
You need a quick walkthrough to understand Photon networking.
You'd like to migrate from PUN1 to PUN2 (if so, check the official migration notes notes and this diff)
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:
Type: Photon Realtime
Name: same as your unity project (keep it neat)
URL: don't care
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.
Create an Avatar game object; for this the sample project uses a simple model blended by yours truly.
Add PhotonView to the avatar; this component enables message passing and state synchronization.
Add PhotonTransformView or PhotonTransformViewClassic; these are handy components which do the work of syncing position, rotation and scale. Do check the boxes to sync position and rotation (Coming from PUN v1? The transform view has been streamlined, but you can recover legacy features via PhotonTransformViewClassic)
Drop your PhotonTransformView into the PhotonView's list of observed components.
Make Avatar into a prefab and place it in a Resources folder; next, deactivate Avatar in the scene (you could remove it but keeping a clone in the scene helps modify and inspect the prefab).
To instantiate the player avatar across the network, after joining a room, call:
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:
Needs to be repeated in many places, which makes it error prone.
Conflates multiplayer logic with control logic.
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?
A function call (would be that.GetComponent<ShootingTarget>().Hit()) is only invoked locally.
The RPC gets called on every clone of the receiving NGO. Also, the RPC is messaged to the photon view; upon receiving the RPC, the target component is automatically selected.
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.