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; // don’t 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;
|
2025-09-16 17:51:02 +05:00
|
|
|
|
Debug.Log("isGrounded: " + IsGrounded());
|
2025-09-09 16:46:17 +05:00
|
|
|
|
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
|
|
|
|
}
|