MiniGames/Assets/Scripts/CrateEscape/CrateEscapePlayerControllerJoystick.cs

371 lines
12 KiB
C#
Raw Normal View History

2025-09-09 16:46:17 +05:00
using UnityEngine;
using UnityEngine.UI;
2025-08-14 20:29:09 +05:00
[RequireComponent(typeof(Rigidbody))]
public class CrateEscapePlayerControllerJoystick : MonoBehaviour
{
[Header("Joystick")]
2025-09-09 16:46:17 +05:00
public Joystick moveJoystick;
2025-08-14 20:29:09 +05:00
[Header("Movement")]
2025-09-09 16:46:17 +05:00
public float moveSpeed = 4f;
public float moveDamp = 12f; // smoothing for XZ
public float maxSpeedMultiplier = 3f; // full-stick multiplier
2025-08-14 20:29:09 +05:00
[Range(0f, 0.5f)] public float inputDeadZone = 0.1f;
[Tooltip("If true, stick is interpreted relative to the camera view.")]
public bool cameraRelative = true;
2025-09-09 16:46:17 +05:00
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;
2025-08-14 20:29:09 +05:00
[Header("Facing")]
2025-09-09 16:46:17 +05:00
public bool faceMoveDirection = true;
public float turnSpeed = 540f; // deg/sec
2025-08-14 20:29:09 +05:00
[Header("Grounding")]
public LayerMask groundMask = ~0;
public float groundCheckRadius = 0.2f;
public Transform groundCheck;
2025-09-09 16:46:17 +05:00
[Header("Jump")]
public float jumpForce = 7f; // upward impulse
public bool allowAirJump = false;
public Button jumpButton;
2025-08-14 20:29:09 +05:00
[Header("Optional Animator Driver")]
public ZibuAnimDriver anim;
[Header("Input Locks")]
2025-09-09 16:46:17 +05:00
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)
2025-08-14 20:29:09 +05:00
// --- runtime ---
Rigidbody _rb;
2025-09-09 16:46:17 +05:00
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;
2025-08-14 20:29:09 +05:00
void Awake()
{
_rb = GetComponent<Rigidbody>();
2025-09-09 16:46:17 +05:00
_rb.useGravity = true;
_rb.isKinematic = false;
// Lock only tilt, never lock Position X/Y/Z here
2025-08-14 20:29:09 +05:00
_rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;
2025-09-09 16:46:17 +05:00
// 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;
2025-08-14 20:29:09 +05:00
if (!groundCheck) groundCheck = transform;
if (!anim) anim = GetComponent<ZibuAnimDriver>();
if (!cameraTransform && Camera.main) cameraTransform = Camera.main.transform;
}
2025-09-09 16:46:17 +05:00
void OnValidate()
{
if (!cameraTransform && Camera.main) cameraTransform = Camera.main.transform;
if (!groundCheck) groundCheck = transform;
}
2025-08-14 20:29:09 +05:00
void OnEnable()
{
_inputEnabled = false;
StopAllCoroutines();
StartCoroutine(EnableAfterDelay(initialLockSeconds, useUnscaledForInitialLock));
2025-09-09 16:46:17 +05:00
if (jumpButton) jumpButton.onClick.AddListener(OnJumpUIButton);
}
void OnDisable()
{
if (jumpButton) jumpButton.onClick.RemoveListener(OnJumpUIButton);
2025-08-14 20:29:09 +05:00
}
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;
2025-09-09 16:46:17 +05:00
if (debugLogs) Debug.Log($"[CrateEscape] Controls ENABLED (delay={seconds}) on {name}");
2025-08-14 20:29:09 +05:00
}
void Update()
{
bool blocked = IsBlocked();
if (!blocked)
2025-09-09 16:46:17 +05:00
{
ReadMoveInput();
}
2025-08-14 20:29:09 +05:00
else
{
_stickMag = 0f;
_moveDirWorld = Vector3.zero;
}
2025-09-09 16:46:17 +05:00
// animator hooks
2025-08-14 20:29:09 +05:00
if (anim)
{
float speedUnits = Mathf.Lerp(0f, maxSpeedMultiplier, _stickMag);
anim.SetGrounded(IsGrounded());
anim.SetSpeed(speedUnits);
}
2025-09-09 16:46:17 +05:00
// 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}");
}
}
2025-08-14 20:29:09 +05:00
}
void FixedUpdate()
{
bool blocked = IsBlocked();
2025-09-09 16:46:17 +05:00
Vector3 targetVelXZ = Vector3.zero;
2025-08-14 20:29:09 +05:00
if (!blocked && _stickMag > inputDeadZone)
{
Vector3 dir = _moveDirWorld.normalized;
float targetSpeed = moveSpeed * Mathf.Lerp(0f, maxSpeedMultiplier, _stickMag);
2025-09-09 16:46:17 +05:00
targetVelXZ = dir * targetSpeed;
2025-08-14 20:29:09 +05:00
2025-09-09 16:46:17 +05:00
if (faceMoveDirection && dir.sqrMagnitude > 0.0001f)
2025-08-14 20:29:09 +05:00
{
Quaternion targetRot = Quaternion.LookRotation(dir, Vector3.up);
_rb.MoveRotation(Quaternion.RotateTowards(_rb.rotation, targetRot, turnSpeed * Time.fixedDeltaTime));
}
}
if (blocked)
{
2025-09-09 16:46:17 +05:00
// Stop horizontal movement but preserve Y (gravity / jump settle)
_velXZ = Vector3.zero;
_rb.velocity = new Vector3(0f, _rb.velocity.y, 0f);
_jumpQueued = false;
return;
2025-08-14 20:29:09 +05:00
}
2025-09-09 16:46:17 +05:00
// 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();
}
2025-08-14 20:29:09 +05:00
}
2025-09-09 16:46:17 +05:00
// ---------- Input ----------
void ReadMoveInput()
2025-08-14 20:29:09 +05:00
{
2025-09-09 16:46:17 +05:00
Vector2 stick = Vector2.zero;
// Prefer joystick if present and non-zero
if (moveJoystick)
{
stick = new Vector2(moveJoystick.Horizontal, moveJoystick.Vertical);
}
2025-08-14 20:29:09 +05:00
2025-09-09 16:46:17 +05:00
#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
2025-08-14 20:29:09 +05:00
_stickMag = Mathf.Clamp01(stick.magnitude);
2025-09-09 16:46:17 +05:00
if (_stickMag <= inputDeadZone)
{
_moveDirWorld = Vector3.zero;
return;
}
2025-08-14 20:29:09 +05:00
Vector2 n = stick.normalized;
if (cameraRelative && cameraTransform)
{
2025-09-09 16:46:17 +05:00
// 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;
2025-08-14 20:29:09 +05:00
_moveDirWorld = camR * n.x + camF * n.y;
}
else
{
2025-09-09 16:46:17 +05:00
// 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;
2025-08-14 20:29:09 +05:00
}
2025-09-09 16:46:17 +05:00
// 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})");
2025-08-14 20:29:09 +05:00
}
2025-09-09 16:46:17 +05:00
// ---------- Ground / Gizmos ----------
2025-08-14 20:29:09 +05:00
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;
2025-09-09 16:46:17 +05:00
_velXZ = Vector3.zero;
if (_rb) _rb.velocity = new Vector3(0f, _rb.velocity.y, 0f);
2025-08-14 20:29:09 +05:00
if (anim) { anim.SetSpeed(0f); }
}
}
public void EnableControlsNow()
{
StopAllCoroutines();
_inputEnabled = true;
2025-09-09 16:46:17 +05:00
if (debugLogs) Debug.Log($"[CrateEscape] Controls ENABLED by call on {name}");
2025-08-14 20:29:09 +05:00
}
// ---------- Helpers ----------
2025-09-09 16:46:17 +05:00
2025-08-14 20:29:09 +05:00
bool IsBlocked()
{
if (!_inputEnabled) return true;
2025-09-09 16:46:17 +05:00
if (!ignoreGameOverLock && autoLockOnGameOver)
2025-08-14 20:29:09 +05:00
{
var gm = CrateEscapeGameManager.Instance;
if (gm != null && gm.isGameOver) return true; // freeze after game over
}
return false;
}
public void SetJoystick(Joystick j) => moveJoystick = j;
2025-09-09 16:46:17 +05:00
// ---------- UI Button Hook ----------
/// <summary>Call this from a UI Button OnClick() to make the player jump.</summary>
public void OnJumpUIButton()
{
if (IsBlocked()) return;
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.");
}
2025-08-14 20:29:09 +05:00
}