# Creating a player controller

This guide will demonstrate how to create a basic player that you can move using WASD and the mouse. This guide serves as the foundation for any multiplayer game that lets you play as a 3rd-person player.

When you're finished, you'll end up with a multiplayer game that looks like this:

We'll start by creating a singleplayer player controller from scratch. We'll get the controls to feel nice and then we'll make it multiplayer. If you're only interested in the multiplayer part, skip to making it multiplayer.

# Creating a singleplayer player controller

Create a copy of the Blank Scene Template scene included in the Normal/Examples folder. Save it as "Player Controller".

Let's start by creating a game object to represent the player. Create an empty game object in the scene named "Player".

Add a Rigidbody to the Player game object so that it can respond to physics. We'll always want our player to remain upright, so let's enable Freeze Rotation on every axis.

Last, we'll add a Sphere game object to represent the body of the player. Set the scale of the sphere to 1.8 and set the y-position at 0.9 so the bottom is lined up with the ground.

Looking good so far! If we enter Play mode, nothing will happen, so let's create a script to control our player. Create a new script called Player and add it to the Player game object.

The first step is to create a method to capture player input in Update() and a method to apply the input to our rigidbody in FixedUpdate()

using UnityEngine;

public class Player : MonoBehaviour {
    // Physics
    private Vector3   _targetMovement;
    private Vector3   _movement;

    private Rigidbody _rigidbody;

    private void Awake() {
        // Set physics timestep to 60hz
        Time.fixedDeltaTime = 1.0f/60.0f;

        // Store a reference to the rigidbody for easy access
        _rigidbody = GetComponent<Rigidbody>();
    }

    private void Update() {
        // Use WASD input and the camera look direction to calculate the movement target
        CalculateTargetMovement();
    }

    private void FixedUpdate() {
        // Move the player based on the input
        MovePlayer();
    }

    private void CalculateTargetMovement() {
        // Get input movement. Multiple by 6.0 to increase speed.
        Vector3 inputMovement = new Vector3();
        inputMovement.x = Input.GetAxisRaw("Horizontal") * 6.0f;
        inputMovement.z = Input.GetAxisRaw("Vertical")   * 6.0f;
        _targetMovement = inputMovement;
    }

    private void MovePlayer() {
        // Start with the current velocity
        Vector3 velocity = _rigidbody.velocity;

        // Smoothly animate towards the target movement velocity
        _movement = Vector3.Lerp(_movement, _targetMovement, Time.fixedDeltaTime * 5.0f);
        velocity.x = _movement.x;
        velocity.z = _movement.z;
        
        // Set the velocity on the rigidbody
        _rigidbody.velocity = velocity;
    }
}

Enter Play mode and give this a shot. Using WASD, you should see the sphere move around and roll over the cubes.

So far so good, but our camera is left behind! Let's fix that.

We could add the camera as a child of the player, but later on we plan to instantiate the player after we connect to a multiplayer room, which means we'd have no camera until after we connect. Instead, we'll create an empty game object to represent a target point on the player that the camera should follow.

Create an empty game object called "Camera Target" and set the y-position to 1.0. This will be the point our camera looks at. We'll also add a Parent Constraint component to the camera. Check the box for Is Active and set the Position At Rest and Rotation At Rest to (0.0, 1.5, -3.0) and (15.0, 0.0, 0.0) respectively to match the camera defaults. Finally, add the CameraTarget game object as the source.

Enter Play mode and use WASD to move around.

Getting there, but it would be really nice if we could turn and look around. Let's use the mouse for this. We'll use the mouse input to rotate the camera target in the direction we're looking:




 
 
 
 
















 
 










 
 
 
 
 
 
 
 
 
 
 
 
 







 
 
 
 
 
 















 
 
 
 
 
 


using UnityEngine;

public class Player : MonoBehaviour {
    // Camera
    public  Transform  cameraTarget;
    private float     _mouseLookX;
    private float     _mouseLookY;

    // Physics
    private Vector3   _targetMovement;
    private Vector3   _movement;

    private Rigidbody _rigidbody;

    private void Awake() {
        // Set physics timestep to 60hz
        Time.fixedDeltaTime = 1.0f/60.0f;

        // Store a reference to the rigidbody for easy access
        _rigidbody = GetComponent<Rigidbody>();
    }

    private void Update() {
        // Move the camera using the mouse
        RotateCamera();

        // Use WASD input and the camera look direction to calculate the movement target
        CalculateTargetMovement();
    }

    private void FixedUpdate() {
        // Move the player based on the input
        MovePlayer();
    }

    private void RotateCamera() {
        // Get the latest mouse movement. Multiple by 4.0 to increase sensitivity.
        _mouseLookX += Input.GetAxis("Mouse X") * 4.0f;
        _mouseLookY += Input.GetAxis("Mouse Y") * 4.0f;

        // Clamp how far you can look up + down
        while (_mouseLookY < -180.0f) _mouseLookY += 360.0f;
        while (_mouseLookY >  180.0f) _mouseLookY -= 360.0f;
        _mouseLookY = Mathf.Clamp(_mouseLookY, -15.0f, 15.0f);

        // Rotate camera
        cameraTarget.localRotation = Quaternion.Euler(-_mouseLookY, _mouseLookX, 0.0f);
    }

    private void CalculateTargetMovement() {
        // Get input movement. Multiple by 6.0 to increase speed.
        Vector3 inputMovement = new Vector3();
        inputMovement.x = Input.GetAxisRaw("Horizontal") * 6.0f;
        inputMovement.z = Input.GetAxisRaw("Vertical")   * 6.0f;

        // Get the direction the camera is looking parallel to the ground plane.
        Vector3    cameraLookForwardVector = ProjectVectorOntoGroundPlane(cameraTarget.forward);
        Quaternion cameraLookForward       = Quaternion.LookRotation(cameraLookForwardVector);

        // Use the camera look direction to convert the input movement from camera space to world space
        _targetMovement = cameraLookForward * inputMovement;
    }

    private void MovePlayer() {
        // Start with the current velocity
        Vector3 velocity = _rigidbody.velocity;

        // Smoothly animate towards the target movement velocity
        _movement = Vector3.Lerp(_movement, _targetMovement, Time.fixedDeltaTime * 5.0f);
        velocity.x = _movement.x;
        velocity.z = _movement.z;
        
        // Set the velocity on the rigidbody
        _rigidbody.velocity = velocity;
    }

    // Given a forward vector, get a y-axis rotation that points in the same direction that's parallel to the ground plane
    private static Vector3 ProjectVectorOntoGroundPlane(Vector3 vector) {
        Vector3 planeNormal = Vector3.up;
        Vector3.OrthoNormalize(ref planeNormal, ref vector);
        return vector;
    }
}

Let's give this version a shot.

Already this is much better! We can look around with the mouse and the player smoothly moves towards the direction we're facing.

Let's add the ability to jump while we're at it. We'll check in Update() to see if the space bar is pressed and in FixedUpdate() we'll set an instantaneous upward velocity to make the player jump:













 
 


















 
 



































 
 
 
 
 










 
 
 
 
 
 
 
 
 
 
 
 
 













using UnityEngine;

public class Player : MonoBehaviour {
    // Camera
    public  Transform  cameraTarget;
    private float     _mouseLookX;
    private float     _mouseLookY;

    // Physics
    private Vector3   _targetMovement;
    private Vector3   _movement;

    private bool      _jumpThisFrame;
    private bool      _jumping;

    private Rigidbody _rigidbody;

    private void Awake() {
        // Set physics timestep to 60hz
        Time.fixedDeltaTime = 1.0f/60.0f;

        // Store a reference to the rigidbody for easy access
        _rigidbody = GetComponent<Rigidbody>();
    }

    private void Update() {
        // Move the camera using the mouse
        RotateCamera();

        // Use WASD input and the camera look direction to calculate the movement target
        CalculateTargetMovement();

        // Check if we should jump this frame
        CheckForJump();
    }

    private void FixedUpdate() {
        // Move the player based on the input
        MovePlayer();
    }

    private void RotateCamera() {
        // Get the latest mouse movement. Multiple by 4.0 to increase sensitivity.
        _mouseLookX += Input.GetAxis("Mouse X") * 4.0f;
        _mouseLookY += Input.GetAxis("Mouse Y") * 4.0f;

        // Clamp how far you can look up + down
        while (_mouseLookY < -180.0f) _mouseLookY += 360.0f;
        while (_mouseLookY >  180.0f) _mouseLookY -= 360.0f;
        _mouseLookY = Mathf.Clamp(_mouseLookY, -15.0f, 15.0f);

        // Rotate camera
        cameraTarget.localRotation = Quaternion.Euler(-_mouseLookY, _mouseLookX, 0.0f);
    }

    private void CalculateTargetMovement() {
        // Get input movement. Multiple by 6.0 to increase speed.
        Vector3 inputMovement = new Vector3();
        inputMovement.x = Input.GetAxisRaw("Horizontal") * 6.0f;
        inputMovement.z = Input.GetAxisRaw("Vertical")   * 6.0f;

        // Get the direction the camera is looking parallel to the ground plane.
        Vector3    cameraLookForwardVector = ProjectVectorOntoGroundPlane(cameraTarget.forward);
        Quaternion cameraLookForward       = Quaternion.LookRotation(cameraLookForwardVector);

        // Use the camera look direction to convert the input movement from camera space to world space
        _targetMovement = cameraLookForward * inputMovement;
    }

    private void CheckForJump() {
        // Jump if the space bar was pressed this frame and we're not already jumping, trigger the jump
        if (Input.GetKeyDown(KeyCode.Space) && !_jumping)
            _jumpThisFrame = true;
    }

    private void MovePlayer() {
        // Start with the current velocity
        Vector3 velocity = _rigidbody.velocity;

        // Smoothly animate towards the target movement velocity
        _movement = Vector3.Lerp(_movement, _targetMovement, Time.fixedDeltaTime * 5.0f);
        velocity.x = _movement.x;
        velocity.z = _movement.z;

        // Jump
        if (_jumpThisFrame) {
            // Instantaneously set the vertical velocity to 6.0 m/s
            velocity.y = 6.0f;

            // Mark the player as currently jumping and clear the jump input
            _jumping       = true;
            _jumpThisFrame = false;
        }

        // Reset jump after the apex
        if (_jumping && velocity.y < -0.1f)
            _jumping = false;
        
        // Set the velocity on the rigidbody
        _rigidbody.velocity = velocity;
    }

    // Given a forward vector, get a y-axis rotation that points in the same direction that's parallel to the ground plane
    private static Vector3 ProjectVectorOntoGroundPlane(Vector3 vector) {
        Vector3 planeNormal = Vector3.up;
        Vector3.OrthoNormalize(ref planeNormal, ref vector);
        return vector;
    }
}

Enter Play mode and give it a shot!

It's looking good! However, this sphere is pretty uninspiring.

Let's grab the Hoverbird Character model from the Normal/Examples/Hoverbird Player folder and add it as a child of our Player. Make sure to zero out the transform for it. We still want the Sphere collider for physics, but let's turn off the Mesh Renderer so it doesn't render to the scene.

This is already much better, but let's add some code to make the Hoverbird more believable. We'll make it rotate towards the direction of travel and lean into turns. The animation logic is a little complex, but don't sweat it! It's not required for using Normcore or making the character multiplayer.


















 
 
























 
 































































 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 








 
 
 
 
 
 

 
 
 
 
 
 
 


using UnityEngine;

public class Player : MonoBehaviour {
    // Camera
    public  Transform  cameraTarget;
    private float     _mouseLookX;
    private float     _mouseLookY;

    // Physics
    private Vector3   _targetMovement;
    private Vector3   _movement;

    private bool      _jumpThisFrame;
    private bool      _jumping;

    private Rigidbody _rigidbody;

    // Hoverbird
    [SerializeField] private Transform _character = default;

    private void Awake() {
        // Set physics timestep to 60hz
        Time.fixedDeltaTime = 1.0f/60.0f;

        // Store a reference to the rigidbody for easy access
        _rigidbody = GetComponent<Rigidbody>();
    }

    private void Update() {
        // Move the camera using the mouse
        RotateCamera();

        // Use WASD input and the camera look direction to calculate the movement target
        CalculateTargetMovement();

        // Check if we should jump this frame
        CheckForJump();
    }

    private void FixedUpdate() {
        // Move the player based on the input
        MovePlayer();

        // Animate the character to match the player movement
        AnimateCharacter();
    }

    private void RotateCamera() {
        // Get the latest mouse movement. Multiple by 4.0 to increase sensitivity.
        _mouseLookX += Input.GetAxis("Mouse X") * 4.0f;
        _mouseLookY += Input.GetAxis("Mouse Y") * 4.0f;

        // Clamp how far you can look up + down
        while (_mouseLookY < -180.0f) _mouseLookY += 360.0f;
        while (_mouseLookY >  180.0f) _mouseLookY -= 360.0f;
        _mouseLookY = Mathf.Clamp(_mouseLookY, -15.0f, 15.0f);

        // Rotate camera
        cameraTarget.localRotation = Quaternion.Euler(-_mouseLookY, _mouseLookX, 0.0f);
    }

    private void CalculateTargetMovement() {
        // Get input movement. Multiple by 6.0 to increase speed.
        Vector3 inputMovement = new Vector3();
        inputMovement.x = Input.GetAxisRaw("Horizontal") * 6.0f;
        inputMovement.z = Input.GetAxisRaw("Vertical")   * 6.0f;

        // Get the direction the camera is looking parallel to the ground plane.
        Vector3    cameraLookForwardVector = ProjectVectorOntoGroundPlane(cameraTarget.forward);
        Quaternion cameraLookForward       = Quaternion.LookRotation(cameraLookForwardVector);

        // Use the camera look direction to convert the input movement from camera space to world space
        _targetMovement = cameraLookForward * inputMovement;
    }

    private void CheckForJump() {
        // Jump if the space bar was pressed this frame and we're not already jumping, trigger the jump
        if (Input.GetKeyDown(KeyCode.Space) && !_jumping)
            _jumpThisFrame = true;
    }

    private void MovePlayer() {
        // Start with the current velocity
        Vector3 velocity = _rigidbody.velocity;

        // Smoothly animate towards the target movement velocity
        _movement = Vector3.Lerp(_movement, _targetMovement, Time.fixedDeltaTime * 5.0f);
        velocity.x = _movement.x;
        velocity.z = _movement.z;

        // Jump
        if (_jumpThisFrame) {
            // Instantaneously set the vertical velocity to 6.0 m/s
            velocity.y = 6.0f;

            // Mark the player as currently jumping and clear the jump input
            _jumping       = true;
            _jumpThisFrame = false;
        }

        // Reset jump after the apex
        if (_jumping && velocity.y < -0.1f)
            _jumping = false;
        
        // Set the velocity on the rigidbody
        _rigidbody.velocity = velocity;
    }

    // Rotate the character to face the direction we're moving. Lean towards the target movement direction.
    private void AnimateCharacter() {
        // Calculate the direction that the character is facing parallel to the ground plane
        Vector3    characterLocalForwardVector = _character.localRotation * Vector3.forward;
        Vector3    characterLookForwardVector  = ProjectVectorOntoGroundPlane(characterLocalForwardVector);
        Quaternion characterLookForward        = Quaternion.LookRotation(characterLookForwardVector);

        // Calculate the angle between the current movement direction and the target movement direction
        Vector3 targetMovementNormalized = _targetMovement.normalized;
        Vector3       movementNormalized =       _movement.normalized;
        float angle = targetMovementNormalized.sqrMagnitude > 0.0f ? SignedAngle2D(targetMovementNormalized, movementNormalized) : 0.0f;

        // Convert the delta between movement direction and the target movement direction to a lean amount. Clamp to +/- 45 degrees so the player doesn't lean too far.
        angle = angle * Mathf.Rad2Deg;
        angle = Mathf.Clamp(angle, -45.0f, 45.0f);

        // Convert the lean angle to a Quaternion that's oriented in the direction the character is facing
        Quaternion leanRotation = characterLookForward * Quaternion.Euler(0.0f, 0.0f, angle);

        // Rotate to face the direction of travel if we're moving forward
        Vector3 targetCharacterLookForwardVector = characterLookForwardVector;
        if (GetRigidbodyForwardVelocity(_rigidbody) >= 2.0f)
            targetCharacterLookForwardVector = _rigidbody.velocity.normalized;

        // Compose the target character rotation from the target look direction + target lean direction
        Quaternion targetRotation = Quaternion.LookRotation(targetCharacterLookForwardVector, leanRotation * Vector3.up);

        // Animate the character towards the target rotation
        _character.localRotation = Quaternion.Slerp(_character.localRotation, targetRotation, 5.0f * Time.fixedDeltaTime);
    }

    // Given a forward vector, get a y-axis rotation that points in the same direction that's parallel to the ground plane
    private static Vector3 ProjectVectorOntoGroundPlane(Vector3 vector) {
        Vector3 planeNormal = Vector3.up;
        Vector3.OrthoNormalize(ref planeNormal, ref vector);
        return vector;
    }

    // Get the rigidbody velocity along the ground plane
    private static float GetRigidbodyForwardVelocity(Rigidbody rigidbody) {
        Vector3 forwardVelocity = rigidbody.velocity;
        forwardVelocity.y = 0.0f;
        return forwardVelocity.magnitude;
    }

    // Get the difference between two angles along the ground plane
    private static float SignedAngle2D(Vector3 a, Vector3 b) {
        float angle = Mathf.Atan2(a.z, a.x) - Mathf.Atan2(b.z, b.x);
        if (angle <= -Mathf.PI) angle += 2.0f * Mathf.PI;
        if (angle >   Mathf.PI) angle -= 2.0f * Mathf.PI;
        return angle;
    }
}

Make sure to wire up the Hoverbird Character game object to the Character input on Player and then let's enter Play mode and try it out.

And there we have it! A nice and simple Hoverbird player with WASD controls, camera controls, jumping, and a simple animated character.

Believe it or not, the hardest part is over. Now let's make it multiplayer!

# Making it multiplayer

We'll start by turning the Player game object into a Realtime prefab that we can instantiate for every player in the multiplayer room. Add a RealtimeView to the Player, create a Resources folder, and drag it into the Resources folder. Go ahead and delete it from the scene once this is done.

Next up, we'll create a script to instantiate a copy of the prefab after we connect to a room. We'll also want to wire up the Parent Constraint reference so the camera follows our newly instantiated player prefab:

using UnityEngine;
using UnityEngine.Animations;
using Normal.Realtime;

public class PlayerManager : MonoBehaviour {
    [SerializeField] private GameObject _camera = default;

    private Realtime _realtime;

    private void Awake() {
        // Get the Realtime component on this game object
        _realtime = GetComponent<Realtime>();

        // Notify us when Realtime successfully connects to the room
        _realtime.didConnectToRoom += DidConnectToRoom;
    }

    private void DidConnectToRoom(Realtime realtime) {
        // Instantiate the Player for this client once we've successfully connected to the room
        GameObject playerGameObject = Realtime.Instantiate(              prefabName: "Player",  // Prefab name
                                                                      ownedByClient: true,      // Make sure the RealtimeView on this prefab is owned by this client
                                                           preventOwnershipTakeover: true,      // Prevent other clients from calling RequestOwnership() on the root RealtimeView.
                                                                        useInstance: realtime); // Use the instance of Realtime that fired the didConnectToRoom event.

        // Get a reference to the player
        Player player = playerGameObject.GetComponent<Player>();

        // Get the constraint used to position the camera behind the player
        ParentConstraint cameraConstraint = _camera.GetComponent<ParentConstraint>();
        
        // Add the camera target so the camera follows it
        ConstraintSource constraintSource = new ConstraintSource { sourceTransform = player.cameraTarget, weight = 1.0f };
        int constraintIndex = cameraConstraint.AddSource(constraintSource);

        // Set the camera offset so it acts like a third-person camera.
        cameraConstraint.SetTranslationOffset(constraintIndex, new Vector3( 0.0f,  1.0f, -4.0f));
        cameraConstraint.SetRotationOffset   (constraintIndex, new Vector3(15.0f,  0.0f,  0.0f));
    }
}

Create an empty game object in the scene, add Realtime and our newly created PlayerManager. Make sure you've configured your app key, and then let's export a build and try it out!

Export a build, open it, and hit Play in the editor.

This looks good but it seems we're controlling all players at the same time. Let's add some logic so we only control our own player.






















 
 








 
 



 
 
 



 
 
 


 
 
 
 
 
 
 
 
 
 

 
 
 
 
 
 
 




















































































































using UnityEngine;
using Normal.Realtime;

public class Player : MonoBehaviour {
    // Camera
    public  Transform  cameraTarget;
    private float     _mouseLookX;
    private float     _mouseLookY;

    // Physics
    private Vector3   _targetMovement;
    private Vector3   _movement;

    private bool      _jumpThisFrame;
    private bool      _jumping;

    private Rigidbody _rigidbody;

    // Character
    [SerializeField] private Transform _character = default;

    // Multiplayer
    private RealtimeView _realtimeView;

    private void Awake() {
        // Set physics timestep to 60hz
        Time.fixedDeltaTime = 1.0f/60.0f;

        // Store a reference to the rigidbody for easy access
        _rigidbody = GetComponent<Rigidbody>();

        // Store a reference to the RealtimeView for easy access
        _realtimeView = GetComponent<RealtimeView>();
    }

    private void Update() {
        // Call LocalUpdate() only if this instance is owned by the local client
        if (_realtimeView.isOwnedLocallyInHierarchy)
            LocalUpdate();
    }

    private void FixedUpdate() {
        // Call LocalFixedUpdate() only if this instance is owned by the local client
        if (_realtimeView.isOwnedLocallyInHierarchy)
            LocalFixedUpdate();
    }

    private void LocalUpdate() {
        // Move the camera using the mouse
        RotateCamera();

        // Use WASD input and the camera look direction to calculate the movement target
        CalculateTargetMovement();

        // Check if we should jump this frame
        CheckForJump();
    }

    private void LocalFixedUpdate() {
        // Move the player based on the input
        MovePlayer();

        // Animate the character to match the player movement
        AnimateCharacter();
    }

    private void RotateCamera() {
        // Get the latest mouse movement. Multiple by 4.0 to increase sensitivity.
        _mouseLookX += Input.GetAxis("Mouse X") * 4.0f;
        _mouseLookY += Input.GetAxis("Mouse Y") * 4.0f;

        // Clamp how far you can look up + down
        while (_mouseLookY < -180.0f) _mouseLookY += 360.0f;
        while (_mouseLookY >  180.0f) _mouseLookY -= 360.0f;
        _mouseLookY = Mathf.Clamp(_mouseLookY, -15.0f, 15.0f);

        // Rotate camera
        cameraTarget.localRotation = Quaternion.Euler(-_mouseLookY, _mouseLookX, 0.0f);
    }

    private void CalculateTargetMovement() {
        // Get input movement. Multiple by 6.0 to increase speed.
        Vector3 inputMovement = new Vector3();
        inputMovement.x = Input.GetAxisRaw("Horizontal") * 6.0f;
        inputMovement.z = Input.GetAxisRaw("Vertical")   * 6.0f;

        // Get the direction the camera is looking parallel to the ground plane.
        Vector3    cameraLookForwardVector = ProjectVectorOntoGroundPlane(cameraTarget.forward);
        Quaternion cameraLookForward       = Quaternion.LookRotation(cameraLookForwardVector);

        // Use the camera look direction to convert the input movement from camera space to world space
        _targetMovement = cameraLookForward * inputMovement;
    }

    private void CheckForJump() {
        // Jump if the space bar was pressed this frame and we're not already jumping, trigger the jump
        if (Input.GetKeyDown(KeyCode.Space) && !_jumping)
            _jumpThisFrame = true;
    }

    private void MovePlayer() {
        // Start with the current velocity
        Vector3 velocity = _rigidbody.velocity;

        // Smoothly animate towards the target movement velocity
        _movement = Vector3.Lerp(_movement, _targetMovement, Time.fixedDeltaTime * 5.0f);
        velocity.x = _movement.x;
        velocity.z = _movement.z;

        // Jump
        if (_jumpThisFrame) {
            // Instantaneously set the vertical velocity to 6.0 m/s
            velocity.y = 6.0f;

            // Mark the player as currently jumping and clear the jump input
            _jumping       = true;
            _jumpThisFrame = false;
        }

        // Reset jump after the apex
        if (_jumping && velocity.y < -0.1f)
            _jumping = false;
        
        // Set the velocity on the rigidbody
        _rigidbody.velocity = velocity;
    }

    // Rotate the character to face the direction we're moving. Lean towards the target movement direction.
    private void AnimateCharacter() {
        // Calculate the direction that the character is facing parallel to the ground plane
        Vector3    characterLocalForwardVector = _character.localRotation * Vector3.forward;
        Vector3    characterLookForwardVector  = ProjectVectorOntoGroundPlane(characterLocalForwardVector);
        Quaternion characterLookForward        = Quaternion.LookRotation(characterLookForwardVector);

        // Calculate the angle between the current movement direction and the target movement direction
        Vector3 targetMovementNormalized = _targetMovement.normalized;
        Vector3       movementNormalized =       _movement.normalized;
        float angle = targetMovementNormalized.sqrMagnitude > 0.0f ? SignedAngle2D(targetMovementNormalized, movementNormalized) : 0.0f;

        // Convert the delta between movement direction and the target movement direction to a lean amount. Clamp to +/- 45 degrees so the player doesn't lean too far.
        angle = angle * Mathf.Rad2Deg;
        angle = Mathf.Clamp(angle, -45.0f, 45.0f);

        // Convert the lean angle to a Quaternion that's oriented in the direction the character is facing
        Quaternion leanRotation = characterLookForward * Quaternion.Euler(0.0f, 0.0f, angle);

        // Rotate to face the direction of travel if we're moving forward
        Vector3 targetCharacterLookForwardVector = characterLookForwardVector;
        if (GetRigidbodyForwardVelocity(_rigidbody) >= 2.0f)
            targetCharacterLookForwardVector = _rigidbody.velocity.normalized;

        // Compose the target character rotation from the target look direction + target lean direction
        Quaternion targetRotation = Quaternion.LookRotation(targetCharacterLookForwardVector, leanRotation * Vector3.up);

        // Animate the character towards the target rotation
        _character.localRotation = Quaternion.Slerp(_character.localRotation, targetRotation, 5.0f * Time.fixedDeltaTime);
    }

    // Given a forward vector, get a y-axis rotation that points in the same direction that's parallel to the ground plane
    private static Vector3 ProjectVectorOntoGroundPlane(Vector3 vector) {
        Vector3 planeNormal = Vector3.up;
        Vector3.OrthoNormalize(ref planeNormal, ref vector);
        return vector;
    }

    // Get the rigidbody velocity along the ground plane
    private static float GetRigidbodyForwardVelocity(Rigidbody rigidbody) {
        Vector3 forwardVelocity = rigidbody.velocity;
        forwardVelocity.y = 0.0f;
        return forwardVelocity.magnitude;
    }

    // Get the difference between two angles along the ground plane
    private static float SignedAngle2D(Vector3 a, Vector3 b) {
        float angle = Mathf.Atan2(a.z, a.x) - Mathf.Atan2(b.z, b.x);
        if (angle <= -Mathf.PI) angle += 2.0f * Mathf.PI;
        if (angle >   Mathf.PI) angle -= 2.0f * Mathf.PI;
        return angle;
    }
}

Here we've renamed Update() and FixedUpdate() to LocalUpdate() and LocalFixedUpdate(). We've also set them to only run if this player is owned by the local client. Inside of PlayerManager.cs we've set ownedByClient: true which takes ownership of the RealtimeView when the Player prefab is instantiated. We then use isOwnedLocallyInHierarchy to determine if the RealtimeView on this prefab is owned locally.

Let's try this version out.

Much better, but not quite there yet. We're now controlling only our own avatar, but if you look at the other build, it's not synchronized over the network. Luckily, Normcore has a built-in component that'll do that for you. We'll add a RealtimeTransform component to the Player to synchronize movement and a RealtimeTransform on the Hoverbird Character game object so we can synchronize the character look direction and lean.

If you're familiar with our Networked Physics guide, you'll know that RealtimeTransform when paired with a rigidbody will attempt to clear ownership automatically when the rigidbody goes to sleep in order to allow other clients to take it over on physics collisions. However, in this case, we always want to retain ownership of the Player RealtimeTransform at all times. In order to do that, we'll want to set it to Maintain Ownership While Sleeping.

The last thing we'll need to do is call RequestOwnership() on each RealtimeTransform when the prefab is instantiated to signal that all other clients should use this client's transform as the source of truth.




































 
 
 
 
 













 
 
 
 
 







































































































































using UnityEngine;
using Normal.Realtime;

public class Player : MonoBehaviour {
    // Camera
    public  Transform  cameraTarget;
    private float     _mouseLookX;
    private float     _mouseLookY;

    // Physics
    private Vector3   _targetMovement;
    private Vector3   _movement;

    private bool      _jumpThisFrame;
    private bool      _jumping;

    private Rigidbody _rigidbody;

    // Character
    [SerializeField] private Transform _character = default;

    // Multiplayer
    private RealtimeView _realtimeView;

    private void Awake() {
        // Set physics timestep to 60hz
        Time.fixedDeltaTime = 1.0f/60.0f;

        // Store a reference to the rigidbody for easy access
        _rigidbody = GetComponent<Rigidbody>();

        // Store a reference to the RealtimeView for easy access
        _realtimeView = GetComponent<RealtimeView>();
    }

    private void Start() {
        // Call LocalStart() only if this instance is owned by the local client
        if (_realtimeView.isOwnedLocallyInHierarchy)
            LocalStart();
    }

    private void Update() {
        // Call LocalUpdate() only if this instance is owned by the local client
        if (_realtimeView.isOwnedLocallyInHierarchy)
            LocalUpdate();
    }

    private void FixedUpdate() {
        // Call LocalFixedUpdate() only if this instance is owned by the local client
        if (_realtimeView.isOwnedLocallyInHierarchy)
            LocalFixedUpdate();
    }

    private void LocalStart() {
        // Request ownership of the Player and the character RealtimeTransforms
                   GetComponent<RealtimeTransform>().RequestOwnership();
        _character.GetComponent<RealtimeTransform>().RequestOwnership();
    }

    private void LocalUpdate() {
        // Move the camera using the mouse
        RotateCamera();

        // Use WASD input and the camera look direction to calculate the movement target
        CalculateTargetMovement();

        // Check if we should jump this frame
        CheckForJump();
    }

    private void LocalFixedUpdate() {
        // Move the player based on the input
        MovePlayer();

        // Animate the character to match the player movement
        AnimateCharacter();
    }

    private void RotateCamera() {
        // Get the latest mouse movement. Multiple by 4.0 to increase sensitivity.
        _mouseLookX += Input.GetAxis("Mouse X") * 4.0f;
        _mouseLookY += Input.GetAxis("Mouse Y") * 4.0f;

        // Clamp how far you can look up + down
        while (_mouseLookY < -180.0f) _mouseLookY += 360.0f;
        while (_mouseLookY >  180.0f) _mouseLookY -= 360.0f;
        _mouseLookY = Mathf.Clamp(_mouseLookY, -15.0f, 15.0f);

        // Rotate camera
        cameraTarget.localRotation = Quaternion.Euler(-_mouseLookY, _mouseLookX, 0.0f);
    }

    private void CalculateTargetMovement() {
        // Get input movement. Multiple by 6.0 to increase speed.
        Vector3 inputMovement = new Vector3();
        inputMovement.x = Input.GetAxisRaw("Horizontal") * 6.0f;
        inputMovement.z = Input.GetAxisRaw("Vertical")   * 6.0f;

        // Get the direction the camera is looking parallel to the ground plane.
        Vector3    cameraLookForwardVector = ProjectVectorOntoGroundPlane(cameraTarget.forward);
        Quaternion cameraLookForward       = Quaternion.LookRotation(cameraLookForwardVector);

        // Use the camera look direction to convert the input movement from camera space to world space
        _targetMovement = cameraLookForward * inputMovement;
    }

    private void CheckForJump() {
        // Jump if the space bar was pressed this frame and we're not already jumping, trigger the jump
        if (Input.GetKeyDown(KeyCode.Space) && !_jumping)
            _jumpThisFrame = true;
    }

    private void MovePlayer() {
        // Start with the current velocity
        Vector3 velocity = _rigidbody.velocity;

        // Smoothly animate towards the target movement velocity
        _movement = Vector3.Lerp(_movement, _targetMovement, Time.fixedDeltaTime * 5.0f);
        velocity.x = _movement.x;
        velocity.z = _movement.z;

        // Jump
        if (_jumpThisFrame) {
            // Instantaneously set the vertical velocity to 6.0 m/s
            velocity.y = 6.0f;

            // Mark the player as currently jumping and clear the jump input
            _jumping       = true;
            _jumpThisFrame = false;
        }

        // Reset jump after the apex
        if (_jumping && velocity.y < -0.1f)
            _jumping = false;
        
        // Set the velocity on the rigidbody
        _rigidbody.velocity = velocity;
    }

    // Rotate the character to face the direction we're moving. Lean towards the target movement direction.
    private void AnimateCharacter() {
        // Calculate the direction that the character is facing parallel to the ground plane
        Vector3    characterLocalForwardVector = _character.localRotation * Vector3.forward;
        Vector3    characterLookForwardVector  = ProjectVectorOntoGroundPlane(characterLocalForwardVector);
        Quaternion characterLookForward        = Quaternion.LookRotation(characterLookForwardVector);

        // Calculate the angle between the current movement direction and the target movement direction
        Vector3 targetMovementNormalized = _targetMovement.normalized;
        Vector3       movementNormalized =       _movement.normalized;
        float angle = targetMovementNormalized.sqrMagnitude > 0.0f ? SignedAngle2D(targetMovementNormalized, movementNormalized) : 0.0f;

        // Convert the delta between movement direction and the target movement direction to a lean amount. Clamp to +/- 45 degrees so the player doesn't lean too far.
        angle = angle * Mathf.Rad2Deg;
        angle = Mathf.Clamp(angle, -45.0f, 45.0f);

        // Convert the lean angle to a Quaternion that's oriented in the direction the character is facing
        Quaternion leanRotation = characterLookForward * Quaternion.Euler(0.0f, 0.0f, angle);

        // Rotate to face the direction of travel if we're moving forward
        Vector3 targetCharacterLookForwardVector = characterLookForwardVector;
        if (GetRigidbodyForwardVelocity(_rigidbody) >= 2.0f)
            targetCharacterLookForwardVector = _rigidbody.velocity.normalized;

        // Compose the target character rotation from the target look direction + target lean direction
        Quaternion targetRotation = Quaternion.LookRotation(targetCharacterLookForwardVector, leanRotation * Vector3.up);

        // Animate the character towards the target rotation
        _character.localRotation = Quaternion.Slerp(_character.localRotation, targetRotation, 5.0f * Time.fixedDeltaTime);
    }

    // Given a forward vector, get a y-axis rotation that points in the same direction that's parallel to the ground plane
    private static Vector3 ProjectVectorOntoGroundPlane(Vector3 vector) {
        Vector3 planeNormal = Vector3.up;
        Vector3.OrthoNormalize(ref planeNormal, ref vector);
        return vector;
    }

    // Get the rigidbody velocity along the ground plane
    private static float GetRigidbodyForwardVelocity(Rigidbody rigidbody) {
        Vector3 forwardVelocity = rigidbody.velocity;
        forwardVelocity.y = 0.0f;
        return forwardVelocity.magnitude;
    }

    // Get the difference between two angles along the ground plane
    private static float SignedAngle2D(Vector3 a, Vector3 b) {
        float angle = Mathf.Atan2(a.z, a.x) - Mathf.Atan2(b.z, b.x);
        if (angle <= -Mathf.PI) angle += 2.0f * Mathf.PI;
        if (angle >   Mathf.PI) angle -= 2.0f * Mathf.PI;
        return angle;
    }
}

And that's it! Enter Play mode and try it out. Everything should work perfectly and be in sync across all clients : )

Looks good! Export a build and send it to a friend. You’ll both be able to join and board around together.

For future reference, the complete project is included in the Normcore unitypackage under Normal/Examples/Realtime + Hoverbird Player.

Looking to learn more about Normcore? Check out our guides on synchronizing custom data and networked physics: