chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
211
Assets/Scripts/Combat/EnemyTargetDetector.cs
Normal file
211
Assets/Scripts/Combat/EnemyTargetDetector.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Finds nearby enemies (tagged "Enemy") that satisfy the assist constraints.
|
||||
/// Designed for zero-allocation queries during combat assists.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class EnemyTargetDetector : MonoBehaviour
|
||||
{
|
||||
private const float MinAssistRadius = 0.5f;
|
||||
|
||||
[Header("Assist Window")]
|
||||
[Tooltip("Maximum distance at which the assist can consider enemies.")]
|
||||
[SerializeField] private float assistRadius = 4.0f;
|
||||
|
||||
[Tooltip("Maximum field of view in degrees around the forward vector where assist is allowed.")]
|
||||
[SerializeField, Range(30f, 180f)] private float assistFOVDegrees = 130f;
|
||||
|
||||
[Tooltip("Optional hard cap on the delta angle to avoid 180° turns.")]
|
||||
[SerializeField, Range(45f, 180f)] private float maxAssistAngle = 150f;
|
||||
|
||||
[Tooltip("Layer mask used for enemy overlap queries.")]
|
||||
[SerializeField] private LayerMask enemyLayers = ~0;
|
||||
|
||||
[Tooltip("Maximum number of enemy candidates that will be considered per query.")]
|
||||
[SerializeField, Min(1)] private int maxCandidates = 12;
|
||||
|
||||
[Header("Line Of Sight")]
|
||||
[Tooltip("If enabled, a raycast must reach the target without hitting the obstruction layers.")]
|
||||
[SerializeField] private bool requireLineOfSight = false;
|
||||
|
||||
[SerializeField] private LayerMask obstructionLayers = ~0;
|
||||
|
||||
[SerializeField, Tooltip("Height offset for line of sight ray origin.")]
|
||||
private float losHeightOffset = 1.6f;
|
||||
|
||||
[SerializeField, Tooltip("Height offset for line of sight ray destination.")]
|
||||
private float targetHeightOffset = 1.2f;
|
||||
|
||||
private Collider[] candidateBuffer;
|
||||
private Transform hardLockOverride;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
maxCandidates = Mathf.Max(1, maxCandidates);
|
||||
candidateBuffer = new Collider[maxCandidates];
|
||||
if (assistRadius < MinAssistRadius)
|
||||
{
|
||||
assistRadius = MinAssistRadius;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optionally sets a manually chosen target that should be prioritised over soft candidates.
|
||||
/// </summary>
|
||||
public void SetHardLockTarget(Transform target)
|
||||
{
|
||||
hardLockOverride = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find the best enemy target within the configured assist window.
|
||||
/// </summary>
|
||||
public bool TryFindBestTarget(out Transform target, out Vector3 targetPosition)
|
||||
{
|
||||
target = null;
|
||||
targetPosition = Vector3.zero;
|
||||
|
||||
if (!enabled)
|
||||
return false;
|
||||
|
||||
if (hardLockOverride != null && IsTargetValid(hardLockOverride, out var hardLockPos))
|
||||
{
|
||||
target = hardLockOverride;
|
||||
targetPosition = hardLockPos;
|
||||
return true;
|
||||
}
|
||||
|
||||
var origin = transform.position;
|
||||
var forward = transform.forward;
|
||||
forward.y = 0f;
|
||||
forward.Normalize();
|
||||
|
||||
int hits = Physics.OverlapSphereNonAlloc(origin, assistRadius, candidateBuffer, enemyLayers, QueryTriggerInteraction.Collide);
|
||||
if (hits <= 0)
|
||||
return false;
|
||||
|
||||
float bestScore = float.MaxValue;
|
||||
Transform bestTarget = null;
|
||||
Vector3 bestPosition = Vector3.zero;
|
||||
|
||||
for (int i = 0; i < hits; i++)
|
||||
{
|
||||
Collider candidate = candidateBuffer[i];
|
||||
if (candidate == null)
|
||||
continue;
|
||||
|
||||
Transform candidateTransform = candidate.transform;
|
||||
if (!candidateTransform.CompareTag("Enemy"))
|
||||
continue;
|
||||
|
||||
if (!IsTargetValid(candidateTransform, out var candidatePosition))
|
||||
continue;
|
||||
|
||||
Vector3 toTarget = candidatePosition - origin;
|
||||
toTarget.y = 0f;
|
||||
float sqrDist = toTarget.sqrMagnitude;
|
||||
if (sqrDist < 0.0001f)
|
||||
continue;
|
||||
|
||||
Vector3 direction = toTarget / Mathf.Sqrt(sqrDist);
|
||||
float angle = Vector3.Angle(forward, direction);
|
||||
if (angle > assistFOVDegrees)
|
||||
continue;
|
||||
|
||||
if (angle > maxAssistAngle)
|
||||
continue;
|
||||
|
||||
if (requireLineOfSight && !HasLineOfSight(origin, candidatePosition))
|
||||
continue;
|
||||
|
||||
// Score by angle first, then distance.
|
||||
float score = angle * 10f + sqrDist;
|
||||
if (score < bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestTarget = candidateTransform;
|
||||
bestPosition = candidatePosition;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestTarget == null)
|
||||
return false;
|
||||
|
||||
target = bestTarget;
|
||||
targetPosition = bestPosition;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsTargetValid(Transform candidate, out Vector3 targetPosition)
|
||||
{
|
||||
targetPosition = candidate.position;
|
||||
|
||||
if (candidate == null)
|
||||
return false;
|
||||
|
||||
// Verify active in hierarchy (common alive check surrogate).
|
||||
if (!candidate.gameObject.activeInHierarchy)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HasLineOfSight(Vector3 origin, Vector3 targetPosition)
|
||||
{
|
||||
Vector3 rayOrigin = origin + Vector3.up * losHeightOffset;
|
||||
Vector3 rayTarget = targetPosition + Vector3.up * targetHeightOffset;
|
||||
Vector3 delta = rayTarget - rayOrigin;
|
||||
float distance = delta.magnitude;
|
||||
if (distance <= 0.001f)
|
||||
return true;
|
||||
|
||||
Vector3 direction = delta / distance;
|
||||
if (Physics.Raycast(rayOrigin, direction, distance, obstructionLayers, QueryTriggerInteraction.Ignore))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Inspector API
|
||||
|
||||
public float AssistRadius
|
||||
{
|
||||
get => assistRadius;
|
||||
set => assistRadius = Mathf.Max(MinAssistRadius, value);
|
||||
}
|
||||
|
||||
public float AssistFOVDegrees
|
||||
{
|
||||
get => assistFOVDegrees;
|
||||
set => assistFOVDegrees = Mathf.Clamp(value, 30f, 180f);
|
||||
}
|
||||
|
||||
public float MaxAssistAngle
|
||||
{
|
||||
get => maxAssistAngle;
|
||||
set => maxAssistAngle = Mathf.Clamp(value, 45f, 180f);
|
||||
}
|
||||
|
||||
public LayerMask EnemyLayers
|
||||
{
|
||||
get => enemyLayers;
|
||||
set => enemyLayers = value;
|
||||
}
|
||||
|
||||
public bool RequireLineOfSight
|
||||
{
|
||||
get => requireLineOfSight;
|
||||
set => requireLineOfSight = value;
|
||||
}
|
||||
|
||||
public LayerMask ObstructionLayers
|
||||
{
|
||||
get => obstructionLayers;
|
||||
set => obstructionLayers = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
2
Assets/Scripts/Combat/EnemyTargetDetector.cs.meta
Normal file
2
Assets/Scripts/Combat/EnemyTargetDetector.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f78091c360e2a7b3bd3c0842f414869
|
||||
253
Assets/Scripts/Combat/FaceTargetOnAttack.cs
Normal file
253
Assets/Scripts/Combat/FaceTargetOnAttack.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Smoothly rotates the player towards a valid enemy target when an attack animation is triggered.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class FaceTargetOnAttack : MonoBehaviour
|
||||
{
|
||||
private const float MinAssistDuration = 0.05f;
|
||||
private const float MinCancelTime = 0.05f;
|
||||
|
||||
[Header("Assist Toggle")]
|
||||
[SerializeField] private bool enableAutoFaceOnAttack = true;
|
||||
[SerializeField] private bool reEvaluateDuringWindow = true;
|
||||
|
||||
[Header("Rotation")]
|
||||
[SerializeField] private float rotationSpeedDegPerSec = 720f;
|
||||
[SerializeField] private float assistWindowSeconds = 0.25f;
|
||||
[SerializeField] private float manualOverrideThreshold = 0.15f;
|
||||
[SerializeField] private float targetLoseDistanceTolerance = 0.25f;
|
||||
|
||||
[Header("Dependencies")]
|
||||
[SerializeField] private EnemyTargetDetector targetDetector;
|
||||
[SerializeField] private LookInputRouter lookInputRouter;
|
||||
[SerializeField] private CharacterAttacks characterAttacks;
|
||||
[SerializeField] private PlayerScript playerScript;
|
||||
[SerializeField] private Animator animator;
|
||||
|
||||
[Header("Animator Output")]
|
||||
[SerializeField] private bool driveAnimatorTurnSpeed = false;
|
||||
[SerializeField] private string turnSpeedParameter = "TurnSpeed";
|
||||
[SerializeField] private float turnSpeedScale = 1f;
|
||||
|
||||
[Header("Attack Filtering")]
|
||||
[Tooltip("Animations that should trigger auto facing. Leave empty to allow all.")]
|
||||
[SerializeField] private List<string> assistedAnimationNames = new List<string>
|
||||
{
|
||||
AnimationNames.Jab,
|
||||
AnimationNames.Haymaker,
|
||||
AnimationNames.SpinningBackfist,
|
||||
AnimationNames.ElbowSmash,
|
||||
AnimationNames.AirPunch,
|
||||
AnimationNames.LowKick,
|
||||
AnimationNames.BasicKick,
|
||||
AnimationNames.SumoSlap
|
||||
};
|
||||
|
||||
private float assistTimer;
|
||||
private Transform currentTarget;
|
||||
private Vector3 currentTargetPosition;
|
||||
private Quaternion lastFrameRotation;
|
||||
private bool assistActive;
|
||||
private bool waitingForCancelWindow;
|
||||
|
||||
private HashSet<string> assistedLookup;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
targetDetector = GetComponent<EnemyTargetDetector>();
|
||||
lookInputRouter = GetComponent<LookInputRouter>();
|
||||
characterAttacks = GetComponent<CharacterAttacks>();
|
||||
playerScript = GetComponent<PlayerScript>();
|
||||
animator = GetComponent<Animator>();
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (targetDetector == null)
|
||||
targetDetector = GetComponent<EnemyTargetDetector>();
|
||||
if (lookInputRouter == null)
|
||||
lookInputRouter = GetComponent<LookInputRouter>();
|
||||
if (characterAttacks == null)
|
||||
characterAttacks = GetComponent<CharacterAttacks>();
|
||||
if (playerScript == null)
|
||||
playerScript = GetComponent<PlayerScript>();
|
||||
if (animator == null)
|
||||
animator = GetComponent<Animator>();
|
||||
|
||||
assistedLookup = assistedAnimationNames != null && assistedAnimationNames.Count > 0
|
||||
? new HashSet<string>(assistedAnimationNames)
|
||||
: null;
|
||||
|
||||
if (characterAttacks != null)
|
||||
{
|
||||
characterAttacks.AnimationAboutToPlay += OnAnimationAboutToPlay;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (characterAttacks != null)
|
||||
{
|
||||
characterAttacks.AnimationAboutToPlay -= OnAnimationAboutToPlay;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!assistActive)
|
||||
return;
|
||||
|
||||
if (!enableAutoFaceOnAttack)
|
||||
{
|
||||
CancelAssist();
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerScript != null && (!playerScript.attack || playerScript.dodge || playerScript.block))
|
||||
{
|
||||
CancelAssist();
|
||||
return;
|
||||
}
|
||||
|
||||
assistTimer -= Time.deltaTime;
|
||||
if (assistTimer <= 0f)
|
||||
{
|
||||
CancelAssist();
|
||||
return;
|
||||
}
|
||||
|
||||
if (lookInputRouter != null && lookInputRouter.TryGetLookDelta(out var lookDelta))
|
||||
{
|
||||
if (waitingForCancelWindow)
|
||||
{
|
||||
if (assistWindowSeconds - assistTimer > MinCancelTime && lookDelta.magnitude >= manualOverrideThreshold)
|
||||
{
|
||||
CancelAssist();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (lookDelta.magnitude >= manualOverrideThreshold)
|
||||
{
|
||||
CancelAssist();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ValidateOrRefreshTarget())
|
||||
{
|
||||
CancelAssist();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 direction = currentTargetPosition - transform.position;
|
||||
direction.y = 0f;
|
||||
if (direction.sqrMagnitude < 0.0001f)
|
||||
{
|
||||
CancelAssist();
|
||||
return;
|
||||
}
|
||||
|
||||
Quaternion desiredRotation = Quaternion.LookRotation(direction, Vector3.up);
|
||||
lastFrameRotation = transform.rotation;
|
||||
transform.rotation = Quaternion.RotateTowards(transform.rotation, desiredRotation, rotationSpeedDegPerSec * Time.deltaTime);
|
||||
|
||||
waitingForCancelWindow = false;
|
||||
|
||||
if (driveAnimatorTurnSpeed && animator != null)
|
||||
{
|
||||
float angleDelta = Quaternion.Angle(lastFrameRotation, transform.rotation);
|
||||
float turnSpeed = (angleDelta / Time.deltaTime) * turnSpeedScale;
|
||||
animator.SetFloat(turnSpeedParameter, turnSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAnimationAboutToPlay(string animationName)
|
||||
{
|
||||
if (!enableAutoFaceOnAttack)
|
||||
return;
|
||||
|
||||
if (!ShouldAssistForAnimation(animationName))
|
||||
return;
|
||||
|
||||
if (targetDetector == null)
|
||||
return;
|
||||
|
||||
if (assistWindowSeconds < MinAssistDuration)
|
||||
assistWindowSeconds = MinAssistDuration;
|
||||
|
||||
if (targetDetector.TryFindBestTarget(out var target, out var position))
|
||||
{
|
||||
BeginAssist(target, position);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldAssistForAnimation(string animationName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(animationName))
|
||||
return false;
|
||||
|
||||
if (assistedLookup == null)
|
||||
return true;
|
||||
|
||||
return assistedLookup.Contains(animationName);
|
||||
}
|
||||
|
||||
private void BeginAssist(Transform target, Vector3 position)
|
||||
{
|
||||
currentTarget = target;
|
||||
currentTargetPosition = position;
|
||||
assistTimer = assistWindowSeconds;
|
||||
assistActive = true;
|
||||
waitingForCancelWindow = true;
|
||||
lastFrameRotation = transform.rotation;
|
||||
|
||||
if (lookInputRouter != null)
|
||||
{
|
||||
lookInputRouter.ResetLookDelta();
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateOrRefreshTarget()
|
||||
{
|
||||
if (currentTarget == null || !currentTarget.gameObject.activeInHierarchy)
|
||||
return false;
|
||||
|
||||
if (targetDetector == null)
|
||||
return false;
|
||||
|
||||
float sqrDistance = (currentTargetPosition - transform.position).sqrMagnitude;
|
||||
float maxDistance = targetDetector.AssistRadius + targetLoseDistanceTolerance;
|
||||
if (sqrDistance > maxDistance * maxDistance)
|
||||
return false;
|
||||
|
||||
if (reEvaluateDuringWindow && targetDetector.TryFindBestTarget(out var newTarget, out var newPosition))
|
||||
{
|
||||
currentTarget = newTarget;
|
||||
currentTargetPosition = newPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentTargetPosition = currentTarget.position;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CancelAssist()
|
||||
{
|
||||
assistActive = false;
|
||||
assistTimer = 0f;
|
||||
currentTarget = null;
|
||||
currentTargetPosition = Vector3.zero;
|
||||
waitingForCancelWindow = false;
|
||||
|
||||
if (driveAnimatorTurnSpeed && animator != null)
|
||||
{
|
||||
animator.SetFloat(turnSpeedParameter, 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Combat/FaceTargetOnAttack.cs.meta
Normal file
2
Assets/Scripts/Combat/FaceTargetOnAttack.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1fe8e63d70dcf637cb383aa5edf263ce
|
||||
427
Assets/Scripts/Combat/LookInputRouter.cs
Normal file
427
Assets/Scripts/Combat/LookInputRouter.cs
Normal file
@@ -0,0 +1,427 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.Controls;
|
||||
using UnityEngine.InputSystem.EnhancedTouch;
|
||||
using UnityEngine.InputSystem.Utilities;
|
||||
using EnhancedTouchSupport = UnityEngine.InputSystem.EnhancedTouch.EnhancedTouchSupport;
|
||||
using EnhancedTouchTouch = UnityEngine.InputSystem.EnhancedTouch.Touch;
|
||||
using EnhancedTouchPhase = UnityEngine.InputSystem.TouchPhase;
|
||||
using UnityTouch = UnityEngine.Touch;
|
||||
using TouchPhaseLegacy = UnityEngine.TouchPhase;
|
||||
|
||||
/// <summary>
|
||||
/// Aggregates look input from mouse, gamepad sticks, and mobile touch into a single delta value.
|
||||
/// Works with either the legacy Input Manager or the newer Input System.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class LookInputRouter : MonoBehaviour
|
||||
{
|
||||
private const float TinyThreshold = 0.0001f;
|
||||
|
||||
[Header("General")]
|
||||
[SerializeField] private bool autoDetectInputSystem = true;
|
||||
[SerializeField] private bool preferNewInputSystem = true;
|
||||
|
||||
[Header("Input System (New)")]
|
||||
[Tooltip("Optional PlayerInput component containing the action map with a Look action.")]
|
||||
[SerializeField] private PlayerInput playerInput;
|
||||
|
||||
[Tooltip("Action name used for look deltas when using the new Input System.")]
|
||||
[SerializeField] private string lookActionName = "Look";
|
||||
|
||||
[Header("Legacy Input Manager")]
|
||||
[SerializeField] private string mouseXAxis = "Mouse X";
|
||||
[SerializeField] private string mouseYAxis = "Mouse Y";
|
||||
[SerializeField] private string rightStickXAxis = "Joystick X";
|
||||
[SerializeField] private string rightStickYAxis = "Joystick Y";
|
||||
|
||||
[Header("Sensitivities")]
|
||||
[SerializeField] private float mouseSensitivity = 1.0f;
|
||||
[SerializeField] private float stickSensitivity = 120.0f;
|
||||
[SerializeField] private float touchSensitivity = 0.15f;
|
||||
|
||||
[Header("Filtering")]
|
||||
[Tooltip("Inputs below this magnitude will be ignored and reset to zero.")]
|
||||
[SerializeField] private float deadzone = 0.05f;
|
||||
|
||||
[Tooltip("Optional smoothing duration in seconds. Set to zero for raw input.")]
|
||||
[SerializeField] private float smoothingTime = 0.08f;
|
||||
|
||||
[Tooltip("Maximum magnitude allowed for a single look delta.")]
|
||||
[SerializeField] private float maxMagnitude = 5.0f;
|
||||
|
||||
private InputAction cachedLookAction;
|
||||
private bool usingNewInputSystem;
|
||||
|
||||
private Vector2 currentDelta;
|
||||
private Vector2 smoothVelocity;
|
||||
|
||||
// Touch state
|
||||
private int activeTouchId = -1;
|
||||
private Vector2 lastTouchPosition;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
DetectInputSystem();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Enable Enhanced Touch so we can use the high-level Touch struct API.
|
||||
EnhancedTouchSupport.Enable();
|
||||
|
||||
if (usingNewInputSystem)
|
||||
{
|
||||
EnableLookAction();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (cachedLookAction != null)
|
||||
{
|
||||
cachedLookAction.Disable();
|
||||
}
|
||||
cachedLookAction = null;
|
||||
|
||||
// Pair with Enable() to keep the global reference count balanced.
|
||||
EnhancedTouchSupport.Disable();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
Vector2 rawDelta = usingNewInputSystem ? ReadNewSystemDelta() : ReadLegacyDelta();
|
||||
|
||||
if (!usingNewInputSystem && preferNewInputSystem)
|
||||
{
|
||||
// If no PlayerInput was supplied but the Input System package is present,
|
||||
// fall back to gathering from Gamepad/Touchscreen directly.
|
||||
rawDelta += ReadDirectDeviceDelta();
|
||||
}
|
||||
|
||||
rawDelta = Vector2.ClampMagnitude(rawDelta, maxMagnitude);
|
||||
|
||||
if (smoothingTime > TinyThreshold)
|
||||
{
|
||||
currentDelta = Vector2.SmoothDamp(currentDelta, rawDelta, ref smoothVelocity, smoothingTime, Mathf.Infinity, Time.unscaledDeltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
currentDelta = rawDelta;
|
||||
}
|
||||
|
||||
if (currentDelta.magnitude < deadzone)
|
||||
{
|
||||
currentDelta = Vector2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetLookDelta(out Vector2 delta)
|
||||
{
|
||||
delta = currentDelta;
|
||||
return delta.sqrMagnitude > TinyThreshold;
|
||||
}
|
||||
|
||||
private void DetectInputSystem()
|
||||
{
|
||||
if (!autoDetectInputSystem)
|
||||
{
|
||||
usingNewInputSystem = preferNewInputSystem && playerInput != null;
|
||||
CacheLookAction();
|
||||
return;
|
||||
}
|
||||
|
||||
// If PlayerInput is explicitly assigned, use it.
|
||||
if (playerInput == null)
|
||||
{
|
||||
playerInput = GetComponent<PlayerInput>();
|
||||
}
|
||||
|
||||
if (playerInput != null && preferNewInputSystem)
|
||||
{
|
||||
usingNewInputSystem = true;
|
||||
CacheLookAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
usingNewInputSystem = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheLookAction()
|
||||
{
|
||||
if (playerInput == null)
|
||||
return;
|
||||
|
||||
var actions = playerInput.actions;
|
||||
if (actions == null)
|
||||
return;
|
||||
|
||||
cachedLookAction = actions.FindAction(lookActionName, false);
|
||||
if (cachedLookAction != null && !cachedLookAction.enabled && isActiveAndEnabled)
|
||||
{
|
||||
cachedLookAction.SafeEnable();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnableLookAction()
|
||||
{
|
||||
if (cachedLookAction == null && playerInput != null)
|
||||
{
|
||||
CacheLookAction();
|
||||
}
|
||||
|
||||
cachedLookAction?.SafeEnable();
|
||||
}
|
||||
|
||||
private Vector2 ReadNewSystemDelta()
|
||||
{
|
||||
if (cachedLookAction == null)
|
||||
return ReadDirectDeviceDelta();
|
||||
|
||||
Vector2 value = cachedLookAction.ReadValue<Vector2>();
|
||||
return value * Time.unscaledDeltaTime;
|
||||
}
|
||||
|
||||
private Vector2 ReadLegacyDelta()
|
||||
{
|
||||
// Replace legacy Input.GetAxis usage with InputSystem device reads where possible.
|
||||
float x = 0f, y = 0f;
|
||||
|
||||
if (UnityEngine.InputSystem.Mouse.current != null)
|
||||
{
|
||||
Vector2 mouseDelta = UnityEngine.InputSystem.Mouse.current.delta.ReadValue();
|
||||
x = mouseDelta.x * mouseSensitivity;
|
||||
y = mouseDelta.y * mouseSensitivity;
|
||||
}
|
||||
|
||||
float stickX = 0f, stickY = 0f;
|
||||
if (UnityEngine.InputSystem.Gamepad.current != null)
|
||||
{
|
||||
Vector2 right = UnityEngine.InputSystem.Gamepad.current.rightStick.ReadValue();
|
||||
stickX = right.x * stickSensitivity * Time.unscaledDeltaTime;
|
||||
stickY = right.y * stickSensitivity * Time.unscaledDeltaTime;
|
||||
}
|
||||
|
||||
return new Vector2(x + stickX, y + stickY);
|
||||
}
|
||||
|
||||
private Vector2 ReadDirectDeviceDelta()
|
||||
{
|
||||
Vector2 aggregate = Vector2.zero;
|
||||
|
||||
if (Mouse.current != null)
|
||||
{
|
||||
Vector2 mouseDelta = Mouse.current.delta.ReadValue();
|
||||
aggregate += mouseDelta * mouseSensitivity * Time.unscaledDeltaTime;
|
||||
}
|
||||
|
||||
if (Gamepad.current != null)
|
||||
{
|
||||
Vector2 stick = Gamepad.current.rightStick.ReadValue();
|
||||
aggregate += stick * stickSensitivity * Time.unscaledDeltaTime;
|
||||
}
|
||||
|
||||
aggregate += ReadTouchDelta();
|
||||
|
||||
return aggregate;
|
||||
}
|
||||
|
||||
private Vector2 ReadTouchDelta()
|
||||
{
|
||||
if (EnhancedTouchSupport.enabled)
|
||||
{
|
||||
var enhancedDelta = ReadEnhancedTouchDelta();
|
||||
if (enhancedDelta != Vector2.zero || EnhancedTouchTouch.activeTouches.Count > 0)
|
||||
return enhancedDelta;
|
||||
}
|
||||
|
||||
if (Touchscreen.current != null)
|
||||
{
|
||||
var touchscreenDelta = ReadTouchscreenDelta();
|
||||
if (touchscreenDelta != Vector2.zero)
|
||||
return touchscreenDelta;
|
||||
}
|
||||
|
||||
return ReadLegacyTouch();
|
||||
}
|
||||
|
||||
private Vector2 ReadEnhancedTouchDelta()
|
||||
{
|
||||
var touches = EnhancedTouchTouch.activeTouches;
|
||||
if (touches.Count <= 0)
|
||||
{
|
||||
ResetTouchTracking();
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
for (int i = 0; i < touches.Count; ++i)
|
||||
{
|
||||
var touch = touches[i];
|
||||
if (!touch.valid)
|
||||
continue;
|
||||
|
||||
if (activeTouchId == -1)
|
||||
{
|
||||
activeTouchId = touch.touchId;
|
||||
lastTouchPosition = touch.screenPosition;
|
||||
}
|
||||
|
||||
if (touch.touchId != activeTouchId)
|
||||
continue;
|
||||
|
||||
if (touch.phase == EnhancedTouchPhase.Ended || touch.phase == EnhancedTouchPhase.Canceled)
|
||||
{
|
||||
ResetTouchTracking();
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
if (touch.phase == EnhancedTouchPhase.Began)
|
||||
{
|
||||
lastTouchPosition = touch.screenPosition;
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
Vector2 delta = (touch.screenPosition - lastTouchPosition) * touchSensitivity * Time.unscaledDeltaTime;
|
||||
lastTouchPosition = touch.screenPosition;
|
||||
return delta;
|
||||
}
|
||||
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
private Vector2 ReadTouchscreenDelta()
|
||||
{
|
||||
if (Touchscreen.current == null)
|
||||
{
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
if (!AnyTouchPressed(Touchscreen.current.touches))
|
||||
{
|
||||
ResetTouchTracking();
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
foreach (var touchControl in Touchscreen.current.touches)
|
||||
{
|
||||
if (!touchControl.press.isPressed)
|
||||
continue;
|
||||
|
||||
int id = touchControl.touchId.ReadValue();
|
||||
Vector2 position = touchControl.position.ReadValue();
|
||||
|
||||
if (activeTouchId == -1)
|
||||
{
|
||||
activeTouchId = id;
|
||||
lastTouchPosition = position;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (activeTouchId != id)
|
||||
continue;
|
||||
|
||||
Vector2 delta = (position - lastTouchPosition) * touchSensitivity;
|
||||
lastTouchPosition = position;
|
||||
return delta * Time.unscaledDeltaTime;
|
||||
}
|
||||
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
private Vector2 ReadLegacyTouch()
|
||||
{
|
||||
// Prefer the Input System touchscreen if available. Fall back to legacy Input.touches only when necessary.
|
||||
if (UnityEngine.InputSystem.Touchscreen.current != null)
|
||||
{
|
||||
var touches = UnityEngine.InputSystem.Touchscreen.current.touches;
|
||||
if (touches.Count <= 0)
|
||||
{
|
||||
ResetTouchTracking();
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
for (int i = 0; i < touches.Count; ++i)
|
||||
{
|
||||
var tc = touches[i];
|
||||
if (!tc.press.isPressed)
|
||||
continue;
|
||||
|
||||
int id = tc.touchId.ReadValue();
|
||||
Vector2 pos = tc.position.ReadValue();
|
||||
|
||||
// We already check tc.press.isPressed above. No additional phase check is required
|
||||
// here for the Input System touch path. This avoids ambiguous TouchPhase enum
|
||||
// comparisons between the legacy and new Input System types.
|
||||
|
||||
if (activeTouchId == -1)
|
||||
{
|
||||
activeTouchId = id;
|
||||
lastTouchPosition = pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (activeTouchId != id)
|
||||
continue;
|
||||
|
||||
Vector2 delta = (pos - lastTouchPosition) * touchSensitivity * Time.unscaledDeltaTime;
|
||||
lastTouchPosition = pos;
|
||||
return delta;
|
||||
}
|
||||
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
// Legacy fallback
|
||||
if (Input.touchCount <= 0)
|
||||
{
|
||||
ResetTouchTracking();
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
for (int i = 0; i < Input.touchCount; i++)
|
||||
{
|
||||
UnityTouch touch = Input.GetTouch(i);
|
||||
if (touch.phase == TouchPhaseLegacy.Ended || touch.phase == TouchPhaseLegacy.Canceled)
|
||||
continue;
|
||||
|
||||
if (activeTouchId == -1)
|
||||
{
|
||||
activeTouchId = touch.fingerId;
|
||||
lastTouchPosition = touch.position;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (activeTouchId != touch.fingerId)
|
||||
continue;
|
||||
|
||||
Vector2 delta = touch.deltaPosition * touchSensitivity * Time.unscaledDeltaTime;
|
||||
lastTouchPosition = touch.position;
|
||||
return delta;
|
||||
}
|
||||
|
||||
return Vector2.zero;
|
||||
}
|
||||
|
||||
private void ResetTouchTracking()
|
||||
{
|
||||
activeTouchId = -1;
|
||||
lastTouchPosition = Vector2.zero;
|
||||
}
|
||||
|
||||
public void ResetLookDelta()
|
||||
{
|
||||
currentDelta = Vector2.zero;
|
||||
smoothVelocity = Vector2.zero;
|
||||
}
|
||||
private static bool AnyTouchPressed(ReadOnlyArray<TouchControl> touches)
|
||||
{
|
||||
for (int i = 0; i < touches.Count; ++i)
|
||||
{
|
||||
if (touches[i].press.isPressed)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Combat/LookInputRouter.cs.meta
Normal file
2
Assets/Scripts/Combat/LookInputRouter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fad288b4c26d28643a108700d669d6d5
|
||||
Reference in New Issue
Block a user