Bootcamp advanced multi-player tutorial

This Bootcamp tutorial is an example that shows how you can make a typical single player game into a multi-player game with the help of uLink. It is a good way to see how uLink is made to integrate into complex movement systems.

This tutorial is not the same tutorial as the http://www.developer.unitypark3d.com/videos/#bootcamp Unity Bootcamp into Multiplayer in 5 min, without writing a single code line video, this is an advanced tutorial to get more out of the game.

You can either start fresh with the Bootcamp files that comes with the Unity editor or you can download the finished code here. If you're starting from scratch you'll need the latest version of uLink which can be downloaded here.

Identifying which parts that needs to be changed

By playing the game you can identify many things that needs to be changed. From a quick playing session you see that things like walking, running, crouching, jumping, aiming and shooting should be network aware.

We see that the player game object is called Soldier and it is a child to the game object SoldierLocomotion.

So where do we find those things in the code? There is a source file named SoldierController.js that seems to be what we're after. Here is where most of the input of the player is handled and player actions are propagated. We also notice that instead of doing the actions that the input represents, the scripts sets some variables that are later on read by one or several separate scripts.

Importing uLink and adding standard components

We start by importing uLink into the game, if it has not already been done. Duplicate the Bootcamp scene by selecting it and pressing Ctrl + D. Name the first scene "Bootcamp Client" and the second scene "Bootcamp Server". We will work in the client scene until further noticed. After that, put the uLinkNetworkView component on the SoldierLocomotion->Soldier game object. Also, add uLinkSmoothCharacter and uLinkObservedList components to this game object.

SoldierController - Setting up state synchronization:

  1. Drag uLinkObservedList to the Observed property under uLinkNetworkView.
  2. In uLinkObservedList, set Size to 2 and drag uLinkSmoothCharacter and SoldierController to the empty components.
  3. Create the proxy object which will be the remote representation of the player soldier. We do this by duplicating the Soldier object, creating a prefab in the Resources folder called ProxySoldier and dragging this new duplicated game object to that prefab.

Now that we have separated the two soldiers we can start to make specific changes for each. Add the component uLinkInstantiateForOthers on the soldier object and drag ProxySoldier to the proxy prefab in uLinkInstantiateForOthers.

Now we can start coding away in SoldierController.

  1. Make the class inherit uLink.MonoBehaviour instead of MonoBehaviour.
  2. In the Start() function, add
GetComponent.<uLinkSmoothCharacter>().arrivalSpeed = 0;
  1. In the OnEnable() and OnDisable() functions put the radar code within this if clause:
if (uLink.Network.status != uLink.NetworkStatus.Connected || networkView.isMine)
  1. Add the callback function uLink_OnSerializeNetworkView(Stream : uLink.BitStream, info : uLink.NetworkMessageInfo) to the SoldierController script. Here we syncronize each member variable that needs to be network aware by writing the line stream.Serialize(variable) here. This will work for both writing to and reading from the stream since we use no auxillary variables during the statesync. The function will look like this, and we'll go into the specifics of the serialized variables later:
function uLink_OnSerializeNetworkView(stream : uLink.BitStream,
                                      info : uLink.NetworkMessageInfo)
{
        stream.Serialize(walk);
        stream.Serialize(crouch);
        stream.Serialize(inAir);
        stream.Serialize(fire);
        stream.Serialize(fireReleased);
        stream.Serialize(isFiringAutomatic);
        stream.Serialize(aim);
        stream.Serialize(reloading);
        stream.Serialize(currentWeapon);
        stream.Serialize(grounded);
        stream.Serialize(jump);
        stream.Serialize(cameraPosition);
        stream.Serialize(cameraRotation);
        stream.Serialize(targetYRotation);
}
  1. Setup the camera variables that will control the head movement and the shooting directions for the Proxy Soldiers. We will also setup some variables that will help shooting and jumping. We do this by adding:
@HideInInspector
public var jump : boolean;

@HideInInspector
public var fireReleased : boolean;

@HideInInspector
public var isFiringAutomatic : boolean;

@HideInInspector
public var cameraPosition : Vector3;

@HideInInspector
public var cameraRotation : Quaternion;

public var soldierCamera : Camera;

To the member variables of the SoldierController script.

RPC:s in SoldierController

There are a few RPC:s that we will need, all of them should be placed in SoldierController, here are the three that we will use:

@RPC
function Reload()
{
        if (weaponSystem.currentGun != null)
                weaponSystem.currentGun.Reload();
}

@RPC
function FireProjectile()
{
        if (weaponSystem.currentGun != null)
                weaponSystem.currentGun.Shoot();
}

@RPC
function ChangeWeapon(weapon : int)
{
        weaponSystem.ChangeToGun(weapon);
}

CharacterMotor

Bootcamp has a very intrinsic script that controls most of the movement of the player, including walking, running and jumping. So how do we get smooth movement for this script? We use the uLinkSmoothCharacter component we added earlier to the soldier object. Normally uLinkSmoothCharacter will move the character for you but here we will uncheck the box called "Move" on the component in the Hierachy view. This way we will get a smoothly updated velocity vector that we can feed to the CharacterMotor script.

The velocity of CharacterMotor is in turn set in SoldierController, so we setup this code in the SoldierController Update() function that will solve the movement for both the local and the Proxy Soldier right inside the if(!dead), replacing the line that currently is there:

if (!dead)
if (networkView.isMine)
{
        GetUserInputs();
        moveDir = transform.TransformDirection(new Vector3(Input.GetAxis("Horizontal"),
                                                       0, Input.GetAxis("Vertical")));
        jump = Input.GetButton("Jump");
        cameraPosition = soldierCamera.transform.position;
        cameraRotation = soldierCamera.transform.rotation;
}
else
{
        moveDir = GetComponent.<uLinkSmoothCharacter>().velocity;
}
if(weaponSystem.currentGun != null)
{
        weaponSystem.currentGun.fire = firing;
        currentWeaponName = weaponSystem.currentGun.gunName;
        currentWeapon = weaponSystem.currentWeapon;
        reloading = weaponSystem.currentGun.reloading;
}

And remove this line at the beginning of the Update() function:

GetUserInputs();

Further down in the function, remove:

motor.inputMoveDirection = transform.TransformDirection(moveDir);
motor.inputJump = Input.GetButton("Jump") && !crouch;

And replace them with:

motor.inputMoveDirection = moveDir;
motor.inputJump = jump && !crouch;

The last thing we'll do in SoldierController is to add a line in the GetUserInputs() function, after the aim = Input.GetButton("Fire2") && !dead; line, add this line:

fireReleased = !fire && Input.GetButtonUp("Fire1");

Gun

The Gun.js script is a bit of a special case. This script has some peculiarly named functions and variables, some of which we will rename so it makes more sense.

To start off with Gun, it is a bit of an ugly class, that bypasses SoldierController to read the input itself. But we want to have all control from SoldierController instead to ease the migration to a multi-player game.

  1. Add a public SoldierController named soldierController as a member variable that we set to the grandparent Soldier object/ProxySoldier prefab in the hierarchy view. We will use this to set a couple of variables in the SoldierController and send RPC:s through.
  2. Remove all the cam variable and it's assignment in OnEnable().
  3. Add a start function looking like this:
function Start()
{
    soldierCamera = GameObject.Find("Soldier Camera").GetComponent.<SoldierCamera>();
}
  1. In the update function, change if(Input.GetButtonUp("Fire1")) to if (soldierController.fireReleased) and add the line soldierController.isFiringAutomatic = false; in the code within the if case.
  2. Now we want to break out some code of the function ShotTheTarget() and also change the name of it since it doesn't make that much sense. We name the function HandleShooting(). We break out this section:
if(capsuleEmitter != null)
{
        for(var i : int = 0; i < capsuleEmitter.Length; i++)
        {
                capsuleEmitter[i].Emit();
        }
}

PlayShootSound();

if(shotingEmitter != null)
        {
                shotingEmitter.ChangeState(true);
        }

if(shotLight != null)
{
        shotLight.enabled = true;
}
 and put it in a new function called Shoot(). This makes the code more modular so that we can call the Shoot() function from different places depending on if the controlling object is a ProxySoldier or the regular Soldier. You should call Shoot() at the point in HandleShooting() where you extracted the code. Remember to change the call to ShotTheTarget() in the Update() function to HandleShooting() instead as well.
  1. Continuing in the HandleShooting() function we add
soldierController.isFiringAutomatic = true;

After the

case FireType.RAYCAST:

Line and remove the CheckRayCastHit(); call two lines below. In the second switch clause we add

soldierController.networkView.RPC("FireProjectile", uLink.RPCMode.Others);

And remove LaunchProjectile();.

  1. Replace all Reload() occurences with TryToReload(), except for the function itself.
  2. Add:
if (!soldierController.networkView.isMine && soldierController.isFiringAutomatic)
{
        if(Time.time > lastShootTime)
        {
                lastShootTime = Time.time + shootDelay;
                Shoot();
        }
}

To the end of the HandleShooting() function.

  1. In the Shoot() function we also add this at the end:
switch(fireType)
{
        case FireType.RAYCAST:
                CheckRaycastHit();
                break;
        case FireType.PHYSIC_PROJECTILE:
                LaunchProjectile();
                break;
}
  1. For the shooting functions that do the raycasts we need to replace the camera code to work for Proxy Soldiers.
  2. We begin in the LaunchProjectile() function by removing the line var camRay : Ray = cam.ScreenPointToRay(new Vector3(Screen.width * 0.5, Screen.height * 0.6, 0));
  3. Then we change the line in the first else clause that assigns startPosition to soldierController.cameraPosition + soldierController.cameraRotation * Vector3.forward;
  4. Change the assignment of projectile.transform.rotation to soldierController.cameraRotation.
  5. Add var cameraDirection = soldierController.cameraRotation * Vector3.forward; on the line before camRay2 is set and set camRay2 to new Ray(soldierController.cameraPosition, cameraDirection);
  6. In the last else clause we change the assignment of projectileRigidbody.velocity to: ((soldierController.cameraPosition + cameraDirection * 40) - weaponTransformReference.position).normalized * projectileSpeed;

CheckRaycastHit is pretty similar, all you need to do is:

  1. Add camRay = new Ray(soldierController.cameraPosition, soldierController.cameraRotation * Vector3.forward); at the beginning of the function, just after the variables are set.
  2. Remove the line that assings camRay in the following if clause, both in the then and else clause.
  3. Set shottingParticles.rotation to soldierController.cameraRotation instead of the old assignment.
  4. Add the function TryToReload(), all it will do is this:
function TryToReload()
{
        if(totalClips > 0 && currentRounds < clipSize)
        {
                soldierController.networkView.RPC("Reload", uLink.RPCMode.All);
        }
}

And replace the Reload() code with this:

function Reload()
{
    PlayReloadSound();
    reloading = true;
    reloadTimer = reloadTime;
    soldierController.isFiringAutomatic = false;
}

GunManager

GunManager is really simple, we only have to add and change a few things. Start off by adding private var uLinkNetworkView : uLinkNetworkView; as a member variable, and assigning it like this in the Start() function:

uLinkNetworkView = transform.parent.GetComponent.<uLinkNetworkView>();

In the Update() function, put all the code inside an if (uLinkNetworkView.isMine) Statement. This will make sure that gun switching and HUD gun info updates is only done for the soldier owned by the player. In the if clause inside the loop, add this line: uLinkNetworkView.RPC("ChangeWeapon", uLink.RPCMode.Others, i); That's all there is to this file.

Grenade

The only thing you have to do in Grenade.js is add an if check around

var distance:float = Vector3.Distance(soldierCamera.transform.position, _explosionPosition);
soldierCamera.StartShake(distance);

Which should turn into this:

if (SoldierCamera != null)
{
   var distance : float = Vector3.Distance(soldierCamera.transform.position, _explosionPosition);
   soldierCamera.StartShake(distance);
}

HeadLookController and SoldierAnimations:

In HeadLookController we

  1. Remove targetTransform.
  2. Add private var soldierController and assign soldierController in the Start() function like this: @@soldierController = GetComponent.<SoldierController>();
  3. Remove target = targetTransform.position and replace it with
var cameraDepth = 40;
target = soldierController.cameraPosition + soldierController.cameraRotation *
         Vector3.forward * cameraDepth;

Using the camera variables we setup in soldierController earlier.

SoldierAnimations has a few things that needs to be taken care of.

  1. Remove aimTarget.
  2. Set aimDir to soldier.cameraRotation * Vector3.forward; instead of (aimTarget.position -aimPivot.position).normalized;
  3. Set moveDir to soldier.moveDir and inAir to soldier.inAir instead of what they were set to before.

Changes in the Editor

  1. Go to ProxySoldier->GunManager and drag the ProxySoldier game object to the Soldier Controller variable in each of the guns, do this for the normal Soldier as well if you haven't already done that.
  2. Remove the ProxySoldier game object and set Max Distance on uLink Smooth Character in the Proxy Soldier prefab to 15.
  3. Add an empty object to the Server scene and name it "Server".
  4. Add a uLinkSimpleServer component to it from the component menu.
  5. Remove the _GameManager object in the Server scene.
  6. Delete the SoldierLocomotion object in the Server scene.
  7. Optionally, add a camera to the server object if you would like to see the game being played on the server.
  8. Add the uLinkClientGui component to the Soldier object in the Client scene.

There are a few last changes that we will need to do in the hierarchy view to make the project work as it should:

  • In the Soldier object, SoldierController component, assign SoldierCamera to Soldier Camera which is a neighbour object to Soldier.
  • Remove GameManager script from _GameManager
  • Set onDisableWhenGui list length in uLinkClientGui to 4 and assign them to SoldierCamera, HeadLookController, SoldierController and _GameManager.

Now you're all set to play Bootcamp over the network, give it a try!

Optional:

If you'd like you can also easily add a chat to the game. You do this by adding the Chat GUI component from the Component->uLink Utilities menu to both the Soldier object and the server object. Set a manual ViewID on both, it should be the same. Then check the "Use Login Data" box, and you're all set to chat.

You can also use labels for the proxy soldiers. By doing this you will see the names of the other players in the game. Do this by creating an empty game object, add a GUIText component from the Component->Rendering menu, create a prefab called Label or something similar in the Resources folder and drag the game object to this prefab. Then you can delete the game object. After this, add the Object Label component from the uLink Utilities component menu to the ProxySoldier prefab, and drag the label prefab to the label slot in the hierarchy.