chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
182
Assets/Scripts/Camera/AutoAddCinemachineCamera.cs
Normal file
182
Assets/Scripts/Camera/AutoAddCinemachineCamera.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using UnityEngine;
|
||||
using Cinemachine;
|
||||
using System.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically configures Cinemachine FreeLook camera targets on scene start.
|
||||
/// Correctly separates Follow (character root) from LookAt (head bone)
|
||||
/// to prevent orbit collapse and camera looking at the sky.
|
||||
/// </summary>
|
||||
public class AutoAddCinemachineCamera : MonoBehaviour
|
||||
{
|
||||
[TagField]
|
||||
public string Tag = string.Empty;
|
||||
|
||||
void Start()
|
||||
{
|
||||
var vcam = GetComponent<CinemachineFreeLook>();
|
||||
if (vcam != null && Tag.Length > 0)
|
||||
{
|
||||
bool isCashSystemMode = SelectionOptions.Instance != null && SelectionOptions.Instance.isCashSystemSelected;
|
||||
|
||||
if (isCashSystemMode)
|
||||
{
|
||||
// In Cash System mode, characters spawn after a delay
|
||||
StartCoroutine(FindPlayerControlledTargetDelayed(vcam, 1.0f));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal mode - find immediately
|
||||
var targets = GameObject.FindGameObjectsWithTag(Tag);
|
||||
if (targets.Length > 0)
|
||||
{
|
||||
SetCameraTargets(vcam, targets[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator FindPlayerControlledTargetDelayed(CinemachineFreeLook vcam, float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
|
||||
// If CameraManager already set targets, don't override
|
||||
if (CameraManager.Instance != null && CameraManager.Instance.GetCurrentTarget() != null)
|
||||
{
|
||||
Debug.Log("[AutoAddCinemachineCamera] CameraManager already handling camera — skipping.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var targets = GameObject.FindGameObjectsWithTag(Tag);
|
||||
|
||||
// Find the character with IsPlayerControlled = true
|
||||
foreach (var target in targets)
|
||||
{
|
||||
TeamMember teamMember = target.GetComponent<TeamMember>();
|
||||
if (teamMember != null && teamMember.IsPlayerControlled)
|
||||
{
|
||||
// Route through CameraManager if available
|
||||
if (CameraManager.Instance != null)
|
||||
{
|
||||
CameraManager.Instance.SetTarget(teamMember);
|
||||
Debug.Log($"[AutoAddCinemachineCamera] Routed to CameraManager for {target.name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
SetCameraTargets(vcam, target);
|
||||
Debug.Log($"[AutoAddCinemachineCamera] Set targets directly for {target.name}");
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if no TeamMember found
|
||||
if (targets.Length > 0)
|
||||
{
|
||||
Debug.LogWarning("[AutoAddCinemachineCamera] No player-controlled character found, using first target");
|
||||
SetCameraTargets(vcam, targets[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set Follow to character ROOT (orbit center) and LookAt to HEAD (aim point).
|
||||
/// This is the correct configuration for CinemachineFreeLook.
|
||||
/// </summary>
|
||||
private void SetCameraTargets(CinemachineFreeLook vcam, GameObject character)
|
||||
{
|
||||
// Follow = character root (stable orbit center at ground level)
|
||||
Transform followTarget = character.transform;
|
||||
|
||||
// LookAt = head bone (camera aims at upper body)
|
||||
Transform lookAtTarget = FindHeadTransform(character) ?? character.transform;
|
||||
|
||||
vcam.Follow = followTarget;
|
||||
vcam.LookAt = lookAtTarget;
|
||||
|
||||
// CRITICAL: Start at middle rig (0.5), not bottom (0)
|
||||
// Without this, camera defaults to looking UP at the character from below
|
||||
vcam.m_YAxis.Value = 0.5f;
|
||||
|
||||
// Force Cinemachine to recalculate from scratch (forget cached underground position)
|
||||
vcam.PreviousStateIsValid = false;
|
||||
|
||||
// Clear input axis names — CinemachineCameraInput handles all input
|
||||
vcam.m_XAxis.m_InputAxisName = "";
|
||||
vcam.m_YAxis.m_InputAxisName = "";
|
||||
|
||||
// Set professional orbit heights (consistent with CameraManager)
|
||||
vcam.m_Orbits = new CinemachineFreeLook.Orbit[] {
|
||||
new CinemachineFreeLook.Orbit(4.5f, 5f), // TopRig
|
||||
new CinemachineFreeLook.Orbit(2.5f, 6f), // MiddleRig
|
||||
new CinemachineFreeLook.Orbit(0.5f, 5f), // BottomRig
|
||||
};
|
||||
|
||||
// Configure Composer aim on each rig
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var rig = vcam.GetRig(i);
|
||||
if (rig == null) continue;
|
||||
var composer = rig.GetCinemachineComponent<CinemachineComposer>();
|
||||
if (composer != null)
|
||||
{
|
||||
composer.m_ScreenX = 0.5f;
|
||||
composer.m_ScreenY = 0.4f;
|
||||
composer.m_DeadZoneWidth = 0.1f;
|
||||
composer.m_DeadZoneHeight = 0.1f;
|
||||
composer.m_SoftZoneWidth = 0.8f;
|
||||
composer.m_SoftZoneHeight = 0.8f;
|
||||
}
|
||||
}
|
||||
|
||||
// Add CinemachineCollider to prevent camera clipping below terrain
|
||||
EnsureCinemachineCollider(vcam);
|
||||
|
||||
Debug.Log($"[AutoAddCinemachineCamera] Follow={followTarget.name}, LookAt={lookAtTarget.name}, YAxis=0.5");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a CinemachineCollider extension so the camera stays above terrain
|
||||
/// and doesn't clip through the environment after changing levels.
|
||||
/// </summary>
|
||||
private void EnsureCinemachineCollider(CinemachineFreeLook vcam)
|
||||
{
|
||||
var colliderExt = vcam.GetComponent<CinemachineCollider>();
|
||||
if (colliderExt == null)
|
||||
{
|
||||
colliderExt = vcam.gameObject.AddComponent<CinemachineCollider>();
|
||||
}
|
||||
|
||||
colliderExt.m_Strategy = CinemachineCollider.ResolutionStrategy.PullCameraForward;
|
||||
colliderExt.m_CollideAgainst = LayerMask.GetMask("Default", "Ground");
|
||||
colliderExt.m_IgnoreTag = "Player";
|
||||
colliderExt.m_CameraRadius = 0.2f;
|
||||
colliderExt.m_Damping = 0f;
|
||||
colliderExt.m_MinimumDistanceFromTarget = 1f;
|
||||
|
||||
Debug.Log("[AutoAddCinemachineCamera] CinemachineCollider extension configured for terrain avoidance");
|
||||
}
|
||||
|
||||
private Transform FindHeadTransform(GameObject character)
|
||||
{
|
||||
string[] headNames = { "Head", "head", "Bip001 Head", "mixamorig:Head", "Bip01 Head" };
|
||||
|
||||
foreach (string headName in headNames)
|
||||
{
|
||||
Transform head = FindChildRecursive(character.transform, headName);
|
||||
if (head != null) return head;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Transform FindChildRecursive(Transform parent, string name)
|
||||
{
|
||||
foreach (Transform child in parent)
|
||||
{
|
||||
if (child.name == name) return child;
|
||||
Transform found = FindChildRecursive(child, name);
|
||||
if (found != null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Camera/AutoAddCinemachineCamera.cs.meta
Normal file
11
Assets/Scripts/Camera/AutoAddCinemachineCamera.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0638b26c337ab6d459ed0f3d377f69f5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
69
Assets/Scripts/Camera/CameraFollow.cs
Normal file
69
Assets/Scripts/Camera/CameraFollow.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Cinemachine;
|
||||
|
||||
/// <summary>
|
||||
/// Legacy camera follow script. Auto-disables when CinemachineBrain is present
|
||||
/// on the same camera to prevent LateUpdate transform conflicts.
|
||||
/// Only active as fallback when NO Cinemachine pipeline exists.
|
||||
/// </summary>
|
||||
public class CameraFollow : MonoBehaviour
|
||||
{
|
||||
public Transform target;
|
||||
public Vector3 offset;
|
||||
|
||||
public float smoothSpeed = 0.1f;
|
||||
|
||||
private bool cinemachineActive = false;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Check if CinemachineBrain is controlling this camera
|
||||
CinemachineBrain brain = GetComponent<CinemachineBrain>();
|
||||
if (brain == null)
|
||||
{
|
||||
// Also check Camera.main in case CameraFollow is on a child
|
||||
Camera mainCam = Camera.main;
|
||||
if (mainCam != null)
|
||||
{
|
||||
brain = mainCam.GetComponent<CinemachineBrain>();
|
||||
}
|
||||
}
|
||||
|
||||
if (brain != null && brain.enabled)
|
||||
{
|
||||
cinemachineActive = true;
|
||||
Debug.Log($"[CameraFollow] CinemachineBrain detected — disabling CameraFollow to prevent LateUpdate conflict.");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Legacy fallback: compute offset from target
|
||||
if (target != null)
|
||||
{
|
||||
offset = transform.position - target.position;
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
// Double-check: if Cinemachine got enabled after Start, bail out
|
||||
if (cinemachineActive) return;
|
||||
|
||||
if (target == null) return;
|
||||
|
||||
SmoothFollow();
|
||||
}
|
||||
|
||||
public void SmoothFollow()
|
||||
{
|
||||
if (target == null) return;
|
||||
|
||||
Vector3 targetPos = target.position + offset;
|
||||
Vector3 smoothFollow = Vector3.Lerp(transform.position, targetPos, smoothSpeed);
|
||||
|
||||
transform.position = smoothFollow;
|
||||
transform.LookAt(target);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Camera/CameraFollow.cs.meta
Normal file
11
Assets/Scripts/Camera/CameraFollow.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5946991bb03d5b341872abb3e254971d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
188
Assets/Scripts/Camera/CameraInitializer.cs
Normal file
188
Assets/Scripts/Camera/CameraInitializer.cs
Normal file
@@ -0,0 +1,188 @@
|
||||
using UnityEngine;
|
||||
using Cinemachine;
|
||||
using System.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Resets the Main Camera to a known-good position BEFORE CinemachineBrain takes over,
|
||||
/// then snaps the camera to the correct position AFTER the character spawns.
|
||||
///
|
||||
/// This solves the camera-underground problem with a two-phase approach:
|
||||
/// Phase 1 (Awake): Force camera above ground, set CinemachineBrain blend to Cut
|
||||
/// Phase 2 (Coroutine): Wait for player character, then force camera to correct orbit position
|
||||
///
|
||||
/// Attach this to the MainCamera GameObject.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-100)] // Run before everything else
|
||||
public class CameraInitializer : MonoBehaviour
|
||||
{
|
||||
[Header("Safe Starting Position")]
|
||||
[Tooltip("Camera starts here before Cinemachine takes control")]
|
||||
public Vector3 resetPosition = new Vector3(0f, 10f, -10f);
|
||||
public Vector3 resetRotation = new Vector3(20f, 0f, 0f);
|
||||
|
||||
[Header("Spawn Wait")]
|
||||
[Tooltip("Maximum time to wait for the player character to spawn")]
|
||||
[SerializeField] private float maxSpawnWaitTime = 5f;
|
||||
|
||||
[Tooltip("How often to check for the player character")]
|
||||
[SerializeField] private float spawnCheckInterval = 0.1f;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool debugLogging = true;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// ===== PHASE 1: Force camera to a known-good position =====
|
||||
transform.position = resetPosition;
|
||||
transform.rotation = Quaternion.Euler(resetRotation);
|
||||
|
||||
if (debugLogging)
|
||||
Debug.Log($"[CameraInitializer] Phase 1: Camera reset to position={resetPosition}");
|
||||
|
||||
// Set CinemachineBrain to Cut (instant snap, no blending from underground)
|
||||
CinemachineBrain brain = GetComponent<CinemachineBrain>();
|
||||
if (brain != null)
|
||||
{
|
||||
brain.m_DefaultBlend = new CinemachineBlendDefinition(
|
||||
CinemachineBlendDefinition.Style.Cut, 0f
|
||||
);
|
||||
if (debugLogging)
|
||||
Debug.Log("[CameraInitializer] CinemachineBrain set to CUT blend");
|
||||
}
|
||||
|
||||
// Invalidate all FreeLook camera states
|
||||
CinemachineFreeLook[] freeLooks = FindObjectsOfType<CinemachineFreeLook>();
|
||||
foreach (var fl in freeLooks)
|
||||
{
|
||||
fl.m_YAxis.Value = 0.5f;
|
||||
fl.PreviousStateIsValid = false;
|
||||
|
||||
// Set orbit heights
|
||||
fl.m_Orbits = new CinemachineFreeLook.Orbit[] {
|
||||
new CinemachineFreeLook.Orbit(4.5f, 5f), // TopRig
|
||||
new CinemachineFreeLook.Orbit(2.5f, 6f), // MiddleRig
|
||||
new CinemachineFreeLook.Orbit(0.5f, 5f), // BottomRig
|
||||
};
|
||||
|
||||
// CRITICAL: Clear default input axis names so Cinemachine doesn't fight
|
||||
// with CinemachineCameraInput. Without this, Cinemachine reads Input.GetAxis("Mouse X")
|
||||
// which returns 0 and snaps Y axis to bottom rig.
|
||||
fl.m_XAxis.m_InputAxisName = "";
|
||||
fl.m_YAxis.m_InputAxisName = "";
|
||||
|
||||
if (debugLogging)
|
||||
Debug.Log($"[CameraInitializer] FreeLook '{fl.name}': YAxis=0.5, axes cleared, state invalidated");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator Start()
|
||||
{
|
||||
// ===== PHASE 2: Wait for player character, then snap camera =====
|
||||
float elapsed = 0f;
|
||||
Transform playerTransform = null;
|
||||
|
||||
while (elapsed < maxSpawnWaitTime)
|
||||
{
|
||||
// Try to find the player character
|
||||
playerTransform = FindPlayerCharacter();
|
||||
|
||||
if (playerTransform != null)
|
||||
{
|
||||
if (debugLogging)
|
||||
Debug.Log($"[CameraInitializer] Phase 2: Found player '{playerTransform.name}' at {playerTransform.position} after {elapsed:F1}s");
|
||||
break;
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(spawnCheckInterval);
|
||||
elapsed += spawnCheckInterval;
|
||||
}
|
||||
|
||||
if (playerTransform == null)
|
||||
{
|
||||
Debug.LogWarning("[CameraInitializer] Phase 2: No player found within timeout — camera may be incorrect");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Force camera to the correct position relative to the character
|
||||
SnapCameraToCharacter(playerTransform);
|
||||
|
||||
// Wait a frame for Cinemachine to process the snap
|
||||
yield return null;
|
||||
yield return null;
|
||||
|
||||
// Restore smooth blending for character switching
|
||||
CinemachineBrain brain = GetComponent<CinemachineBrain>();
|
||||
if (brain != null)
|
||||
{
|
||||
brain.m_DefaultBlend = new CinemachineBlendDefinition(
|
||||
CinemachineBlendDefinition.Style.EaseInOut, 1f
|
||||
);
|
||||
if (debugLogging)
|
||||
Debug.Log("[CameraInitializer] Phase 2: Blend restored to EaseInOut");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force-snap the camera to the correct orbit position relative to the character.
|
||||
/// </summary>
|
||||
private void SnapCameraToCharacter(Transform character)
|
||||
{
|
||||
CinemachineFreeLook[] freeLooks = FindObjectsOfType<CinemachineFreeLook>();
|
||||
foreach (var fl in freeLooks)
|
||||
{
|
||||
if (!fl.isActiveAndEnabled) continue;
|
||||
|
||||
// Ensure target is set
|
||||
if (fl.Follow == null)
|
||||
{
|
||||
fl.Follow = character;
|
||||
fl.LookAt = character;
|
||||
}
|
||||
|
||||
// Reset to middle rig
|
||||
fl.m_YAxis.Value = 0.5f;
|
||||
fl.PreviousStateIsValid = false;
|
||||
|
||||
// Force camera transform to the correct position
|
||||
// MiddleRig: Height 2.5, Radius 6 — camera should be behind and above
|
||||
Vector3 targetPos = character.position;
|
||||
Vector3 cameraPos = targetPos + new Vector3(0f, 2.5f, -6f);
|
||||
|
||||
// Set the Main Camera transform directly
|
||||
transform.position = cameraPos;
|
||||
transform.LookAt(targetPos + Vector3.up * 1.5f);
|
||||
|
||||
if (debugLogging)
|
||||
{
|
||||
Debug.Log($"[CameraInitializer] Camera snapped to {cameraPos} (character at {targetPos})");
|
||||
Debug.Log($"[CameraInitializer] FreeLook Follow={fl.Follow?.name}, LookAt={fl.LookAt?.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the player-controlled character in the scene.
|
||||
/// </summary>
|
||||
private Transform FindPlayerCharacter()
|
||||
{
|
||||
// Try CameraManager first
|
||||
if (CameraManager.Instance != null)
|
||||
{
|
||||
var target = CameraManager.Instance.GetCurrentTarget();
|
||||
if (target != null) return target.transform;
|
||||
}
|
||||
|
||||
// Try finding by tag
|
||||
GameObject player = GameObject.FindGameObjectWithTag("Player");
|
||||
if (player != null) return player.transform;
|
||||
|
||||
// Try TeamMember with IsPlayerControlled
|
||||
TeamMember[] teamMembers = FindObjectsOfType<TeamMember>();
|
||||
foreach (var tm in teamMembers)
|
||||
{
|
||||
if (tm.IsPlayerControlled) return tm.transform;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Camera/CameraInitializer.cs.meta
Normal file
2
Assets/Scripts/Camera/CameraInitializer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16d9747b21ad6547384cf9a5e0f0abab
|
||||
50
Assets/Scripts/Camera/CameraShake.cs
Normal file
50
Assets/Scripts/Camera/CameraShake.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using UnityEngine;
|
||||
using Cinemachine;
|
||||
using System.Collections;
|
||||
|
||||
public class CameraShake : MonoBehaviour
|
||||
{
|
||||
private CinemachineVirtualCamera virtualCamera;
|
||||
private CinemachineBasicMultiChannelPerlin perlin;
|
||||
private Coroutine shakeCoroutine;
|
||||
|
||||
[Header("Shake Settings")]
|
||||
public float defaultShakeAmplitude = 2.0f;
|
||||
public float defaultShakeFrequency = 2.0f;
|
||||
public float defaultShakeDuration = 0.3f;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
virtualCamera = GetComponent<CinemachineVirtualCamera>();
|
||||
if (virtualCamera != null)
|
||||
perlin = virtualCamera.GetCinemachineComponent<CinemachineBasicMultiChannelPerlin>();
|
||||
}
|
||||
|
||||
public void ShakeCamera(float amplitude, float frequency, float duration)
|
||||
{
|
||||
if (shakeCoroutine != null)
|
||||
StopCoroutine(shakeCoroutine);
|
||||
shakeCoroutine = StartCoroutine(DoShake(amplitude, frequency, duration));
|
||||
}
|
||||
|
||||
public void ShakeCamera() // Overload for default values
|
||||
{
|
||||
ShakeCamera(defaultShakeAmplitude, defaultShakeFrequency, defaultShakeDuration);
|
||||
}
|
||||
|
||||
private IEnumerator DoShake(float amplitude, float frequency, float duration)
|
||||
{
|
||||
if (perlin == null)
|
||||
yield break;
|
||||
perlin.m_AmplitudeGain = amplitude;
|
||||
perlin.m_FrequencyGain = frequency;
|
||||
float elapsed = 0f;
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
perlin.m_AmplitudeGain = 0f;
|
||||
perlin.m_FrequencyGain = 0f;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Camera/CameraShake.cs.meta
Normal file
2
Assets/Scripts/Camera/CameraShake.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e80f5ecd3b46f8c448c32ad78d00ce04
|
||||
203
Assets/Scripts/Camera/CinemachineCameraInput.cs
Normal file
203
Assets/Scripts/Camera/CinemachineCameraInput.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.InputSystem;
|
||||
using Cinemachine;
|
||||
|
||||
/// <summary>
|
||||
/// Custom Cinemachine input handler that replaces CinemachineInputProvider.
|
||||
/// Uses CinemachineCore.GetInputAxis delegate for total control over camera orbit.
|
||||
///
|
||||
/// Supports:
|
||||
/// - Gamepad right stick (X = horizontal orbit, Y = vertical orbit)
|
||||
/// - Mobile touch drag (right half of screen, ignoring UI)
|
||||
/// - Mouse delta (desktop/editor)
|
||||
///
|
||||
/// Attach this to the CinemachineFreeLook camera GameObject.
|
||||
/// Remove or disable CinemachineInputProvider if present.
|
||||
/// </summary>
|
||||
public class CinemachineCameraInput : MonoBehaviour
|
||||
{
|
||||
[Header("Sensitivity")]
|
||||
[SerializeField] private float gamepadSensitivityX = 200f;
|
||||
[SerializeField] private float gamepadSensitivityY = 3f;
|
||||
[SerializeField] private float touchSensitivityX = 0.15f;
|
||||
[SerializeField] private float touchSensitivityY = 0.003f;
|
||||
[SerializeField] private float mouseSensitivityX = 3f;
|
||||
[SerializeField] private float mouseSensitivityY = 0.03f;
|
||||
|
||||
[Header("Touch Settings")]
|
||||
[Tooltip("Only process touches on the right portion of the screen (0.5 = right half)")]
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float touchActiveScreenRatio = 0.4f;
|
||||
|
||||
[Tooltip("Ignore touches over UI elements (buttons, joysticks, etc.)")]
|
||||
[SerializeField] private bool ignoreUITouches = true;
|
||||
|
||||
[Header("Filtering")]
|
||||
[SerializeField] private float stickDeadzone = 0.15f;
|
||||
[SerializeField] private float smoothing = 0.1f;
|
||||
|
||||
// Touch tracking
|
||||
private int activeTouchId = -1;
|
||||
private Vector2 lastTouchPos;
|
||||
|
||||
// Smoothed output
|
||||
private float smoothedX;
|
||||
private float smoothedY;
|
||||
private float rawX;
|
||||
private float rawY;
|
||||
|
||||
// Axis names used by CinemachineFreeLook
|
||||
private const string XAxisName = "Mouse X";
|
||||
private const string YAxisName = "Mouse Y";
|
||||
|
||||
private CinemachineFreeLook freeLookCam;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
freeLookCam = GetComponent<CinemachineFreeLook>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Override Cinemachine's input gathering with our custom handler
|
||||
CinemachineCore.GetInputAxis = HandleCinemachineInput;
|
||||
|
||||
// Disable CinemachineInputProvider if present (we replace it)
|
||||
var provider = GetComponent<CinemachineInputProvider>();
|
||||
if (provider != null)
|
||||
{
|
||||
provider.enabled = false;
|
||||
Debug.Log("[CinemachineCameraInput] Disabled CinemachineInputProvider — using custom input handler");
|
||||
}
|
||||
|
||||
// Clear the default input axis names on the FreeLook camera
|
||||
// This prevents Cinemachine from also reading legacy Input Manager axes
|
||||
if (freeLookCam != null)
|
||||
{
|
||||
freeLookCam.m_XAxis.m_InputAxisName = "";
|
||||
freeLookCam.m_YAxis.m_InputAxisName = "";
|
||||
}
|
||||
|
||||
Debug.Log("[CinemachineCameraInput] Custom camera input enabled (gamepad + touch + mouse)");
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// Restore default input handling when disabled
|
||||
CinemachineCore.GetInputAxis = UnityEngine.Input.GetAxis;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Gather raw input from all sources
|
||||
rawX = 0f;
|
||||
rawY = 0f;
|
||||
|
||||
ReadGamepadInput();
|
||||
ReadTouchInput();
|
||||
ReadMouseInput();
|
||||
|
||||
// Smooth the input
|
||||
if (smoothing > 0.001f)
|
||||
{
|
||||
smoothedX = Mathf.Lerp(smoothedX, rawX, Time.unscaledDeltaTime / smoothing);
|
||||
smoothedY = Mathf.Lerp(smoothedY, rawY, Time.unscaledDeltaTime / smoothing);
|
||||
}
|
||||
else
|
||||
{
|
||||
smoothedX = rawX;
|
||||
smoothedY = rawY;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate callback — Cinemachine calls this instead of Input.GetAxis
|
||||
/// </summary>
|
||||
private float HandleCinemachineInput(string axisName)
|
||||
{
|
||||
if (axisName == XAxisName) return smoothedX;
|
||||
if (axisName == YAxisName) return smoothedY;
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// ======================== GAMEPAD ========================
|
||||
private void ReadGamepadInput()
|
||||
{
|
||||
if (Gamepad.current == null) return;
|
||||
|
||||
Vector2 rightStick = Gamepad.current.rightStick.ReadValue();
|
||||
|
||||
// Apply deadzone
|
||||
if (rightStick.magnitude < stickDeadzone)
|
||||
return;
|
||||
|
||||
rawX += rightStick.x * gamepadSensitivityX * Time.unscaledDeltaTime;
|
||||
rawY += rightStick.y * gamepadSensitivityY * Time.unscaledDeltaTime;
|
||||
}
|
||||
|
||||
// ======================== TOUCH ========================
|
||||
private void ReadTouchInput()
|
||||
{
|
||||
if (Touchscreen.current == null) return;
|
||||
|
||||
var touches = Touchscreen.current.touches;
|
||||
bool foundActiveTouch = false;
|
||||
|
||||
for (int i = 0; i < touches.Count; i++)
|
||||
{
|
||||
var touch = touches[i];
|
||||
if (!touch.press.isPressed) continue;
|
||||
|
||||
int id = touch.touchId.ReadValue();
|
||||
Vector2 pos = touch.position.ReadValue();
|
||||
|
||||
// If no active touch, try to start tracking this one
|
||||
if (activeTouchId == -1)
|
||||
{
|
||||
// Only track touches on the right portion of the screen
|
||||
if (pos.x < Screen.width * touchActiveScreenRatio)
|
||||
continue;
|
||||
|
||||
// Ignore touches over UI elements
|
||||
if (ignoreUITouches && EventSystem.current != null &&
|
||||
EventSystem.current.IsPointerOverGameObject(id))
|
||||
continue;
|
||||
|
||||
activeTouchId = id;
|
||||
lastTouchPos = pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (touch.touchId.ReadValue() != activeTouchId)
|
||||
continue;
|
||||
|
||||
foundActiveTouch = true;
|
||||
Vector2 delta = pos - lastTouchPos;
|
||||
lastTouchPos = pos;
|
||||
|
||||
rawX += delta.x * touchSensitivityX;
|
||||
rawY += delta.y * touchSensitivityY;
|
||||
}
|
||||
|
||||
// Reset tracking if active touch released
|
||||
if (activeTouchId != -1 && !foundActiveTouch)
|
||||
{
|
||||
activeTouchId = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== MOUSE ========================
|
||||
private void ReadMouseInput()
|
||||
{
|
||||
if (Mouse.current == null) return;
|
||||
|
||||
// Only use mouse if right button is held (prevents accidental camera rotation)
|
||||
if (!Mouse.current.rightButton.isPressed) return;
|
||||
|
||||
Vector2 mouseDelta = Mouse.current.delta.ReadValue();
|
||||
|
||||
rawX += mouseDelta.x * mouseSensitivityX * Time.unscaledDeltaTime;
|
||||
rawY += mouseDelta.y * mouseSensitivityY * Time.unscaledDeltaTime;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Camera/CinemachineCameraInput.cs.meta
Normal file
2
Assets/Scripts/Camera/CinemachineCameraInput.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85b5c64e9ac2e5829b1e858392b5955c
|
||||
Reference in New Issue
Block a user