Tutorial 2 - Using Player Controllers to Control Server Avatars

Summary

Create a game where each player controls a server entity using a player controller. The controller runs on both the client and the server, and on the client it's used to predict motion to give the feeling of immediate response.

Requirements

Scene and Prefab Setup

Create a Room

  1. Right click in the hierarachy window and create a 'Reactor->Physics Room' game object.

Create a Static Platform to be the Ground

  1. Right click in the hierarachy window and create a cube. Rename it to 'Platform'.
  2. Set the transform position to (0, 0, 0).
  3. Set the transform scale to (20, 1, 20).
  4. Click the 'Add Component' button in the inspector and add a 'Reactor->ksEntityComponent'.

Create Large Static Cubes on the Platform

  1. Right click in the hierarachy window and create a cube.
  2. Rename it to 'Static Cube'.
  3. Set the scale to (4, 4, 4).
  4. Add a ksEntityComponent.
  5. Place the static cube on the platform. Copy and paste it to place more static cubes on the platform wherever you like.

Create Dynamic Cubes on the Platform for the Player to Push Around

  1. Right click in the hierarachy window and create a cube.
  2. Add a ksEntityComponent.
  3. Add a Rigidbody component.
  4. Place the cube on the platform. Copy and paste it to place more dynamic cubes wherever you like. The player will be able to push these around. You can make stacks of dynamic cubes or place them on the static cubes.

Create an Avatar Entity Prefab that the Player will Control

  1. Create a '3D Object->Capsule' in your scene and name it 'Avatar'.
  2. Add a ksEntityComponent.
  3. Add a Rigidbody component.
  4. In the inspector for the Rigidbody, check all of the 'Freeze Rotation' constraint properties. This will prevent the physics simulation from rotating the avatar.
  5. Add a cube as a child of the avatar.
  6. Set the cube scale to (.25, .25, 1) and the position to (0, 0, .25). The cube will stick out in front of the avatar, allowing you to tell what direction it is facing.
  7. Make the avatar a prefab in the 'Resources' folder.
  8. Delete the avatar from the hierarchy.

Scripting

Create a Player Controller Script

Player controllers allow players to control a server entity. They are executed both on the client and the server. On the client they are used to provide input prediction, and on the server they are used to apply player inputs to entities.

  1. Select the '/Assets/ReactorScripts/Common' folder in the project window and right click. Select 'Create->Reactor->Player Controller' to create a player controller and name it 'AvatarController'.
    • All controllers must be placed under '/Assets/ReactorScripts/Common' or another common folder, thus the context menu will only allow creating player controllers from common folders.
    • All scripts in common folders are available on both the client and the server, and may not contain any Unity references.
    • To create a common folder, first create a folder. Then right-click in that folder in the project browser and select 'Create->Assembly Defintion Reference'. In the inspector for the assembly reference, set 'Assembly Defintion' to 'KSScripts-Common'

AvatarController.cs

using System;
using System.Collections.Generic;
using System.Collections;
using KS.Reactor;

// IDs for axes inputs
public class Axes
{
    public const uint X = 0;
    public const uint Y = 1;
    public const uint ROTATION = 2;
}

// IDs for button inputs
public class Buttons
{
    public const uint JUMP = 0;
}

public class AvatarController : ksPlayerController
{
    [ksEditable] private float m_speed = 5f;
    [ksEditable] private float m_turnSpeed = 360f;
    [ksEditable] private float m_jumpSpeed = 6f;

    // Unique non-zero identifier for this player controller class.
    public override uint Type
    {
        get { return 1; }
    }

    // Register all buttons and axes you will be using here.
    public override void RegisterInputs(ksInputRegistrar registrar)
    {
        registrar.RegisterAxes(Axes.X, Axes.Y, Axes.ROTATION);
        registrar.RegisterButtons(Buttons.JUMP);
    }

    // Called during the update cycle.
    public override void Update()
    {
        // Convert input to rotation in degrees between -180 and 180.
        float targetRotation = Input.GetAxis(Axes.ROTATION) * 180;

        // Convert rotation to a direction vector.
        ksVector2 direction = ksVector2.FromDegrees(targetRotation);

        // Add the direction vector to the entity's position to get the target.
        ksVector3 target = Transform.Position + new ksVector3(direction.X, 0f, direction.Y);

        // Rotate the entity towards the target.
        Transform.RotateTowards(target, m_turnSpeed * Time.Delta);

        // Convert input to horizontal velocity
        ksVector3 velocity = new ksVector3(Input.GetAxis(Axes.X), 0f, Input.GetAxis(Axes.Y)).Normalized() * m_speed;

        // Preserve vertical velocity from the rigid body.
        velocity.Y = RigidBody.Velocity.Y;

        if (Input.IsPressed(Buttons.JUMP))
        {
            // Set vertical jump velocity.
            velocity.Y = m_jumpSpeed;
        }

        // Set velocity.
        RigidBody.Velocity = velocity;
    }
}

Create an Avatar Controller Asset

Once you create a player controller class, you can create assets from it and edit their ksEditable fields.

Player controller assets must be in a 'Resources' folder or asset bundle. To use player controller assets from an asset bundle, you must call ksReactor.RegisterAssetBundle with the asset bundle.

  1. Right click in a 'Resources' folder and select 'Create->Reactor->AvatarController' and name it 'AvatarController'.

Create an Input Binding Script

Input bindings need to be registered in the Reactor API for the controller to work. We will bind the predefined Unity inputs "Horizontal", "Vertical", and "Jump" to the Axes and Button constants we defined in the AvatarController.

  1. Create a Monobehaviour and name it 'BindInputs'.
  2. Attach it to your 'Room' object.

BindInputs.cs

using System.Collections.Generic;
using UnityEngine;
using KS.Reactor;
using KS.Reactor.Client.Unity;

// Bind inputs with Reactor
public class BindInputs : MonoBehaviour
{
    // Run when the game starts.
    void Start()
    {
        // Bind Unity input to Reactor input.
        ksReactor.InputManager.BindAxis(Axes.X, "Horizontal");
        ksReactor.InputManager.BindAxis(Axes.Y, "Vertical");
        ksReactor.InputManager.BindButton(Buttons.JUMP, "Jump");
    }
}

Create a Server Room Script

Our server room script will spawn an avatar for players when they connect, and destroy the avatar when they disconnect.

  1. Select the 'Room' object.
  2. In the 'Add Component' menu, select 'Reactor->New Server Room Script'.
  3. Name the script 'ServerRoom'.
  4. Edit the script to contain the code below.
  5. Assign the 'AvatarController' asset you created to the Controller property in the inspector.

ServerRoom.cs

using KS.Reactor;
using KS.Reactor.Server;

public class ServerRoom : ksServerRoomScript
{
    // The player controller asset used to control avatars.
    [ksEditable]
    public ksPlayerController Controller;

    private ksRandom m_random = new ksRandom();

    // Initialize the script. Called once when the script is loaded.
    public override void Initialize()
    {
        // Register a event handler that will be called when a new player joins the server.
        Room.OnPlayerJoin += OnPlayerJoin;

        // Register a event handler that will be called when a player leaves the server.
        Room.OnPlayerLeave += OnPlayerLeave;

        // Register an update event to be called at every frame at order 0.
        Room.OnUpdate[0] += Update;
    }

    // Cleanup the script. Called once when the script is unloaded.
    public override void Detached()
    {
        Room.OnPlayerJoin -= OnPlayerJoin;
        Room.OnPlayerLeave -= OnPlayerLeave;
        Room.OnUpdate[0] -= Update;
    }

    // Spawn an avatar for a player.
    public void SpawnAvatar(ksIServerPlayer player)
    {
        // Spawn the avatar at a random position in a circle with radius 5 at a height of 10.
        ksVector2 position = m_random.NextVector2() * 5f;
        ksIServerEntity avatar = Room.SpawnEntity("Avatar", new ksVector3(position.X, 10f, position.Y));
        if (Controller != null)
        {
            // Create and attach a copy of the controller asset.
            avatar.SetController(Controller.Clone(), player);
        }
        else
        {
            // If there is no controller asset, construct a new AvatarController and attach it.
            avatar.SetController(new AvatarController(), player);
        }
    }

    // Handle player join events.
    private void OnPlayerJoin(ksIServerPlayer player)
    {
        ksLog.Info("Player " + player.Id + " joined");
        SpawnAvatar(player);
    }

    // Handle player leave events.
    private void OnPlayerLeave(ksIServerPlayer player)
    {
        ksLog.Info("Player " + player.Id + " left");
        // Destroy the avatar when the player disconnects.
        player.DestroyControlledEntities();
    }

    // Called once per frame.
    private void Update()
    {
        // Check the Y position of all entities and destroy those with values less than -10.
        // This prevents dynamic entities such as the cube from falling forever after they fall off the platform.
        foreach (ksIServerEntity entity in Room.Entities)
        {
            if (entity.Transform.Position.Y < -10.0f)
            {
                entity.Destroy();
            }
        }
    }
}

Create a Follow Camera Script

The follow camera script will follow a target game object from a topdown perspective. Later we will set the target to be the local player's avatar.

  1. Create a Monobehaviour named 'FollowCamera' and add it to the 'Main Camera'.

FollowCamera.cs

using UnityEngine;

public class FollowCamera : MonoBehaviour
{
    // Angle to view target at. 90 is directly above.
    [Range(0f, 90f)] public float Angle = 75f;
    // Distance to keep from the target.
    [Range(1f, 30f)] public float Distance = 10f;

    private Vector3 m_offset;
    private float m_lastAngle;
    private float m_lastDistance;

    // Target to follow.
    public static GameObject Target;

    private void LateUpdate()
    {
        if (m_lastAngle != Angle || m_lastDistance != Distance)
        {
            // Compute an offset from the Angle and Distance.
            float radians = Angle * Mathf.Deg2Rad;
            m_offset = new Vector3(0f, Mathf.Sin(radians) * Distance, -Mathf.Cos(radians) * Distance);
            m_lastAngle = Angle;
            m_lastDistance = Distance;
        }
        if (Target != null)
        {
            // Add the offset to the target position to get the camera position, and look at the target.
            transform.position = Target.transform.position + m_offset;
            transform.LookAt(Target.transform);
        }
    }
}

Create a Server Avatar Script

The server avatar script will spawn a new avatar for a player if they die (from walking off the platform).

  1. Select the 'Avatar' prefab.
  2. In the 'Add Component' menu, select 'Reactor->New Server Entity Script'.
  3. Name the script 'ServerAvatar'.

ServerAvatar.cs

using KS.Reactor.Server;
using KS.Reactor;

public class ServerAvatar : ksServerEntityScript
{
    // Called when the script is attached.
    public override void Initialize()
    {

    }

    // Called when the script is detached.
    public override void Detached()
    {
        // Spawn a new avatar for the player when the avatar is destroyed (the player dies).
        // To prevent spawning an avatar when the player disconnects, we check Entity.Owner.Connected.
        if (Entity.Owner != null && Entity.Owner.Connected)
        {
            Room.Scripts.Get<ServerRoom>().SpawnAvatar(Entity.Owner);
        }
    }
}

Create a Client Avatar Script

The client avatar script sets the FollowCamera target to be the avatar if the avatar is controlled by the local player, and converts the mouse position to a rotation input.

  1. Select the 'Avatar' prefab.
  2. In the 'Add Component' menu, select 'Reactor->New Client Entity Script'.
  3. Name the script 'ClientAvatar'.

ClientAvatar.cs

using UnityEngine;
using KS.Reactor.Client.Unity;
using KS.Reactor.Client;
using KS.Reactor;

public class ClientAvatar : ksEntityScript
{
    // Called after properties are initialized.
    public override void Initialize()
    {
        // The entity will have a player controller if it is controlled by the local player.
        if (Entity.PlayerController != null)
        {
            // Set the camera to follow the avatar.
            FollowCamera.Target = gameObject;
        }
    }

    // Called when the script is detached.
    public override void Detached()
    {

    }

    private void Update()
    {
        if (Entity.PlayerController != null)
        {
            // Calculate the mouse position relative to the center of the screen.
            ksVector2 mouse = new ksVector2();
            mouse.X = -(Input.mousePosition.x - Screen.width / 2f);
            mouse.Y = -(Input.mousePosition.y - Screen.height / 2f);
            // Convert the mouse position to degrees, divide by 180 and subtract 1 to get a value between -1 and 1.
            float value = mouse.ToDegrees() / 180 - 1;
            // Set the rotation input value. Axis values must be between -1 and 1.
            ksReactor.InputManager.SetAxis(Axes.ROTATION, value);
        }
    }
}

Testing

  1. Build your scene config (CTRL+F2).
  2. Start a local server.
  3. Enter play mode.

When you connect, an avatar should spawn at a random location above the platform. You can move around using WASD or the arrow keys. Pressing space will jump. You can keep jumping in mid-air. Your avatar will rotate to look at the mouse. If you fall off the platform, you will respawn above the platform. The avatar will be destroyed when you disconnect.