371 lines
12 KiB
C#
371 lines
12 KiB
C#
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; // don’t 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.");
|
||
}
|
||
}
|