Загрузка данных
using UnityEngine;
[DisallowMultipleComponent]
public class PlayerMovement : MonoBehaviour
{
[Header("Main Paths (max 2)")]
[SerializeField] private Path mainPath1;
[SerializeField] private Path mainPath2;
[Header("Movement")]
[SerializeField] private float moveSpeed = 3.5f;
[SerializeField] private float rotateSpeed = 14f;
[Header("Switching (no teleport)")]
[SerializeField] private float switchBlendDuration = 0.15f;
[Header("Camera Follow")]
[SerializeField] private Camera cam;
[SerializeField] private float cameraSmoothTime = 0.12f;
[SerializeField] private float cameraRotateSpeed = 10f;
[SerializeField] private float cameraLookAhead = 0.5f;
[Header("Headbob")]
[SerializeField] private float bobFrequency = 8f;
[SerializeField] private float bobAmplitude = 0.03f;
[SerializeField] private float bobSwayAmplitude = 0.015f;
[Header("Animations")]
[SerializeField] private Animator animator;
[Header("Visuals")]
[SerializeField] private Transform meshRoot;
[SerializeField] private float meshRotateSpeed = 18f;
[Tooltip("0 or 180. If the character faces backwards, switch between 0 and 180.")]
[SerializeField] private float modelYawOffset = 180f;
[Tooltip("If true: when idle the mesh faces the camera (Y axis only).")]
[SerializeField] private bool idleFaceCamera = true;
[Header("FBX Offset Fix (IMPORTANT)")]
[Tooltip("A point that represents the TRUE character position (hips/pelvis or skinned mesh). Used for snapping to path so FBX pivot offsets won't teleport you.")]
[SerializeField] private Transform positionReference;
[Tooltip("Optional tweak if your reference point is slightly off (usually keep at 0,0,0).")]
[SerializeField] private Vector3 referenceOffset = Vector3.zero;
// Paths / state
private Path activePath;
private Path lastMainPath;
private float s;
// Door trigger state
private bool inDoorTrigger;
private Path doorPathInTrigger;
// Main<->Main trigger state
private bool inMainSwitchTrigger;
private Collider mainSwitchColliderIn;
// No-teleport blend
private bool blending;
private float blendT;
private Vector3 blendFromPos;
// Camera rig
private Transform camRig;
private Vector3 camRigVel;
private Vector3 camLocalBasePos;
private float bobTimer;
// Camera follow math
private Vector3 camOffsetWorld;
private Vector3 camOffsetLocal;
private Quaternion camRigRotOffset;
private Vector3 camFwdSmoothed;
// door-camera lock
private bool camLockedOnDoor;
private Quaternion camLockedPathRot;
// Animation state so we don't spam triggers
private bool wasMoving;
private void Start()
{
if (mainPath1 == null)
{
Debug.LogError("[PlayerMovement] mainPath1 missing.");
enabled = false;
return;
}
if (meshRoot == null) meshRoot = transform;
activePath = mainPath1;
lastMainPath = mainPath1;
// Use reference (mesh/hips) to decide where we are on the path
s = activePath.ClosestS(GetReferenceWorldPos());
// Snap root so that reference point lands on the path (fixes FBX pivot offsets)
SnapRootToPathAtS(s);
// After snap, start blending from current (already correct) pos
blending = false;
SetupCamera();
wasMoving = false;
SetAnimIdle();
}
private void Update()
{
HandleSwitches();
HandleMovement();
HandleCamera();
}
// === REFERENCE / OFFSET FIX ===
private Vector3 GetReferenceWorldPos()
{
if (positionReference != null) return positionReference.position + referenceOffset;
return transform.position;
}
private void SnapRootToPathAtS(float sValue)
{
if (activePath == null || activePath.TotalLength <= 0f) return;
Vector3 desiredRefPos = activePath.EvaluatePosition(sValue);
if (positionReference == null)
{
transform.position = desiredRefPos;
return;
}
Vector3 currentRefPos = positionReference.position + referenceOffset;
Vector3 delta = desiredRefPos - currentRefPos;
transform.position += delta;
}
private void SetupCamera()
{
if (cam == null) cam = Camera.main;
if (cam == null) return;
camRig = new GameObject("CameraRig_Runtime").transform;
camRig.position = cam.transform.position;
camRig.rotation = cam.transform.rotation;
cam.transform.SetParent(camRig, true);
camLocalBasePos = cam.transform.localPosition;
camOffsetWorld = camRig.position - transform.position;
Vector3 fwd0 = GetPathForward(activePath, s);
Quaternion pathRot0 = Quaternion.LookRotation(fwd0, Vector3.up);
camOffsetLocal = Quaternion.Inverse(pathRot0) * camOffsetWorld;
camRigRotOffset = Quaternion.Inverse(pathRot0) * camRig.rotation;
camFwdSmoothed = fwd0;
camLockedOnDoor = false;
}
// === INPUT HELPERS ===
private float GetMoveAxis(Path path)
{
if (path == null) return 0f;
float axis = 0f;
if (path.MoveAxis == PathMoveAxis.Horizontal)
{
if (Input.GetKey(KeyCode.A)) axis -= 1f;
if (Input.GetKey(KeyCode.D)) axis += 1f;
}
else
{
if (Input.GetKey(KeyCode.S)) axis -= 1f;
if (Input.GetKey(KeyCode.W)) axis += 1f;
}
return axis;
}
private bool OppositeAxisPressedDown(Path path)
{
if (path == null) return false;
if (path.MoveAxis == PathMoveAxis.Horizontal)
return Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.S);
else
return Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.D);
}
// === SWITCHING LOGIC ===
private void HandleSwitches()
{
if (activePath == null) return;
if (!activePath.isDoorPath)
{
if (inDoorTrigger && doorPathInTrigger != null && OppositeAxisPressedDown(activePath))
{
lastMainPath = activePath;
SwitchActivePath(doorPathInTrigger);
return;
}
if (inMainSwitchTrigger && mainSwitchColliderIn != null)
{
if (activePath.MainSwitchTrigger != null && activePath.MainSwitchTrigger == mainSwitchColliderIn)
{
Path target = activePath.OtherMainPath;
if (target != null && OppositeAxisPressedDown(activePath))
{
SwitchActivePath(target);
return;
}
}
}
}
else
{
if (inDoorTrigger && doorPathInTrigger == activePath && OppositeAxisPressedDown(activePath))
{
Path back = lastMainPath != null ? lastMainPath : mainPath1;
SwitchActivePath(back);
return;
}
}
}
private void SwitchActivePath(Path newPath)
{
if (newPath == null) return;
if (activePath == newPath) return;
bool switchingToDoor = newPath.isDoorPath;
if (switchingToDoor)
{
camLockedOnDoor = true;
Vector3 fwd = camFwdSmoothed.sqrMagnitude < 0.0001f
? transform.forward
: camFwdSmoothed.normalized;
camLockedPathRot = Quaternion.LookRotation(fwd, Vector3.up);
}
else
{
camLockedOnDoor = false;
}
activePath = newPath;
if (!activePath.isDoorPath)
lastMainPath = activePath;
// IMPORTANT: use reference point for ClosestS
s = activePath.ClosestS(GetReferenceWorldPos());
if (!switchingToDoor && activePath != null && activePath.TotalLength > 0f)
{
camFwdSmoothed = GetPathForward(activePath, s);
}
BeginBlend();
}
private void BeginBlend()
{
blending = true;
blendT = 0f;
blendFromPos = transform.position;
}
// === MOVEMENT ===
private void HandleMovement()
{
if (activePath == null || activePath.TotalLength <= 0f) return;
float axis = GetMoveAxis(activePath);
bool moving = Mathf.Abs(axis) > 0.001f;
// --- Animations (trigger only when state changes) ---
if (animator != null && moving != wasMoving)
{
if (moving) SetAnimWalk();
else SetAnimIdle();
wasMoving = moving;
}
// --- Move along spline ---
s = Mathf.Clamp(s + axis * moveSpeed * Time.deltaTime, 0f, activePath.TotalLength);
Vector3 desiredRefPos = activePath.EvaluatePosition(s);
Vector3 pathPosForRoot;
if (positionReference != null)
{
Vector3 currentRefPos = positionReference.position + referenceOffset;
Vector3 delta = desiredRefPos - currentRefPos;
pathPosForRoot = transform.position + delta;
}
else
{
pathPosForRoot = desiredRefPos;
}
if (blending)
{
blendT += Time.deltaTime / Mathf.Max(0.0001f, switchBlendDuration);
float t = Mathf.Clamp01(blendT);
transform.position = Vector3.Lerp(blendFromPos, pathPosForRoot, t);
if (t >= 1f) blending = false;
}
else
{
transform.position = pathPosForRoot;
}
// --- Root rotation (movement direction) ---
if (moving)
{
Vector3 tan = activePath.EvaluateTangent(s);
if (axis < 0f) tan = -tan;
Vector3 flat = new Vector3(tan.x, 0f, tan.z);
if (flat.sqrMagnitude < 0.0001f) flat = tan;
Quaternion targetRot = Quaternion.LookRotation(flat.normalized, Vector3.up);
// FIX: apply model offset so it doesn't walk backwards
targetRot *= Quaternion.Euler(0f, modelYawOffset, 0f);
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRot,
1f - Mathf.Exp(-rotateSpeed * Time.deltaTime)
);
}
// --- Mesh visual rotation rules ---
UpdateMeshFacing(axis, moving);
HandleHeadbob(moving);
}
private void UpdateMeshFacing(float axis, bool moving)
{
if (meshRoot == null) return;
float t = 1f - Mathf.Exp(-meshRotateSpeed * Time.deltaTime);
if (moving && activePath != null)
{
Vector3 tan = activePath.EvaluateTangent(s);
if (axis < 0f) tan = -tan;
Vector3 flat = new Vector3(tan.x, 0f, tan.z);
if (flat.sqrMagnitude < 0.0001f) return;
Quaternion meshTarget = Quaternion.LookRotation(flat.normalized, Vector3.up);
// FIX: apply model offset so mesh faces correct way
meshTarget *= Quaternion.Euler(0f, modelYawOffset, 0f);
meshRoot.rotation = Quaternion.Slerp(meshRoot.rotation, meshTarget, t);
return;
}
// IDLE: face TOWARDS the camera (Y axis only)
if (idleFaceCamera && cam != null)
{
Vector3 toCam = cam.transform.position - meshRoot.position; // towards camera
Vector3 flatToCam = new Vector3(toCam.x, 0f, toCam.z);
if (flatToCam.sqrMagnitude < 0.0001f) return;
Quaternion meshTarget = Quaternion.LookRotation(flatToCam.normalized, Vector3.up);
// same model offset
meshTarget *= Quaternion.Euler(0f, modelYawOffset, 0f);
meshRoot.rotation = Quaternion.Slerp(meshRoot.rotation, meshTarget, t);
}
}
private void SetAnimWalk()
{
animator.ResetTrigger("OnIdle");
animator.SetTrigger("OnWalk");
}
private void SetAnimIdle()
{
animator.ResetTrigger("OnWalk");
animator.SetTrigger("OnIdle");
}
// === CAMERA ===
private void HandleCamera()
{
if (camRig == null || cam == null) return;
Quaternion pathRot;
if (camLockedOnDoor)
{
pathRot = camLockedPathRot;
}
else
{
if (activePath == null || activePath.TotalLength <= 0f) return;
Vector3 targetFwd = GetPathForward(activePath, s);
float rotT = 1f - Mathf.Exp(-cameraRotateSpeed * Time.deltaTime);
camFwdSmoothed = Vector3.Slerp(camFwdSmoothed, targetFwd, rotT);
if (camFwdSmoothed.sqrMagnitude < 0.0001f) camFwdSmoothed = targetFwd;
pathRot = Quaternion.LookRotation(camFwdSmoothed.normalized, Vector3.up);
}
Vector3 desiredRigPos = transform.position + (pathRot * camOffsetLocal);
camRig.position = Vector3.SmoothDamp(
camRig.position,
desiredRigPos,
ref camRigVel,
cameraSmoothTime
);
Quaternion desiredRigRot = pathRot * camRigRotOffset;
float rotBlend = 1f - Mathf.Exp(-cameraRotateSpeed * Time.deltaTime);
camRig.rotation = Quaternion.Slerp(camRig.rotation, desiredRigRot, rotBlend);
}
private Vector3 GetPathForward(Path path, float sNow)
{
if (path == null || path.TotalLength <= 0f) return transform.forward;
float a = Mathf.Clamp(sNow - cameraLookAhead, 0f, path.TotalLength);
float b = Mathf.Clamp(sNow + cameraLookAhead, 0f, path.TotalLength);
Vector3 pa = path.EvaluatePosition(a);
Vector3 pb = path.EvaluatePosition(b);
Vector3 d = pb - pa;
Vector3 flat = new Vector3(d.x, 0f, d.z);
if (flat.sqrMagnitude < 0.0001f) flat = transform.forward;
return flat.normalized;
}
private void HandleHeadbob(bool moving)
{
if (cam == null) return;
if (moving)
{
bobTimer += Time.deltaTime * bobFrequency;
float bobY = Mathf.Sin(bobTimer) * bobAmplitude;
float bobX = Mathf.Cos(bobTimer * 0.5f) * bobSwayAmplitude;
cam.transform.localPosition = camLocalBasePos + new Vector3(bobX, bobY, 0f);
}
else
{
bobTimer = 0f;
cam.transform.localPosition = Vector3.Lerp(
cam.transform.localPosition,
camLocalBasePos,
1f - Mathf.Exp(-12f * Time.deltaTime)
);
}
}
// === TRIGGERS ===
private void OnTriggerEnter(Collider other)
{
Path p = other.GetComponentInParent<Path>();
if (p != null && p.isDoorPath)
{
inDoorTrigger = true;
doorPathInTrigger = p;
}
if (mainPath1 != null && mainPath1.MainSwitchTrigger == other)
{
inMainSwitchTrigger = true;
mainSwitchColliderIn = other;
}
else if (mainPath2 != null && mainPath2.MainSwitchTrigger == other)
{
inMainSwitchTrigger = true;
mainSwitchColliderIn = other;
}
}
private void OnTriggerExit(Collider other)
{
Path p = other.GetComponentInParent<Path>();
if (p != null && p == doorPathInTrigger)
{
inDoorTrigger = false;
doorPathInTrigger = null;
}
if (inMainSwitchTrigger && other == mainSwitchColliderIn)
{
inMainSwitchTrigger = false;
mainSwitchColliderIn = null;
}
}
}