using System; using Unity.BossRoom.Gameplay.Configuration; using Unity.BossRoom.Navigation; using Unity.Netcode; using UnityEngine; using UnityEngine.AI; using UnityEngine.Assertions; namespace Unity.BossRoom.Gameplay.GameplayObjects.Character { public enum MovementState { Idle = 0, PathFollowing = 1, Charging = 2, Knockback = 3, } /// /// Component responsible for moving a character on the server side based on inputs. /// /*[RequireComponent(typeof(NetworkCharacterState), typeof(NavMeshAgent), typeof(ServerCharacter)), RequireComponent(typeof(Rigidbody))]*/ public class ServerCharacterMovement : NetworkBehaviour { [SerializeField] NavMeshAgent m_NavMeshAgent; [SerializeField] Rigidbody m_Rigidbody; private NavigationSystem m_NavigationSystem; private DynamicNavPath m_NavPath; private MovementState m_MovementState; MovementStatus m_PreviousState; [SerializeField] private ServerCharacter m_CharLogic; // when we are in charging and knockback mode, we use these additional variables private float m_ForcedSpeed; private float m_SpecialModeDurationRemaining; // this one is specific to knockback mode private Vector3 m_KnockbackVector; #if UNITY_EDITOR || DEVELOPMENT_BUILD public bool TeleportModeActivated { get; set; } const float k_CheatSpeed = 20; public bool SpeedCheatActivated { get; set; } #endif void Awake() { // disable this NetworkBehavior until it is spawned enabled = false; } public override void OnNetworkSpawn() { if (IsServer) { // Only enable server component on servers enabled = true; // On the server enable navMeshAgent and initialize m_NavMeshAgent.enabled = true; m_NavigationSystem = GameObject.FindGameObjectWithTag(NavigationSystem.NavigationSystemTag).GetComponent(); m_NavPath = new DynamicNavPath(m_NavMeshAgent, m_NavigationSystem); } } /// /// Sets a movement target. We will path to this position, avoiding static obstacles. /// /// Position in world space to path to. public void SetMovementTarget(Vector3 position) { #if UNITY_EDITOR || DEVELOPMENT_BUILD if (TeleportModeActivated) { Teleport(position); return; } #endif m_MovementState = MovementState.PathFollowing; m_NavPath.SetTargetPosition(position); } public void StartForwardCharge(float speed, float duration) { m_NavPath.Clear(); m_MovementState = MovementState.Charging; m_ForcedSpeed = speed; m_SpecialModeDurationRemaining = duration; } public void StartKnockback(Vector3 knocker, float speed, float duration) { m_NavPath.Clear(); m_MovementState = MovementState.Knockback; m_KnockbackVector = transform.position - knocker; m_ForcedSpeed = speed; m_SpecialModeDurationRemaining = duration; } /// /// Follow the given transform until it is reached. /// /// The transform to follow public void FollowTransform(Transform followTransform) { m_MovementState = MovementState.PathFollowing; m_NavPath.FollowTransform(followTransform); } /// /// Returns true if the current movement-mode is unabortable (e.g. a knockback effect) /// /// public bool IsPerformingForcedMovement() { return m_MovementState == MovementState.Knockback || m_MovementState == MovementState.Charging; } /// /// Returns true if the character is actively moving, false otherwise. /// /// public bool IsMoving() { return m_MovementState != MovementState.Idle; } /// /// Cancels any moves that are currently in progress. /// public void CancelMove() { if (m_NavPath != null) { m_NavPath.Clear(); } m_MovementState = MovementState.Idle; } /// /// Instantly moves the character to a new position. NOTE: this cancels any active movement operation! /// This does not notify the client that the movement occurred due to teleportation, so that needs to /// happen in some other way, such as with the custom action visualization in DashAttackActionFX. (Without /// this, the clients will animate the character moving to the new destination spot, rather than instantly /// appearing in the new spot.) /// /// new coordinates the character should be at public void Teleport(Vector3 newPosition) { CancelMove(); if (!m_NavMeshAgent.Warp(newPosition)) { // warping failed! We're off the navmesh somehow. Weird... but we can still teleport Debug.LogWarning($"NavMeshAgent.Warp({newPosition}) failed!", gameObject); transform.position = newPosition; } m_Rigidbody.position = transform.position; m_Rigidbody.rotation = transform.rotation; } private void FixedUpdate() { PerformMovement(); var currentState = GetMovementStatus(m_MovementState); if (m_PreviousState != currentState) { m_CharLogic.MovementStatus.Value = currentState; m_PreviousState = currentState; } } public override void OnNetworkDespawn() { if (m_NavPath != null) { m_NavPath.Dispose(); } if (IsServer) { // Disable server components when despawning enabled = false; m_NavMeshAgent.enabled = false; } } private void PerformMovement() { if (m_MovementState == MovementState.Idle) return; Vector3 movementVector; if (m_MovementState == MovementState.Charging) { // if we're done charging, stop moving m_SpecialModeDurationRemaining -= Time.fixedDeltaTime; if (m_SpecialModeDurationRemaining <= 0) { m_MovementState = MovementState.Idle; return; } var desiredMovementAmount = m_ForcedSpeed * Time.fixedDeltaTime; movementVector = transform.forward * desiredMovementAmount; } else if (m_MovementState == MovementState.Knockback) { m_SpecialModeDurationRemaining -= Time.fixedDeltaTime; if (m_SpecialModeDurationRemaining <= 0) { m_MovementState = MovementState.Idle; return; } var desiredMovementAmount = m_ForcedSpeed * Time.fixedDeltaTime; movementVector = m_KnockbackVector * desiredMovementAmount; } else { var desiredMovementAmount = GetBaseMovementSpeed() * Time.fixedDeltaTime; movementVector = m_NavPath.MoveAlongPath(desiredMovementAmount); // If we didn't move stop moving. if (movementVector == Vector3.zero) { m_MovementState = MovementState.Idle; return; } } m_NavMeshAgent.Move(movementVector); transform.rotation = Quaternion.LookRotation(movementVector); // After moving adjust the position of the dynamic rigidbody. m_Rigidbody.position = transform.position; m_Rigidbody.rotation = transform.rotation; } /// /// Retrieves the speed for this character's class. /// private float GetBaseMovementSpeed() { #if UNITY_EDITOR || DEVELOPMENT_BUILD if (SpeedCheatActivated) { return k_CheatSpeed; } #endif CharacterClass characterClass = GameDataSource.Instance.CharacterDataByType[m_CharLogic.CharacterType]; Assert.IsNotNull(characterClass, $"No CharacterClass data for character type {m_CharLogic.CharacterType}"); return characterClass.Speed; } /// /// Determines the appropriate MovementStatus for the character. The /// MovementStatus is used by the client code when animating the character. /// private MovementStatus GetMovementStatus(MovementState movementState) { switch (movementState) { case MovementState.Idle: return MovementStatus.Idle; case MovementState.Knockback: return MovementStatus.Uncontrolled; default: return MovementStatus.Normal; } } } }