Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. Dismiss Notice

Feedback Nightmare with Netcode!

Discussion in 'Netcode for GameObjects' started by pacificeve, Mar 3, 2024.

  1. pacificeve

    pacificeve

    Joined:
    Jan 6, 2018
    Posts:
    9
    I'm trying to build a TPS using Starter Assets - ThirdPerson | Updates in new CharacterController package | Essentials | Unity Asset Store for now and I'm getting a lot of odd behavior and I'm tired of tweaking this, any feedback would be appreciated. I'm using latest version of netcode. Id like a player to select different types of players before spawning as or host or as a client(joining host) I can get host and client to connect but players can't see each other, also I have 2 player prefabs and I can only select one or other on both client and host or it doesn't work. Im using the person controller from the Starter Assets package (end of this)


    This is my player selection UI and spawn logic (at least I think)
    using UnityEngine;
    using UnityEngine.UI;
    using Unity.Netcode;
    using UnityEngine.Events;
    using System.Collections.Generic;

    [System.Serializable]
    public class PlayerSelectionPrefab
    {
    public GameObject prefab; // Direct reference to the prefab to spawn
    public Image characterImage;
    public Button selectionButton;
    public UnityEvent onSelected = new UnityEvent(); // Unity Event to handle character selection
    }

    public class PlayerSelectionUI : NetworkBehaviour
    {
    public RectTransform uiPanel;
    public GameObject playerSelectionPanel;
    public PlayerSelectionPrefab[] playerSelections;
    private Dictionary<ulong, int> clientPrefabSelectionIndices = new Dictionary<ulong, int>();
    public GameObject spawnLocationGameObject;
    public float buttonOffsetY = -100f;
    public GameObject startingMenuPanel;
    public Button playButton;
    private int hostPrefabIndex = -1;
    private int clientPrefabIndex = -1;
    private int clientSelectedCharacterIndex = -1;
    // New variables for host/client selection UI
    public GameObject hostClientSelectionPanel;
    public Button hostButton;
    public Button clientButton;
    private GameObject selectedPrefab;

    private Transform spawnLocation;

    void Update()
    {
    if (!IsOwner) return;

    // Handle movement and other inputs here
    }
    void Start()
    {
    startingMenuPanel.SetActive(true);
    playerSelectionPanel.SetActive(false);
    hostClientSelectionPanel.SetActive(false); // Initialize as inactive

    spawnLocation = spawnLocationGameObject.transform;
    SetupPlayerSelectionUI();

    playButton.onClick.AddListener(ShowCharacterSelection);
    hostButton.onClick.AddListener(StartHost);
    clientButton.onClick.AddListener(StartClient);
    }

    private void SetupPlayerSelectionUI()
    {
    float totalWidth = uiPanel.rect.width;
    float spacing = totalWidth / (playerSelections.Length + 1);

    for (int i = 0; i < playerSelections.Length; i++)
    {
    RectTransform imageRect = playerSelections.characterImage.rectTransform;
    float normalizedPosition = (i + 1) / (float)(playerSelections.Length + 1);
    imageRect.anchoredPosition = new Vector2(normalizedPosition * totalWidth - (totalWidth / 2), imageRect.anchoredPosition.y);

    RectTransform buttonRect = playerSelections.selectionButton.GetComponent<RectTransform>();
    buttonRect.anchoredPosition = new Vector2(imageRect.anchoredPosition.x, imageRect.anchoredPosition.y + buttonOffsetY);

    int index = i;

    playerSelections.onSelected.AddListener(() => OnCharacterSelected(index));
    }
    }

    [ServerRpc]
    private void SelectCharacterServerRpc(int characterIndex, ServerRpcParams rpcParams = default)
    {
    ulong clientId = rpcParams.Receive.SenderClientId;
    SpawnCharacterForClient(clientId, characterIndex);
    }


    public void ShowCharacterSelection()
    {
    startingMenuPanel.SetActive(false);
    playerSelectionPanel.SetActive(true);
    }

    public void OnCharacterSelected(int index)
    {
    if (IsClient && IsOwner)
    {
    // Send the selected character index to the server
    SelectCharacterServerRpc(index);
    }

    // Transition to host/client selection panel after selecting a character
    playerSelectionPanel.SetActive(false);
    hostClientSelectionPanel.SetActive(true);
    }


    public void StartHost()
    {
    // Only allow host to start the session
    if (!NetworkManager.Singleton.IsClient && !NetworkManager.Singleton.IsServer)
    {
    NetworkManager.Singleton.StartHost();
    hostClientSelectionPanel.SetActive(false);

    // Once the host starts, spawn the selected character
    if (hostPrefabIndex != -1)
    {
    RequestSpawnServerRpc(playerSelections[hostPrefabIndex].prefab.GetComponent<NetworkObject>().NetworkObjectId);
    }
    }
    else
    {
    Debug.LogWarning("A network session is already running or you are not allowed to start a host.");
    }
    }

    private void StartClient()
    {
    NetworkManager.Singleton.StartClient();
    hostClientSelectionPanel.SetActive(false);
    // Optionally, show a UI for connecting to the host or update UI elements as needed
    }

    private void SpawnCharacterForClient(ulong clientId, int characterIndex)
    {
    if (!IsServer) return;

    // Ensure the characterIndex is within the bounds of the playerSelections array
    if (characterIndex >= 0 && characterIndex < playerSelections.Length)
    {
    GameObject prefabToSpawn = playerSelections[characterIndex].prefab;
    Vector3 spawnPosition = spawnLocation.position;

    GameObject playerObject = Instantiate(prefabToSpawn, spawnPosition, Quaternion.identity);

    NetworkObject networkObject = playerObject.GetComponent<NetworkObject>();
    if (networkObject != null)
    {
    networkObject.SpawnAsPlayerObject(clientId);
    }
    else
    {
    Debug.LogError("The selected prefab does not have a NetworkObject component.");
    }
    }
    else
    {
    Debug.LogError("Invalid character index for playerSelections array.");
    }
    }



    [ServerRpc(RequireOwnership = false)]
    private void RequestSpawnServerRpc(ulong prefabNetworkObjectId, ServerRpcParams rpcParams = default)
    {
    var spawnPosition = spawnLocation.position;

    // Spawn the selected prefab for the player
    foreach (var playerSelection in playerSelections)
    {
    if (playerSelection.prefab.GetComponent<NetworkObject>().NetworkObjectId == prefabNetworkObjectId)
    {
    var playerObject = Instantiate(playerSelection.prefab, spawnPosition, Quaternion.identity);
    playerObject.GetComponent<NetworkObject>().Spawn();
    break;
    }
    }
    }
    }
    ------------------------------------------------------------------------------
    I added this but it does nothing..

    using UnityEngine;
    using Unity.Netcode;

    public class NetworkedPrefab : NetworkBehaviour
    {
    // Update rate for synchronizing position and rotation
    [SerializeField] private float _synchronizationInterval = 0.1f;

    // Variables to store the last synchronized position and rotation
    private Vector3 _lastPosition;
    private Quaternion _lastRotation;

    private void Start()
    {
    // Initialize last position and rotation
    _lastPosition = transform.position;
    _lastRotation = transform.rotation;

    // Check if this instance is the owner (host or client)
    if (IsOwner)
    {
    // Start sending updates to the server
    InvokeRepeating(nameof(SendUpdateToServer), 0f, _synchronizationInterval);
    }
    }

    private void SendUpdateToServer()
    {
    // Check if the position or rotation has changed since the last update
    if (Vector3.Distance(transform.position, _lastPosition) > 0.01f || Quaternion.Angle(transform.rotation, _lastRotation) > 1f)
    {
    // Update last synchronized position and rotation
    _lastPosition = transform.position;
    _lastRotation = transform.rotation;

    // Send position and rotation updates to the server
    UpdatePositionAndRotationServerRpc(_lastPosition, _lastRotation);
    }
    }

    [ServerRpc]
    private void UpdatePositionAndRotationServerRpc(Vector3 position, Quaternion rotation)
    {
    // Update position and rotation on all clients
    transform.position = position;
    transform.rotation = rotation;

    // Send position and rotation updates to all clients except the owner
    UpdatePositionAndRotationClientRpc(position, rotation);
    }

    [ClientRpc]
    private void UpdatePositionAndRotationClientRpc(Vector3 position, Quaternion rotation)
    {
    // Update position and rotation on all clients except the owner
    if (!IsOwner)
    {
    transform.position = position;
    transform.rotation = rotation;
    }
    }
    }
    ------------------------------------------------------------------------------------

    using UnityEngine;
    using UnityEngine.UI;
    using Unity.Netcode;
    using UnityEngine.Events;
    #if ENABLE_INPUT_SYSTEM
    using UnityEngine.InputSystem;
    #endif

    /* Note: animations are called via the controller for both the character and capsule using animator null checks
    */

    namespace StarterAssets
    {
    [RequireComponent(typeof(CharacterController))]
    #if ENABLE_INPUT_SYSTEM
    [RequireComponent(typeof(PlayerInput))]
    #endif
    public class ThirdPersonController : NetworkBehaviour
    {
    [Header("Player")]
    [Tooltip("Move speed of the character in m/s")]
    public float MoveSpeed = 2.0f;

    [Tooltip("Sprint speed of the character in m/s")]
    public float SprintSpeed = 5.335f;

    [Tooltip("How fast the character turns to face movement direction")]
    [Range(0.0f, 0.3f)]
    public float RotationSmoothTime = 0.12f;

    [Tooltip("Acceleration and deceleration")]
    public float SpeedChangeRate = 10.0f;

    public AudioClip LandingAudioClip;
    public AudioClip[] FootstepAudioClips;
    [Range(0, 1)] public float FootstepAudioVolume = 0.5f;

    [Space(10)]
    [Tooltip("The height the player can jump")]
    public float JumpHeight = 1.2f;

    [Tooltip("The character uses its own gravity value. The engine default is -9.81f")]
    public float Gravity = -15.0f;

    [Space(10)]
    [Tooltip("Time required to pass before being able to jump again. Set to 0f to instantly jump again")]
    public float JumpTimeout = 0.50f;

    [Tooltip("Time required to pass before entering the fall state. Useful for walking down stairs")]
    public float FallTimeout = 0.15f;

    [Header("Player Grounded")]
    [Tooltip("If the character is grounded or not. Not part of the CharacterController built in grounded check")]
    public bool Grounded = true;

    [Tooltip("Useful for rough ground")]
    public float GroundedOffset = -0.14f;

    [Tooltip("The radius of the grounded check. Should match the radius of the CharacterController")]
    public float GroundedRadius = 0.28f;

    [Tooltip("What layers the character uses as ground")]
    public LayerMask GroundLayers;

    [Header("Cinemachine")]
    [Tooltip("The follow target set in the Cinemachine Virtual Camera that the camera will follow")]
    public GameObject CinemachineCameraTarget;

    [Tooltip("How far in degrees can you move the camera up")]
    public float TopClamp = 70.0f;

    [Tooltip("How far in degrees can you move the camera down")]
    public float BottomClamp = -30.0f;

    [Tooltip("Additional degress to override the camera. Useful for fine tuning camera position when locked")]
    public float CameraAngleOverride = 0.0f;

    [Tooltip("For locking the camera position on all axis")]
    public bool LockCameraPosition = false;

    // cinemachine
    private float _cinemachineTargetYaw;
    private float _cinemachineTargetPitch;

    // player
    private float _speed;
    private float _animationBlend;
    private float _targetRotation = 0.0f;
    private float _rotationVelocity;
    private float _verticalVelocity;
    private float _terminalVelocity = 53.0f;

    // timeout deltatime
    private float _jumpTimeoutDelta;
    private float _fallTimeoutDelta;

    // animation IDs
    private int _animIDSpeed;
    private int _animIDGrounded;
    private int _animIDJump;
    private int _animIDFreeFall;
    private int _animIDMotionSpeed;

    #if ENABLE_INPUT_SYSTEM
    private PlayerInput _playerInput;
    #endif
    private Animator _animator;
    private CharacterController _controller;
    private StarterAssetsInputs _input;
    private GameObject _mainCamera;

    private const float _threshold = 0.01f;

    private bool _hasAnimator;

    private bool IsCurrentDeviceMouse
    {
    get
    {
    #if ENABLE_INPUT_SYSTEM
    return _playerInput.currentControlScheme == "KeyboardMouse";
    #else
    return false;
    #endif
    }
    }


    private void Awake()
    {
    // get a reference to our main camera
    if (_mainCamera == null)
    {
    _mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
    }
    }

    private void Start()
    {
    _cinemachineTargetYaw = CinemachineCameraTarget.transform.rotation.eulerAngles.y;

    _hasAnimator = TryGetComponent(out _animator);
    _controller = GetComponent<CharacterController>();
    _input = GetComponent<StarterAssetsInputs>();
    #if ENABLE_INPUT_SYSTEM
    _playerInput = GetComponent<PlayerInput>();
    #else
    Debug.LogError( "Starter Assets package is missing dependencies. Please use Tools/Starter Assets/Reinstall Dependencies to fix it");
    #endif

    AssignAnimationIDs();

    // reset our timeouts on start
    _jumpTimeoutDelta = JumpTimeout;
    _fallTimeoutDelta = FallTimeout;
    }

    private void Update()
    {
    _hasAnimator = TryGetComponent(out _animator);

    JumpAndGravity();
    GroundedCheck();
    Move();
    }

    private void LateUpdate()
    {
    CameraRotation();
    }

    private void AssignAnimationIDs()
    {
    _animIDSpeed = Animator.StringToHash("Speed");
    _animIDGrounded = Animator.StringToHash("Grounded");
    _animIDJump = Animator.StringToHash("Jump");
    _animIDFreeFall = Animator.StringToHash("FreeFall");
    _animIDMotionSpeed = Animator.StringToHash("MotionSpeed");
    }

    private void GroundedCheck()
    {
    // set sphere position, with offset
    Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset,
    transform.position.z);
    Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers,
    QueryTriggerInteraction.Ignore);

    // update animator if using character
    if (_hasAnimator)
    {
    _animator.SetBool(_animIDGrounded, Grounded);
    }
    }

    private void CameraRotation()
    {
    // if there is an input and camera position is not fixed
    if (_input.look.sqrMagnitude >= _threshold && !LockCameraPosition)
    {
    //Don't multiply mouse input by Time.deltaTime;
    float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;

    _cinemachineTargetYaw += _input.look.x * deltaTimeMultiplier;
    _cinemachineTargetPitch += _input.look.y * deltaTimeMultiplier;
    }

    // clamp our rotations so our values are limited 360 degrees
    _cinemachineTargetYaw = ClampAngle(_cinemachineTargetYaw, float.MinValue, float.MaxValue);
    _cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);

    // Cinemachine will follow this target
    CinemachineCameraTarget.transform.rotation = Quaternion.Euler(_cinemachineTargetPitch + CameraAngleOverride,
    _cinemachineTargetYaw, 0.0f);
    }

    private void Move()
    {
    // set target speed based on move speed, sprint speed and if sprint is pressed
    float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;

    // a simplistic acceleration and deceleration designed to be easy to remove, replace, or iterate upon

    // note: Vector2's == operator uses approximation so is not floating point error prone, and is cheaper than magnitude
    // if there is no input, set the target speed to 0
    if (_input.move == Vector2.zero) targetSpeed = 0.0f;

    // a reference to the players current horizontal velocity
    float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;

    float speedOffset = 0.1f;
    float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f;

    // accelerate or decelerate to target speed
    if (currentHorizontalSpeed < targetSpeed - speedOffset ||
    currentHorizontalSpeed > targetSpeed + speedOffset)
    {
    // creates curved result rather than a linear one giving a more organic speed change
    // note T in Lerp is clamped, so we don't need to clamp our speed
    _speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude,
    Time.deltaTime * SpeedChangeRate);

    // round speed to 3 decimal places
    _speed = Mathf.Round(_speed * 1000f) / 1000f;
    }
    else
    {
    _speed = targetSpeed;
    }

    _animationBlend = Mathf.Lerp(_animationBlend, targetSpeed, Time.deltaTime * SpeedChangeRate);
    if (_animationBlend < 0.01f) _animationBlend = 0f;

    // normalise input direction
    Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;

    // note: Vector2's != operator uses approximation so is not floating point error prone, and is cheaper than magnitude
    // if there is a move input rotate player when the player is moving
    if (_input.move != Vector2.zero)
    {
    _targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg +
    _mainCamera.transform.eulerAngles.y;
    float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, _targetRotation, ref _rotationVelocity,
    RotationSmoothTime);

    // rotate to face input direction relative to camera position
    transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
    }


    Vector3 targetDirection = Quaternion.Euler(0.0f, _targetRotation, 0.0f) * Vector3.forward;

    // move the player
    _controller.Move(targetDirection.normalized * (_speed * Time.deltaTime) +
    new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);

    // update animator if using character
    if (_hasAnimator)
    {
    _animator.SetFloat(_animIDSpeed, _animationBlend);
    _animator.SetFloat(_animIDMotionSpeed, inputMagnitude);
    }
    }

    private void JumpAndGravity()
    {
    if (Grounded)
    {
    // reset the fall timeout timer
    _fallTimeoutDelta = FallTimeout;

    // update animator if using character
    if (_hasAnimator)
    {
    _animator.SetBool(_animIDJump, false);
    _animator.SetBool(_animIDFreeFall, false);
    }

    // stop our velocity dropping infinitely when grounded
    if (_verticalVelocity < 0.0f)
    {
    _verticalVelocity = -2f;
    }

    // Jump
    if (_input.jump && _jumpTimeoutDelta <= 0.0f)
    {
    // the square root of H * -2 * G = how much velocity needed to reach desired height
    _verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);

    // update animator if using character
    if (_hasAnimator)
    {
    _animator.SetBool(_animIDJump, true);
    }
    }

    // jump timeout
    if (_jumpTimeoutDelta >= 0.0f)
    {
    _jumpTimeoutDelta -= Time.deltaTime;
    }
    }
    else
    {
    // reset the jump timeout timer
    _jumpTimeoutDelta = JumpTimeout;

    // fall timeout
    if (_fallTimeoutDelta >= 0.0f)
    {
    _fallTimeoutDelta -= Time.deltaTime;
    }
    else
    {
    // update animator if using character
    if (_hasAnimator)
    {
    _animator.SetBool(_animIDFreeFall, true);
    }
    }

    // if we are not grounded, do not jump
    _input.jump = false;
    }

    // apply gravity over time if under terminal (multiply by delta time twice to linearly speed up over time)
    if (_verticalVelocity < _terminalVelocity)
    {
    _verticalVelocity += Gravity * Time.deltaTime;
    }
    }

    private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
    {
    if (lfAngle < -360f) lfAngle += 360f;
    if (lfAngle > 360f) lfAngle -= 360f;
    return Mathf.Clamp(lfAngle, lfMin, lfMax);
    }

    private void OnDrawGizmosSelected()
    {
    Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f);
    Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f);

    if (Grounded) Gizmos.color = transparentGreen;
    else Gizmos.color = transparentRed;

    // when selected, draw a gizmo in the position of, and matching radius of, the grounded collider
    Gizmos.DrawSphere(
    new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z),
    GroundedRadius);
    }

    private void OnFootstep(AnimationEvent animationEvent)
    {
    if (animationEvent.animatorClipInfo.weight > 0.5f)
    {
    if (FootstepAudioClips.Length > 0)
    {
    var index = Random.Range(0, FootstepAudioClips.Length);
    AudioSource.PlayClipAtPoint(FootstepAudioClips[index], transform.TransformPoint(_controller.center), FootstepAudioVolume);
    }
    }
    }

    private void OnLand(AnimationEvent animationEvent)
    {
    if (animationEvent.animatorClipInfo.weight > 0.5f)
    {
    AudioSource.PlayClipAtPoint(LandingAudioClip, transform.TransformPoint(_controller.center), FootstepAudioVolume);
    }
    }
    }
    }
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,922
    Networking is hard. ;)
    Way harder than most would tend to believe.
    Be sure to take very simply steps one at a time. First the players should spawn for each other and be locally controllable. Until you get that working under all conditions there's absolutely no point in trying to work on other things like player mesh selection, audio playback, character animations - just to name a few things I noticed in this wall of code (please use code tags, this is not legible code). In fact, if you try to jump ahead and do a second thing before the first one works perfectly, it's only going to get harder to make both things work.

    Expect constantly having to change things. And keep your scripts simple, ideally a networked script should only do one simple thing. For example, I have three NetworkWeapon scripts: one for switching weapons and providing the active weapon, one for shooting the weapon and one for reloading it. That makes it conceptually a lot easier to reason and debug when each network script only focuses on one simple aspect.

    It has taken me 3 weeks now to enable splitscreen networked first-person multiplayer where each local player has its own first person view while everyone else sees the third person view. I have just yesterday enabled networked weapon switching but haven't done projectile spawning nor displaying the weapon in third person (only first person). Just to give a sense of how much time it takes someone with a lot of experience to implement even the basics.

    I'll publish this work as a multiplayer action game template which will be available likely end of April. I'll likely call it "MultiPal" unless I come up with a better name so check the store in another 6 weeks or so. ;)
     
    alysonlupo likes this.