Simple Getting Started Tutorial
This document contains the steps to produce a demo scene from scratch without any coding and using only the supplied components and some Unity Standard Assets. As such, it is not a cosmetically beautiful demonstration. The goal of this demonstration is to show how the assists can be used to quickly produce useful networking.
Contents
- A word about Assists
- Create New Project and Import Latest PUN2 / Simple
- Create a New Scene
- Create a Player
- Convert BasicKyle to Simple Net Object
- Add PUN2 Networking Code
- Create Vital and Inventory Pickup Items
- Add a Health Pickup
- Add a Pickup Item (IInventoryable)
- Add Mounts to Player
- Mounting Assignments
- Add Networked Hitscan
- Add Networked Projectile Cannon
- Add a Hitscan Remote Actions
- Add Moving Terrain
- Adding an NPC
- Create a PackObject with SyncVarAttributes
A word about Assists
Assists are menu items that automate the tasks of creating networked objects and they are used almost entirely for this demo. They are basically one-click wizards as they have no interface. They will attempt to perform a task on the gameobject that is selected in Unity or will create a new scene object if applicable.
Create New Project and Import Latest PUN2 / Simple
- Create a new Project in any version of Unity above 2017.4.
- Import the latest PUN2 from the Asset Store.
- Download Simple and import.
Create a New Scene
- Create new Scene (if not already in one)
- Set up a basic floor/walls by using the assist:
GameObject > Simple > Tutorial > Create Starting Scene
This assist does nothing of any real networking interest. It prepares a scene for this tutorial by adding some walls and a floor and moves the camera to a better position.
We now have an empty scene with a ground plane.
For Unity 2019 if the scene appears dark:
You may need to go to enable AutoGenerate in:
Window > Rendering > Lighting
Create a Player
First we want to add an instance of the BasicKyle prefab to the scene so we can make a variant of it meant for networking.
- Use search in Project to find the prefab in Assets.
A lite version of Kyle/Ethan from Standard Assets is included that you may use.
- Drag the prefab into the scene
Convert BasicKyle to Simple Net Object
Controllable objects like a player or vehicle need to be 'netified' to sync anything. The Convert To > Player assist automates most of the work needed to do this. For this example ThirdPersonController from Unity Standard Assets is used but the process will be similar for most player object types. This assist attempts to make a best guess starting point for making single player objects that move/animate into networked objects.
Select the 'BasicKyle' GameObject in the scene
Convert 'BasicKyle' to Net Object using the assist:
GameObject > Simple > Convert To > Player
This should add placeholder health bars to the player object, as well as an assortment of networking components.PhotonView
NetObject
SyncAnimator
(if an Animator was found on the object)SyncTransform
SyncVitals
AutoOwnerComponentEnable
ContactTrigger
MountManager
Mount
(The Root Mount)BasicInventory
SyncState
OnStateChangeKinematic
SyncSpawnTimer
AutoDestroyUnspawned
OnStateChangeToggle
on children
NetObject
is the root component that works as an extension ofPhotonView
, and handles passing callbacks from theNetMaster
to child SyncObjects and PackObjects, as well as managing serialization and deserialization.NetObject
gets its timings from theNetMaster
singleton, which is automatically added at runtime.
NOTE:AutoDestroyUnspawned
is automatically added. For some objects you may want to remove this. Game Objects with this component in the starting scene will be destroyed at runtime. This allows you work on the prefab instances in the editor without having to remove them before playing or building out tests. Objects you intend to spawn as part of the scene will want this removed.
Drag the scene 'BasicKyle' prefab instance into any Resource folder
You may want to add a new Resources folder rather than dig for an existing one. Newer versions of Unity will ask if you want to make this a Variant. That is fine.
Add PUN2 Networking Code
There is no code yet to set up and join a PUN Room, so let's add that so we can test our progress.
Typically this will all be your own custom code for matchmaking, creating a lobby & room and handling player object instantiation and such. PUN2 has a couple components that will automate this for you and Simple has an assist that will automatically add them to your scene.
Add Components for Connecting and Room Creation
Select 'KyleBasic' in the scene before running the assist
Add Room Launchers using the assist:
GameObject > Simple > Add To Scene > Auto Room Launchers
This will create (or modify if it already exists) a scene object with the components:ConnectAndJoinRandom
OnJoinedInstantiate
(any selected prefab to the Prefabs To Instantiate list)
You can rerun the assist with other prefabs select to add them to the
OnJoinedInstantiate
prefab list.
Playing the scene should now:
- Connect to the Master Server
- Create or Join a Room
- Spawn our player.
Create Vital and Inventory Pickup Items
Pickups are objects with IContactReactor
component(s) that can interact with IContactSystems
, such as Inventory or Vitals.
These objects can be triggered, picked up, attached or mounted to/by IContactSystem
objects.
Typically this would be a player (or AI) that would trigger these items.
There are assists for creating prototypes of these objects, with all of the networking components automatically added. These generated objects include placeholder child objects, for different states.
- Items have the
IInventoryable
components that can be picked up and mounted toIInventorySystem
. - Vitals has the
IVitalsContactReactor
components needed to interact with theIVitalsSystem
.
The assist generated items come with a set of placeholders with OnStateChangeToggle
components, set up for a few of the possible states.
Replace these with actual graphics, sound triggers, particles systems, etc.
You can also create your own scripts that implement IOnStateChange to capture callbacks when the state of objects change.
Add a Health Pickup
Add a Pickup Vital using the assist:
GameObject > Simple > Add To Scene > Pickup > Vital: Static
Vital: Static is the same as Vital: Dynamic, but noRigidbody
orSyncTransform
is added, since this object is not expected to be dropped/thrown.Move the Item so it is not overlapping the player spawn points
Set Position X to 3
Running the game at this point should allow the player to pick up the item. When the player triggers this pickup, it will add to the players health and attach to the player, as the pickup is set to vital type 'Health' and the playerSyncVitals
has a Health Vital.
Add a Pickup Item (IInventoryable)
We will produce an inventory item using a similar method.
Add a Pickup Item using the assist:
GameObject > Simple > Add To Scene > Pickup > Item: Dynamic
Item: Dynamic is the same as Item: Static, but has a Rigidbody added as well as aSyncTransform
, so it can be dropped/thrown.Move the Item so it's not overlapping the player spawn points
Set Position X to -3
Running the game at this point should allow the player to pick up the items.
Currently the onlyMount
on Kyle is the default 'Root' mount, which was automatically added by 'Convert To > Player'. We can add more Mounts to Kyle and assign them to the Inventory and Vitals systems. The next section shows how to add and use additional mounts.
Add Mounts to Player
Mount
components can be added to child GameObjects of any NetObject
.
Any items that are picked up can be designated to mount to these transforms.
A default Mount
is automatically added to the root and is named 'Root', as well as a MountsManager
component (which handles automatic mount indexing).
Select the Player instance in the scene
Type 'wrist' into the Hierarchy search box
Select 'Left_Wrist_Joint_01'
Add
Mount
Component to 'Left_Wrist_Joint_01'
Select the Mount Type 'LeftHand'
This associates this mount with the named type LeftHand. Each Net Object can only have one mount associated with each named type.
Optional: Modify the global mount types
If 'LeftHand' doesn't exist in the current list in mount settings, add it in by expanding Mount Settings. This exposes the Settings ScriptableObject for Mounts. MountSettings apply to the entire project, and all Mounts.Add
Mount Throw
component to 'Left_Wrist_Joint_01'Set Throw Key on
MountThrow
to Alpha 5
Hitting the '5' key will drop attached Inventory items after they have been picked up. We now have a mount on the left hand, however there is nothing specifically telling objects to use this new mount. We need to do the next step for this mount to get used...
Mounting Assignments
Inventory and Vital items can only be mounted if they have Mountable To named types that match the mount settings for Vitals and Inventory Systems they interact with.
Assign Inventory to a Specific Mount
SyncState
handles attaching a Net Object to a Mount
on other Net Objects.
As such, you can restrict which Mounts a Net Object can attach to, by setting the Mountable To values in SyncState
.
Select our 'Pickup Item' created in the previous step.
Enable 'LeftHand' in the
SyncState
Mountable To selectorThis mask is used to restrict Attach to only happen between compatible types. This object can now only be mounted to an IContactSystem that use the 'LeftHand' mount.
Set
BasicInventory
to use LeftHand as mountOn the root of BasicKyle, find
BasicInventory
, and set the Default Mounting to LeftHand.This indicates that any IInventoryable picked up by this IInventorySystem will attach to the Mount associated with 'LeftHand'. IInventoryable items must have 'LeftHand' checked in the
SyncState
Mountable To. (See the step above)
BE SURE TO APPLY CHANGES TO THE PREFAB!
Now when the player touches the 'Pickup Item', it should attach to Kyle's hand, as that is now the first valid inventory that will be found.
If not, double check that you set the inventory to use LeftHand and that LeftHand is selected as a valid mount on the SyncState
of the pickup.
Add Networked Hitscan
Select 'Robot2' or any child gameobject of our 'BasicKyle' player in the scene
Add a Damage Scan using the assist: GameObject > Simple > Add To Object > Remote Contact > Damage Scan
Reposition the created Damage Scan child
Set the Position to (-0.5, 1.5, 0)The weapon assists add child placeholder objects. You can replace these with your own models.
BE SURE TO APPLY CHANGES TO THE PREFAB!
Hitting Play, you can see the default hitscan fire when you press 'R'.
Note: Contact Scans generate ContactEvents similar to Enter, Stay and Exit.
As such they can be used to trigger the ContactTrigger remotely.
This means it is possible to 'Raycast' or 'Overlap' any ContactTriggers.
Hitscans are not just for weapons.
Add Networked Projectile Cannon
Select child 'Robot2' of 'BasicKyle' in the scene
Add the networked Projectile Cannon using the assist:
GameObject > Simple > Add To Object > Remote Contact > Damage Cannon
Move the created Projectile Cannon child
(right now he is shooting out of his toes)
Set the Position to (0.5, 1.5, 0).
The weapon assists add placeholder objects. You can replace these with your own models.
BE SURE TO APPLY CHANGES TO THE PREFAB!
Hitting Play, you can see the default projectile fire when you press 'F'.
Note: Projectiles act as proxy ContactEvents. As such they can be used to trigger the ContactTrigger as if the shooting object collided with the object. This means it is possible to 'shoot' pickups. Projectiles are not just weapons._
Add a Hitscan Remote Actions
Hitscans are not just weapons, they can also be used as a means of triggering the ContactTrigger component by proxy and become extensions of the ContactTrigger of the objects firing them.
Select child node 'Robot2' of the 'BasicKyle' instance in the scene
Add a ContactProxy hitscan using the assist:
GameObject > Simple> Add To Object > Remote Contact > Contact Scan
Hitting Play, you should now be able to pick up IInventoryable items (Vital/Item Pickups) by hitting the '5' key.
You will also see a visual representation of the hitscan momentarily.
The default hitscan is an OverlapSphere but can be changed to any other type of overlap or raycast type.
By default, you should also see a yellow visualization object of the hitscan for a moment after triggering.
Any ContactTriggers
hit by the scan will trigger an OnContactEvent with the ContactType.Hitscan.
The 'Contact Scan' assist adds the same set of components as 'Damage Scan', except it doesn't add a IContactReactor component. This means that all Hitscan Weapons are actually capable of triggering IInventory -> IInventoryable interactions.
Add Moving Terrain
Platforms/Moving Terrain can be added with the ITransformController components derived from SyncMoverBase
.
For this example we will be using SyncNodeMover
, which allows us to create time-based lerping between two points.
All of the networking is automatically handled (which is true for any of the components starting with the word 'Sync').
If the moving terrain is a Net Object with a Mount
, the AutoMountHitscan
component on the player can automatically detect its proximity and re-parent to that platform.
This networks the players location relative to the moving platform, which prevents delayed 'floating' that will occur if a player is attached to a moving object that is owned by someone else.
Select the leftmost section of flooring named 'Floor Tile 1'
Convert to a Platform using the assist:
GameObject > Simple > Convert To > Platform
This converts the flooring section into a Net Object, and addsMount
andSyncNodeMover
components. When the scene is built or played, this section of flooring will now oscillate up and down.Add
AutoMountHitscan
to 'BasicKyle' using the assist:
GameObject > Simple > Add To Object > Auto Mount Hitscan
This adds a child GameObject named 'AutoMount', which contains theAutoMountHitscan
component.
On play, a raycast continuously tests downward from the base of 'BasicKyle' to see if a Net Object is in close proximity.
BasicKyle will mount (re-parent) to the closest NetObject with a compatible Mount.
The settings for this hitscan test are defined by the Hitscan Definition setting on the AutoMountHitscan
component.
Adding an NPC
First we need to make a scene object that moves autonomously.
Create the tutorial NPC using the assist:
GameObject > Simple > Tutorial > Add NPC
This produces a basic networked cube that moves in a ring.
Select the Example NPC object if it has been deselected
Convert the NPC to a Net Object using the assist:
GameObject > Simple > Convert To > Player
This adds the extra components, similar to what we added to our BasicKyle. Now this object has a health system, inventory system, health bars, etc.
Running the scene now should result in this object moving in a circle. This object will be owned by the Master Client. The NPC has a health system and as such it is able to be damaged and can trigger IVitalsAffector and IInventoryable pickups just like a regular player. The MasterClient will own this object, as is true with all scene objects.
Create a PackObject with SyncVarAttributes
EXPERIMENTAL
Basic SyncVarAttribute Implementation
Add a new Cube to the scene:
GameObject > 3D Object > CubeCreate a script named SyncvarTest.cs and paste in the code below
using UnityEngine; using Photon.Compression; using Photon.Pun; using Photon.Pun.Simple; /// The PackObject attribute tells the code gen engine and the NetObject that this class/struct /// contains [SyncVar] attributes that should be serialized and synced. [PackObject] [RequireComponent(typeof(NetObject))] public class SyncvarTest : MonoBehaviour { /// THIS IS OUR SYNCVAR. [SyncVar] public float syncedZ; public void FixedUpdate() { /// We only want to set the value on the MasterClient. if (PhotonNetwork.IsMasterClient) { /// Changes to syncvars only are networked if the master changes them. syncedZ = (float)(System.Math.Sin(Time.time) * 720); } transform.localEulerAngles = new Vector3(0, 0, syncedZ); } }
Add the SyncvarTest script to the Cube
Set the Cube position to (0, 3f, 0)
Build and Run
The values are set on the MasterClient and are synced to other Clients.
Advanced SyncVarAttribute Implementation
The same functionality as above, but some advanced features being used. Interpolation is now enabled, so the remote value automatically interpolates every Update.
using UnityEngine;
using Photon.Compression;
using Photon.Pun;
using Photon.Pun.Simple;
/// The PackObject attribute tells the code gen engine and the NetObject that this class/struct
/// contains [SyncVar] attributes that should be serialized and synced.
[PackObject]
[RequireComponent(typeof(NetObject))]
public class SyncvarTest : NetComponent
{
/// THIS IS OUR SYNCVAR.
[SyncVar(applyCallback = "MyOnApply", snapshotCallback = "MyOnSnapshot", interpolate = true, keyRate = KeyRate.Every10th)]
public float syncedZ;
// NOTE: With interpolate enabled, this callback isn't needed for this example.
// This is just included to show how it would be enabled.
/// applyCallback fires remotely when the synced value has changed.
public void MyOnApply(float newValue, float oldValue)
{
// This is redundant with the interpolation being true, and values being applied in Update()
// But is included to show how this callback would normally be used.
transform.localEulerAngles = new Vector3(0, 0, newValue);
}
// NOTE: With interpolate enabled, this callback isn't needed for this example.
// This is just included to show how it would be enabled.
/// snapshotCallback fires remotely every simulation tick, whether or not the value changed.
public void MyOnSnapshot(float snap, float targ)
{
/// snapshotCallback gets called every tick, whether the value has changed or not.
Debug.Log("Applying Syncvar Snapshot " + snap);
}
/// We are using NetComponent so we have access to better timing callbacks for networking.
/// However, this could work in FixedUpdate as well.
public void FixedUpdate()
{
/// We only want to set the value on the MasterClient.
if (PhotonNetwork.IsMasterClient)
{
/// Changes to syncvars only are networked if the master changes them.
syncedZ = (float)(System.Math.Sin(Time.time) * 720);
}
}
public void Update()
{
/// We set interpolate = true for this syncvar,
/// so it will get modified remotely before every update
/// to have interpolation automatically applied, using the Snap and Targ values.
transform.localEulerAngles = new Vector3(0, 0, syncedZ);
}
}