chunk 1: core gameplay scripts scenes runtime assets

This commit is contained in:
2026-04-06 11:02:34 +03:00
parent fa0388bc79
commit 0d11a097d8
703 changed files with 2292651 additions and 0 deletions

View 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
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4f78091c360e2a7b3bd3c0842f414869

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1fe8e63d70dcf637cb383aa5edf263ce

View 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;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fad288b4c26d28643a108700d669d6d5