using UnityEngine; using Fusion; using Opsive.Shared.Events; using Opsive.UltimateCharacterController.Networking; using Opsive.UltimateCharacterController.Networking.Character; using Opsive.UltimateCharacterController.Character; using Opsive.UltimateCharacterController.Character.Abilities; using Opsive.UltimateCharacterController.Character.Abilities.Items; using Opsive.UltimateCharacterController.Camera; using Opsive.UltimateCharacterController.Input; using OnlyScove.Scripts; using Opsive.UltimateCharacterController.Game; namespace Hallucinate.Network { // Ensure Opsive components load before this [DefaultExecutionOrder(100)] public class FusionClientMovementBridge : NetworkBehaviour, INetworkInfo, INetworkCharacter, ILookSource { public static FusionClientMovementBridge Local { get; private set; } [Networked] public PlayerInputData SyncInput { get; set; } private UltimateCharacterLocomotion m_CharacterLocomotion; private UltimateCharacterLocomotionHandler m_LocoHandler; private Opsive.UltimateCharacterController.Input.PlayerInput m_PlayerInput; private void Awake() { m_CharacterLocomotion = GetComponent(); m_LocoHandler = GetComponent(); m_PlayerInput = GetComponent(); } public override void Spawned() { // Because we passed State Authority to the client in BasicSpawner, // HasStateAuthority is true ONLY for the local player. bool isLocal = Object.HasStateAuthority; if (isLocal) { Local = this; } // 1. Isolate Input: Only the local player should read keyboard/mouse inputs. if (m_LocoHandler != null) m_LocoHandler.enabled = isLocal; var activeInput = GetComponent(); // Corrected class reference if (activeInput != null) activeInput.enabled = isLocal; // 2. Isolate Camera: Only attach the camera if this is the local player. if (isLocal) { var cameraController = UnityEngine.Object.FindFirstObjectByType(); if (cameraController != null) { cameraController.Character = gameObject; } } else { // For remote players, register this bridge as the LookSource EventHandler.ExecuteEvent(gameObject, "OnCharacterAttachLookSource", this); } } public override void Despawned(NetworkRunner runner, bool hasState) { if (Local == this) { Local = null; } } public override void FixedUpdateNetwork() { if (Object.HasStateAuthority) { if (GetInput(out var input)) { SyncInput = input; } } if (!Object.HasStateAuthority) { // Ensure look source is attached for remote players if (m_CharacterLocomotion != null && m_CharacterLocomotion.LookSource != (ILookSource)this) { EventHandler.ExecuteEvent(gameObject, "OnCharacterAttachLookSource", this); } // Sync the movement inputs to KinematicObjectManager so it moves the remote player character if (m_CharacterLocomotion != null && m_CharacterLocomotion.KinematicObjectIndex != -1) { KinematicObjectManager.SetCharacterMovementInput( m_CharacterLocomotion.KinematicObjectIndex, SyncInput.Direction.x, SyncInput.Direction.y ); } if (m_CharacterLocomotion != null) { m_CharacterLocomotion.InputVector = SyncInput.InputVector; m_CharacterLocomotion.RawInputVector = SyncInput.RawInputVector; m_CharacterLocomotion.DeltaRotation = SyncInput.DeltaRotation; // Sync remote abilities based on state UpdateRemoteAbility(SyncInput.sprint); UpdateRemoteAbility(SyncInput.jump); UpdateRemoteAbility(SyncInput.crouch); UpdateRemoteAbility(SyncInput.aim); UpdateRemoteAbility(SyncInput.use); UpdateRemoteAbility(SyncInput.reload); } } } private void UpdateRemoteAbility(bool shouldBeActive) where T : Ability { if (m_CharacterLocomotion == null) return; var ability = m_CharacterLocomotion.GetAbility(); if (ability != null) { if (shouldBeActive && !ability.IsActive) { m_CharacterLocomotion.TryStartAbility(ability, true); } else if (!shouldBeActive && ability.IsActive) { m_CharacterLocomotion.TryStopAbility(ability, true); } } } public PlayerInputData GetLocalInputData() { var data = new PlayerInputData(); if (m_CharacterLocomotion == null) return data; if (m_PlayerInput != null) { data.Direction = new Vector2(m_PlayerInput.GetAxisRaw("Horizontal"), m_PlayerInput.GetAxisRaw("Vertical")); // Sync abilities active states or buttons data.sprint = IsAbilityActive() || m_PlayerInput.GetButton("Change Speeds"); data.jump = IsAbilityActive() || m_PlayerInput.GetButton("Jump"); data.crouch = IsAbilityActive() || m_PlayerInput.GetButton("Crouch"); data.aim = IsAbilityActive() || m_PlayerInput.GetButton("Aim"); data.use = IsAbilityActive() || m_PlayerInput.GetButton("Fire1"); data.reload = IsAbilityActive() || m_PlayerInput.GetButton("Reload"); } // Sync locomotion internal state parameters data.InputVector = m_CharacterLocomotion.InputVector; data.RawInputVector = m_CharacterLocomotion.RawInputVector; data.DeltaRotation = m_CharacterLocomotion.DeltaRotation; data.rot = transform.rotation; // Sync Look direction and look pitch var lookSource = m_CharacterLocomotion.LookSource; if (lookSource != null) { data.LookPitch = lookSource.Pitch; data.LookDirection = lookSource.LookDirection(true); } return data; } private bool IsAbilityActive() where T : Ability { if (m_CharacterLocomotion == null) return false; var ability = m_CharacterLocomotion.GetAbility(); return ability != null && ability.IsActive; } // --- ILookSource Implementation --- public GameObject GameObject => gameObject; public Transform Transform => transform; public float LookDirectionDistance => 1f; public float Pitch => SyncInput.LookPitch; public Vector3 LookPosition() { var animator = GetComponent(); if (animator != null) { var head = animator.GetBoneTransform(HumanBodyBones.Head); if (head != null) return head.position; } return transform.position + Vector3.up * 1.5f; } public Vector3 LookDirection(bool characterLookDirection) { return SyncInput.LookDirection == Vector3.zero ? transform.forward : SyncInput.LookDirection; } public Vector3 LookDirection(Vector3 lookPosition, bool characterLookDirection, int layerMask, bool useRecoil) { return LookDirection(characterLookDirection); } // --- INetworkInfo Implementation --- public bool IsLocalPlayer() => Object.HasStateAuthority; public bool IsServer() => Runner.IsServer; // Return FALSE because we are doing Client-Authoritative movement! public bool IsServerAuthoritative() => false; // --- INetworkCharacter Implementation --- // Since we are using Fusion's NetworkTransform to sync position, // we can leave these methods empty. Opsive will handle the local // movement, and Fusion's NetworkTransform will drag the remote players. public void SetPosition(Vector3 position, bool snapAnimator) { } public void SetRotation(Quaternion rotation, bool snapAnimator) { } public void SetPositionAndRotation(Vector3 position, Quaternion rotation, bool snapAnimator) { } public void ResetRotationPosition() { } public void SetActive(bool active, bool uiEvent) { } public void LoadDefaultLoadout() { } public void EquipUnequipItem(uint itemID, int slotID, bool equip) { } public void ItemIdentifierPickup(uint id, int amount, int slot, bool immediate, bool force) { } public void RemoveAllItems() { } public void Fire(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, float strength) { } public void StartItemReload(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction) { } public void ReloadItem(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, bool fullClip) { } public void ItemReloadComplete(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, bool success, bool immediateReload) { } public void MeleeHitCollider(Opsive.UltimateCharacterController.Items.Actions.ItemAction itemAction, int hitboxIndex, RaycastHit raycastHit, GameObject hitGameObject, UltimateCharacterLocomotion hitCharacterLocomotion) { } public void ThrowItem(Opsive.UltimateCharacterController.Items.Actions.ItemAction action) { } public void EnableThrowableObjectMeshRenderers(Opsive.UltimateCharacterController.Items.Actions.ItemAction action) { } public void StartStopBeginEndMagicActions(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, bool begin, bool start) { } public void MagicCast(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, int index, uint castID, Vector3 dir, Vector3 target) { } public void MagicImpact(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, uint castID, GameObject source, GameObject target, Vector3 pos, Vector3 norm) { } public void StopMagicCast(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, int index, uint castID) { } public void ToggleFlashlight(Opsive.UltimateCharacterController.Items.Actions.ItemAction action, bool active) { } public void PushRigidbody(Rigidbody rb, Vector3 force, Vector3 point) { } } }