MiniGames/Assets/LaserBeam.cs
2025-08-18 17:52:40 +05:00

468 lines
14 KiB
C#

using UnityEngine;
[RequireComponent(typeof(LineRenderer))]
public class LaserBeam : MonoBehaviour
{
[Header("Laser Settings")]
public float maxDistance = 20f;
public LayerMask collisionMask = ~0;
public float chargeDuration = 3f;
public float fireDuration = 1f;
[Header("Laser Appearance")]
public Color laserColor = Color.red;
public Color warningColor = new Color(1f, 0.5f, 0f); // Orange
public float laserWidth = 0.05f;
public float emissionStrength = 5f;
public float scrollSpeed = 1f;
[Header("Player Hit")]
public string playerTag = "Player";
[Tooltip("Approximate beam thickness for hit tests.")]
public float hitRadius = 0.1f;
[Tooltip("Should raycasts consider trigger colliders? (Player often uses triggers)")]
public QueryTriggerInteraction queryTriggerMode = QueryTriggerInteraction.Collide;
[Header("Intro Show")]
public bool showIntro = true;
public float initialShowDuration = 3f;
[Tooltip("If true, the intro beam can kill the player too.")]
public bool introIsDeadly = true;
[Header("Debug")]
public bool debugDraw = false;
private LineRenderer line;
private float timer = 0f;
private enum LaserState { Idle, Charging, Firing }
private LaserState currentState = LaserState.Idle;
private Vector3 laserStart;
private Vector3 laserEnd;
private bool introRunning = false;
private bool hasTriggeredDeathThisBurst = false;
// cache root to ignore self-hits
Transform _selfRoot;
[Header("Deflection")]
public bool enableDeflection = true;
public string deflectTag = "Box";
[Tooltip("How far the beam travels after deflection.")]
public float deflectDistance = 10f;
[Tooltip("Push along the deflected direction to avoid re-hitting the same face.")]
public float deflectEdgePush = 0.02f;
// runtime cache so the left/right choice doesn't flicker
Collider _currentDeflectCol;
int _currentDeflectSign = 1; // +1 = right, -1 = left
bool _isDeflecting;
Vector3 _bouncePoint;
Vector3 _bounceEnd;
[Header("Deflection (Box side center)")]
[Tooltip("Tiny push so the second segment isn't inside the face.")]
// runtime
bool _segmented;
[Header("Deflection (Box side center)")]
public bool deflectFromBoxSides = true;
public string boxTag = "Box";
public float outDistance = 12f;
public float sideExitPush = 0.02f; // push outward from side face
public float entryPush = 0.005f; // push outward from front face so it shows
// runtime
bool _routeViaSide;
Vector3 _entryPoint; // front-face hit
Vector3 _midPoint; // side-face center
Collider _lastBox;
int _sideSign = 1; // -1 = left, +1 = right (box local right axis)
void Awake()
{
_selfRoot = transform.root;
SetupLaserRenderer();
}
void Start()
{
if (showIntro)
StartCoroutine(IntroShow());
}
void Update()
{
if (introRunning) return;
timer += Time.deltaTime;
switch (currentState)
{
case LaserState.Idle:
hasTriggeredDeathThisBurst = false; // reset per cycle
if (timer >= chargeDuration)
{
timer = 0f;
currentState = LaserState.Charging;
StartCharging();
}
break;
case LaserState.Charging:
if (timer >= 1f)
{
timer = 0f;
currentState = LaserState.Firing;
FireLaser();
}
else
{
BlinkWarning();
}
break;
case LaserState.Firing:
if (timer >= fireDuration)
{
timer = 0f;
currentState = LaserState.Idle;
DisableLaser();
}
else
{
UpdateLaserPath();
CheckHit();
}
break;
}
if (debugDraw)
{
Debug.DrawLine(laserStart, laserEnd, Color.cyan, 0f, false);
}
}
// ---------------- Intro show ----------------
System.Collections.IEnumerator IntroShow()
{
introRunning = true;
float t = 0f;
line.enabled = true;
SetLineColor(laserColor);
hasTriggeredDeathThisBurst = false;
while (t < initialShowDuration)
{
UpdateLaserPath();
if (introIsDeadly && !hasTriggeredDeathThisBurst)
CheckHit();
t += Time.deltaTime;
yield return null;
}
line.enabled = false;
timer = 0f;
currentState = LaserState.Idle;
introRunning = false;
}
// ---------------- States ----------------
void StartCharging()
{
UpdateLaserPath();
line.enabled = true;
SetLineColor(warningColor);
}
void BlinkWarning()
{
float blink = Mathf.PingPong(Time.time * 5f, 1f);
Color blinkColor = Color.Lerp(Color.clear, warningColor, blink);
SetLineColor(blinkColor);
}
void FireLaser()
{
UpdateLaserPath();
SetLineColor(laserColor);
hasTriggeredDeathThisBurst = false;
CheckHit(); // initial frame
}
void DisableLaser()
{
line.enabled = false;
}
// ---------------- Hit Detection (robust) ----------------
//void CheckHit()
//{
// if (hasTriggeredDeathThisBurst) return;
// Vector3 dir = (laserEnd - laserStart).normalized;
// float distToEnd = Vector3.Distance(laserStart, laserEnd);
// // Cast along visible beam, then pick the nearest valid hit
// RaycastHit[] hits = Physics.SphereCastAll(
// laserStart, hitRadius, dir,
// distToEnd, collisionMask, queryTriggerMode
// );
// if (hits == null || hits.Length == 0) return;
// float bestDist = float.MaxValue;
// Transform best = null;
// foreach (var h in hits)
// {
// // Ignore self (any collider in our own hierarchy)
// if (h.collider && h.collider.GetComponentInParent<LaserBeam>() == this)
// continue;
// // Keep the nearest hit under our visible segment
// if (h.distance < bestDist)
// {
// bestDist = h.distance;
// best = h.collider.transform;
// }
// }
// if (best == null) return;
// // Player detection: check both object and its root
// bool isPlayer =
// best.CompareTag(playerTag) ||
// (best.root != null && best.root.CompareTag(playerTag));
// if (isPlayer)
// {
// Debug.Log("Laser hit player: " + best.name);
// hasTriggeredDeathThisBurst = true;
// CrateEscapeGameManager.Instance?.OnPlayerHitByLaser();
// }
//}
void CheckHit()
{
if (hasTriggeredDeathThisBurst) return;
bool CheckSegment(Vector3 a, Vector3 b)
{
Vector3 d = b - a;
float len = d.magnitude;
if (len <= 0.0001f) return false;
d /= len;
var hits = Physics.SphereCastAll(a, hitRadius, d, len, collisionMask, queryTriggerMode);
if (hits == null || hits.Length == 0) return false;
float best = float.MaxValue;
Transform bestT = null;
foreach (var h in hits)
{
if (h.collider && h.collider.GetComponentInParent<LaserBeam>() == this) continue;
if (h.distance < best) { best = h.distance; bestT = h.collider.transform; }
}
if (!bestT) return false;
bool isPlayer = bestT.CompareTag(playerTag) || (bestT.root && bestT.root.CompareTag(playerTag));
if (isPlayer)
{
hasTriggeredDeathThisBurst = true;
CrateEscapeGameManager.Instance?.OnPlayerHitByLaser();
return true;
}
return false;
}
if (_routeViaSide)
{
if (CheckSegment(laserStart, _entryPoint)) return;
if (CheckSegment(_entryPoint, _midPoint)) return;
CheckSegment(_midPoint, laserEnd);
}
else
{
CheckSegment(laserStart, laserEnd);
}
}
void UpdateLaserPath()
{
laserStart = transform.position;
Vector3 dir = transform.forward;
float radius = Mathf.Max(0.0001f, hitRadius);
// first: straight cast to find the first collider
RaycastHit[] hits = Physics.SphereCastAll(laserStart, radius, dir, maxDistance, collisionMask, queryTriggerMode);
Vector3 straightEnd = laserStart + dir * maxDistance;
RaycastHit bestHit = default;
float bestDist = float.MaxValue;
bool gotHit = false;
if (hits != null)
{
foreach (var h in hits)
{
if (!h.collider) continue;
if (h.collider.GetComponentInParent<LaserBeam>() == this) continue;
if (h.distance < bestDist)
{
bestDist = h.distance;
bestHit = h;
gotHit = true;
straightEnd = h.point;
}
}
}
// default: straight line
_routeViaSide = false;
laserEnd = straightEnd;
line.positionCount = 2;
line.SetPosition(0, laserStart);
line.SetPosition(1, laserEnd);
// route via side center only for tagged Box with a BoxCollider
if (!deflectFromBoxSides || !gotHit || !bestHit.collider.CompareTag(boxTag))
return;
var box = bestHit.collider.GetComponent<BoxCollider>();
if (!box) return;
// choose/stabilize left/right per box
if (_lastBox != bestHit.collider)
{
_lastBox = bestHit.collider;
_sideSign = (Random.value < 0.5f) ? -1 : 1;
}
Transform t = box.transform;
// 1) ENTRY POINT on the FRONT face (push a hair outward so it's visible)
_entryPoint = bestHit.point + bestHit.normal * entryPush;
// 2) SIDE-CENTER (left/right in box local space), and outward direction
Vector3 centerW = t.TransformPoint(box.center);
Vector3 half = Vector3.Scale(box.size * 0.5f, t.lossyScale);
Vector3 rightW = t.right.normalized;
Vector3 outDir = rightW * _sideSign;
_midPoint = centerW + rightW * (_sideSign * half.x);
// 3) SECOND LEG: from side-center outward
// remaining distance after traveling to entry + to side-center
float traveled = bestHit.distance + Vector3.Distance(bestHit.point, _midPoint);
float remain = Mathf.Max(0f, maxDistance - traveled);
float leg = Mathf.Min(outDistance, remain);
Vector3 secondStart = _midPoint + outDir * sideExitPush;
Vector3 secondEnd = secondStart + outDir * leg;
if (Physics.SphereCast(secondStart, radius, outDir, out var h2, leg, collisionMask, queryTriggerMode))
{
if (!(h2.collider && h2.collider.GetComponentInParent<LaserBeam>() == this))
secondEnd = h2.point;
}
// draw the full polyline: START -> ENTRY -> SIDE-CENTER -> OUT
_routeViaSide = true;
laserEnd = secondEnd;
line.positionCount = 4;
line.SetPosition(0, laserStart);
line.SetPosition(1, _entryPoint);
line.SetPosition(2, _midPoint);
line.SetPosition(3, laserEnd);
}
//void UpdateLaserPath()
//{
// laserStart = transform.position;
// Vector3 dir = transform.forward;
// // SphereCast to find the FIRST valid hit along the visible beam,
// // ignoring ONLY this laser's own colliders.
// float radius = Mathf.Max(0.0001f, hitRadius);
// RaycastHit[] hits = Physics.SphereCastAll(
// laserStart, radius, dir, maxDistance, collisionMask, queryTriggerMode
// );
// Vector3 end = laserStart + dir * maxDistance;
// float bestDist = float.MaxValue;
// if (hits != null && hits.Length > 0)
// {
// foreach (var h in hits)
// {
// if (!h.collider) continue;
// // Ignore ONLY this LaserBeam's colliders (not the whole root/level)
// if (h.collider.GetComponentInParent<LaserBeam>() == this)
// continue;
// if (h.distance < bestDist)
// {
// bestDist = h.distance;
// end = h.point;
// }
// }
// }
// laserEnd = end;
// line.SetPosition(0, laserStart);
// line.SetPosition(1, laserEnd);
//}
// ---------------- Utils ----------------
void SetLineColor(Color c)
{
line.material.color = c;
line.startColor = c;
line.endColor = c;
}
public void SetupLaserRenderer()
{
line = GetComponent<LineRenderer>();
if (!line) line = gameObject.AddComponent<LineRenderer>();
line.positionCount = 2;
line.useWorldSpace = true;
line.loop = false;
line.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
line.receiveShadows = false;
line.widthMultiplier = laserWidth;
Shader laserShader = Shader.Find("Custom/EmissiveLaser");
if (laserShader != null)
{
Material laserMat = new Material(laserShader);
laserMat.SetColor("_Color", laserColor);
laserMat.SetFloat("_Emission", emissionStrength);
laserMat.SetFloat("_ScrollSpeed", scrollSpeed);
line.material = laserMat;
}
else
{
Debug.LogWarning("Custom/EmissiveLaser shader not found. Using fallback.");
line.material = new Material(Shader.Find("Sprites/Default"));
line.material.color = laserColor;
}
line.enabled = false;
}
void OnDrawGizmosSelected()
{
if (!debugDraw) return;
Gizmos.color = Color.magenta;
Gizmos.DrawWireSphere(transform.position, hitRadius);
}
}