395 lines
14 KiB
C#
395 lines
14 KiB
C#
using DG.Tweening;
|
||
using UnityEngine;
|
||
using System.Collections;
|
||
using System.Threading; // for Interlocked
|
||
|
||
[RequireComponent(typeof(Renderer))]
|
||
public class CubeClash_CubeLid : MonoBehaviour
|
||
{
|
||
[Header("Launch")]
|
||
public float launchForce = 20f;
|
||
|
||
[Header("Launch Direction (camera-relative)")]
|
||
[Tooltip("x = camera right, y = camera forward")]
|
||
public Vector2 camSpaceDir = new Vector2(0f, 1f);
|
||
[Tooltip("Scale applied to the forward part relative to launchForce (final = launchForce * forwardScale)")]
|
||
public float forwardScale = 10f;
|
||
[Tooltip("Flatten the camera so direction is on XZ plane")]
|
||
public bool flattenCamera = true;
|
||
|
||
[Header("Cycle Timing")]
|
||
[Tooltip("Full cycle: (rest) + (pre-blink) + (active-glow). Repeats.")]
|
||
public float activeInterval = 8f;
|
||
[Tooltip("Blink time BEFORE activation.")]
|
||
public float preBlinkSeconds = 1.5f;
|
||
[Tooltip("Time the cube stays steadily glowing AFTER activation.")]
|
||
public float activeGlowSeconds = 2.0f;
|
||
|
||
[Header("Blink / Glow")]
|
||
[Tooltip("One full blink (up+down) duration.")]
|
||
public float blinkPulseDuration = 0.25f;
|
||
[Tooltip("Fade times for entering/exiting steady glow.")]
|
||
public float glowFadeIn = 0.12f, glowFadeOut = 0.12f;
|
||
|
||
[Header("Emission (last material slot)")]
|
||
[Tooltip("Enable Emission on the last material of this renderer in the Inspector.")]
|
||
public Color emissionBaseColor = Color.white;
|
||
public float offEmission = 0f;
|
||
public float blinkEmission = 2.0f;
|
||
public float activeGlowIntensity = 2.0f;
|
||
public bool usePropertyBlock = true;
|
||
|
||
[Header("Detection Volume")]
|
||
[Tooltip("If true, OverlapBox uses this BoxCollider; else uses Local Box below.")]
|
||
public bool useColliderForVolume = true;
|
||
public BoxCollider volumeSource; // auto-found if null
|
||
public LayerMask detectMask = ~0;
|
||
public QueryTriggerInteraction triggerQuery = QueryTriggerInteraction.Ignore;
|
||
[Tooltip("Fallback volume if no BoxCollider is assigned.")]
|
||
public Vector3 localBoxCenter = Vector3.zero;
|
||
public Vector3 localBoxSize = new Vector3(1, 1, 1);
|
||
|
||
[Header("Gizmos")]
|
||
public bool drawGizmos = true;
|
||
public Color gizmoFill = new Color(0f, 1f, 1f, 0.15f);
|
||
public Color gizmoWire = new Color(0f, 1f, 1f, 1f);
|
||
|
||
// ─────────────────────────── Global Coordination ───────────────────────────
|
||
[Header("Global Coordination")]
|
||
[Tooltip("If true, lids coordinate so only one activates at a time.")]
|
||
public bool coordinateGlobally = true;
|
||
[Tooltip("Random extra wait added to each lid’s rest period to desync starts.")]
|
||
public Vector2 restJitterRange = new Vector2(0.0f, 1.0f);
|
||
[Tooltip("Random jitter while waiting for the global slot, to randomize who acquires it.")]
|
||
public Vector2 acquireJitterRange = new Vector2(0.0f, 0.2f);
|
||
[Tooltip("Minimum spacing between two different lids activating (seconds).")]
|
||
public float minGlobalSeparation = 0.25f;
|
||
|
||
private static int s_gateToken = 0; // 0 = free, 1 = held
|
||
private static float s_nextAllowedTime = 0f; // spacing across lids
|
||
private bool _holdingGate = false;
|
||
// ───────────────────────────────────────────────────────────────────────────
|
||
|
||
// --- internals ---
|
||
private Renderer _rend;
|
||
private int _submeshIndex = -1;
|
||
private Material _instancedMat; // used if not MPB
|
||
private MaterialPropertyBlock _mpb; // used if MPB
|
||
private float _emissionIntensity;
|
||
private Tween _glowTween; // current tween
|
||
private Coroutine _loopCo;
|
||
|
||
private static readonly int EmissionColorID = Shader.PropertyToID("_EmissionColor");
|
||
private static readonly int EmissiveColorID = Shader.PropertyToID("_EmissiveColor");
|
||
|
||
void Awake()
|
||
{
|
||
_rend = GetComponentInChildren<Renderer>();
|
||
if (!volumeSource) volumeSource = GetComponent<BoxCollider>();
|
||
if (!volumeSource) volumeSource = GetComponentInChildren<BoxCollider>();
|
||
SetupGlowTarget();
|
||
ApplyEmission(offEmission);
|
||
}
|
||
|
||
void OnEnable()
|
||
{
|
||
StopTweens();
|
||
if (_loopCo != null) StopCoroutine(_loopCo);
|
||
_loopCo = StartCoroutine(LidLoop());
|
||
}
|
||
|
||
void OnDisable()
|
||
{
|
||
StopTweens();
|
||
if (_loopCo != null) StopCoroutine(_loopCo);
|
||
_loopCo = null;
|
||
// Release the gate if this lid was active and got disabled mid-cycle
|
||
if (_holdingGate)
|
||
{
|
||
_holdingGate = false;
|
||
Interlocked.Exchange(ref s_gateToken, 0);
|
||
s_nextAllowedTime = Time.time + minGlobalSeparation;
|
||
}
|
||
}
|
||
|
||
// ====== Main cycle ======
|
||
private IEnumerator LidLoop()
|
||
{
|
||
while (true)
|
||
{
|
||
// Rest until pre-blink window, with per-lid random jitter so they don't check the gate at the same moment
|
||
float rest = Mathf.Max(0f, activeInterval - preBlinkSeconds - activeGlowSeconds);
|
||
float jitter = (restJitterRange.y > 0f || restJitterRange.x > 0f)
|
||
? Random.Range(restJitterRange.x, restJitterRange.y)
|
||
: 0f;
|
||
if (rest + jitter > 0f) yield return new WaitForSeconds(rest + Mathf.Max(0f, jitter));
|
||
|
||
// Acquire global slot (only one lid runs blink+activate at a time)
|
||
if (coordinateGlobally)
|
||
yield return StartCoroutine(AcquireGlobalGate());
|
||
|
||
// Pre-blink (warning)
|
||
yield return BlinkForSeconds(preBlinkSeconds);
|
||
|
||
// Activate: launch & steady glow
|
||
ActivateLid();
|
||
yield return SteadyGlow(activeGlowIntensity, activeGlowSeconds);
|
||
|
||
// Deactivate: fade to OFF
|
||
yield return FadeEmissionTo(offEmission, glowFadeOut);
|
||
|
||
// Release global slot
|
||
if (coordinateGlobally)
|
||
{
|
||
_holdingGate = false;
|
||
Interlocked.Exchange(ref s_gateToken, 0);
|
||
s_nextAllowedTime = Time.time + minGlobalSeparation;
|
||
}
|
||
}
|
||
}
|
||
|
||
private IEnumerator AcquireGlobalGate()
|
||
{
|
||
// Wait for spacing time first
|
||
while (Time.time < s_nextAllowedTime)
|
||
yield return null;
|
||
|
||
// Try to acquire with small random jitter between attempts to randomize which lid wins
|
||
while (true)
|
||
{
|
||
if (Interlocked.CompareExchange(ref s_gateToken, 1, 0) == 0)
|
||
{
|
||
_holdingGate = true;
|
||
yield break;
|
||
}
|
||
|
||
float jitter = (acquireJitterRange.y > 0f || acquireJitterRange.x > 0f)
|
||
? Random.Range(acquireJitterRange.x, acquireJitterRange.y)
|
||
: 0.05f;
|
||
yield return new WaitForSeconds(Mathf.Max(0.01f, jitter));
|
||
}
|
||
}
|
||
|
||
// ====== Gameplay ======
|
||
[Header("Spring Animation")]
|
||
[Tooltip("How high the cube jumps visually when launching a player.")]
|
||
public float springHeight = 0.5f;
|
||
[Tooltip("How long the up and down tween takes (seconds).")]
|
||
public float springDuration = 0.2f;
|
||
|
||
|
||
[ContextMenu("Activate Lid")]
|
||
private void ActivateLid()
|
||
{
|
||
GetOverlapBoxWorld(out Vector3 center, out Vector3 halfExtents, out Quaternion orientation);
|
||
Collider[] cols = Physics.OverlapBox(center, halfExtents, orientation, detectMask, triggerQuery);
|
||
|
||
Vector3 worldForward = GetCameraRelativeDirection(camSpaceDir, flattenCamera);
|
||
|
||
foreach (Collider col in cols)
|
||
{
|
||
if (!col) continue;
|
||
if (col.CompareTag("Zibu") && col.attachedRigidbody != null)
|
||
{
|
||
Vector3 impulse = (Vector3.up * launchForce) + (worldForward * (launchForce * forwardScale));
|
||
col.attachedRigidbody.AddForce(impulse, ForceMode.Impulse);
|
||
}
|
||
}
|
||
|
||
// --- NEW spring animation ---
|
||
DoSpringAnimation();
|
||
}
|
||
|
||
private void DoSpringAnimation()
|
||
{
|
||
// kill any existing spring
|
||
DOTween.Kill("CubeSpring_" + GetInstanceID());
|
||
|
||
Vector3 startPos = transform.position;
|
||
Vector3 upPos = startPos + Vector3.up * springHeight;
|
||
|
||
Sequence seq = DOTween.Sequence();
|
||
seq.Append(transform.DOMoveY(upPos.y, springDuration).SetEase(Ease.OutQuad));
|
||
seq.Append(transform.DOMoveY(startPos.y, springDuration).SetEase(Ease.InQuad));
|
||
seq.SetId("CubeSpring_" + GetInstanceID());
|
||
}
|
||
|
||
|
||
private static Vector3 GetCameraRelativeDirection(Vector2 camDir, bool flatten)
|
||
{
|
||
Transform cam = Camera.main ? Camera.main.transform : null;
|
||
|
||
Vector3 camFwd = cam ? cam.forward : Vector3.forward;
|
||
Vector3 camRight = cam ? cam.right : Vector3.right;
|
||
|
||
if (flatten)
|
||
{
|
||
camFwd.y = 0f; camRight.y = 0f;
|
||
if (camFwd.sqrMagnitude < 1e-6f) camFwd = Vector3.forward;
|
||
if (camRight.sqrMagnitude < 1e-6f) camRight = Vector3.right;
|
||
camFwd.Normalize(); camRight.Normalize();
|
||
}
|
||
|
||
Vector3 dir = camRight * camDir.x + camFwd * camDir.y;
|
||
if (dir.sqrMagnitude > 1f) dir.Normalize();
|
||
return dir.sqrMagnitude < 1e-6f ? (flatten ? Vector3.forward : (cam ? cam.forward : Vector3.forward)) : dir;
|
||
}
|
||
|
||
// ====== Blink / Glow visuals ======
|
||
private IEnumerator BlinkForSeconds(float seconds)
|
||
{
|
||
if (_rend == null || seconds <= 0f || blinkPulseDuration <= 0.02f)
|
||
yield break;
|
||
|
||
StopTweens();
|
||
ApplyEmission(offEmission);
|
||
|
||
int blinks = Mathf.Max(1, Mathf.RoundToInt(seconds / blinkPulseDuration));
|
||
float half = blinkPulseDuration * 0.5f;
|
||
_glowTween = DOTween
|
||
.To(() => _emissionIntensity,
|
||
v => { _emissionIntensity = v; ApplyEmission(v); },
|
||
blinkEmission,
|
||
half)
|
||
.SetEase(Ease.InOutSine)
|
||
.SetLoops(blinks * 2, LoopType.Yoyo)
|
||
.SetTarget(this);
|
||
|
||
yield return _glowTween.WaitForCompletion();
|
||
_glowTween = null;
|
||
}
|
||
|
||
private IEnumerator SteadyGlow(float targetIntensity, float holdSeconds)
|
||
{
|
||
yield return FadeEmissionTo(targetIntensity, glowFadeIn);
|
||
if (holdSeconds > 0f) yield return new WaitForSeconds(holdSeconds);
|
||
}
|
||
|
||
private IEnumerator FadeEmissionTo(float target, float duration)
|
||
{
|
||
StopTweens();
|
||
if (duration <= 0f) { ApplyEmission(target); yield break; }
|
||
|
||
_glowTween = DOTween
|
||
.To(() => _emissionIntensity,
|
||
v => { _emissionIntensity = v; ApplyEmission(v); },
|
||
target,
|
||
duration)
|
||
.SetEase(target > _emissionIntensity ? Ease.OutSine : Ease.InSine)
|
||
.SetTarget(this);
|
||
|
||
yield return _glowTween.WaitForCompletion();
|
||
_glowTween = null;
|
||
}
|
||
|
||
private void StopTweens()
|
||
{
|
||
if (_glowTween != null && _glowTween.IsActive()) _glowTween.Kill();
|
||
_glowTween = null;
|
||
}
|
||
|
||
// ====== Emission plumbing ======
|
||
private Renderer RendererOrChild()
|
||
{
|
||
if (_rend) return _rend;
|
||
_rend = GetComponentInChildren<Renderer>();
|
||
return _rend;
|
||
}
|
||
|
||
private void SetupGlowTarget()
|
||
{
|
||
if (RendererOrChild() == null) return;
|
||
|
||
_submeshIndex = Mathf.Max(0, _rend.sharedMaterials.Length - 1);
|
||
|
||
if (usePropertyBlock)
|
||
{
|
||
if (_mpb == null) _mpb = new MaterialPropertyBlock();
|
||
_rend.GetPropertyBlock(_mpb, _submeshIndex);
|
||
// NOTE: Enable Emission on the material in the Inspector.
|
||
}
|
||
else
|
||
{
|
||
var mats = _rend.materials;
|
||
if (mats != null && mats.Length > 0)
|
||
{
|
||
_instancedMat = mats[_submeshIndex];
|
||
if (_instancedMat != null) _instancedMat.EnableKeyword("_EMISSION");
|
||
}
|
||
}
|
||
}
|
||
|
||
private void ApplyEmission(float intensity)
|
||
{
|
||
_emissionIntensity = Mathf.Max(0f, intensity);
|
||
Color emissive = emissionBaseColor * _emissionIntensity;
|
||
|
||
if (usePropertyBlock)
|
||
{
|
||
if (_mpb == null || _rend == null) return;
|
||
_mpb.SetColor(EmissionColorID, emissive);
|
||
_mpb.SetColor(EmissiveColorID, emissive);
|
||
_rend.SetPropertyBlock(_mpb, _submeshIndex);
|
||
}
|
||
else
|
||
{
|
||
if (_instancedMat == null) return;
|
||
_instancedMat.SetColor(EmissionColorID, emissive);
|
||
_instancedMat.SetColor(EmissiveColorID, emissive);
|
||
}
|
||
}
|
||
|
||
// ====== Overlap box helpers + gizmo ======
|
||
private static Vector3 Abs(Vector3 v) => new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z));
|
||
|
||
private void GetOverlapBoxWorld(out Vector3 center, out Vector3 halfExtents, out Quaternion orientation)
|
||
{
|
||
if (useColliderForVolume && volumeSource != null)
|
||
{
|
||
center = volumeSource.transform.TransformPoint(volumeSource.center);
|
||
Vector3 lossy = Abs(volumeSource.transform.lossyScale);
|
||
halfExtents = Vector3.Scale(volumeSource.size * 0.5f, lossy);
|
||
orientation = volumeSource.transform.rotation;
|
||
}
|
||
else
|
||
{
|
||
center = transform.TransformPoint(localBoxCenter);
|
||
Vector3 lossy = Abs(transform.lossyScale);
|
||
halfExtents = Vector3.Scale(localBoxSize * 0.5f, lossy);
|
||
orientation = transform.rotation;
|
||
}
|
||
}
|
||
|
||
private void OnDrawGizmosSelected()
|
||
{
|
||
if (!drawGizmos) return;
|
||
GetOverlapBoxWorld(out Vector3 center, out Vector3 halfExtents, out Quaternion rotation);
|
||
|
||
Matrix4x4 m = Matrix4x4.TRS(center, rotation, Vector3.one);
|
||
Gizmos.matrix = m;
|
||
|
||
Gizmos.color = gizmoFill;
|
||
Gizmos.DrawCube(Vector3.zero, halfExtents * 2f);
|
||
|
||
Gizmos.color = gizmoWire;
|
||
Gizmos.DrawWireCube(Vector3.zero, halfExtents * 2f);
|
||
}
|
||
|
||
private void OnValidate()
|
||
{
|
||
activeInterval = Mathf.Max(0.1f, activeInterval);
|
||
preBlinkSeconds = Mathf.Max(0f, preBlinkSeconds);
|
||
activeGlowSeconds = Mathf.Max(0f, activeGlowSeconds);
|
||
blinkPulseDuration = Mathf.Max(0.05f, blinkPulseDuration);
|
||
glowFadeIn = Mathf.Max(0f, glowFadeIn);
|
||
glowFadeOut = Mathf.Max(0f, glowFadeOut);
|
||
|
||
offEmission = Mathf.Max(0f, offEmission);
|
||
blinkEmission = Mathf.Max(offEmission, blinkEmission);
|
||
activeGlowIntensity = Mathf.Max(offEmission, activeGlowIntensity);
|
||
|
||
if (!volumeSource) volumeSource = GetComponent<BoxCollider>();
|
||
if (!volumeSource) volumeSource = GetComponentInChildren<BoxCollider>();
|
||
}
|
||
}
|