450 lines
16 KiB
C#
450 lines
16 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using Cinemachine;
|
|
|
|
|
|
public enum SinglePlayerCameraMode
|
|
{
|
|
Stationary,
|
|
FollowAndOrbit
|
|
}
|
|
|
|
public class CameraManager : Singleton<CameraManager>
|
|
{
|
|
[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<CinemachineFreeLook>();
|
|
}
|
|
}
|
|
|
|
if (mainVirtualCamera == null)
|
|
{
|
|
mainVirtualCamera = FindObjectOfType<CinemachineVirtualCamera>();
|
|
}
|
|
}
|
|
|
|
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<Camera>();
|
|
}
|
|
|
|
// ==================== NEW: ControlType-aware Camera Targeting ====================
|
|
|
|
/// <summary>
|
|
/// Subscribe to OnControlTypeChanged for all TeamMembers in the scene
|
|
/// </summary>
|
|
private void SubscribeToAllTeamMembers()
|
|
{
|
|
TeamMember[] allMembers = FindObjectsOfType<TeamMember>();
|
|
foreach (var member in allMembers)
|
|
{
|
|
member.OnControlTypeChanged += OnMemberControlTypeChanged;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register a newly spawned TeamMember
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unregister a TeamMember (e.g., on death)
|
|
/// </summary>
|
|
public void UnregisterTeamMember(TeamMember member)
|
|
{
|
|
if (member != null)
|
|
{
|
|
member.OnControlTypeChanged -= OnMemberControlTypeChanged;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when any TeamMember's control type changes
|
|
/// </summary>
|
|
private void OnMemberControlTypeChanged(TeamMember.ControlType newType)
|
|
{
|
|
// Find the new human-controlled character
|
|
if (newType == TeamMember.ControlType.HumanControlled)
|
|
{
|
|
FindAndSetHumanControlledTarget();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the HumanControlled character and set as camera target
|
|
/// </summary>
|
|
public void FindAndSetHumanControlledTarget()
|
|
{
|
|
TeamMember[] allMembers = FindObjectsOfType<TeamMember>();
|
|
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!");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the camera target to a specific TeamMember
|
|
/// </summary>
|
|
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<CameraFollow>();
|
|
if (cameraFollow != null)
|
|
{
|
|
cameraFollow.target = lookAtTransform;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private void ConfigureRigComposers()
|
|
{
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
var rig = mainFreeLookCamera.GetRig(i);
|
|
if (rig == null) continue;
|
|
|
|
var composer = rig.GetCinemachineComponent<CinemachineComposer>();
|
|
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}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configure heading settings on each rig's orbital transposer
|
|
/// </summary>
|
|
private void ConfigureRigHeadings()
|
|
{
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
var rig = mainFreeLookCamera.GetRig(i);
|
|
if (rig != null)
|
|
{
|
|
var transposer = rig.GetCinemachineComponent<CinemachineOrbitalTransposer>();
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set target by GameObject (convenience method)
|
|
/// </summary>
|
|
public void SetTarget(GameObject target)
|
|
{
|
|
if (target == null) return;
|
|
|
|
TeamMember member = target.GetComponent<TeamMember>();
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the currently followed character
|
|
/// </summary>
|
|
public TeamMember GetCurrentTarget()
|
|
{
|
|
return currentTarget;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the character's head transform for camera targeting
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|