using System.Collections; using System.Collections.Generic; using UnityEngine; using Cinemachine; public enum SinglePlayerCameraMode { Stationary, FollowAndOrbit } public class CameraManager : Singleton { [Header("Component References")] public GameObject gameplayCameraObject; public GameObject uiOverlayCameraObject; [Header("Virtual Cameras")] public GameObject VCamStationaryObject; public GameObject VCamSinglePlayerOrbitObject; [Header("Cinemachine References")] [SerializeField] private CinemachineFreeLook mainFreeLookCamera; [SerializeField] private CinemachineVirtualCamera mainVirtualCamera; [Header("Current Target")] [SerializeField] private TeamMember currentTarget; [SerializeField] private bool debugLogging = true; public void SetupManager() { SetCameraObjectNewState(gameplayCameraObject, true); SetCameraObjectNewState(uiOverlayCameraObject, false); // Find camera references if not set FindCameraReferences(); } private void Start() { FindCameraReferences(); SubscribeToAllTeamMembers(); } private void FindCameraReferences() { if (mainFreeLookCamera == null) { GameObject camObj = GameObject.Find("CinemachineCamera"); if (camObj != null) { mainFreeLookCamera = camObj.GetComponent(); } } if (mainVirtualCamera == null) { mainVirtualCamera = FindObjectOfType(); } } public void SetupSinglePlayerCamera(SinglePlayerCameraMode currentCameraMode) { switch (currentCameraMode) { case SinglePlayerCameraMode.Stationary: SetCameraObjectNewState(VCamStationaryObject, true); SetCameraObjectNewState(VCamSinglePlayerOrbitObject, false); break; case SinglePlayerCameraMode.FollowAndOrbit: SetCameraObjectNewState(VCamStationaryObject, false); SetCameraObjectNewState(VCamSinglePlayerOrbitObject, true); break; } } void SetCameraObjectNewState(GameObject cameraObject, bool newState) { if (cameraObject != null) cameraObject.SetActive(newState); } //This is called by UIBillboardBehaviour so they can orient to wherever the gameplay camera is. public Transform GetGameplayCameraTransform() { return gameplayCameraObject?.transform; } public Camera GetGameplayCamera() { return gameplayCameraObject?.GetComponent(); } // ==================== NEW: ControlType-aware Camera Targeting ==================== /// /// Subscribe to OnControlTypeChanged for all TeamMembers in the scene /// private void SubscribeToAllTeamMembers() { TeamMember[] allMembers = FindObjectsOfType(); foreach (var member in allMembers) { member.OnControlTypeChanged += OnMemberControlTypeChanged; } } /// /// Register a newly spawned TeamMember /// public void RegisterTeamMember(TeamMember member) { if (member != null) { member.OnControlTypeChanged += OnMemberControlTypeChanged; // If this member is HumanControlled, set as target if (member.CurrentControlType == TeamMember.ControlType.HumanControlled) { SetTarget(member); } } } /// /// Unregister a TeamMember (e.g., on death) /// public void UnregisterTeamMember(TeamMember member) { if (member != null) { member.OnControlTypeChanged -= OnMemberControlTypeChanged; } } /// /// Called when any TeamMember's control type changes /// private void OnMemberControlTypeChanged(TeamMember.ControlType newType) { // Find the new human-controlled character if (newType == TeamMember.ControlType.HumanControlled) { FindAndSetHumanControlledTarget(); } } /// /// Find the HumanControlled character and set as camera target /// public void FindAndSetHumanControlledTarget() { TeamMember[] allMembers = FindObjectsOfType(); foreach (var member in allMembers) { if (member.CurrentControlType == TeamMember.ControlType.HumanControlled) { SetTarget(member); return; } } if (debugLogging) { Debug.LogWarning("[CameraManager] No HumanControlled character found in scene!"); } } /// /// Set the camera target to a specific TeamMember /// public void SetTarget(TeamMember target) { if (target == null) { Debug.LogWarning("[CameraManager] Attempted to set null target"); return; } currentTarget = target; // Use root for Follow (stable orbit center) Transform followTransform = target.transform; // Use head for LookAt (camera points at upper body) Transform lookAtTransform = FindCharacterHead(target.gameObject); // Set FreeLook camera target if (mainFreeLookCamera != null) { mainFreeLookCamera.Follow = followTransform; mainFreeLookCamera.LookAt = lookAtTransform; // CRITICAL: Tell Cinemachine to forget its cached (underground) position // and recalculate from scratch based on the new Follow target position mainFreeLookCamera.PreviousStateIsValid = false; // Configure camera to automatically stay behind player ConfigureCameraRecentering(); if (debugLogging) { Debug.Log($"[CameraManager] FreeLook camera - Follow: {followTransform.name}, LookAt: {lookAtTransform.name} (state reset)"); } } // Set VirtualCamera target if (mainVirtualCamera != null) { mainVirtualCamera.Follow = followTransform; mainVirtualCamera.LookAt = lookAtTransform; } // Also update CameraFollow if present CameraFollow cameraFollow = FindObjectOfType(); if (cameraFollow != null) { cameraFollow.target = lookAtTransform; } } /// /// Configure camera to automatically recenter behind the player. /// Also initializes orbit heights, Y axis position, and Composer aim settings /// to ensure the camera starts BEHIND and ABOVE the character, never below. /// private void ConfigureCameraRecentering() { if (mainFreeLookCamera == null) return; // ====== FIX #1: Start at MIDDLE rig (0.5), not bottom rig (0) ====== // Y Axis: 0 = bottom rig, 0.5 = middle rig, 1 = top rig // Without this, camera defaults to bottom rig and looks UP at the character mainFreeLookCamera.m_YAxis.Value = 0.5f; // CRITICAL: Clear default input axis names — CinemachineCameraInput handles all input. // Without this, Cinemachine also reads Input.GetAxis("Mouse X") which returns 0 // and snaps the Y axis to the bottom rig (= camera underground). //mainFreeLookCamera.m_XAxis.m_InputAxisName = ""; //mainFreeLookCamera.m_YAxis.m_InputAxisName = ""; // ====== FIX #2: Set proper orbit heights and radii ====== // These define the 3 orbital rings the camera blends between. // Height = vertical offset from Follow target (character root) // Radius = horizontal distance from character mainFreeLookCamera.m_Orbits = new CinemachineFreeLook.Orbit[] { new CinemachineFreeLook.Orbit(4.5f, 5f), // TopRig: bird's eye, medium distance new CinemachineFreeLook.Orbit(2.5f, 6f), // MiddleRig: shoulder height, wider orbit new CinemachineFreeLook.Orbit(0.5f, 5f), // BottomRig: just above ground level }; // Set binding mode to follow player's heading (critical for auto-follow) // "SimpleFollowWithWorldUp" makes the camera orbit relative to where the player is facing mainFreeLookCamera.m_BindingMode = CinemachineTransposer.BindingMode.SimpleFollowWithWorldUp; // Configure Heading - this is how the camera knows which direction is "behind" the player // Velocity-based heading follows the direction the player is moving mainFreeLookCamera.m_Heading.m_Definition = CinemachineOrbitalTransposer.Heading.HeadingDefinition.Velocity; mainFreeLookCamera.m_Heading.m_VelocityFilterStrength = 5; // Smooth out velocity changes // Try to enable X Axis recentering (works in Cinemachine 2.x) try { mainFreeLookCamera.m_XAxis.m_Recentering.m_enabled = true; mainFreeLookCamera.m_XAxis.m_Recentering.m_WaitTime = 0.3f; mainFreeLookCamera.m_XAxis.m_Recentering.m_RecenteringTime = 0.8f; } catch (System.Exception) { Debug.Log("[CameraManager] X Axis recentering not available via m_XAxis.m_Recentering"); } // Configure Y Axis (vertical tilt) recentering mainFreeLookCamera.m_YAxis.m_Recentering.m_enabled = true; mainFreeLookCamera.m_YAxis.m_Recentering.m_WaitTime = 0.3f; mainFreeLookCamera.m_YAxis.m_Recentering.m_RecenteringTime = 0.8f; // ====== FIX #3: Configure Composer aim on each rig ====== // This controls WHERE on screen the character appears. // ScreenY = 0.4 means character is at 40% from bottom (slightly above center) // This prevents the camera from tilting upward to look at the character ConfigureRigComposers(); // Configure heading settings on each rig's orbital transposer ConfigureRigHeadings(); if (debugLogging) { Debug.Log("[CameraManager] Camera fully initialized:"); Debug.Log($" Y Axis = {mainFreeLookCamera.m_YAxis.Value} (0.5 = middle rig)"); Debug.Log($" Orbits: Top({mainFreeLookCamera.m_Orbits[0].m_Height}h, {mainFreeLookCamera.m_Orbits[0].m_Radius}r) " + $"Mid({mainFreeLookCamera.m_Orbits[1].m_Height}h, {mainFreeLookCamera.m_Orbits[1].m_Radius}r) " + $"Bot({mainFreeLookCamera.m_Orbits[2].m_Height}h, {mainFreeLookCamera.m_Orbits[2].m_Radius}r)"); Debug.Log($" Heading: {mainFreeLookCamera.m_Heading.m_Definition}"); } } /// /// Configure the Composer aim component on each rig. /// This sets WHERE on screen the character is framed. /// Without this, ScreenY defaults to 0.27 which makes the camera look UP. /// Pattern from WaitingSceneManager which correctly sets these values. /// private void ConfigureRigComposers() { for (int i = 0; i < 3; i++) { var rig = mainFreeLookCamera.GetRig(i); if (rig == null) continue; var composer = rig.GetCinemachineComponent(); if (composer != null) { composer.m_ScreenX = 0.5f; // Character centered horizontally composer.m_ScreenY = 0.4f; // Character at 40% — standard third person composer.m_DeadZoneWidth = 0.1f; // Small dead zone for responsive aiming composer.m_DeadZoneHeight = 0.1f; composer.m_SoftZoneWidth = 0.8f; // Wide soft zone for smooth transitions composer.m_SoftZoneHeight = 0.8f; composer.m_LookaheadTime = 0f; // No lookahead for combat (responsive) composer.m_LookaheadSmoothing = 0f; if (debugLogging) { string rigName = i == 0 ? "Top" : (i == 1 ? "Middle" : "Bottom"); Debug.Log($"[CameraManager] {rigName} rig Composer: ScreenY={composer.m_ScreenY}, ScreenX={composer.m_ScreenX}"); } } } } /// /// Configure heading settings on each rig's orbital transposer /// private void ConfigureRigHeadings() { for (int i = 0; i < 3; i++) { var rig = mainFreeLookCamera.GetRig(i); if (rig != null) { var transposer = rig.GetCinemachineComponent(); if (transposer != null) { // Enable recenter to target heading on each rig transposer.m_RecenterToTargetHeading.m_enabled = true; transposer.m_RecenterToTargetHeading.m_WaitTime = 0.3f; transposer.m_RecenterToTargetHeading.m_RecenteringTime = 0.8f; } } } } /// /// Set target by GameObject (convenience method) /// public void SetTarget(GameObject target) { if (target == null) return; TeamMember member = target.GetComponent(); if (member != null) { SetTarget(member); } else { // Fallback: set camera directly with correct separation Transform followTransform = target.transform; // Root = orbit center Transform lookAtTransform = FindCharacterHead(target); // Head = aim point if (mainFreeLookCamera != null) { mainFreeLookCamera.Follow = followTransform; mainFreeLookCamera.LookAt = lookAtTransform; } } } /// /// Get the currently followed character /// public TeamMember GetCurrentTarget() { return currentTarget; } /// /// Find the character's head transform for camera targeting /// private Transform FindCharacterHead(GameObject character) { if (character == null) return null; // Try to find common head bone names string[] headNames = { "mixamorig:Head", "Head", "head", "Bip001 Head", "Bone_Head", "head_bone" }; foreach (string headName in headNames) { Transform head = FindChildRecursive(character.transform, headName); if (head != null) { if (debugLogging) { Debug.Log($"[CameraManager] Found head bone '{headName}' for {character.name}"); } return head; } } // Try character-specific paths string characterName = character.name; try { Transform headTransform = null; if (characterName.Contains("Cheetah")) headTransform = character.transform.GetChild(9)?.GetChild(6)?.GetChild(1); else if (characterName.Contains("Rabbit") || characterName.Contains("Eagle") || characterName.Contains("Hyena") || characterName.Contains("HM") || characterName.Contains("Cat")) headTransform = character.transform.GetChild(7)?.GetChild(6)?.GetChild(1); if (headTransform != null) { if (debugLogging) { Debug.Log($"[CameraManager] Found head via hardcoded path for {characterName}: {headTransform.name}"); } return headTransform; } } catch { } // Final fallback - log warning Debug.LogWarning($"[CameraManager] Could not find head bone for {characterName}, using root transform. This will cause camera to show legs!"); return character.transform; } private Transform FindChildRecursive(Transform parent, string name) { if (parent.name == name) return parent; foreach (Transform child in parent) { Transform result = FindChildRecursive(child, name); if (result != null) return result; } return null; } }