using UnityEngine; using DG.Tweening; /// /// Визуальная "живость" рук от первого лица: свэй за движением мыши, покачивание /// при ходьбе и сочная отдача при выстреле через DOTween. /// Висит на FPV-Hands (активен только у владельца), поэтому проверки IsOwner не нужны. /// Всё считается как смещение поверх исходного localPosition/localRotation, /// а отдача пишется в отдельные поля — свэй/боб и DOTween не конфликтуют за трансформ. /// public class WeaponView : MonoBehaviour { [Header("Sway (свэй за мышью)")] [SerializeField] private float swayAmount = 2f; [SerializeField] private float swayMax = 6f; [SerializeField] private float swaySmooth = 8f; [Header("Bob (покачивание при ходьбе)")] [SerializeField] private float bobSpeed = 9f; [SerializeField] private float bobAmount = 0.03f; [SerializeField] private float bobSmooth = 10f; [Header("Recoil (отдача при выстреле)")] [SerializeField] private float recoilKickback = 0.12f; // назад по Z [SerializeField] private float recoilKickUp = 9f; // поворот ствола вверх (X) [SerializeField] private float recoilDuration = 0.22f; private Vector3 originPos; private Quaternion originRot; private Vector3 swayRotOffset; private Vector3 bobPosOffset; private Vector3 recoilPos; private Vector3 recoilRot; private float bobTimer; private Sequence recoilSequence; private void Awake() { originPos = transform.localPosition; originRot = transform.localRotation; } private void LateUpdate() { ApplySway(); ApplyBob(); transform.localPosition = originPos + bobPosOffset + recoilPos; transform.localRotation = originRot * Quaternion.Euler(swayRotOffset + recoilRot); } private void ApplySway() { float _mouseX = Input.GetAxis("Mouse X"); float _mouseY = Input.GetAxis("Mouse Y"); // руки "отстают" от поворота камеры float _targetPitch = Mathf.Clamp(-_mouseY * swayAmount, -swayMax, swayMax); float _targetYaw = Mathf.Clamp(-_mouseX * swayAmount, -swayMax, swayMax); Vector3 _target = new Vector3(_targetPitch, _targetYaw, 0f); swayRotOffset = Vector3.Lerp(swayRotOffset, _target, swaySmooth * Time.deltaTime); } private void ApplyBob() { float _moveMagnitude = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")).magnitude; Vector3 _target = Vector3.zero; if (_moveMagnitude > 0.1f) { bobTimer += Time.deltaTime * bobSpeed; float _x = Mathf.Cos(bobTimer) * bobAmount; float _y = Mathf.Abs(Mathf.Sin(bobTimer)) * bobAmount; _target = new Vector3(_x, _y, 0f); } else { bobTimer = 0f; } bobPosOffset = Vector3.Lerp(bobPosOffset, _target, bobSmooth * Time.deltaTime); } public void PlayShoot() { recoilSequence?.Kill(); recoilPos = Vector3.zero; recoilRot = Vector3.zero; float _kick = recoilDuration * 0.35f; float _return = recoilDuration * 0.65f; recoilSequence = DOTween.Sequence().SetTarget(this); // Позиция: резкий отскок назад, затем плавный возврат recoilSequence.Append(DOTween.To(() => recoilPos, _v => recoilPos = _v, new Vector3(0f, 0f, -recoilKickback), _kick).SetEase(Ease.OutQuad)); recoilSequence.Append(DOTween.To(() => recoilPos, _v => recoilPos = _v, Vector3.zero, _return).SetEase(Ease.OutBack)); // Поворот: ствол подбрасывает вверх и возвращает — параллельно позиции recoilSequence.Insert(0f, DOTween.To(() => recoilRot, _v => recoilRot = _v, new Vector3(-recoilKickUp, 0f, 0f), _kick).SetEase(Ease.OutQuad)); recoilSequence.Insert(_kick, DOTween.To(() => recoilRot, _v => recoilRot = _v, Vector3.zero, _return).SetEase(Ease.OutBack)); } private void OnDisable() { recoilSequence?.Kill(); recoilPos = Vector3.zero; recoilRot = Vector3.zero; } }