Crate Escape Completed

This commit is contained in:
Ali Sharoz 2025-09-09 16:46:17 +05:00
parent fc8f230187
commit 885657b910
11 changed files with 1223 additions and 220 deletions

View File

@ -2,13 +2,18 @@
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 6
serializedVersion: 8
m_ObjectHideFlags: 0
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: em_clenchteeth2
m_Shader: {fileID: 211, guid: 0000000000000000f000000000000000, type: 0}
m_ShaderKeywords: _ALPHABLEND_ON
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _ALPHABLEND_ON
m_InvalidKeywords: []
m_LightmapFlags: 0
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
@ -16,7 +21,8 @@ Material:
stringTagMap:
RenderType: Transparent
disabledShaderPasses:
- ALWAYS
- GRABPASS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
@ -32,6 +38,7 @@ Material:
m_Texture: {fileID: 2800000, guid: deb28cb372e1ee54fad618f6df43247f, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BlendOp: 0
- _BumpScale: 1
@ -63,3 +70,4 @@ Material:
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _SoftParticleFadeParams: {r: 0, g: 0, b: 0, a: 0}
- _TintColor: {r: 0.5, g: 0.5, b: 0.5, a: 0.5}
m_BuildTextureStacks: []

View File

@ -580,6 +580,32 @@ AnimatorStateTransition:
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1102 &-3331804382887163694
AnimatorState:
serializedVersion: 6
m_ObjectHideFlags: 1
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Celebration
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 3060610079319413300, guid: f4cc7d9b87c59db4b83522e99e842364, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1101 &-2923408276037091438
AnimatorStateTransition:
m_ObjectHideFlags: 1
@ -956,6 +982,9 @@ AnimatorStateMachine:
- serializedVersion: 1
m_State: {fileID: 7977088065826294681}
m_Position: {x: 1635, y: 2665, z: 0}
- serializedVersion: 1
m_State: {fileID: -3331804382887163694}
m_Position: {x: 420, y: 400, z: 0}
m_ChildStateMachines:
- serializedVersion: 1
m_StateMachine: {fileID: 7036095546467220257}

File diff suppressed because it is too large Load Diff

View File

@ -1,64 +1,106 @@
using UnityEngine;
using UnityEngine;
using UnityEngine.UI;
// Joystick Pack (Asset Store)
// Drag any Joystick (Fixed/Floating/Dynamic/Variable) into moveJoystick
[RequireComponent(typeof(Rigidbody))]
public class CrateEscapePlayerControllerJoystick : MonoBehaviour
{
[Header("Joystick")]
public Joystick moveJoystick; // assign from Joystick Pack
public Joystick moveJoystick;
[Header("Movement")]
public float moveSpeed = 4f; // base speed
public float moveDamp = 12f; // velocity smoothing
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; // auto-fills with Camera.main if null
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; // rotate toward move direction
public float turnSpeed = 540f; // deg/sec rotation speed
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 = 4f; // joystick disabled for first N seconds
public bool useUnscaledForInitialLock = false; // set true if you pause time at start
public bool autoLockOnGameOver = true; // auto-freeze if GameManager reports isGameOver
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 _vel; // smoothed velocity
Vector3 _moveDirWorld; // desired movement direction in world space
Vector3 _velXZ; // horizontal velocity (XZ only)
Vector3 _moveDirWorld; // desired move dir on movement plane
float _stickMag; // 0..1
bool _inputEnabled = false; // starts locked; enabled after initialLockSeconds
bool _inputEnabled = false; // true when controls active
bool _jumpQueued;
bool _usedAirJump;
void Awake()
{
_rb = GetComponent<Rigidbody>();
// keep physics stable
_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()
{
// start locked, then enable after delay
_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)
@ -69,6 +111,7 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
else yield return new WaitForSeconds(seconds);
}
_inputEnabled = true;
if (debugLogs) Debug.Log($"[CrateEscape] Controls ENABLED (delay={seconds}) on {name}");
}
void Update()
@ -76,35 +119,52 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
bool blocked = IsBlocked();
if (!blocked)
ReadJoystick();
{
ReadMoveInput();
}
else
{
// when blocked, zero input so the player stops + anim goes idle
_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 targetVel = Vector3.zero;
Vector3 targetVelXZ = Vector3.zero;
if (!blocked && _stickMag > inputDeadZone)
{
Vector3 dir = _moveDirWorld.normalized;
float targetSpeed = moveSpeed * Mathf.Lerp(0f, maxSpeedMultiplier, _stickMag);
targetVel = dir * targetSpeed;
targetVelXZ = dir * targetSpeed;
if (faceMoveDirection)
if (faceMoveDirection && dir.sqrMagnitude > 0.0001f)
{
Quaternion targetRot = Quaternion.LookRotation(dir, Vector3.up);
_rb.MoveRotation(Quaternion.RotateTowards(_rb.rotation, targetRot, turnSpeed * Time.fixedDeltaTime));
@ -113,47 +173,131 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
if (blocked)
{
// hard-stop while locked
_vel = Vector3.zero;
_rb.velocity = Vector3.zero;
return; // don't MovePosition this frame
// 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 velocity and move
_vel = Vector3.Lerp(_vel, targetVel, 1f - Mathf.Exp(-moveDamp * Time.fixedDeltaTime));
_rb.MovePosition(_rb.position + _vel * Time.fixedDeltaTime);
}
// Smooth horizontal velocity (keep physics-driven Y)
_velXZ = Vector3.Lerp(_velXZ, targetVelXZ, 1f - Mathf.Exp(-moveDamp * Time.fixedDeltaTime));
void ReadJoystick()
// 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)
{
if (!moveJoystick) { _stickMag = 0f; _moveDirWorld = Vector3.zero; return; }
_jumpQueued = false;
DoJump();
}
}
float h = moveJoystick.Horizontal; // -1..1
float v = moveJoystick.Vertical; // -1..1
// ---------- 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
Vector2 stick = new Vector2(h, v);
_stickMag = Mathf.Clamp01(stick.magnitude);
if (_stickMag <= inputDeadZone) { _moveDirWorld = Vector3.zero; return; }
if (_stickMag <= inputDeadZone)
{
_moveDirWorld = Vector3.zero;
return;
}
Vector2 n = stick.normalized;
if (cameraRelative && cameraTransform)
{
// Project camera forward to XZ and build a right vector
Vector3 camF = cameraTransform.forward; camF.y = 0f;
camF = camF.sqrMagnitude > 0.0001f ? camF.normalized : Vector3.forward;
Vector3 camR = new Vector3(camF.z, 0f, -camF.x);
// 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 (X/Z)
_moveDirWorld = new Vector3(n.x, 0f, n.y);
// 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);
@ -168,7 +312,6 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
// ---------- Public API ----------
/// <summary>Enable/disable all player input & movement immediately.</summary>
public void LockControls(bool locked)
{
_inputEnabled = !locked;
@ -176,25 +319,26 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
{
_stickMag = 0f;
_moveDirWorld = Vector3.zero;
_vel = Vector3.zero;
if (_rb) _rb.velocity = Vector3.zero;
_velXZ = Vector3.zero;
if (_rb) _rb.velocity = new Vector3(0f, _rb.velocity.y, 0f);
if (anim) { anim.SetSpeed(0f); }
}
}
/// <summary>Enable controls now (e.g., if you want to skip the initial delay).</summary>
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 (autoLockOnGameOver)
if (!ignoreGameOverLock && autoLockOnGameOver)
{
var gm = CrateEscapeGameManager.Instance;
if (gm != null && gm.isGameOver) return true; // freeze after game over
@ -203,6 +347,24 @@ public class CrateEscapePlayerControllerJoystick : MonoBehaviour
return false;
}
// For runtime hookup if you spawn the joystick UI
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;
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.");
}
}

View File

@ -4,11 +4,15 @@ using System.Collections;
[RequireComponent(typeof(LineRenderer))]
public class LaserBeam : MonoBehaviour
{
[Header("Control")]
[Tooltip("If true, this beam will NOT run its own timers or intro; it only responds to the controller.")]
public bool externalControl = true;
[Header("Laser Settings")]
public float maxDistance = 20f;
public LayerMask collisionMask = ~0;
public float chargeDuration = 3f;
public float fireDuration = 1f;
public float chargeDuration = 3f; // used only if externalControl == false
public float fireDuration = 1f; // used only if externalControl == false
[Header("Laser Appearance")]
public Color laserColor = Color.red;
@ -22,7 +26,7 @@ public class LaserBeam : MonoBehaviour
public float hitRadius = 0.1f;
public QueryTriggerInteraction queryTriggerMode = QueryTriggerInteraction.Collide;
[Header("Intro Show")]
[Header("Intro Show (ignored if externalControl)")]
public bool showIntro = true;
public float initialShowDuration = 3f;
public bool introIsDeadly = true;
@ -59,16 +63,30 @@ public class LaserBeam : MonoBehaviour
void Awake()
{
SetupLaserRenderer();
// Always start safe
currentPhase = LaserPhase.Idle;
hasTriggeredDeathThisBurst = false;
DisableLaser();
StopAllCoroutines();
}
void Start()
{
if (showIntro)
// Only self-run if not externally controlled
if (!externalControl && showIntro)
StartCoroutine(IntroShow());
}
void Update()
{
if (externalControl)
{
// Passive: only draw debug line if visible
if (debugDraw) Debug.DrawLine(laserStart, laserEnd, Color.cyan);
return;
}
// Legacy self-FSM (optional use)
if (introRunning) return;
timer += Time.deltaTime;
@ -113,8 +131,19 @@ public class LaserBeam : MonoBehaviour
Debug.DrawLine(laserStart, laserEnd, Color.cyan);
}
// LaserBeam.cs — replace SetLaserPhase with this version
public void SetLaserPhase(LaserPhase phase)
{
// If were externally controlled, cancel any internal coroutines immediately
if (externalControl)
{
StopAllCoroutines();
introRunning = false;
}
if (!line) SetupLaserRenderer(); // make sure 'line' exists even if GO was inactive
if (phase != currentPhase) hasTriggeredDeathThisBurst = false;
currentPhase = phase;
switch (phase)
@ -131,6 +160,7 @@ public class LaserBeam : MonoBehaviour
case LaserPhase.Firing:
UpdateLaserPath();
line.enabled = true;
SetLineColor(laserColor);
hasTriggeredDeathThisBurst = false;
CheckHit();
@ -140,21 +170,17 @@ public class LaserBeam : MonoBehaviour
public void TickLaserDuringFiring()
{
if (currentPhase == LaserPhase.Firing)
{
if (currentPhase != LaserPhase.Firing) return;
UpdateLaserPath();
CheckHit();
}
}
public void TickLaserDuringCharging()
{
if (currentPhase == LaserPhase.Charging)
{
if (currentPhase != LaserPhase.Charging) return;
BlinkWarning();
UpdateLaserPath();
}
}
IEnumerator IntroShow()
{
@ -183,6 +209,9 @@ public class LaserBeam : MonoBehaviour
void DisableLaser()
{
line.enabled = false;
// Also clear endpoint for safety/debug
laserStart = transform.position;
laserEnd = laserStart;
}
void BlinkWarning()
@ -194,6 +223,13 @@ public class LaserBeam : MonoBehaviour
void CheckHit()
{
// HARD GATES: never kill unless were allowed to
if (!isActiveAndEnabled) return;
if (!gameObject.activeInHierarchy) return;
bool lethalNow = (currentPhase == LaserPhase.Firing) || (introRunning && introIsDeadly);
if (!lethalNow) return;
if (hasTriggeredDeathThisBurst) return;
bool CheckSegment(Vector3 a, Vector3 b)
@ -224,7 +260,7 @@ public class LaserBeam : MonoBehaviour
return true;
}
if (bestCol.CompareTag(boxTag))
if (bestCol && bestCol.CompareTag(boxTag))
{
var boxHealth = bestCol.GetComponent<LaserBoxHealth>();
if (boxHealth != null)

View File

@ -5,112 +5,133 @@ using static LaserBeam;
public class LaserBeamController : MonoBehaviour
{
public float interval = 20f;
public float chargeDuration = 3f;
public float fireDuration = 1f;
[Header("Cycle Durations")]
public float interval = 20f; // how long each batch runs before switching
public float idleBeforeCharge = 3f; // Idle time before Charging
public float chargingWindup = 1f; // Charging duration (blink)
public float fireDuration = 1f; // Firing duration (lethal)
private List<LaserBeam> allLasers = new List<LaserBeam>();
private List<List<LaserBeam>> batches = new List<List<LaserBeam>>();
private List<LaserBeam> activeLasers = new List<LaserBeam>();
[Header("Beams (auto-populated)")]
public List<LaserBeam> allLasers = new List<LaserBeam>();
private float timer = 0f;
private LaserPhase currentPhase = LaserPhase.Idle;
private readonly List<List<LaserBeam>> batches = new();
private readonly List<LaserBeam> activeLasers = new();
void Start()
{
// Use active-only; if you need inactive too, use FindObjectsOfType<LaserBeam>(true)
allLasers.AddRange(FindObjectsOfType<LaserBeam>());
foreach (var lb in allLasers)
{
if (!lb) continue;
lb.externalControl = true; // controller owns timing
lb.ResetCycle(); // Idle + line off
lb.enabled = false; // completely inert until selected
}
// Shuffle
for (int i = 0; i < allLasers.Count; i++)
{
var temp = allLasers[i];
int rand = Random.Range(i, allLasers.Count);
allLasers[i] = allLasers[rand];
allLasers[rand] = temp;
int r = Random.Range(i, allLasers.Count);
(allLasers[i], allLasers[r]) = (allLasers[r], allLasers[i]);
}
// Split into 3 batches
int batchSize = Mathf.CeilToInt(allLasers.Count / 3f);
int batchSize = Mathf.CeilToInt(Mathf.Max(1, allLasers.Count) / 3f);
for (int i = 0; i < allLasers.Count; i += batchSize)
{
batches.Add(allLasers.GetRange(i, Mathf.Min(batchSize, allLasers.Count - i)));
StartCoroutine(RunBatches());
}
IEnumerator RunBatches()
{
for (int bi = 0; bi < batches.Count; bi++)
{
// Disable everything first
foreach (var lb in allLasers)
lb.enabled = false;
if (lb) { lb.enabled = false; lb.ResetCycle(); }
StartCoroutine(ActivateBatches());
// Activate this batch
activeLasers.Clear();
activeLasers.AddRange(batches[bi]);
foreach (var l in activeLasers)
{
if (!l) continue;
l.enabled = true;
l.ResetCycle();
l.SetLaserPhase(LaserPhase.Idle);
}
IEnumerator ActivateBatches()
float batchEnd = Time.time + interval;
// Loop phases for the duration of this batch
while (Time.time < batchEnd)
{
for (int i = 0; i < batches.Count; i++)
// Compute runway required to complete a Charge->Fire sequence
float runway = Mathf.Max(0f, chargingWindup) + Mathf.Max(0f, fireDuration);
float timeLeft = batchEnd - Time.time;
// If not enough time to do Charging+Firing, just stay Idle until the batch ends.
if (timeLeft < runway)
{
activeLasers.AddRange(batches[i]);
foreach (var laser in activeLasers)
laser.enabled = true;
ResetAllLasers();
yield return new WaitForSeconds(interval);
}
}
void Update()
{
if (activeLasers.Count == 0) return;
timer += Time.deltaTime;
switch (currentPhase)
{
case LaserPhase.Idle:
if (timer >= chargeDuration)
{
timer = 0f;
currentPhase = LaserPhase.Charging;
foreach (var laser in activeLasers)
laser.SetLaserPhase(LaserPhase.Charging);
}
break;
case LaserPhase.Charging:
foreach (var laser in activeLasers)
laser.TickLaserDuringCharging();
if (timer >= 1f)
{
timer = 0f;
currentPhase = LaserPhase.Firing;
foreach (var laser in activeLasers)
laser.SetLaserPhase(LaserPhase.Firing);
}
break;
case LaserPhase.Firing:
foreach (var laser in activeLasers)
laser.TickLaserDuringFiring();
if (timer >= fireDuration)
{
timer = 0f;
currentPhase = LaserPhase.Idle;
foreach (var laser in activeLasers)
laser.SetLaserPhase(LaserPhase.Idle);
}
yield return PhaseBlock(LaserPhase.Idle, timeLeft, batchEnd);
break;
}
// IDLE
float idleTime = Mathf.Max(0f, idleBeforeCharge);
// If idleTime itself would consume too much and leave < runway, trim idle to fit
if (idleTime > 0f && idleTime > timeLeft - runway)
idleTime = Mathf.Max(0f, timeLeft - runway);
if (idleTime > 0f)
{
yield return PhaseBlock(LaserPhase.Idle, idleTime, batchEnd);
if (Time.time >= batchEnd) break;
}
void ResetAllLasers()
// CHARGING (blink)
if (chargingWindup > 0f)
{
timer = 0f;
currentPhase = LaserPhase.Idle;
yield return PhaseBlock(LaserPhase.Charging, chargingWindup, batchEnd);
// we guaranteed runway, so we should still have time left for Firing
}
foreach (var laser in activeLasers)
// FIRING (lethal)
if (fireDuration > 0f)
{
laser.SetLaserPhase(LaserPhase.Idle);
yield return PhaseBlock(LaserPhase.Firing, fireDuration, batchEnd);
}
}
}
}
// Set phase for all active lasers and tick per-frame for 'duration' (clamped by batch end)
IEnumerator PhaseBlock(LaserPhase phase, float duration, float batchHardEnd)
{
SetPhaseForAll(phase);
float end = Mathf.Min(Time.time + duration, batchHardEnd);
while (Time.time < end)
{
if (phase == LaserPhase.Charging)
{
foreach (var l in activeLasers) if (l && l.isActiveAndEnabled) l.TickLaserDuringCharging();
}
else if (phase == LaserPhase.Firing)
{
foreach (var l in activeLasers) if (l && l.isActiveAndEnabled) l.TickLaserDuringFiring();
}
yield return null;
}
}
void SetPhaseForAll(LaserPhase p)
{
foreach (var l in activeLasers)
if (l && l.isActiveAndEnabled) l.SetLaserPhase(p);
}
}

View File

@ -4,7 +4,10 @@ using UnityEngine;
public class LevelCompleteDoorTrigger : MonoBehaviour
{
public string playerTag = "Player";
private void Start()
{
}
void Reset()
{
// Make sure the collider is a trigger
@ -16,7 +19,7 @@ public class LevelCompleteDoorTrigger : MonoBehaviour
{
if (other.CompareTag(playerTag))
{
CrateEscapeGameManager.Instance?.OnLevelCompleteTriggered();
//CrateEscapeGameManager.Instance?.OnLevelCompleteTriggered();
}
}
}

View File

@ -1,54 +1,134 @@
using UnityEngine;
using DG.Tweening;
[RequireComponent(typeof(Collider))]
public class SmoothChildRotation : MonoBehaviour
{
[Header("References")]
public Transform targetChild; // Child object to rotate
public string playerTag = "Player"; // Tag for player detection
[Header("Door Parts")]
public Transform targetChild; // door mesh child to rotate
public string playerTag = "Player";
[Header("Rotation Settings")]
public float rotationDuration = 0.5f; // Time in seconds for rotation
public Ease rotationEase = Ease.OutQuad; // Easing type
[Header("Door Rotation")]
public float rotationDuration = 0.5f;
public Ease rotationEase = Ease.OutQuad;
public Vector3 targetRotationOnEnter = new Vector3(0, 0, -90);
public Vector3 targetRotationOnExit = new Vector3(0, 0, 0);
private Tween currentTween;
[Header("Cutscene: Exit Path")]
[Tooltip("If not set, we use (this.transform.position + this.transform.forward * exitForwardDistance).")]
public Transform exitPoint;
public float exitForwardDistance = 3f; // used when exitPoint is null
public float faceDuration = 0.2f; // how quickly the player faces exit
private void Start()
[Header("Cutscene: Anim States")]
public string celebrationStateName = "Celebration";
public string walkStateName = "Run"; // or your locomotion state/BT name
[Header("Cutscene: Timing")]
public float celebrationDuration = 1.2f; // seconds to hold Celebration
public float walkMoveDuration = 1.5f; // seconds to walk to exit
public Ease walkMoveEase = Ease.Linear;
[Header("Behavior")]
public bool disableTriggerAfterUse = true; // prevents re-entrant triggers
private Tween _doorTween;
private bool _cutsceneRunning;
void Reset()
{
if (targetChild == null)
var col = GetComponent<Collider>();
if (col) col.isTrigger = true;
}
void Start()
{
Debug.LogError("Target child is not assigned!");
if (!targetChild)
{
Debug.LogError("[SmoothChildRotation] Target child not assigned!", this);
enabled = false;
return;
}
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag(playerTag))
void OnTriggerEnter(Collider other)
{
if (_cutsceneRunning) return;
if (!other.CompareTag(playerTag)) return;
// Open the door
RotateTo(targetRotationOnEnter);
}
// Start the cutscene
StartCoroutine(RunDoorCutscene(other));
}
private void OnTriggerExit(Collider other)
{
if (other.CompareTag(playerTag))
void OnTriggerExit(Collider other)
{
if (!other.CompareTag(playerTag)) return;
if (_cutsceneRunning) return; // dont close while cutscene is ongoing
RotateTo(targetRotationOnExit);
}
}
private void RotateTo(Vector3 targetRot)
void RotateTo(Vector3 targetRot)
{
// Kill any existing tween to avoid stacking
currentTween?.Kill();
_doorTween?.Kill();
_doorTween = targetChild
.DOLocalRotate(targetRot, rotationDuration)
.SetEase(rotationEase);
}
currentTween = targetChild.DOLocalRotate(
targetRot,
rotationDuration
).SetEase(rotationEase);
System.Collections.IEnumerator RunDoorCutscene(Collider hit)
{
_cutsceneRunning = true;
if (disableTriggerAfterUse) { var c = GetComponent<Collider>(); if (c) c.enabled = false; }
// Find player components
Transform root = hit.attachedRigidbody ? hit.attachedRigidbody.transform : hit.transform.root;
var controller = root.GetComponentInChildren<CrateEscapePlayerControllerJoystick>();
var anim = root.GetComponentInChildren<ZibuAnimDriver>();
var rb = root.GetComponentInChildren<Rigidbody>();
// 1) Freeze player control & velocity
if (controller) controller.LockControls(true);
if (rb) { rb.velocity = Vector3.zero; rb.angularVelocity = Vector3.zero; }
// 2) Work out exit position / facing
Vector3 dst = exitPoint ? exitPoint.position : (transform.position + transform.forward * exitForwardDistance);
if (rb) dst.y = rb.position.y; else dst.y = root.position.y;
Vector3 faceDir = dst - root.position; faceDir.y = 0f;
if (faceDir.sqrMagnitude > 0.0001f)
{
Quaternion look = Quaternion.LookRotation(faceDir.normalized, Vector3.up);
// quick face toward the door exit
root.DORotateQuaternion(look, faceDuration).SetEase(Ease.OutQuad);
}
// 3) Celebration anim
if (anim && !string.IsNullOrEmpty(celebrationStateName))
anim.PlayState(celebrationStateName, 0.1f);
yield return new WaitForSeconds(celebrationDuration);
// 4) Walk/Run anim
if (anim && !string.IsNullOrEmpty(walkStateName))
anim.PlayState(walkStateName, 0.1f);
// 5) Move player outside the door
Tween moveT;
if (rb != null)
moveT = rb.DOMove(dst, walkMoveDuration).SetEase(walkMoveEase).SetUpdate(UpdateType.Fixed);
else
moveT = root.DOMove(dst, walkMoveDuration).SetEase(walkMoveEase);
yield return moveT.WaitForCompletion();
// 6) Level complete signal
CrateEscapeGameManager.Instance?.OnLevelCompleteTriggered();
// (Optional) keep controls locked while level completes. Remove next line if you want to give control back:
// if (controller) controller.LockControls(false);
_cutsceneRunning = false;
}
}

View File

@ -41,6 +41,11 @@ public class ZibuAnimDriver : MonoBehaviour
default: CrossFadeSafe(state.ToString(), xfade); break;
}
}
public void PlayState(string stateName, float xfade = 0.1f)
{
if (string.IsNullOrEmpty(stateName)) return;
CrossFadeSafe(stateName, xfade);
}
void CrossFadeSafe(string stateName, float transition)
{

View File

@ -35,7 +35,7 @@ EditorBuildSettings:
- enabled: 0
path: Assets/Scenes/ChaseRun.unity
guid: be6c423b3d68dcb48bc49a7d2ed4957d
- enabled: 0
- enabled: 1
path: Assets/Scenes/CrateEscape.unity
guid: af5c5d2a2d201e24f8c4913ae531addf
- enabled: 0
@ -44,7 +44,7 @@ EditorBuildSettings:
- enabled: 0
path: Assets/Scenes/BlockDrop.unity
guid: a99fde5053c68e0478ef4665b9c7b510
- enabled: 1
- enabled: 0
path: Assets/Scenes/SkyWalker.unity
guid: 71a02188e9a650b47a6c28ddde2142d6
- enabled: 0

View File

@ -44,10 +44,10 @@ PlayerSettings:
m_HolographicTrackingLossScreen: {fileID: 0}
defaultScreenWidth: 1920
defaultScreenHeight: 1080
defaultScreenWidthWeb: 960
defaultScreenHeightWeb: 600
defaultScreenWidthWeb: 600
defaultScreenHeightWeb: 960
m_StereoRenderingPath: 0
m_ActiveColorSpace: 0
m_ActiveColorSpace: 1
unsupportedMSAAFallback: 0
m_SpriteBatchVertexThreshold: 300
m_MTRendering: 1
@ -536,7 +536,7 @@ PlayerSettings:
m_Automatic: 1
- m_BuildTarget: WebGLSupport
m_APIs: 0b000000
m_Automatic: 0
m_Automatic: 1
m_BuildTargetVRSettings:
- m_BuildTarget: Standalone
m_Enabled: 0
@ -828,7 +828,7 @@ PlayerSettings:
webGLWasmArithmeticExceptions: 0
webGLLinkerTarget: 1
webGLThreadsSupport: 0
webGLDecompressionFallback: 1
webGLDecompressionFallback: 0
webGLInitialMemorySize: 32
webGLMaximumMemorySize: 2048
webGLMemoryGrowthMode: 2