MiniGames/Assets/Scripts/CrateEscape/CrateEscapePlayerControllerJoystick.cs
2025-09-16 17:51:02 +05:00

371 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(Rigidbody))]
public class CrateEscapePlayerControllerJoystick : MonoBehaviour
{
[Header("Joystick")]
public Joystick moveJoystick;
[Header("Movement")]
public float moveSpeed = 4f;
public float moveDamp = 12f; // smoothing for XZ
public float maxSpeedMultiplier = 3f; // full-stick multiplier
[Range(0f, 0.5f)] public float inputDeadZone = 0.1f;
[Tooltip("If true, stick is interpreted relative to the camera view.")]
public bool cameraRelative = true;
public Transform cameraTransform;
[Header("Movement Plane")]
[Tooltip("If OFF, uses Static Up as the movement plane normal. If ON, raycasts to read ground normal under the character.")]
public bool useDynamicGroundNormal = false;
[Tooltip("Used when useDynamicGroundNormal = false. Set this to your level's 'up' direction (the plane normal).")]
public Vector3 staticUp = Vector3.up;
[Tooltip("Layer mask for reading ground normal when dynamic is ON.")]
public LayerMask groundForNormalMask = ~0;
[Tooltip("Ray length for sampling ground normal beneath groundCheck.")]
public float groundNormalRayLength = 3f;
[Header("Facing")]
public bool faceMoveDirection = true;
public float turnSpeed = 540f; // deg/sec
[Header("Grounding")]
public LayerMask groundMask = ~0;
public float groundCheckRadius = 0.2f;
public Transform groundCheck;
[Header("Jump")]
public float jumpForce = 7f; // upward impulse
public bool allowAirJump = false;
public Button jumpButton;
[Header("Optional Animator Driver")]
public ZibuAnimDriver anim;
[Header("Input Locks")]
public float initialLockSeconds = 0f; // keep 0 while testing
public bool useUnscaledForInitialLock = false;
public bool autoLockOnGameOver = true;
[Header("Debug / Safety")]
public bool debugLogs = true; // show values in console
public bool drawMoveRay = true; // shows a cyan ray for move direction
public bool keyboardFallbackInEditor = true; // WASD/Arrow fallback when joystick is null or zero
public bool ignoreGameOverLock = false; // bypass GameManager lock (testing)
// --- runtime ---
Rigidbody _rb;
Vector3 _velXZ; // horizontal velocity (XZ only)
Vector3 _moveDirWorld; // desired move dir on movement plane
float _stickMag; // 0..1
bool _inputEnabled = false; // true when controls active
bool _jumpQueued;
bool _usedAirJump;
void Awake()
{
_rb = GetComponent<Rigidbody>();
_rb.useGravity = true;
_rb.isKinematic = false;
// Lock only tilt, never lock Position X/Y/Z here
_rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;
// Good physics defaults
if (_rb.mass <= 0f) _rb.mass = 1f;
_rb.drag = 0f; // dont damp velocity away
_rb.angularDrag = 0.05f;
_rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
if (!groundCheck) groundCheck = transform;
if (!anim) anim = GetComponent<ZibuAnimDriver>();
if (!cameraTransform && Camera.main) cameraTransform = Camera.main.transform;
}
void OnValidate()
{
if (!cameraTransform && Camera.main) cameraTransform = Camera.main.transform;
if (!groundCheck) groundCheck = transform;
}
void OnEnable()
{
_inputEnabled = false;
StopAllCoroutines();
StartCoroutine(EnableAfterDelay(initialLockSeconds, useUnscaledForInitialLock));
if (jumpButton) jumpButton.onClick.AddListener(OnJumpUIButton);
}
void OnDisable()
{
if (jumpButton) jumpButton.onClick.RemoveListener(OnJumpUIButton);
}
System.Collections.IEnumerator EnableAfterDelay(float seconds, bool unscaled)
{
if (seconds > 0f)
{
if (unscaled) yield return new WaitForSecondsRealtime(seconds);
else yield return new WaitForSeconds(seconds);
}
_inputEnabled = true;
if (debugLogs) Debug.Log($"[CrateEscape] Controls ENABLED (delay={seconds}) on {name}");
}
void Update()
{
bool blocked = IsBlocked();
if (!blocked)
{
ReadMoveInput();
}
else
{
_stickMag = 0f;
_moveDirWorld = Vector3.zero;
}
// animator hooks
if (anim)
{
float speedUnits = Mathf.Lerp(0f, maxSpeedMultiplier, _stickMag);
anim.SetGrounded(IsGrounded());
anim.SetSpeed(speedUnits);
}
// Reset air-jump on ground contact
if (IsGrounded()) _usedAirJump = false;
// Debug ray
if (drawMoveRay && _moveDirWorld.sqrMagnitude > 0.0001f)
Debug.DrawRay(transform.position + Vector3.up * 0.1f, _moveDirWorld.normalized, Color.cyan, 0f, false);
if (debugLogs)
{
if (Time.frameCount % 30 == 0)
{
Debug.Log($"[CrateEscape] {name} inputEnabled={_inputEnabled} blocked={blocked} stickMag={_stickMag:F2} dir={_moveDirWorld}");
}
}
}
void FixedUpdate()
{
bool blocked = IsBlocked();
Vector3 targetVelXZ = Vector3.zero;
if (!blocked && _stickMag > inputDeadZone)
{
Vector3 dir = _moveDirWorld.normalized;
float targetSpeed = moveSpeed * Mathf.Lerp(0f, maxSpeedMultiplier, _stickMag);
targetVelXZ = dir * targetSpeed;
if (faceMoveDirection && dir.sqrMagnitude > 0.0001f)
{
Quaternion targetRot = Quaternion.LookRotation(dir, Vector3.up);
_rb.MoveRotation(Quaternion.RotateTowards(_rb.rotation, targetRot, turnSpeed * Time.fixedDeltaTime));
}
}
if (blocked)
{
// Stop horizontal movement but preserve Y (gravity / jump settle)
_velXZ = Vector3.zero;
_rb.velocity = new Vector3(0f, _rb.velocity.y, 0f);
_jumpQueued = false;
return;
}
// Smooth horizontal velocity (keep physics-driven Y)
_velXZ = Vector3.Lerp(_velXZ, targetVelXZ, 1f - Mathf.Exp(-moveDamp * Time.fixedDeltaTime));
// Apply horizontal velocity while preserving current Y
_rb.velocity = new Vector3(_velXZ.x, _rb.velocity.y, _velXZ.z);
// Handle jump in physics step
if (_jumpQueued)
{
_jumpQueued = false;
DoJump();
}
}
// ---------- Input ----------
void ReadMoveInput()
{
Vector2 stick = Vector2.zero;
// Prefer joystick if present and non-zero
if (moveJoystick)
{
stick = new Vector2(moveJoystick.Horizontal, moveJoystick.Vertical);
}
#if UNITY_EDITOR
// Optional keyboard fallback (Editor/testing)
if (keyboardFallbackInEditor)
{
if (stick.sqrMagnitude <= 0.0001f)
{
float h = Input.GetAxisRaw("Horizontal"); // A/D or Left/Right
float v = Input.GetAxisRaw("Vertical"); // W/S or Up/Down
if (Mathf.Abs(h) > 0.01f || Mathf.Abs(v) > 0.01f)
stick = new Vector2(h, v);
}
}
#endif
_stickMag = Mathf.Clamp01(stick.magnitude);
if (_stickMag <= inputDeadZone)
{
_moveDirWorld = Vector3.zero;
return;
}
Vector2 n = stick.normalized;
if (cameraRelative && cameraTransform)
{
// Plane-aware camera relative vectors
Vector3 planeUp = GetMovementUp();
// Project camera forward onto the plane
Vector3 camF = Vector3.ProjectOnPlane(cameraTransform.forward, planeUp);
if (camF.sqrMagnitude < 0.0001f)
camF = Vector3.ProjectOnPlane(cameraTransform.up, planeUp);
camF.Normalize();
// Right is perpendicular on the plane
Vector3 camR = Vector3.Cross(planeUp, camF).normalized;
// Re-orthonormalize camF to be safe
camF = Vector3.Cross(camR, planeUp).normalized;
_moveDirWorld = camR * n.x + camF * n.y;
}
else
{
// World-relative, but still constrained to the plane
Vector3 planeUp = GetMovementUp();
Vector3 worldX = Vector3.ProjectOnPlane(Vector3.right, planeUp).normalized;
Vector3 worldZ = Vector3.ProjectOnPlane(Vector3.forward, planeUp).normalized;
_moveDirWorld = (worldX * n.x + worldZ * n.y);
}
}
Vector3 GetMovementUp()
{
if (useDynamicGroundNormal && groundCheck)
{
// Cast along character's local down to find the surface normal
Ray ray = new Ray(groundCheck.position + Vector3.up * 0.05f, -transform.up);
if (Physics.Raycast(ray, out var hit, groundNormalRayLength, groundForNormalMask, QueryTriggerInteraction.Ignore))
{
return hit.normal.normalized;
}
}
// Fallback to static up
return staticUp.sqrMagnitude > 0.0001f ? staticUp.normalized : Vector3.up;
}
void DoJump()
{
bool grounded = IsGrounded();
if (!grounded)
{
if (!(allowAirJump && !_usedAirJump)) return;
_usedAirJump = true;
}
// Reset vertical velocity then apply impulse for consistent jumps
Vector3 v = _rb.velocity;
v.y = 0f;
_rb.velocity = v;
_rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
if (anim) anim.PlayAnimation(AnimationState.JumpStart);
if (debugLogs) Debug.Log($"[CrateEscape] {name} JUMP (grounded={grounded}, airUsed={_usedAirJump})");
}
// ---------- Ground / Gizmos ----------
bool IsGrounded()
{
return Physics.CheckSphere(groundCheck.position, groundCheckRadius, groundMask, QueryTriggerInteraction.Ignore);
}
void OnDrawGizmosSelected()
{
if (!groundCheck) groundCheck = transform;
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
// ---------- Public API ----------
public void LockControls(bool locked)
{
_inputEnabled = !locked;
if (locked)
{
_stickMag = 0f;
_moveDirWorld = Vector3.zero;
_velXZ = Vector3.zero;
if (_rb) _rb.velocity = new Vector3(0f, _rb.velocity.y, 0f);
if (anim) { anim.SetSpeed(0f); }
}
}
public void EnableControlsNow()
{
StopAllCoroutines();
_inputEnabled = true;
if (debugLogs) Debug.Log($"[CrateEscape] Controls ENABLED by call on {name}");
}
// ---------- Helpers ----------
bool IsBlocked()
{
if (!_inputEnabled) return true;
if (!ignoreGameOverLock && autoLockOnGameOver)
{
var gm = CrateEscapeGameManager.Instance;
if (gm != null && gm.isGameOver) return true; // freeze after game over
}
return false;
}
public void SetJoystick(Joystick j) => moveJoystick = j;
// ---------- UI Button Hook ----------
/// <summary>Call this from a UI Button OnClick() to make the player jump.</summary>
public void OnJumpUIButton()
{
if (IsBlocked()) return;
Debug.Log("isGrounded: " + IsGrounded());
if (IsGrounded() || (allowAirJump && !_usedAirJump))
_jumpQueued = true;
}
// ---------- Inspector Utilities ----------
[ContextMenu("Test/Nudge Forward")]
void NudgeForward()
{
// Small forward push to verify movement pipeline
_rb.velocity = new Vector3(2f, _rb.velocity.y, 0f);
if (debugLogs) Debug.Log($"[CrateEscape] {name} NudgeForward called.");
}
}