In NetworkCharacterController (Fusion's sample code) why is Velocity and IsGrounded Networked?

I am trying to learn photon fusion through the sample codes. I am just wondering why is there a need for IsGrounded and Velocity to be networked when the transform is already sent across the other players? Does it have something to do with client predictions and stuff?

using System;
using Fusion;
using UnityEngine;


[RequireComponent(typeof(CharacterController))]
[OrderBefore(typeof(NetworkTransform))]
[DisallowMultipleComponent]
// ReSharper disable once CheckNamespace
public class NetworkCharacterController : NetworkTransform {
  [Header("Character Controller Settings")]
  public float gravity       = -20.0f;
  public float jumpImpulse   = 8.0f;
  public float acceleration  = 10.0f;
  public float braking       = 10.0f;
  public float maxSpeed      = 2.0f;
  public float rotationSpeed = 15.0f;
  public float verticalRotationSpeed = 50.0f;


  [Networked]
  [HideInInspector]
  public bool IsGrounded { get; set; }


  [Networked]
  [HideInInspector]
  public Vector3 Velocity { get; set; }


  /// <summary>
  /// Sets the default teleport interpolation velocity to be the CC's current velocity.
  /// For more details on how this field is used, see <see cref="NetworkTransform.TeleportToPosition"/>.
  /// </summary>
  protected override Vector3 DefaultTeleportInterpolationVelocity => Velocity;


  /// <summary>
  /// Sets the default teleport interpolation angular velocity to be the CC's rotation speed on the Z axis.
  /// For more details on how this field is used, see <see cref="NetworkTransform.TeleportToRotation"/>.
  /// </summary>
  protected override Vector3 DefaultTeleportInterpolationAngularVelocity => new Vector3(0f, 0f, rotationSpeed);


  public CharacterController Controller { get; private set; }


  protected override void Awake() {
    base.Awake();
    CacheController();
  }


  public override void Spawned() {
    base.Spawned();
    CacheController();


    // Caveat: this is needed to initialize the Controller's state and avoid unwanted spikes in its perceived velocity
    // Controller.Move(transform.position);
  }


  private void CacheController() {
    if (Controller == null) {
      Controller = GetComponent<CharacterController>();


      Assert.Check(Controller != null, $"An object with {nameof(NetworkCharacterControllerPrototype)} must also have a {nameof(CharacterController)} component.");
    }
  }


  protected override void CopyFromBufferToEngine() {
    // Trick: CC must be disabled before resetting the transform state
    Controller.enabled = false;


    // Pull base (NetworkTransform) state from networked data buffer
    base.CopyFromBufferToEngine();


    // Re-enable CC
    Controller.enabled = true;
  }


  /// <summary>
  /// Basic implementation of a jump impulse (immediately integrates a vertical component to Velocity).
  /// <param name="ignoreGrounded">Jump even if not in a grounded state.</param>
  /// <param name="overrideImpulse">Optional field to override the jump impulse. If null, <see cref="jumpImpulse"/> is used.</param>
  /// </summary>
  public virtual void Jump(bool ignoreGrounded = false, float? overrideImpulse = null) {
    if (IsGrounded || ignoreGrounded) {
      var newVel = Velocity;
      newVel.y += overrideImpulse ?? jumpImpulse;
      Velocity =  newVel;
    }
  }


  public void Rotate(float rotationValue)
    {
        transform.Rotate(0, rotationValue * Runner.DeltaTime * rotationSpeed, 0);
    }


  /// <summary>
  /// Basic implementation of a character controller's movement function based on an intended direction.
  /// <param name="direction">Intended movement direction, subject to movement query, acceleration and max speed values.</param>
  /// </summary>
  public virtual void Move(Vector3 direction) {
    var deltaTime    = Runner.DeltaTime;
    var previousPos  = transform.position;
    var moveVelocity = Velocity;


    direction = direction.normalized;


    if (IsGrounded && moveVelocity.y < 0) {
      moveVelocity.y = 0f;
    }


    moveVelocity.y += gravity * Runner.DeltaTime;


    var horizontalVel = default(Vector3);
    horizontalVel.x = moveVelocity.x;
    horizontalVel.z = moveVelocity.z;


    if (direction == default) {
      Debug.Log("direction == Default");
      horizontalVel = Vector3.Lerp(horizontalVel, default, braking * deltaTime);
    } else {
      Debug.Log("direction else");
      Debug.Log($"HorizontalVel {horizontalVel} + direcction {direction} * deltaTime {deltaTime}, maxSpeed {maxSpeed}");
      horizontalVel      = Vector3.ClampMagnitude(horizontalVel + direction * acceleration * deltaTime, maxSpeed);
      // LEFT RIGHT Movements will affect the rotation so commenting out is desirable
      //transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(direction), rotationSpeed * Runner.DeltaTime);
    }


    moveVelocity.x = horizontalVel.x;
    moveVelocity.z = horizontalVel.z;


    Controller.Move(moveVelocity * deltaTime);


    Debug.Log($"current pos {transform.position} - previous pos {previousPos} * runner tickrate {Runner.Simulation.Config.TickRate}");
    Debug.Log($"current pos {transform.position} - previous pos {previousPos} = {transform.position - previousPos}");


    Vector3 simulatedVelocity = (transform.position - previousPos) * Runner.Simulation.Config.TickRate;
    //simulatedVelocity.y = simulatedVelocity.y * 0.5f;
    Velocity = simulatedVelocity;
    Debug.Log("Velocity:: " + Velocity);
    IsGrounded = Controller.isGrounded;
  }
}

Answers

  • Correct, the main reason typically for Networked values is for correct tick accuracy on clients and for client prediction.

    Typically values are networked because they are considered "State", which for client prediction is important because these values need to correctly "rewind" during client reconciliation. The client needs to know when it is rewound what the exact server state was for that tick.

  • @emotitron Thanks for the clarification.

    I am still reading tons of sample code and I think I am getting the grasps of networked variables.

    I still haven't been able to wrap my head around this line

    horizontalVel      = Vector3.ClampMagnitude(horizontalVel + direction * acceleration * deltaTime, maxSpeed);
    

    In my understanding the only variable that is affected by the networked "Velocity" is the horizontalVel which gets the x and y of the Velocity. The other variable that is going to change is direction which is controlled via networkInputData. Which i suspect can give a bug when a player is moving through stairs (or any uneven surface) with the code below.

    Velocity = (transform.position - previousPos) * Runner.Simulation.Config.TickRate;

    say for example a player is trying to walk into stairs which has a height that is too high for a player to walk into but walking into the edge of it is possible. this scenario might give the following result

    Velocity = (1, 1.5, 1) - (1, 1.34, 1) * Runner.Simulation.Config.TickRate;

    = (0, 9.6, 0)

    which in this will make a player jump very high (this actually happens in fusion sample code, so I am just wondering if I am missing something or not).

  • emotitron
    emotitron ✭✭✭
    edited October 2022

    I am not personally familiar enough with NCCP to comment, but someone else may who knows more about that implementation may chime in.

  • @emotitron Okay ! Thanks for sharing your thoughts!

  • Is there a solution to this? Because this happens to me all the time, the player bumps into something and goes flying into the air.