chore: Project Restructuring & Theatre

This commit is contained in:
Alexandre Coirier
2026-05-13 12:24:48 +02:00
parent 4070916c1f
commit e96d0bf6e6
2145 changed files with 20957 additions and 44792 deletions
@@ -0,0 +1,49 @@
using UnityEngine;
using Unheimlich.Core.Interaction;
using Unheimlich.Core.Utils;
namespace Unheimlich.World.Environment
{
public class DoorInteractable : InteractableBase
{
[Header("Door Settings")]
public bool IsOpen = false;
[SerializeField] private float _openAngle = 90f;
[SerializeField] private float _animationDuration = 0.5f;
[SerializeField] private LeanTweenType _easeType = LeanTweenType.easeInOutQuad;
private Quaternion _closedRotation;
private Quaternion _openRotation;
protected virtual void Start()
{
_closedRotation = transform.rotation;
_openRotation = _closedRotation * Quaternion.Euler(0, _openAngle, 0);
InteractionText = IsOpen ? "Close" : "Open";
}
public override void Interact()
{
ToggleDoor();
}
public void ToggleDoor()
{
gameObject.CancelAllTweens();
if (!IsOpen)
{
TweenHelper.RotateLocal(gameObject, _openRotation.eulerAngles, _animationDuration, null, _easeType);
IsOpen = true;
InteractionText = "Close";
}
else
{
TweenHelper.RotateLocal(gameObject, _closedRotation.eulerAngles, _animationDuration, null, _easeType);
IsOpen = false;
InteractionText = "Open";
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d620ab476d55f2542a31783a6684f189
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 581e1488df9d96149925217dbedad049
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c9e77f5824725bb49a41c5775a81bf2b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using UnityEngine;
using Unheimlich.Features.Hallucination;
namespace Unheimlich.World.Environment
{
/// <summary>
/// Configuration for a single stair slot.
/// Slot indices are relative: 0 = ground level, positive = up, negative = down.
/// </summary>
[System.Serializable]
public class StaircaseSlotData
{
[Tooltip("Slot index this data applies to. 0 = ground, positive = up, negative = down.")]
public int SlotIndex;
[Tooltip("Word displayed on the FloatingLabel of this segment.")]
public string Word;
[Tooltip("Optional hallucination event fired when the player enters this segment's zone.")]
public HallucinationEvent HallucinationEvent;
}
/// <summary>
/// ScriptableObject holding the full word/event sequence for one staircase.
/// Each slot entry binds a display word and an optional hallucination event to a given slot index.
/// Slots not listed here receive no word and fire no event.
/// </summary>
/// <remarks>
/// Create via Assets › Create › Unheimlich › Hallucination › Staircase Sequence Data.
/// </remarks>
[CreateAssetMenu(
fileName = "StaircaseSequenceData",
menuName = "Unheimlich/Hallucination/Staircase Sequence Data")]
public class StaircaseSequenceData : ScriptableObject
{
[Tooltip("Per-slot configuration. Slots not listed here get no word and no event.")]
public List<StaircaseSlotData> Slots = new();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7f9be4fbaead86548b21371bf4598477
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 18369b1856c5f624cb3fd42a13f0292c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,516 @@
using System.Collections.Generic;
using UnityEngine;
using Unheimlich.Features.Player;
using Unheimlich.Features.Hallucination;
namespace Unheimlich.World.Environment
{
// ─── Controller ───────────────────────────────────────────────────────────────
public class InfiniteStaircaseController : MonoBehaviour
{
// ─── Prefabs ─────────────────────────────────────────────────────────
[Header("Stair Prefabs")]
[Tooltip("Standard segment used to fill the tower.")]
[SerializeField] private GameObject _stairPrefab;
[Tooltip("Segment always placed at the very TOP of the tower (slot +maxIterationsUp).")]
[SerializeField] private GameObject _stairTopPrefab;
[Tooltip("Segment always placed at the very BOTTOM of the tower (slot -maxIterationsDown).")]
[SerializeField] private GameObject _stairBottomPrefab;
[Tooltip("If true, the first child with renderers is reused as slot 0 instead of being instantiated from the prefab.")]
[SerializeField] private bool _useExistingChildAsBase = true;
// ─── Iterations ──────────────────────────────────────────────────────
[Header("Iterations")]
[Tooltip("Number of segments above ground level (upward traversal).")]
[SerializeField] private int _maxIterationsUp = 4;
[Tooltip("Number of segments below ground level (downward traversal).")]
[SerializeField] private int _maxIterationsDown = 4;
// ─── Segment Bounds ──────────────────────────────────────────────────
[Header("Segment Bounds — Auto-Detection")]
[Tooltip("Renderer of the central column mesh. When assigned, segment height is read from its world bounds.")]
[SerializeField] private Renderer _centralColumnRenderer;
[Tooltip("Fallback segment height (m) used when no central column renderer is assigned.")]
[SerializeField] private float _segmentHeight = 4.82f;
[Tooltip("Y offset added to the segment height for fine-tuning snapping.")]
[SerializeField] private float _segmentHeightOffset = 0f;
// ─── Scroll Control ───────────────────────────────────────────────────
[Header("Scroll Speed Control")]
[Tooltip("Proportional gain: scroll speed (m/s) per metre of player deviation from the trigger center.")]
[SerializeField] private float _correctionGain = 4f;
[Tooltip("Maximum tower scroll speed in m/s.")]
[SerializeField] private float _maxScrollSpeed = 8f;
[Tooltip("SmoothDamp time for scroll speed transitions — prevents sudden jolts.")]
[SerializeField] private float _scrollSmoothTime = 0.1f;
// ─── Rotation Offset ─────────────────────────────────────────────────
[Header("Rotation Offset")]
[Tooltip("Y rotation applied to every spawned segment around its geometric bounds center.")]
[SerializeField] private float _segmentRotationOffsetY = 0f;
// ─── Hallucination / Word Data ────────────────────────────────────────
[Header("Hallucination — Word & Event Data")]
[Tooltip("ScriptableObject holding word + event data per slot. " +
"Recommended: create a StaircaseSequenceData asset and assign it here.")]
[SerializeField] private StaircaseSequenceData _sequenceData;
[Space]
[Tooltip("When true, the word for each slot is picked randomly from _randomWordPool instead of from _sequenceData.")]
[SerializeField] private bool _useRandomWords = false;
[Tooltip("Pool of words to pick from when _useRandomWords is enabled.")]
[SerializeField] private List<string> _randomWordPool = new();
[Tooltip("HallucinationEvent applied to all slots when _useRandomWords is enabled " +
"(slot-specific events from _sequenceData are ignored in random mode).")]
[SerializeField] private HallucinationEvent _randomModeEvent;
// ─── Debug ───────────────────────────────────────────────────────────
[Header("Debug")]
[SerializeField] private bool _drawGizmos = true;
// ─── State ───────────────────────────────────────────────────────────
private enum StairState { Idle, Active, Stopping, Done }
private StairState _currentState = StairState.Idle;
private Transform _playerTransform;
/// <summary>Single parent moved to scroll the entire tower at once.</summary>
private Transform _towerRoot;
/// <summary>World Y of the trigger collider center, cached on Awake.</summary>
private float _triggerCenterWorldY;
/// <summary>True when the player entered the trigger from below (travelling upward).</summary>
private bool _enteredFromBelow;
/// <summary>
/// towerRoot.localPosition.y threshold signalling stock exhaustion.
/// Negative when going up, positive when going down.
/// </summary>
private float _limitLocalY;
/// <summary>Current scroll speed (signed). Positive means the tower moves downward.</summary>
private float _scrollSpeed;
private float _scrollSpeedVelocity;
private readonly Dictionary<int, GameObject> _segmentsBySlot = new();
// ─── Properties ──────────────────────────────────────────────────────
private float EffectiveSegmentHeight => _segmentHeight + _segmentHeightOffset;
private float MaxTowerDisplacementUp => _maxIterationsUp * EffectiveSegmentHeight;
private float MaxTowerDisplacementDown => _maxIterationsDown * EffectiveSegmentHeight;
// ─── Lifecycle ───────────────────────────────────────────────────────
private void Awake()
{
AutoDetectBoundsFromColumn();
CacheTriggerCenterY();
}
private void Update()
{
EnsurePlayerCached();
switch (_currentState)
{
case StairState.Active:
UpdateScrollSpeed();
ApplyScrollToTower();
CheckLimitReached();
break;
case StairState.Stopping:
DecelerateAndFinish();
break;
}
}
// ─── Trigger Events ───────────────────────────────────────────────────
/// <summary>
/// Called by StaircaseTriggerZone when the player enters the collider.
/// </summary>
public void OnPlayerEnteredTrigger()
{
if (_currentState != StairState.Idle && _currentState != StairState.Done) return;
EnsurePlayerCached();
if (_playerTransform == null) return;
_enteredFromBelow = _playerTransform.position.y < _triggerCenterWorldY;
_scrollSpeed = 0f;
_scrollSpeedVelocity = 0f;
BuildTower();
_currentState = StairState.Active;
}
/// <summary>
/// Called by StaircaseTriggerZone when the player exits the collider.
/// </summary>
public void OnPlayerExitedTrigger()
{
if (_currentState != StairState.Active && _currentState != StairState.Stopping) return;
bool towerHardlyMoved = _towerRoot != null &&
Mathf.Abs(_towerRoot.localPosition.y) < EffectiveSegmentHeight * 0.5f;
if (towerHardlyMoved)
{
_segmentsBySlot.TryGetValue(0, out GameObject baseSegment);
if (baseSegment != null)
{
baseSegment.transform.SetParent(transform, true);
baseSegment.transform.position = transform.position;
}
DestroyTower();
if (baseSegment != null)
_segmentsBySlot[0] = baseSegment;
_scrollSpeed = 0f;
_scrollSpeedVelocity = 0f;
_playerTransform = null;
_currentState = StairState.Idle;
}
else
{
_currentState = StairState.Stopping;
}
}
// ─── Tower Construction ───────────────────────────────────────────────
private void BuildTower()
{
DestroyTower();
_towerRoot = new GameObject("_TowerRoot").transform;
_towerRoot.SetParent(transform, false);
_towerRoot.localPosition = Vector3.zero;
_towerRoot.localRotation = Quaternion.identity;
_towerRoot.localScale = Vector3.one;
if (_enteredFromBelow)
{
for (int i = 0; i <= _maxIterationsUp; i++)
SpawnSegmentAtSlot(i);
_limitLocalY = -MaxTowerDisplacementUp;
}
else
{
for (int i = 0; i >= -_maxIterationsDown; i--)
SpawnSegmentAtSlot(i);
_limitLocalY = MaxTowerDisplacementDown;
}
}
private void DestroyTower()
{
if (_towerRoot != null)
{
Destroy(_towerRoot.gameObject);
_towerRoot = null;
}
_segmentsBySlot.Clear();
}
private void SpawnSegmentAtSlot(int slotIndex)
{
if (_segmentsBySlot.ContainsKey(slotIndex)) return;
float localY = slotIndex * EffectiveSegmentHeight;
GameObject segment;
// Slot 0 — attempt to reuse the existing scene child.
if (slotIndex == 0 && _useExistingChildAsBase)
{
Transform existing = FindExistingStairChild();
if (existing != null)
{
segment = existing.gameObject;
segment.transform.SetParent(_towerRoot, false);
segment.transform.localPosition = new Vector3(0f, localY, 0f);
segment.transform.localRotation = Quaternion.identity;
ApplyRotationOffsetFromCenter(segment);
ConfigureSegment(segment, slotIndex);
_segmentsBySlot[slotIndex] = segment;
return;
}
}
GameObject prefabToUse = PickPrefabForSlot(slotIndex);
if (prefabToUse == null) return;
segment = Instantiate(prefabToUse, _towerRoot);
segment.transform.localPosition = new Vector3(0f, localY, 0f);
segment.transform.localRotation = Quaternion.identity;
ApplyRotationOffsetFromCenter(segment);
ConfigureSegment(segment, slotIndex);
_segmentsBySlot[slotIndex] = segment;
}
private GameObject PickPrefabForSlot(int slotIndex)
{
if (slotIndex == _maxIterationsUp && _stairTopPrefab != null) return _stairTopPrefab;
if (slotIndex == -_maxIterationsDown && _stairBottomPrefab != null) return _stairBottomPrefab;
return _stairPrefab;
}
// ─── Segment Configuration ────────────────────────────────────────────
/// <summary>
/// Resolves the word and event for this slot then calls StaircaseSegment.Configure().
/// If the segment has no StaircaseSegment component, this is a silent no-op.
/// </summary>
private void ConfigureSegment(GameObject segmentGO, int slotIndex)
{
StaircaseSegment segment = segmentGO.GetComponent<StaircaseSegment>();
if (segment == null) return;
string word = ResolveWord(slotIndex);
HallucinationEvent evt = ResolveEvent(slotIndex);
segment.Configure(word, evt);
}
private string ResolveWord(int slotIndex)
{
if (_useRandomWords)
{
if (_randomWordPool == null || _randomWordPool.Count == 0)
return null;
return _randomWordPool[Random.Range(0, _randomWordPool.Count)];
}
return FindSlotData(slotIndex)?.Word;
}
private HallucinationEvent ResolveEvent(int slotIndex)
{
if (_useRandomWords)
return _randomModeEvent;
return FindSlotData(slotIndex)?.HallucinationEvent;
}
/// <summary>Returns the StaircaseSlotData matching slotIndex, or null if not found.</summary>
private StaircaseSlotData FindSlotData(int slotIndex)
{
if (_sequenceData == null) return null;
foreach (StaircaseSlotData data in _sequenceData.Slots)
{
if (data.SlotIndex == slotIndex)
return data;
}
return null;
}
// ─── Scroll ───────────────────────────────────────────────────────────
private void UpdateScrollSpeed()
{
if (_playerTransform == null) return;
float error = _playerTransform.position.y - _triggerCenterWorldY;
float rawTarget = Mathf.Clamp(error * _correctionGain, -_maxScrollSpeed, _maxScrollSpeed);
bool towerHasMoved = _enteredFromBelow
? _towerRoot != null && _towerRoot.localPosition.y < -0.05f
: _towerRoot != null && _towerRoot.localPosition.y > 0.05f;
float targetSpeed = towerHasMoved
? rawTarget
: (_enteredFromBelow ? Mathf.Max(rawTarget, 0f) : Mathf.Min(rawTarget, 0f));
_scrollSpeed = Mathf.SmoothDamp(_scrollSpeed, targetSpeed, ref _scrollSpeedVelocity, _scrollSmoothTime);
}
private void ApplyScrollToTower()
{
if (_towerRoot == null) return;
Vector3 pos = _towerRoot.localPosition;
pos.y -= _scrollSpeed * Time.deltaTime;
pos.y = _enteredFromBelow
? Mathf.Max(pos.y, _limitLocalY)
: Mathf.Min(pos.y, _limitLocalY);
_towerRoot.localPosition = pos;
}
private void CheckLimitReached()
{
if (_towerRoot == null) return;
bool reached = _enteredFromBelow
? _towerRoot.localPosition.y <= _limitLocalY
: _towerRoot.localPosition.y >= _limitLocalY;
if (reached)
_currentState = StairState.Stopping;
}
private void DecelerateAndFinish()
{
_scrollSpeed = Mathf.SmoothDamp(_scrollSpeed, 0f, ref _scrollSpeedVelocity, _scrollSmoothTime);
ApplyScrollToTower();
if (Mathf.Abs(_scrollSpeed) < 0.02f)
FinishAndCleanup();
}
// ─── Cleanup ─────────────────────────────────────────────────────────
private void FinishAndCleanup()
{
_currentState = StairState.Done;
if (_towerRoot == null) return;
int keepSlot = _enteredFromBelow ? _maxIterationsUp : -_maxIterationsDown;
_segmentsBySlot.TryGetValue(keepSlot, out GameObject keepSegment);
if (keepSegment != null)
{
keepSegment.transform.SetParent(transform, true);
keepSegment.transform.position = transform.position;
}
DestroyTower();
if (keepSegment != null)
_segmentsBySlot[0] = keepSegment;
_scrollSpeed = 0f;
_scrollSpeedVelocity = 0f;
_playerTransform = null;
}
// ─── Helpers ─────────────────────────────────────────────────────────
private void AutoDetectBoundsFromColumn()
{
if (_centralColumnRenderer == null) return;
_segmentHeight = _centralColumnRenderer.bounds.size.y;
}
private void CacheTriggerCenterY()
{
StaircaseTriggerZone zone = GetComponentInChildren<StaircaseTriggerZone>();
if (zone == null)
{
Debug.LogWarning("[InfiniteStaircase] No StaircaseTriggerZone found in children.", this);
return;
}
Collider col = zone.GetComponent<Collider>();
if (col != null)
_triggerCenterWorldY = col.bounds.center.y;
}
private void EnsurePlayerCached()
{
if (_playerTransform != null) return;
if (PlayerController.Instance != null)
_playerTransform = PlayerController.Instance.transform;
}
private Transform FindExistingStairChild()
{
foreach (Transform child in transform)
{
if (child == _towerRoot) continue;
if (child.GetComponentInChildren<MeshRenderer>() != null)
return child;
}
return null;
}
private void ApplyRotationOffsetFromCenter(GameObject segment)
{
if (Mathf.Approximately(_segmentRotationOffsetY, 0f)) return;
segment.transform.RotateAround(ComputeWorldBoundsCenter(segment), Vector3.up, _segmentRotationOffsetY);
}
private Vector3 ComputeWorldBoundsCenter(GameObject segment)
{
Renderer[] renderers = segment.GetComponentsInChildren<Renderer>();
if (renderers.Length == 0)
return segment.transform.position + Vector3.up * (EffectiveSegmentHeight * 0.5f);
Bounds combined = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++)
combined.Encapsulate(renderers[i].bounds);
return combined.center;
}
// ─── Gizmos ──────────────────────────────────────────────────────────
private void OnDrawGizmosSelected()
{
if (!_drawGizmos) return;
float h = EffectiveSegmentHeight;
Vector3 o = transform.position;
Gizmos.color = new Color(0f, 1f, 0.5f, 0.35f);
for (int i = 1; i <= _maxIterationsUp; i++)
Gizmos.DrawWireCube(new Vector3(o.x, o.y + i * h + h * 0.5f, o.z), new Vector3(1.8f, h * 0.95f, 1.8f));
Gizmos.color = new Color(1f, 0.6f, 0f, 0.25f);
for (int i = 1; i <= _maxIterationsDown; i++)
Gizmos.DrawWireCube(new Vector3(o.x, o.y - i * h + h * 0.5f, o.z), new Vector3(1.8f, h * 0.95f, 1.8f));
Gizmos.color = Color.cyan;
Gizmos.DrawLine(
new Vector3(o.x - 2f, _triggerCenterWorldY, o.z),
new Vector3(o.x + 2f, _triggerCenterWorldY, o.z)
);
if (_stairTopPrefab != null)
{
Gizmos.color = Color.magenta;
Gizmos.DrawWireCube(
new Vector3(o.x, o.y + _maxIterationsUp * h + h * 0.5f, o.z),
new Vector3(2f, h, 2f)
);
}
if (_stairBottomPrefab != null)
{
Gizmos.color = Color.red;
Gizmos.DrawWireCube(
new Vector3(o.x, o.y - _maxIterationsDown * h + h * 0.5f, o.z),
new Vector3(2f, h, 2f)
);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3462960ec1dfaee448d860de31230d02
@@ -0,0 +1,53 @@
using UnityEngine;
using Unheimlich.Features.Hallucination;
using Unheimlich.Features.Dialogue.UI;
namespace Unheimlich.World.Environment
{
/// <summary>
/// Sits on each stair-segment prefab and acts as the single configuration point
/// for the InfiniteStaircaseController.
///
/// Responsibilities:
/// - Holds a reference to the segment's FloatingLabel (set in prefab).
/// - Holds a reference to the segment's HallucinationTriggerZone (set in prefab).
/// - Exposes Configure() so the controller can push the right word and event
/// at spawn time without any GetComponent chains on the controller side.
/// </summary>
public class StaircaseSegment : MonoBehaviour
{
[Header("References")]
[Tooltip("FloatingLabel present on this prefab. Assign in the prefab inspector.")]
[SerializeField] private FloatingLabel _floatingLabel;
[Tooltip("HallucinationTriggerZone present on this prefab. Assign in the prefab inspector.")]
[SerializeField] private HallucinationTriggerZone _triggerZone;
// ─── Public API ────────────────────────────────────────────────────────
/// <summary>
/// Called by InfiniteStaircaseController right after this segment is spawned.
/// Sets the word on the FloatingLabel and overrides the HallucinationEvent on the zone.
/// </summary>
/// <param name="word">Text to display. Null or empty = label keeps its prefab default.</param>
/// <param name="hallucinationEvent">Event to fire when the player enters the zone. Null = no event.</param>
public void Configure(string word, HallucinationEvent hallucinationEvent)
{
if (_floatingLabel != null && !string.IsNullOrEmpty(word))
_floatingLabel.SetText(word);
if (_triggerZone != null)
_triggerZone.SetEvent(hallucinationEvent);
}
// ─── Editor helpers ────────────────────────────────────────────────────
#if UNITY_EDITOR
private void Reset()
{
_floatingLabel = GetComponentInChildren<FloatingLabel>();
_triggerZone = GetComponentInChildren<HallucinationTriggerZone>();
}
#endif
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ba6fa2b368d1e4f4bb84bc6af9f777bc
@@ -0,0 +1,40 @@
using UnityEngine;
namespace Unheimlich.World.Environment
{
/// <summary>
/// Placed on the trigger child of an <see cref="InfiniteStaircaseController"/>.
/// Forwards Player trigger enter / exit events to the parent controller.
/// </summary>
[RequireComponent(typeof(Collider))]
public class StaircaseTriggerZone : MonoBehaviour
{
private const string PlayerTag = "Player";
private InfiniteStaircaseController _controller;
private void Awake()
{
_controller = GetComponentInParent<InfiniteStaircaseController>();
if (_controller == null)
Debug.LogWarning($"[StaircaseTriggerZone] No InfiniteStaircaseController found in parent of '{name}'.", this);
}
private void OnTriggerEnter(Collider other)
{
if (_controller == null) return;
if (!other.CompareTag(PlayerTag)) return;
_controller.OnPlayerEnteredTrigger();
}
private void OnTriggerExit(Collider other)
{
if (_controller == null) return;
if (!other.CompareTag(PlayerTag)) return;
_controller.OnPlayerExitedTrigger();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 89e1fa0fefb27a245bf290633fa7fdb3
@@ -0,0 +1,99 @@
using UnityEngine;
using Unheimlich.Features.Player;
namespace Unheimlich.World.Environment
{
public class SecurityCameraController : MonoBehaviour
{
[Header("Tracking Settings")]
[SerializeField] private float _trackingDistance = 10f;
[SerializeField] private float _rotationSpeed = 2f;
[SerializeField] private Transform _pivotPoint;
[Header("LED Settings")]
[SerializeField] private MeshRenderer _ledRenderer;
[SerializeField] private float _blinkSpeed = 2f;
[SerializeField] private float _minSmoothness = 0f;
[SerializeField] private float _maxSmoothness = 0.5f;
private CharacterController _playerController;
private Material _ledMaterial;
private float _blinkTimer;
private bool _isTracking;
void Start()
{
if (PlayerController.Instance != null)
_playerController = PlayerController.Instance.GetComponent<CharacterController>();
if (_ledRenderer != null)
{
_ledMaterial = _ledRenderer.material;
}
if (_pivotPoint == null)
_pivotPoint = transform;
}
void Update()
{
if (_playerController == null) return;
float distanceToPlayer = Vector3.Distance(_pivotPoint.position, _playerController.transform.position);
_isTracking = distanceToPlayer <= _trackingDistance;
if (_isTracking)
{
TrackPlayer();
}
UpdateLedBlink();
}
void TrackPlayer()
{
Vector3 directionToPlayer = _playerController.transform.position - _pivotPoint.position;
if (directionToPlayer.sqrMagnitude > 0.001f)
{
Vector3 forward = -directionToPlayer.normalized;
Vector3 right = Vector3.Cross(Vector3.up, forward).normalized;
Vector3 up = Vector3.Cross(forward, right);
Quaternion targetRotation = Quaternion.LookRotation(up, forward);
_pivotPoint.rotation = Quaternion.Slerp(_pivotPoint.rotation, targetRotation, Time.deltaTime * _rotationSpeed);
}
}
void UpdateLedBlink()
{
if (_ledMaterial == null) return;
_blinkTimer += Time.deltaTime * _blinkSpeed;
float smoothness = Mathf.Lerp(_minSmoothness, _maxSmoothness, Mathf.PingPong(_blinkTimer, 1f));
_ledMaterial.SetFloat("_Smoothness", smoothness);
}
void OnDestroy()
{
if (_ledMaterial != null)
Destroy(_ledMaterial);
}
void OnDrawGizmosSelected()
{
if (_pivotPoint == null)
_pivotPoint = transform;
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(_pivotPoint.position, _trackingDistance);
Gizmos.color = Color.red;
Gizmos.DrawLine(_pivotPoint.position, _pivotPoint.position + _pivotPoint.forward * 2f);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 17a573927733b4343acc662995710bc2