Advanced POC - portals, dialogue system + floating words, room loading
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 188b6b92f90c1d248a52afe96fff07b7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3392c3a642d4da4eb1d6311bc686747
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Unheimlich.Interactions.Dialogues
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializable data structure representing a single dialogue choice option.
|
||||
/// Links choice text to the next dialogue node via ID.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class DialogueChoice
|
||||
{
|
||||
public string ChoiceText; // Text on the choice button
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7a6b90d8597f824a9451b58900227b9
|
||||
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Data.Dialogues;
|
||||
using Unheimlich.Managers;
|
||||
using Unheimlich.UI;
|
||||
|
||||
namespace Unheimlich.Interactions.Dialogues
|
||||
{
|
||||
public class DialogueStateHandler
|
||||
{
|
||||
public bool IsCompleted { get; private set; }
|
||||
public bool HasPausedDialogue => _pausedConversation != null && _pausedNode != null;
|
||||
|
||||
private DialogueConversation _pausedConversation;
|
||||
private DialogueNode _pausedNode;
|
||||
private Transform _pausedChoiceAnchor;
|
||||
private DialogueAnimationSettings _pausedAnimationSettings;
|
||||
private DialogueUI _pausedDialogueUI;
|
||||
private object _owner;
|
||||
|
||||
public DialogueStateHandler(object owner)
|
||||
{
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
public void PauseDialogue()
|
||||
{
|
||||
if (DialogueManager.Instance == null || !DialogueManager.Instance.IsDialogueActive)
|
||||
return;
|
||||
|
||||
DialogueManager dialogueManager = DialogueManager.Instance;
|
||||
|
||||
_pausedConversation = dialogueManager.CurrentConversation;
|
||||
_pausedNode = dialogueManager.CurrentNode;
|
||||
_pausedChoiceAnchor = dialogueManager.CurrentChoiceAnchor;
|
||||
_pausedAnimationSettings = dialogueManager.CurrentAnimationSettings;
|
||||
_pausedDialogueUI = dialogueManager.CurrentDialogueUI;
|
||||
|
||||
DialogueManager.Instance.EndDialogue();
|
||||
}
|
||||
|
||||
public void ResumeDialogue()
|
||||
{
|
||||
if (!HasPausedDialogue || DialogueManager.Instance == null)
|
||||
return;
|
||||
|
||||
DialogueManager.Instance.StartConversation(
|
||||
_pausedConversation,
|
||||
_pausedChoiceAnchor,
|
||||
_pausedAnimationSettings,
|
||||
_pausedDialogueUI,
|
||||
_owner
|
||||
);
|
||||
|
||||
ClearPausedDialogue();
|
||||
}
|
||||
|
||||
public void MarkAsCompleted()
|
||||
{
|
||||
IsCompleted = true;
|
||||
ClearPausedDialogue();
|
||||
}
|
||||
|
||||
public void AllowRetrigger()
|
||||
{
|
||||
IsCompleted = false;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
IsCompleted = false;
|
||||
ClearPausedDialogue();
|
||||
}
|
||||
|
||||
private void ClearPausedDialogue()
|
||||
{
|
||||
_pausedConversation = null;
|
||||
_pausedNode = null;
|
||||
_pausedChoiceAnchor = null;
|
||||
_pausedAnimationSettings = null;
|
||||
_pausedDialogueUI = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b328dc63103543940898cfa2dbd4ec36
|
||||
@@ -0,0 +1,185 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Data.Dialogues;
|
||||
using Unheimlich.Managers;
|
||||
using Unheimlich.UI;
|
||||
|
||||
namespace Unheimlich.Interactions.Dialogues
|
||||
{
|
||||
/// <summary>
|
||||
/// Trigger zone that starts a dialogue sequence when player enters.
|
||||
/// Supports pause/resume on exit/enter and optional one-time triggering.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class DialogueTriggerZone : MonoBehaviour
|
||||
{
|
||||
[Header("Dialogue Reference")]
|
||||
[Tooltip("DialogueConversation to use for this trigger zone")]
|
||||
[SerializeField] private DialogueConversation _conversation;
|
||||
|
||||
[Header("Trigger Settings")]
|
||||
[Tooltip("Can the dialogue be retriggered after completion")]
|
||||
[SerializeField] private bool _isRetriggerable = false;
|
||||
[SerializeField] private bool _requirePlayerTag = true;
|
||||
[SerializeField] private string _playerTag = "Player";
|
||||
|
||||
[Header("Distance Check")]
|
||||
[Tooltip("Pause dialogue if player exits the trigger zone")]
|
||||
[SerializeField] private bool _pauseOnExit = true;
|
||||
|
||||
[Header("Floating Word Anchor")]
|
||||
[SerializeField] private Transform _floatingWordAnchor;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[SerializeField] private DialogueAnimationSettings _animationSettings;
|
||||
|
||||
[Header("UI Reference")]
|
||||
[Tooltip("Local DialogueUI for this trigger zone. Uses DialogueManager's UI if null")]
|
||||
[SerializeField] private DialogueUI _localDialogueUI;
|
||||
|
||||
[Header("Blur Effect")]
|
||||
[Tooltip("Enable blur effect when dialogue starts")]
|
||||
[SerializeField] private bool _enableBlurEffect = true;
|
||||
|
||||
private bool _hasTriggered = false;
|
||||
private Collider _triggerCollider;
|
||||
private DialogueStateHandler _dialogueState;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_triggerCollider = GetComponent<Collider>();
|
||||
_dialogueState = new DialogueStateHandler(this);
|
||||
|
||||
if (!_triggerCollider.isTrigger)
|
||||
{
|
||||
Debug.LogWarning($"{gameObject.name}: Collider should be marked as Trigger!", this);
|
||||
_triggerCollider.isTrigger = true;
|
||||
}
|
||||
|
||||
if (_floatingWordAnchor == null)
|
||||
_floatingWordAnchor = transform;
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (_requirePlayerTag && !other.CompareTag(_playerTag))
|
||||
return;
|
||||
|
||||
if (_dialogueState.IsCompleted && !_isRetriggerable)
|
||||
return;
|
||||
|
||||
if (_dialogueState.HasPausedDialogue)
|
||||
{
|
||||
_dialogueState.ResumeDialogue();
|
||||
|
||||
if (_enableBlurEffect && DialogueBlurEffectURP.Instance != null)
|
||||
DialogueBlurEffectURP.Instance.ShowBlur();
|
||||
}
|
||||
else if (!_hasTriggered || (_isRetriggerable && _dialogueState.IsCompleted))
|
||||
{
|
||||
TriggerDialogue();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerExit(Collider other)
|
||||
{
|
||||
if (_requirePlayerTag && !other.CompareTag(_playerTag))
|
||||
return;
|
||||
|
||||
if (_pauseOnExit && DialogueManager.Instance != null && DialogueManager.Instance.IsDialogueActive)
|
||||
{
|
||||
if ((Object)DialogueManager.Instance.CurrentOwner == this)
|
||||
{
|
||||
_dialogueState.PauseDialogue();
|
||||
|
||||
if (_enableBlurEffect && DialogueBlurEffectURP.Instance != null)
|
||||
DialogueBlurEffectURP.Instance.HideBlur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerDialogue()
|
||||
{
|
||||
if (DialogueManager.Instance == null)
|
||||
{
|
||||
Debug.LogError("DialogueManager not found in scene!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_conversation == null)
|
||||
{
|
||||
Debug.LogError($"{gameObject.name}: No conversation assigned!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
_hasTriggered = true;
|
||||
|
||||
if (_enableBlurEffect && DialogueBlurEffectURP.Instance != null)
|
||||
{
|
||||
DialogueBlurEffectURP.Instance.ShowBlur();
|
||||
}
|
||||
|
||||
DialogueManager.Instance.StartConversation(
|
||||
_conversation,
|
||||
_floatingWordAnchor,
|
||||
_animationSettings,
|
||||
_localDialogueUI,
|
||||
this
|
||||
);
|
||||
|
||||
DialogueManager.Instance.OnDialogueCompleted += OnDialogueCompleted;
|
||||
}
|
||||
|
||||
private void OnDialogueCompleted(object owner)
|
||||
{
|
||||
if ((Object)owner == this)
|
||||
{
|
||||
_dialogueState.MarkAsCompleted();
|
||||
|
||||
if (_isRetriggerable)
|
||||
{
|
||||
_hasTriggered = false;
|
||||
_dialogueState.AllowRetrigger();
|
||||
}
|
||||
|
||||
if (_enableBlurEffect && DialogueBlurEffectURP.Instance != null)
|
||||
DialogueBlurEffectURP.Instance.HideBlur();
|
||||
}
|
||||
|
||||
if (DialogueManager.Instance != null)
|
||||
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
|
||||
}
|
||||
|
||||
public void ResetTrigger()
|
||||
{
|
||||
_hasTriggered = false;
|
||||
_dialogueState.Reset();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (DialogueManager.Instance != null)
|
||||
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Collider col = GetComponent<Collider>();
|
||||
if (col != null)
|
||||
{
|
||||
Gizmos.color = _hasTriggered ? Color.gray : Color.yellow;
|
||||
Gizmos.matrix = transform.localToWorldMatrix;
|
||||
|
||||
if (col is BoxCollider box)
|
||||
Gizmos.DrawWireCube(box.center, box.size);
|
||||
else if (col is SphereCollider sphere)
|
||||
Gizmos.DrawWireSphere(sphere.center, sphere.radius);
|
||||
}
|
||||
|
||||
if (_floatingWordAnchor != null)
|
||||
{
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(_floatingWordAnchor.position, 0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f33be20afd5d4c843a800ba1d8dd8437
|
||||
@@ -0,0 +1,142 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Interactions.Core;
|
||||
using Unheimlich.Data.Dialogues;
|
||||
using Unheimlich.Managers;
|
||||
using Unheimlich.Gameplay.Player;
|
||||
using Unheimlich.UI;
|
||||
|
||||
namespace Unheimlich.Interactions.Dialogues
|
||||
{
|
||||
/// <summary>
|
||||
/// NPC interactable that starts dialogue conversations.
|
||||
/// Rotates to face player during dialogue and pauses if player moves too far away.
|
||||
/// </summary>
|
||||
public class NPCDialogueTrigger : InteractableBase
|
||||
{
|
||||
[Header("Dialogue Reference")]
|
||||
[Tooltip("DialogueConversation to use for this NPC")]
|
||||
public DialogueConversation Conversation;
|
||||
|
||||
[Header("Trigger Settings")]
|
||||
[Tooltip("Can the dialogue be retriggered after completion")]
|
||||
[SerializeField] private bool _isRetriggerable = true;
|
||||
|
||||
[Header("Floating Word Setting")]
|
||||
[SerializeField] private Transform _floatingWordAnchor;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[SerializeField] private DialogueAnimationSettings _animationSettings;
|
||||
|
||||
[Header("UI Reference")]
|
||||
[Tooltip("Local DialogueUI for this NPC. Uses DialogueManager's UI if null")]
|
||||
[SerializeField] private DialogueUI _localDialogueUI;
|
||||
|
||||
[Header("Distance Settings")]
|
||||
[Tooltip("Maximum distance before dialogue pauses")]
|
||||
[SerializeField] private float _maxDialogueDistance = 5f;
|
||||
|
||||
[Header("Blur Effect")]
|
||||
[Tooltip("Enable blur effect when dialogue starts")]
|
||||
[SerializeField] private bool _enableBlurEffect = false;
|
||||
|
||||
public Transform FloatingWordAnchor => _floatingWordAnchor;
|
||||
|
||||
private bool _isDialogueActive = false;
|
||||
private DialogueStateHandler _dialogueState;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_dialogueState = new DialogueStateHandler(this);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
InteractionText = "Talk";
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_isDialogueActive && Player.Instance != null)
|
||||
{
|
||||
float distance = Vector3.Distance(transform.position, Player.Instance.transform.position);
|
||||
|
||||
if (distance > _maxDialogueDistance && DialogueManager.Instance != null && DialogueManager.Instance.IsDialogueActive)
|
||||
{
|
||||
if ((Object)DialogueManager.Instance.CurrentOwner == this)
|
||||
{
|
||||
_dialogueState.PauseDialogue();
|
||||
_isDialogueActive = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
LookAtPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Interact(GameObject interactor)
|
||||
{
|
||||
if (DialogueManager.Instance == null)
|
||||
return;
|
||||
|
||||
if (_dialogueState.IsCompleted && !_isRetriggerable)
|
||||
return;
|
||||
|
||||
if (_dialogueState.HasPausedDialogue)
|
||||
{
|
||||
_dialogueState.ResumeDialogue();
|
||||
DialogueManager.Instance.OnDialogueCompleted += OnDialogueCompleted;
|
||||
_isDialogueActive = true;
|
||||
|
||||
if (_enableBlurEffect && DialogueBlurEffectURP.Instance != null)
|
||||
DialogueBlurEffectURP.Instance.ShowBlur();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Conversation == null)
|
||||
{
|
||||
Debug.LogWarning($"NPC {gameObject.name} has no conversation configured!");
|
||||
return;
|
||||
}
|
||||
|
||||
DialogueManager.Instance.StartConversation(Conversation, _floatingWordAnchor, _animationSettings, _localDialogueUI, this);
|
||||
DialogueManager.Instance.OnDialogueCompleted += OnDialogueCompleted;
|
||||
_isDialogueActive = true;
|
||||
|
||||
if (_enableBlurEffect && DialogueBlurEffectURP.Instance != null)
|
||||
DialogueBlurEffectURP.Instance.ShowBlur();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDialogueCompleted(object owner)
|
||||
{
|
||||
if ((Object)owner == this)
|
||||
{
|
||||
_dialogueState.MarkAsCompleted();
|
||||
|
||||
if (_isRetriggerable)
|
||||
_dialogueState.AllowRetrigger();
|
||||
|
||||
_isDialogueActive = false;
|
||||
}
|
||||
|
||||
if (DialogueManager.Instance != null)
|
||||
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (DialogueManager.Instance != null)
|
||||
DialogueManager.Instance.OnDialogueCompleted -= OnDialogueCompleted;
|
||||
}
|
||||
|
||||
public void LookAtPlayer()
|
||||
{
|
||||
if (Player.Instance == null) return;
|
||||
|
||||
Vector3 lookAtPosition = Player.Instance.transform.position;
|
||||
lookAtPosition.y = transform.position.y;
|
||||
|
||||
transform.LookAt(lookAtPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8696519b7eabaab4dbe80f2a4744cea0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2c6fc4373abfa54fa4ce7c0392875f1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+501
@@ -0,0 +1,501 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using System.Collections.Generic;
|
||||
using Unheimlich.Data.Dialogues;
|
||||
using Unheimlich.Managers;
|
||||
using Unheimlich.Gameplay.Player;
|
||||
|
||||
namespace Unheimlich.Interactions.FloatingWords
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages individual dialogue choice button behavior including visual feedback during focus,
|
||||
/// dynamic collider sizing, and smooth animations. Responds to player gaze through the focus system.
|
||||
/// </summary>
|
||||
public class ChoiceFocusHandler : MonoBehaviour
|
||||
{
|
||||
[Header("Components")]
|
||||
[SerializeField] private TextMeshProUGUI _choiceTextComponent;
|
||||
[SerializeField] private TextMeshProUGUI _ghostTextComponent;
|
||||
|
||||
public DialogueChoiceData DialogueData { get; private set; }
|
||||
|
||||
public string GetChoiceText()
|
||||
{
|
||||
return DialogueData?.ChoiceText ?? string.Empty;
|
||||
}
|
||||
|
||||
public DialogueNode GetNextNode()
|
||||
{
|
||||
return DialogueData?.NextNode;
|
||||
}
|
||||
|
||||
private FloatingWordsSpawner.SpawnConfig _config;
|
||||
private bool _hasConfig;
|
||||
|
||||
[Header("Local Feedback Settings")]
|
||||
[SerializeField] private float _focusLerpSpeed = 10f;
|
||||
[SerializeField] private float _colliderSizeMultiplier = 1.4f;
|
||||
[SerializeField] private float _choiceDistanceFromPlayer = 1f;
|
||||
|
||||
[Header("Text Sizing Settings")]
|
||||
[Tooltip("Maximum width for short texts (1-3 words)")]
|
||||
[SerializeField] private float _shortTextMaxWidth = 80f;
|
||||
[Tooltip("Maximum width for medium texts (4-5 words)")]
|
||||
[SerializeField] private float _mediumTextMaxWidth = 150f;
|
||||
[Tooltip("Maximum width for long texts (6-10 words)")]
|
||||
[SerializeField] private float _longTextMaxWidth = 200f;
|
||||
[Tooltip("Maximum width for very long texts (11+ words)")]
|
||||
[SerializeField] private float _veryLongTextMaxWidth = 250f;
|
||||
[Tooltip("Minimum height for text container")]
|
||||
[SerializeField] private float _minTextHeight = 40f;
|
||||
|
||||
private const int FOCUSED_SORTING_ORDER = 10;
|
||||
private const float MAX_FOCUS_SCALE = 1.2f;
|
||||
|
||||
private BoxCollider _choiceCollider;
|
||||
private Canvas _canvas;
|
||||
private int _originalSortingOrder;
|
||||
private Vector3 _startLocalPos;
|
||||
private float _originalScale;
|
||||
private bool _positionCaptured = false;
|
||||
|
||||
private Vector3 _shakeOffset = Vector3.zero;
|
||||
private Vector3 _shakeTarget = Vector3.zero;
|
||||
private float _shakeTimer = 0f;
|
||||
private bool _isShaking = false;
|
||||
|
||||
private bool _disableFloatingEffects = false;
|
||||
|
||||
private Queue<Vector3> _positionHistory = new Queue<Vector3>();
|
||||
private float _historyTimer = 0f;
|
||||
private const float HISTORY_SAMPLE_RATE = 0.016f;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_canvas = GetComponentInParent<Canvas>();
|
||||
if (_canvas != null)
|
||||
_originalSortingOrder = _canvas.sortingOrder;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (_isShaking)
|
||||
UpdateOrganicShake();
|
||||
|
||||
UpdateGhostTrail();
|
||||
}
|
||||
|
||||
private void SetupGhostTrail()
|
||||
{
|
||||
if (!GetEnableGhostTrail())
|
||||
return;
|
||||
|
||||
if (_ghostTextComponent == null && _choiceTextComponent != null)
|
||||
{
|
||||
GameObject ghostObj = new GameObject($"GhostText_{gameObject.name}");
|
||||
ghostObj.transform.SetParent(transform.parent, false);
|
||||
|
||||
ghostObj.transform.localPosition = transform.localPosition;
|
||||
ghostObj.transform.localRotation = transform.localRotation;
|
||||
ghostObj.transform.localScale = transform.localScale;
|
||||
|
||||
ghostObj.layer =gameObject.layer;
|
||||
|
||||
_ghostTextComponent = ghostObj.AddComponent<TextMeshProUGUI>();
|
||||
_ghostTextComponent.font = _choiceTextComponent.font;
|
||||
_ghostTextComponent.fontSize = _choiceTextComponent.fontSize;
|
||||
_ghostTextComponent.alignment = _choiceTextComponent.alignment;
|
||||
_ghostTextComponent.textWrappingMode = _choiceTextComponent.textWrappingMode;
|
||||
_ghostTextComponent.text = _choiceTextComponent.text;
|
||||
|
||||
_ghostTextComponent.rectTransform.sizeDelta = _choiceTextComponent.rectTransform.sizeDelta;
|
||||
_ghostTextComponent.rectTransform.anchorMin = _choiceTextComponent.rectTransform.anchorMin;
|
||||
_ghostTextComponent.rectTransform.anchorMax = _choiceTextComponent.rectTransform.anchorMax;
|
||||
_ghostTextComponent.rectTransform.pivot = _choiceTextComponent.rectTransform.pivot;
|
||||
|
||||
Color ghostColor = _choiceTextComponent.color;
|
||||
ghostColor.a = GetGhostTrailAlpha();
|
||||
_ghostTextComponent.color = ghostColor;
|
||||
|
||||
Canvas ghostCanvas = ghostObj.AddComponent<Canvas>();
|
||||
ghostCanvas.overrideSorting = true;
|
||||
ghostCanvas.sortingOrder = -1;
|
||||
|
||||
_positionHistory.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGhostTrail()
|
||||
{
|
||||
if (!GetEnableGhostTrail() || _ghostTextComponent == null || _choiceTextComponent == null)
|
||||
return;
|
||||
|
||||
_historyTimer += Time.deltaTime;
|
||||
|
||||
if (_historyTimer >= HISTORY_SAMPLE_RATE)
|
||||
{
|
||||
_positionHistory.Enqueue(transform.localPosition);
|
||||
_historyTimer = 0f;
|
||||
|
||||
int maxHistorySize = Mathf.CeilToInt(GetGhostTrailDelay() / HISTORY_SAMPLE_RATE);
|
||||
|
||||
while (_positionHistory.Count > maxHistorySize && _positionHistory.Count > 0)
|
||||
_positionHistory.Dequeue();
|
||||
}
|
||||
|
||||
if (_positionHistory.Count > 0)
|
||||
{
|
||||
Vector3 delayedPosition = _positionHistory.Peek();
|
||||
_ghostTextComponent.transform.localPosition = delayedPosition;
|
||||
_ghostTextComponent.transform.localRotation = transform.localRotation;
|
||||
}
|
||||
else
|
||||
{
|
||||
_ghostTextComponent.transform.localPosition = transform.localPosition;
|
||||
_ghostTextComponent.transform.localRotation = transform.localRotation;
|
||||
}
|
||||
|
||||
_ghostTextComponent.text = _choiceTextComponent.text;
|
||||
_ghostTextComponent.fontSize = _choiceTextComponent.fontSize;
|
||||
_ghostTextComponent.rectTransform.sizeDelta = _choiceTextComponent.rectTransform.sizeDelta;
|
||||
|
||||
Color ghostColor = _choiceTextComponent.color;
|
||||
ghostColor.a = GetGhostTrailAlpha();
|
||||
_ghostTextComponent.color = ghostColor;
|
||||
}
|
||||
|
||||
private void UpdateOrganicShake()
|
||||
{
|
||||
if (!GetEnableOrganicShake())
|
||||
return;
|
||||
|
||||
_shakeTimer += Time.deltaTime * GetShakeSpeed();
|
||||
|
||||
if (_shakeTimer >= 1f)
|
||||
{
|
||||
_shakeTimer = 0f;
|
||||
_shakeTarget = Random.insideUnitSphere * GetShakeRadius();
|
||||
}
|
||||
|
||||
_shakeOffset = Vector3.Lerp(_shakeOffset, _shakeTarget, Time.deltaTime * GetShakeSmoothness());
|
||||
|
||||
if (!_disableFloatingEffects)
|
||||
transform.localPosition = _startLocalPos + _shakeOffset;
|
||||
}
|
||||
|
||||
public Vector3 GetShakeOffset(float convergenceFactor)
|
||||
{
|
||||
return _shakeOffset * convergenceFactor;
|
||||
}
|
||||
|
||||
public void StartOrganicShake()
|
||||
{
|
||||
if (GetEnableOrganicShake())
|
||||
{
|
||||
_isShaking = true;
|
||||
_shakeTimer = 0f;
|
||||
_shakeTarget = Random.insideUnitSphere * GetShakeRadius();
|
||||
}
|
||||
}
|
||||
|
||||
public void StopOrganicShake()
|
||||
{
|
||||
_isShaking = false;
|
||||
_shakeOffset = Vector3.zero;
|
||||
}
|
||||
|
||||
public void HideGhostTrail()
|
||||
{
|
||||
if (_ghostTextComponent != null)
|
||||
_ghostTextComponent.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public void ShowGhostTrail()
|
||||
{
|
||||
if (_ghostTextComponent != null)
|
||||
_ghostTextComponent.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies progressive visual feedback as player focuses on this choice.
|
||||
/// Scales the choice, changes color, optionally moves toward player, and adjusts sorting order.
|
||||
/// Progress ranges from 0 (unfocused) to 1 (fully focused).
|
||||
/// </summary>
|
||||
public void ApplyFocusFeedback(float progress)
|
||||
{
|
||||
if (!_positionCaptured)
|
||||
{
|
||||
_startLocalPos = transform.localPosition;
|
||||
_positionCaptured = true;
|
||||
|
||||
LeanTween.cancel(gameObject);
|
||||
StopOrganicShake();
|
||||
}
|
||||
|
||||
transform.localScale = Vector3.one * Mathf.Lerp(_originalScale, MAX_FOCUS_SCALE, progress);
|
||||
|
||||
bool shouldMoveToPlayer = GetMoveTowardPlayerOnFocus();
|
||||
|
||||
if (shouldMoveToPlayer && Player.Instance != null)
|
||||
{
|
||||
float distance = GetFocusDistanceFromPlayer();
|
||||
Vector3 targetPos = Player.Instance.transform.position
|
||||
+ Player.Instance.transform.forward * distance;
|
||||
transform.position = Vector3.Lerp(transform.position, targetPos, Time.deltaTime * _focusLerpSpeed);
|
||||
}
|
||||
|
||||
if (_choiceTextComponent != null)
|
||||
{
|
||||
_choiceTextComponent.color = Color.Lerp(Color.white, Color.red, progress);
|
||||
|
||||
if (_ghostTextComponent != null)
|
||||
{
|
||||
float ghostAlpha = GetGhostTrailAlpha();
|
||||
Color ghostColor = Color.Lerp(Color.white, Color.red, progress);
|
||||
ghostColor.a = ghostAlpha;
|
||||
_ghostTextComponent.color = ghostColor;
|
||||
}
|
||||
}
|
||||
|
||||
if (_canvas != null && _canvas.sortingOrder != FOCUSED_SORTING_ORDER)
|
||||
{
|
||||
_canvas.sortingOrder = FOCUSED_SORTING_ORDER;
|
||||
_canvas.overrideSorting = true;
|
||||
}
|
||||
}
|
||||
|
||||
private DialogueAnimationSettings GetAnimationSettings()
|
||||
{
|
||||
if (DialogueManager.Instance != null)
|
||||
return DialogueManager.Instance.CurrentAnimationSettings;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool GetEnableGhostTrail()
|
||||
{
|
||||
if (_hasConfig) return _config.EnableGhostTrail;
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
return settings != null && settings.EnableGhostTrail;
|
||||
}
|
||||
|
||||
private float GetGhostTrailAlpha()
|
||||
{
|
||||
if (_hasConfig) return _config.GhostTrailAlpha;
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
return settings != null ? settings.GhostTrailAlpha : 0.3f;
|
||||
}
|
||||
|
||||
private float GetGhostTrailDelay()
|
||||
{
|
||||
if (_hasConfig) return _config.GhostTrailDelay;
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
return settings != null ? settings.GhostTrailDelay : 0.1f;
|
||||
}
|
||||
|
||||
private bool GetEnableOrganicShake()
|
||||
{
|
||||
if (_hasConfig) return _config.EnableOrganicShake;
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
return settings != null && settings.EnableOrganicShake;
|
||||
}
|
||||
|
||||
private float GetShakeRadius()
|
||||
{
|
||||
if (_hasConfig) return _config.ShakeRadius;
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
return settings != null ? settings.ShakeRadius : 0.01f;
|
||||
}
|
||||
|
||||
private float GetShakeSpeed()
|
||||
{
|
||||
if (_hasConfig) return _config.ShakeSpeed;
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
return settings != null ? settings.ShakeSpeed : 1f;
|
||||
}
|
||||
|
||||
private float GetShakeSmoothness()
|
||||
{
|
||||
if (_hasConfig) return _config.ShakeSmoothness;
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
return settings != null ? settings.ShakeSmoothness : 5f;
|
||||
}
|
||||
|
||||
private bool GetMoveTowardPlayerOnFocus()
|
||||
{
|
||||
if (_hasConfig) return _config.MoveTowardPlayerOnFocus;
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
return settings == null || settings.MoveTowardPlayerOnFocus;
|
||||
}
|
||||
|
||||
private float GetFocusDistanceFromPlayer()
|
||||
{
|
||||
if (_hasConfig) return _config.FocusDistanceFromPlayer;
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
return settings != null ? settings.FocusDistanceFromPlayer : _choiceDistanceFromPlayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the choice button with dialogue data.
|
||||
/// Sets up text, creates/configures collider based on text bounds, and caches initial state.
|
||||
/// </summary>
|
||||
public void Setup(DialogueChoiceData dialogueData, FloatingWordsSpawner.SpawnConfig config, bool disableFloatingEffects = false)
|
||||
{
|
||||
DialogueData = dialogueData;
|
||||
_config = config;
|
||||
_hasConfig = true;
|
||||
_disableFloatingEffects = disableFloatingEffects;
|
||||
|
||||
PerformSetup();
|
||||
}
|
||||
|
||||
private void PerformSetup()
|
||||
{
|
||||
if (_choiceTextComponent != null)
|
||||
{
|
||||
_choiceTextComponent.text = GetChoiceText();
|
||||
_choiceTextComponent.ForceMeshUpdate();
|
||||
|
||||
AdjustTextSizeForContent();
|
||||
}
|
||||
|
||||
_choiceCollider = GetComponent<BoxCollider>();
|
||||
if (_choiceCollider == null)
|
||||
{
|
||||
_choiceCollider = gameObject.AddComponent<BoxCollider>();
|
||||
_choiceCollider.isTrigger = true;
|
||||
}
|
||||
|
||||
if (_choiceTextComponent != null)
|
||||
{
|
||||
Vector3 textSize = _choiceTextComponent.bounds.size;
|
||||
_choiceCollider.size = textSize * _colliderSizeMultiplier;
|
||||
_choiceCollider.center = _choiceTextComponent.rectTransform.localPosition;
|
||||
}
|
||||
|
||||
_startLocalPos = transform.localPosition;
|
||||
_originalScale = transform.localScale.x;
|
||||
|
||||
LeanTween.cancel(gameObject);
|
||||
|
||||
SetupGhostTrail();
|
||||
|
||||
if (_ghostTextComponent != null)
|
||||
_ghostTextComponent.text = GetChoiceText();
|
||||
|
||||
StartOrganicShake();
|
||||
}
|
||||
|
||||
private void AdjustTextSizeForContent()
|
||||
{
|
||||
if (_choiceTextComponent == null) return;
|
||||
|
||||
string text = GetChoiceText();
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
|
||||
int wordCount = text.Split(new[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries).Length;
|
||||
|
||||
float maxWidth = GetMaxWidthForWordCount(wordCount);
|
||||
|
||||
string longestWord = GetLongestWord(text);
|
||||
Vector2 longestWordSize = _choiceTextComponent.GetPreferredValues(longestWord, float.MaxValue, 0f);
|
||||
|
||||
if (longestWordSize.x > maxWidth)
|
||||
maxWidth = longestWordSize.x + 10f;
|
||||
|
||||
_choiceTextComponent.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, maxWidth);
|
||||
_choiceTextComponent.ForceMeshUpdate();
|
||||
|
||||
Vector2 textSize = _choiceTextComponent.GetPreferredValues(text, maxWidth, 0f);
|
||||
float finalHeight = Mathf.Max(textSize.y, _minTextHeight);
|
||||
|
||||
Vector2 newSize = new Vector2(maxWidth, finalHeight);
|
||||
|
||||
RectTransform parentRect = transform as RectTransform;
|
||||
if (parentRect != null)
|
||||
parentRect.sizeDelta = newSize;
|
||||
|
||||
ConfigureTextComponent(_choiceTextComponent, newSize);
|
||||
|
||||
if (_ghostTextComponent != null)
|
||||
ConfigureTextComponent(_ghostTextComponent, newSize);
|
||||
}
|
||||
|
||||
private void ConfigureTextComponent(TextMeshProUGUI textComponent, Vector2 size)
|
||||
{
|
||||
textComponent.textWrappingMode = TextWrappingModes.Normal;
|
||||
textComponent.overflowMode = TextOverflowModes.Overflow;
|
||||
textComponent.rectTransform.anchorMin = Vector2.one * 0.5f;
|
||||
textComponent.rectTransform.anchorMax = Vector2.one * 0.5f;
|
||||
textComponent.rectTransform.pivot = Vector2.one * 0.5f;
|
||||
textComponent.rectTransform.anchoredPosition = Vector2.zero;
|
||||
textComponent.rectTransform.sizeDelta = size;
|
||||
textComponent.ForceMeshUpdate();
|
||||
}
|
||||
|
||||
private string GetLongestWord(string text)
|
||||
{
|
||||
string[] words = text.Split(new[] { ' ' }, System.StringSplitOptions.RemoveEmptyEntries);
|
||||
string longest = "";
|
||||
|
||||
foreach (string word in words)
|
||||
{
|
||||
if (word.Length > longest.Length)
|
||||
longest = word;
|
||||
}
|
||||
|
||||
return longest;
|
||||
}
|
||||
|
||||
private float GetMaxWidthForWordCount(int wordCount)
|
||||
{
|
||||
if (wordCount <= 2)
|
||||
return _shortTextMaxWidth;
|
||||
else if (wordCount <= 5)
|
||||
return _mediumTextMaxWidth;
|
||||
else if (wordCount <= 10)
|
||||
return _longTextMaxWidth;
|
||||
else
|
||||
return _veryLongTextMaxWidth;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Smoothly returns the choice to its original state after losing focus.
|
||||
/// Animates position back to start, resets scale/color, and unlocks the focus system when complete.
|
||||
/// </summary>
|
||||
public void ResetFeedback(float duration)
|
||||
{
|
||||
LeanTween.cancel(gameObject);
|
||||
StopOrganicShake();
|
||||
|
||||
LeanTween.moveLocal(gameObject, _startLocalPos, duration)
|
||||
.setEase(LeanTweenType.easeOutQuad)
|
||||
.setOnComplete(() => {
|
||||
_positionCaptured = false;
|
||||
|
||||
if (DialogueManager.Instance != null)
|
||||
DialogueManager.Instance.UnlockFocus();
|
||||
|
||||
StartOrganicShake();
|
||||
});
|
||||
|
||||
transform.localScale = Vector3.one;
|
||||
|
||||
if (_choiceTextComponent != null)
|
||||
{
|
||||
_choiceTextComponent.color = Color.white;
|
||||
|
||||
if (_ghostTextComponent != null)
|
||||
{
|
||||
float ghostAlpha = GetGhostTrailAlpha();
|
||||
Color ghostColor = Color.white;
|
||||
ghostColor.a = ghostAlpha;
|
||||
_ghostTextComponent.color = ghostColor;
|
||||
}
|
||||
}
|
||||
|
||||
if (_canvas != null)
|
||||
_canvas.sortingOrder = _originalSortingOrder;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abd0f63a44b2272448eb41767c2ce44e
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using Unheimlich.Data.Dialogues;
|
||||
using Unheimlich.Gameplay.Player;
|
||||
|
||||
namespace Unheimlich.Interactions.FloatingWords
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility class for spawning floating 3D text choices in world space.
|
||||
/// Used for both dialogue choices and object descriptions. Handles positioning,
|
||||
/// rotation, animations (spawn, merge, floating) using LeanTween for smooth effects.
|
||||
/// </summary>
|
||||
public static class FloatingWordsSpawner
|
||||
{
|
||||
private const float ROTATOR_SCALE = 0.01f;
|
||||
|
||||
/// <summary>
|
||||
/// Internal configuration struct to unify settings from different sources.
|
||||
/// </summary>
|
||||
public struct SpawnConfig
|
||||
{
|
||||
public float SpawnRadius;
|
||||
public float SpawnHeightOffset;
|
||||
public RotationAxis RotationAxis;
|
||||
public bool EnableOrganicShake;
|
||||
public float ForwardOffset;
|
||||
public float TiltAngle;
|
||||
public float RotationSpeed;
|
||||
public bool EnableRotation;
|
||||
public ChoiceSpawnMode SpawnMode;
|
||||
public bool EnableFloating;
|
||||
public float MergeDuration;
|
||||
public float MergeScalePulse;
|
||||
|
||||
public bool EnableGhostTrail;
|
||||
public float GhostTrailAlpha;
|
||||
public float GhostTrailDelay;
|
||||
public float ShakeRadius;
|
||||
public float ShakeSpeed;
|
||||
public float ShakeSmoothness;
|
||||
public bool MoveTowardPlayerOnFocus;
|
||||
public float FocusDistanceFromPlayer;
|
||||
public bool DisableFloatingEffects;
|
||||
}
|
||||
|
||||
private static SpawnConfig GetConfig(ObjectDescriptionSettings settings)
|
||||
{
|
||||
return new SpawnConfig
|
||||
{
|
||||
SpawnRadius = settings.SpawnRadius,
|
||||
SpawnHeightOffset = settings.SpawnHeightOffset,
|
||||
RotationAxis = settings.RotationAxis,
|
||||
EnableOrganicShake = settings.EnableOrganicShake,
|
||||
ForwardOffset = settings.ForwardOffset,
|
||||
TiltAngle = settings.TiltAngle,
|
||||
RotationSpeed = settings.RotationSpeed,
|
||||
EnableRotation = settings.EnableRotation,
|
||||
SpawnMode = settings.SpawnMode,
|
||||
EnableFloating = settings.EnableFloating,
|
||||
MergeDuration = settings.MergeDuration,
|
||||
MergeScalePulse = settings.MergeScalePulse,
|
||||
|
||||
EnableGhostTrail = settings.EnableGhostTrail,
|
||||
GhostTrailAlpha = settings.GhostTrailAlpha,
|
||||
GhostTrailDelay = settings.GhostTrailDelay,
|
||||
ShakeRadius = settings.ShakeRadius,
|
||||
ShakeSpeed = settings.ShakeSpeed,
|
||||
ShakeSmoothness = settings.ShakeSmoothness,
|
||||
MoveTowardPlayerOnFocus = false,
|
||||
FocusDistanceFromPlayer = 1f,
|
||||
DisableFloatingEffects = true
|
||||
};
|
||||
}
|
||||
|
||||
private static SpawnConfig GetConfig(DialogueAnimationSettings settings)
|
||||
{
|
||||
return new SpawnConfig
|
||||
{
|
||||
SpawnRadius = settings.SpawnRadius,
|
||||
SpawnHeightOffset = settings.SpawnHeightOffset,
|
||||
RotationAxis = settings.RotationAxis,
|
||||
EnableOrganicShake = settings.EnableOrganicShake,
|
||||
ForwardOffset = 0f,
|
||||
TiltAngle = 0f,
|
||||
RotationSpeed = settings.NormalRotationSpeed,
|
||||
EnableRotation = settings.EnableRotation,
|
||||
SpawnMode = settings.SpawnMode,
|
||||
EnableFloating = settings.EnableFloating,
|
||||
MergeDuration = 0.3f,
|
||||
MergeScalePulse = 1.2f,
|
||||
|
||||
EnableGhostTrail = settings.EnableGhostTrail,
|
||||
GhostTrailAlpha = settings.GhostTrailAlpha,
|
||||
GhostTrailDelay = settings.GhostTrailDelay,
|
||||
ShakeRadius = settings.ShakeRadius,
|
||||
ShakeSpeed = settings.ShakeSpeed,
|
||||
ShakeSmoothness = settings.ShakeSmoothness,
|
||||
MoveTowardPlayerOnFocus = settings.MoveTowardPlayerOnFocus,
|
||||
FocusDistanceFromPlayer = settings.FocusDistanceFromPlayer,
|
||||
DisableFloatingEffects = false
|
||||
};
|
||||
}
|
||||
|
||||
public static List<ChoiceFocusHandler> SpawnByMode(List<DialogueChoiceData> nodeChoices, ChoiceFocusHandler prefab, Transform container, DialogueAnimationSettings settings, out Vector3[] originalPositions)
|
||||
{
|
||||
return SpawnByMode(nodeChoices, prefab, container, GetConfig(settings), out originalPositions);
|
||||
}
|
||||
|
||||
public static List<ChoiceFocusHandler> SpawnByMode(List<DialogueChoiceData> choices, ChoiceFocusHandler prefab, Transform container, ObjectDescriptionSettings settings, out Vector3[] originalPositions)
|
||||
{
|
||||
return SpawnByMode(choices, prefab, container, GetConfig(settings), out originalPositions);
|
||||
}
|
||||
|
||||
private static List<ChoiceFocusHandler> SpawnByMode(List<DialogueChoiceData> nodeChoices, ChoiceFocusHandler prefab, Transform container, SpawnConfig config, out Vector3[] originalPositions)
|
||||
{
|
||||
switch (config.SpawnMode)
|
||||
{
|
||||
case ChoiceSpawnMode.AroundAnchor:
|
||||
return SpawnWordsInCircle(nodeChoices, prefab, container, config, out originalPositions);
|
||||
case ChoiceSpawnMode.AtAnchorPosition:
|
||||
originalPositions = new Vector3[0];
|
||||
return SpawnAtAnchor(nodeChoices, prefab, container, config);
|
||||
case ChoiceSpawnMode.InFrontOfPlayer:
|
||||
originalPositions = new Vector3[0];
|
||||
return SpawnInFrontOfPlayer(nodeChoices, prefab, container, config);
|
||||
case ChoiceSpawnMode.CircleAroundPlayer:
|
||||
return SpawnAroundPlayer(nodeChoices, prefab, container, config, out originalPositions);
|
||||
default:
|
||||
return SpawnWordsInCircle(nodeChoices, prefab, container, config, out originalPositions);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ChoiceFocusHandler> SpawnWordsInCircle(List<DialogueChoiceData> choices, ChoiceFocusHandler wordPrefab, Transform parent, SpawnConfig config, out Vector3[] originalPositions)
|
||||
{
|
||||
List<ChoiceFocusHandler> words = new List<ChoiceFocusHandler>();
|
||||
originalPositions = new Vector3[choices.Count];
|
||||
float angleStep = 360f / choices.Count;
|
||||
|
||||
for (int i = 0; i < choices.Count; i++)
|
||||
{
|
||||
float angle = i * angleStep * Mathf.Deg2Rad;
|
||||
Vector3 localOffset = CalculateCircularPosition(angle, config.SpawnRadius, config.RotationAxis, config.SpawnHeightOffset);
|
||||
originalPositions[i] = localOffset;
|
||||
|
||||
ChoiceFocusHandler word = Object.Instantiate(wordPrefab, parent);
|
||||
word.transform.localPosition = localOffset;
|
||||
word.transform.localRotation = Quaternion.identity;
|
||||
|
||||
CanvasGroup canvasGroup = word.GetComponent<CanvasGroup>();
|
||||
if (canvasGroup == null)
|
||||
canvasGroup = word.gameObject.AddComponent<CanvasGroup>();
|
||||
|
||||
canvasGroup.alpha = 1f;
|
||||
|
||||
word.Setup(choices[i], config, config.DisableFloatingEffects);
|
||||
|
||||
if (config.EnableOrganicShake) word.StartOrganicShake();
|
||||
|
||||
words.Add(word);
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
private static List<ChoiceFocusHandler> SpawnAtAnchor(List<DialogueChoiceData> choices, ChoiceFocusHandler prefab, Transform parent, SpawnConfig config)
|
||||
{
|
||||
List<ChoiceFocusHandler> words = new List<ChoiceFocusHandler>();
|
||||
for (int i = 0; i < choices.Count; i++)
|
||||
{
|
||||
ChoiceFocusHandler word = Object.Instantiate(prefab, parent);
|
||||
word.transform.localPosition = Vector3.up * i * 50f;
|
||||
word.transform.localRotation = Quaternion.identity;
|
||||
word.Setup(choices[i], config);
|
||||
words.Add(word);
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
private static List<ChoiceFocusHandler> SpawnInFrontOfPlayer(List<DialogueChoiceData> choices, ChoiceFocusHandler prefab, Transform parent, SpawnConfig config)
|
||||
{
|
||||
List<ChoiceFocusHandler> words = new List<ChoiceFocusHandler>();
|
||||
if (Player.Instance == null) return words;
|
||||
|
||||
Vector3 playerForward = Player.Instance.transform.forward;
|
||||
for (int i = 0; i < choices.Count; i++)
|
||||
{
|
||||
ChoiceFocusHandler word = Object.Instantiate(prefab, parent);
|
||||
Vector3 offset = playerForward * 2f + Vector3.up * (i - choices.Count / 2f) * (config.SpawnRadius / choices.Count / 100f);
|
||||
word.transform.position = Player.Instance.transform.position + offset;
|
||||
word.transform.LookAt(Player.Instance.transform);
|
||||
word.Setup(choices[i], config);
|
||||
words.Add(word);
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
private static List<ChoiceFocusHandler> SpawnAroundPlayer(List<DialogueChoiceData> choices, ChoiceFocusHandler prefab, Transform parent, SpawnConfig config, out Vector3[] originalPositions)
|
||||
{
|
||||
List<ChoiceFocusHandler> words = new List<ChoiceFocusHandler>();
|
||||
originalPositions = new Vector3[choices.Count];
|
||||
if (Player.Instance == null) return words;
|
||||
|
||||
parent.position = Player.Instance.transform.position;
|
||||
parent.rotation = Quaternion.identity;
|
||||
|
||||
float angleStep = 360f / choices.Count;
|
||||
for (int i = 0; i < choices.Count; i++)
|
||||
{
|
||||
float angle = i * angleStep * Mathf.Deg2Rad;
|
||||
Vector3 localOffset = new Vector3(Mathf.Cos(angle) * config.SpawnRadius, config.SpawnHeightOffset, Mathf.Sin(angle) * config.SpawnRadius);
|
||||
originalPositions[i] = localOffset;
|
||||
|
||||
ChoiceFocusHandler word = Object.Instantiate(prefab, parent);
|
||||
word.transform.localPosition = localOffset;
|
||||
word.transform.LookAt(Player.Instance.transform);
|
||||
word.Setup(choices[i], config);
|
||||
words.Add(word);
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
public static void MergeWords(List<ChoiceFocusHandler> words, ObjectDescriptionSettings settings, System.Action onMergeComplete = null)
|
||||
{
|
||||
MergeWords(words, GetConfig(settings), onMergeComplete);
|
||||
}
|
||||
|
||||
private static void MergeWords(List<ChoiceFocusHandler> words, SpawnConfig config, System.Action onMergeComplete)
|
||||
{
|
||||
for (int i = 0; i < words.Count; i++)
|
||||
{
|
||||
if (words[i] != null)
|
||||
{
|
||||
words[i].StopOrganicShake();
|
||||
words[i].HideGhostTrail();
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i < words.Count; i++)
|
||||
{
|
||||
if (words[i] != null)
|
||||
{
|
||||
CanvasGroup canvasGroup = words[i].GetComponent<CanvasGroup>();
|
||||
if (canvasGroup != null)
|
||||
{
|
||||
LeanTween.value(words[i].gameObject, 1f, 0f, config.MergeDuration)
|
||||
.setOnUpdate((float val) => { canvasGroup.alpha = val; })
|
||||
.setEaseInQuad();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (words.Count > 0 && words[0] != null)
|
||||
{
|
||||
LeanTween.scale(words[0].gameObject, Vector3.one * config.MergeScalePulse, config.MergeDuration / 2f)
|
||||
.setEaseOutBack()
|
||||
.setOnComplete(() =>
|
||||
{
|
||||
if (words.Count > 0 && words[0] != null)
|
||||
{
|
||||
LeanTween.scale(words[0].gameObject, Vector3.one, config.MergeDuration / 2f)
|
||||
.setEaseInBack()
|
||||
.setOnComplete(() => onMergeComplete?.Invoke());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void UnmergeWords(List<ChoiceFocusHandler> words, ObjectDescriptionSettings settings)
|
||||
{
|
||||
UnmergeWords(words, GetConfig(settings));
|
||||
}
|
||||
|
||||
private static void UnmergeWords(List<ChoiceFocusHandler> words, SpawnConfig config)
|
||||
{
|
||||
for (int i = 0; i < words.Count; i++)
|
||||
{
|
||||
if (words[i] != null)
|
||||
{
|
||||
words[i].StartOrganicShake();
|
||||
words[i].ShowGhostTrail();
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i < words.Count; i++)
|
||||
{
|
||||
if (words[i] != null)
|
||||
{
|
||||
LeanTween.cancel(words[i].gameObject);
|
||||
CanvasGroup canvasGroup = words[i].GetComponent<CanvasGroup>();
|
||||
if (canvasGroup != null)
|
||||
{
|
||||
LeanTween.value(words[i].gameObject, canvasGroup.alpha, 1f, config.MergeDuration)
|
||||
.setOnUpdate((float val) => { canvasGroup.alpha = val; })
|
||||
.setEaseOutQuad();
|
||||
}
|
||||
words[i].transform.localScale = Vector3.one;
|
||||
}
|
||||
}
|
||||
|
||||
if (words.Count > 0 && words[0] != null)
|
||||
{
|
||||
LeanTween.cancel(words[0].gameObject);
|
||||
words[0].transform.localScale = Vector3.one;
|
||||
}
|
||||
}
|
||||
|
||||
#region For obj description
|
||||
public static GameObject CreateWordsContainer(string containerName, Transform anchor, ObjectDescriptionSettings settings)
|
||||
{
|
||||
SpawnConfig config = GetConfig(settings);
|
||||
GameObject rotator = new GameObject($"{containerName}_Rotator");
|
||||
rotator.transform.localPosition = Vector3.forward * config.ForwardOffset;
|
||||
rotator.transform.localRotation = Quaternion.Euler(config.TiltAngle, 0f, 0f);
|
||||
rotator.transform.localScale = Vector3.one * ROTATOR_SCALE;
|
||||
|
||||
Canvas containerCanvas = rotator.AddComponent<Canvas>();
|
||||
containerCanvas.renderMode = RenderMode.WorldSpace;
|
||||
|
||||
return rotator;
|
||||
}
|
||||
|
||||
|
||||
public static void UpdateContainerRotation(GameObject container, GameObject rotator, Transform playerTransform, ObjectDescriptionSettings settings)
|
||||
{
|
||||
if (container == null || playerTransform == null) return;
|
||||
|
||||
Vector3 lookAtPosition = playerTransform.position;
|
||||
lookAtPosition.y = container.transform.position.y;
|
||||
container.transform.LookAt(lookAtPosition);
|
||||
|
||||
if (!settings.EnableRotation || rotator == null) return;
|
||||
|
||||
Vector3 rotationAxis = GetRotationAxisVector(settings.RotationAxis);
|
||||
float rotationAmount = (360f / settings.RotationSpeed) * Time.deltaTime;
|
||||
rotator.transform.Rotate(rotationAxis, rotationAmount, Space.Self);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Tools
|
||||
private static Vector3 CalculateCircularPosition(float angleRad, float radius, RotationAxis axis, float heightOffset)
|
||||
{
|
||||
switch (axis)
|
||||
{
|
||||
case RotationAxis.Forward:
|
||||
return new Vector3(Mathf.Cos(angleRad) * radius, Mathf.Sin(angleRad) * radius + heightOffset, 0f);
|
||||
case RotationAxis.Up:
|
||||
return new Vector3(Mathf.Cos(angleRad) * radius, heightOffset, Mathf.Sin(angleRad) * radius);
|
||||
case RotationAxis.Right:
|
||||
return new Vector3(0f, Mathf.Cos(angleRad) * radius + heightOffset, Mathf.Sin(angleRad) * radius);
|
||||
default:
|
||||
return new Vector3(Mathf.Cos(angleRad) * radius, heightOffset, Mathf.Sin(angleRad) * radius);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3 GetRotationAxisVector(RotationAxis axis)
|
||||
{
|
||||
switch (axis)
|
||||
{
|
||||
case RotationAxis.Forward: return Vector3.forward;
|
||||
case RotationAxis.Up: return Vector3.up;
|
||||
case RotationAxis.Right: return Vector3.right;
|
||||
default: return Vector3.up;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b52b115a50463d46b645f5579cf22fd
|
||||
@@ -0,0 +1,87 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Managers;
|
||||
|
||||
namespace Unheimlich.Interactions.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects interactable objects in front of the player camera using raycasts.
|
||||
/// Triggers interaction when player presses interact button while looking at an interactable.
|
||||
/// </summary>
|
||||
public class InteractionHandler : MonoBehaviour
|
||||
{
|
||||
public static InteractionHandler Instance { get; private set; }
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private float _interactionRange = 1f;
|
||||
[SerializeField] private LayerMask _interactableLayers;
|
||||
[SerializeField] private float _raycastInterval = 0.1f;
|
||||
|
||||
private InteractableBase _currentInteractable;
|
||||
private Camera _playerCamera;
|
||||
private float _raycastTimer;
|
||||
|
||||
private static readonly Vector3 ViewportCenter = new Vector3(0.5f, 0.5f, 0f);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
_playerCamera = GetComponent<Camera>();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
_raycastTimer += Time.deltaTime;
|
||||
|
||||
if (_raycastTimer >= _raycastInterval)
|
||||
{
|
||||
CheckForInteractable();
|
||||
_raycastTimer = 0f;
|
||||
}
|
||||
|
||||
HandleInteractionInput();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs raycast from camera center to detect interactable objects within range.
|
||||
/// Updates the current interactable reference for input handling.
|
||||
/// </summary>
|
||||
private void CheckForInteractable()
|
||||
{
|
||||
Ray ray = _playerCamera.ViewportPointToRay(ViewportCenter);
|
||||
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, _interactionRange, _interactableLayers))
|
||||
{
|
||||
InteractableBase hitInteractable = hit.collider.GetComponent<InteractableBase>();
|
||||
_currentInteractable = hitInteractable;
|
||||
}
|
||||
else
|
||||
_currentInteractable = null;
|
||||
}
|
||||
|
||||
private void HandleInteractionInput()
|
||||
{
|
||||
if (PlayerInputManager.Instance != null &&
|
||||
PlayerInputManager.Instance.InteractAction.action.WasPressedThisFrame())
|
||||
{
|
||||
if (_currentInteractable != null)
|
||||
_currentInteractable.Interact(transform.parent.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (_playerCamera != null)
|
||||
{
|
||||
Gizmos.color = _currentInteractable != null ? Color.green : Color.red;
|
||||
|
||||
Ray ray = _playerCamera.ViewportPointToRay(ViewportCenter);
|
||||
Gizmos.DrawRay(ray.origin, ray.direction * _interactionRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d96ceb0ee513b1e4197e55e9b926269b
|
||||
@@ -0,0 +1,17 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unheimlich.Interactions.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all interactable objects in the game.
|
||||
/// Derived classes must implement Interact() to define interaction behavior.
|
||||
/// </summary>
|
||||
public abstract class InteractableBase : MonoBehaviour
|
||||
{
|
||||
[Header("Interaction Settings")]
|
||||
[Tooltip("Text displayed when the player looks at the object")]
|
||||
public string InteractionText = "Interact";
|
||||
|
||||
public abstract void Interact(GameObject gameObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2bd6cb9667f8674294172efbdf8afd3
|
||||
@@ -0,0 +1,304 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using Unheimlich.Data.Dialogues;
|
||||
using Unheimlich.Gameplay.Player;
|
||||
|
||||
namespace Unheimlich.Interactions.FloatingWords
|
||||
{
|
||||
public class ObjectDescriptionTrigger : MonoBehaviour
|
||||
{
|
||||
[Header("Description")]
|
||||
[SerializeField] private string _descriptionText = "Mysterious Object";
|
||||
|
||||
[Header("Settings")]
|
||||
[SerializeField] private ObjectDescriptionSettings _settings;
|
||||
|
||||
[Header("Anchor")]
|
||||
[SerializeField] private Transform _descriptionAnchor;
|
||||
|
||||
[Header("Prefab")]
|
||||
[SerializeField] private ChoiceFocusHandler _wordPrefab;
|
||||
|
||||
private Transform _playerTransform;
|
||||
private Camera _mainCamera;
|
||||
private List<ChoiceFocusHandler> _spawnedWords = new List<ChoiceFocusHandler>();
|
||||
private GameObject _wordsContainer;
|
||||
private GameObject _wordsRotator;
|
||||
private bool _isActive = false;
|
||||
private bool _isMerged = false;
|
||||
private bool _hasBeenMergedOnce = false;
|
||||
private bool _hasCompletedMergeOnce = false;
|
||||
private Vector3[] _originalSpawnPositions;
|
||||
private Vector3[] _basePositions;
|
||||
private float _currentConvergenceRadius;
|
||||
|
||||
public bool IsAnimationActive => _isActive && (!_settings.MergeOnce || !_hasCompletedMergeOnce);
|
||||
public bool IsInMergeOnceMode => _settings != null && _settings.MergeOnce;
|
||||
|
||||
void Start()
|
||||
{
|
||||
_playerTransform = Player.Instance.transform;
|
||||
_mainCamera = Camera.main;
|
||||
|
||||
if (_playerTransform == null)
|
||||
Debug.LogWarning($"[ObjectDescription] No player found for {gameObject.name}");
|
||||
|
||||
if (_descriptionAnchor == null)
|
||||
_descriptionAnchor = transform;
|
||||
|
||||
if (_settings == null)
|
||||
{
|
||||
Debug.LogError($"[ObjectDescription] No settings assigned for {gameObject.name}");
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (_playerTransform == null || _settings == null) return;
|
||||
|
||||
float distanceToPlayer = Vector3.Distance(_playerTransform.position, transform.position);
|
||||
|
||||
if (!_isActive && distanceToPlayer <= _settings.ActivationDistance)
|
||||
{
|
||||
if (!_settings.MergeOnce || !_hasCompletedMergeOnce)
|
||||
SpawnWords();
|
||||
}
|
||||
else if (_isActive && distanceToPlayer >= _settings.DeactivationDistance)
|
||||
DespawnWords();
|
||||
|
||||
if (_isActive)
|
||||
{
|
||||
if (!_settings.MergeOnce || !_hasBeenMergedOnce)
|
||||
{
|
||||
UpdateWordConvergence();
|
||||
UpdateContainerRotation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnWords()
|
||||
{
|
||||
if (_wordPrefab == null)
|
||||
{
|
||||
Debug.LogError($"[ObjectDescription] No word prefab assigned for {gameObject.name}");
|
||||
return;
|
||||
}
|
||||
|
||||
_isActive = true;
|
||||
_isMerged = false;
|
||||
_currentConvergenceRadius = _settings.SpawnRadius;
|
||||
|
||||
_wordsContainer = new GameObject($"{_descriptionText}_Container");
|
||||
_wordsContainer.transform.position = _descriptionAnchor.position;
|
||||
_wordsContainer.transform.rotation = _descriptionAnchor.rotation;
|
||||
|
||||
_wordsRotator = FloatingWordsSpawner.CreateWordsContainer(_descriptionText, _descriptionAnchor, _settings);
|
||||
_wordsRotator.transform.SetParent(_wordsContainer.transform, false);
|
||||
|
||||
ObjectDescriptionTriggerReference triggerRef = _wordsRotator.AddComponent<ObjectDescriptionTriggerReference>();
|
||||
triggerRef.Trigger = this;
|
||||
|
||||
List<DialogueChoiceData> dummyChoices = new List<DialogueChoiceData>();
|
||||
for (int i = 0; i < _settings.WordCount; i++)
|
||||
{
|
||||
dummyChoices.Add(new DialogueChoiceData
|
||||
{
|
||||
ChoiceText = _descriptionText,
|
||||
NextNode = null
|
||||
});
|
||||
}
|
||||
|
||||
_spawnedWords = FloatingWordsSpawner.SpawnByMode(
|
||||
dummyChoices,
|
||||
_wordPrefab,
|
||||
_wordsRotator.transform,
|
||||
_settings,
|
||||
out _originalSpawnPositions
|
||||
);
|
||||
|
||||
_basePositions = new Vector3[_spawnedWords.Count];
|
||||
for (int i = 0; i < _spawnedWords.Count; i++)
|
||||
_basePositions[i] = _spawnedWords[i].transform.localPosition;
|
||||
}
|
||||
|
||||
private void DespawnWords()
|
||||
{
|
||||
_isActive = false;
|
||||
_isMerged = false;
|
||||
|
||||
foreach (var word in _spawnedWords)
|
||||
{
|
||||
if (word != null)
|
||||
Destroy(word.gameObject);
|
||||
}
|
||||
|
||||
_spawnedWords.Clear();
|
||||
|
||||
if (_wordsContainer != null)
|
||||
Destroy(_wordsContainer);
|
||||
|
||||
// Debug.Log($"[ObjectDescription] Despawned words for '{descriptionText}'");
|
||||
}
|
||||
|
||||
private void UpdateWordConvergence()
|
||||
{
|
||||
if (_mainCamera == null || _spawnedWords.Count == 0) return;
|
||||
|
||||
Vector2 screenCenter = new Vector2(Screen.width / 2f, Screen.height / 2f);
|
||||
Ray ray = _mainCamera.ScreenPointToRay(screenCenter);
|
||||
RaycastHit hit;
|
||||
|
||||
int layerMask = ~LayerMask.GetMask("Words", "UI");
|
||||
|
||||
bool isHoveringObject = false;
|
||||
|
||||
if (Physics.Raycast(ray, out hit, 100f, layerMask))
|
||||
{
|
||||
if (hit.collider.gameObject == gameObject || hit.collider.transform.IsChildOf(transform))
|
||||
isHoveringObject = true;
|
||||
}
|
||||
|
||||
float targetRadius = isHoveringObject ? _settings.MinConvergenceRadius : _settings.SpawnRadius;
|
||||
_currentConvergenceRadius = Mathf.Lerp(_currentConvergenceRadius, targetRadius, Time.deltaTime * _settings.ConvergenceSpeed);
|
||||
|
||||
if (isHoveringObject && !_isMerged && _currentConvergenceRadius <= _settings.MinConvergenceRadius * 5f)
|
||||
MergeWords();
|
||||
else if (!isHoveringObject && _isMerged)
|
||||
{
|
||||
if (!_settings.MergeOnce || !_hasBeenMergedOnce)
|
||||
UnmergeWords();
|
||||
}
|
||||
|
||||
if (_isMerged) return;
|
||||
|
||||
float convergenceFactor = Mathf.InverseLerp(0f, _settings.SpawnRadius, _currentConvergenceRadius);
|
||||
|
||||
for (int i = 0; i < _spawnedWords.Count; i++)
|
||||
{
|
||||
if (_spawnedWords[i] == null) continue;
|
||||
|
||||
Vector3 targetLocalPos = GetConvergenceTargetPosition(_originalSpawnPositions[i], _currentConvergenceRadius);
|
||||
|
||||
_basePositions[i] = Vector3.Lerp(
|
||||
_basePositions[i],
|
||||
targetLocalPos,
|
||||
Time.deltaTime * _settings.ConvergenceSpeed
|
||||
);
|
||||
|
||||
_spawnedWords[i].transform.localPosition = _basePositions[i] + _spawnedWords[i].GetShakeOffset(convergenceFactor);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 GetConvergenceTargetPosition(Vector3 originalPosition, float radius)
|
||||
{
|
||||
if (radius < 0.01f) return Vector3.zero;
|
||||
|
||||
switch (_settings.RotationAxis)
|
||||
{
|
||||
case RotationAxis.Forward:
|
||||
Vector2 xyNormalized = new Vector2(originalPosition.x, originalPosition.y - _settings.SpawnHeightOffset).normalized;
|
||||
return new Vector3(
|
||||
xyNormalized.x * radius,
|
||||
xyNormalized.y * radius + _settings.SpawnHeightOffset,
|
||||
originalPosition.z
|
||||
);
|
||||
|
||||
case RotationAxis.Up:
|
||||
Vector2 xzNormalized = new Vector2(originalPosition.x, originalPosition.z).normalized;
|
||||
return new Vector3(
|
||||
xzNormalized.x * radius,
|
||||
originalPosition.y,
|
||||
xzNormalized.y * radius
|
||||
);
|
||||
|
||||
case RotationAxis.Right:
|
||||
Vector2 yzNormalized = new Vector2(originalPosition.y - _settings.SpawnHeightOffset, originalPosition.z).normalized;
|
||||
return new Vector3(
|
||||
originalPosition.x,
|
||||
yzNormalized.x * radius + _settings.SpawnHeightOffset,
|
||||
yzNormalized.y * radius
|
||||
);
|
||||
|
||||
default:
|
||||
return originalPosition.normalized * radius;
|
||||
}
|
||||
}
|
||||
|
||||
private void MergeWords()
|
||||
{
|
||||
if (_isMerged) return;
|
||||
|
||||
_isMerged = true;
|
||||
_hasBeenMergedOnce = true;
|
||||
|
||||
FloatingWordsSpawner.MergeWords(_spawnedWords, _settings, () =>
|
||||
{
|
||||
if (_settings.MergeOnce)
|
||||
{
|
||||
LeanTween.delayedCall(_settings.MergeOnceDisplayDuration, () =>
|
||||
{
|
||||
FadeOutAndComplete();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void FadeOutAndComplete()
|
||||
{
|
||||
if (_spawnedWords.Count > 0 && _spawnedWords[0] != null)
|
||||
{
|
||||
CanvasGroup canvasGroup = _spawnedWords[0].GetComponent<CanvasGroup>();
|
||||
if (canvasGroup != null)
|
||||
{
|
||||
LeanTween.value(_spawnedWords[0].gameObject, 1f, 0f, _settings.MergeDuration)
|
||||
.setOnUpdate((float val) => { canvasGroup.alpha = val; })
|
||||
.setEaseInQuad()
|
||||
.setOnComplete(() =>
|
||||
{
|
||||
_hasCompletedMergeOnce = true;
|
||||
DespawnWords();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UnmergeWords()
|
||||
{
|
||||
if (!_isMerged) return;
|
||||
|
||||
_isMerged = false;
|
||||
|
||||
FloatingWordsSpawner.UnmergeWords(_spawnedWords, _settings);
|
||||
}
|
||||
|
||||
private void UpdateContainerRotation()
|
||||
{
|
||||
FloatingWordsSpawner.UpdateContainerRotation(_wordsContainer, _wordsRotator, _playerTransform, _settings);
|
||||
}
|
||||
|
||||
public ObjectDescriptionSettings GetSettings()
|
||||
{
|
||||
return _settings;
|
||||
}
|
||||
|
||||
void OnDrawGizmosSelected()
|
||||
{
|
||||
if (_settings == null) return;
|
||||
|
||||
Transform anchor = _descriptionAnchor != null ? _descriptionAnchor : transform;
|
||||
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireSphere(transform.position, _settings.ActivationDistance);
|
||||
|
||||
Gizmos.color = Color.red;
|
||||
Gizmos.DrawWireSphere(transform.position, _settings.DeactivationDistance);
|
||||
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawWireSphere(anchor.position, _settings.SpawnRadius);
|
||||
|
||||
Gizmos.color = Color.cyan;
|
||||
Gizmos.DrawWireSphere(anchor.position, _settings.MinConvergenceRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1fe462730ebdae488f89f5868a4c74c
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unheimlich.Interactions.FloatingWords
|
||||
{
|
||||
public class ObjectDescriptionTriggerReference : MonoBehaviour
|
||||
{
|
||||
public ObjectDescriptionTrigger Trigger;
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ddb0acc42d86604c9dbf91872a216d9
|
||||
@@ -0,0 +1,142 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using Unheimlich.Gameplay.Items;
|
||||
using InputManager = Unheimlich.Managers.PlayerInputManager;
|
||||
|
||||
namespace Unheimlich.Interactions.Core
|
||||
{
|
||||
public class PlayerInspectionHandler : MonoBehaviour
|
||||
{
|
||||
public static PlayerInspectionHandler Instance { get; private set; }
|
||||
|
||||
[Header("Components")]
|
||||
[SerializeField] private Transform _inspectionHoldPoint;
|
||||
|
||||
[Header("State")]
|
||||
public bool IsInspecting { get; private set; } = false;
|
||||
private InspectableItem _currentInspectedItem;
|
||||
|
||||
private Coroutine _interactionListener;
|
||||
private bool _isUsingGamepad = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles inspection mode for an item. When inspecting, disables player movement
|
||||
/// and allows item rotation using look input until interaction key is pressed again.
|
||||
/// </summary>
|
||||
public void ToggleInspection(InspectableItem item)
|
||||
{
|
||||
if (!IsInspecting)
|
||||
{
|
||||
StartInspection(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_interactionListener != null)
|
||||
StopInspection();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartInspection(InspectableItem item)
|
||||
{
|
||||
IsInspecting = true;
|
||||
_currentInspectedItem = item;
|
||||
|
||||
InputManager.ToggleInput(false);
|
||||
|
||||
item.PrepareForInspection(_inspectionHoldPoint);
|
||||
|
||||
if (_interactionListener == null)
|
||||
_interactionListener = StartCoroutine(ListenForInteraction());
|
||||
|
||||
}
|
||||
|
||||
private void StopInspection()
|
||||
{
|
||||
if (_interactionListener != null)
|
||||
{
|
||||
StopCoroutine(_interactionListener);
|
||||
_interactionListener = null;
|
||||
}
|
||||
|
||||
StopInspectionInternal();
|
||||
}
|
||||
|
||||
private void StopInspectionInternal()
|
||||
{
|
||||
if (_currentInspectedItem != null)
|
||||
{
|
||||
_currentInspectedItem.Release();
|
||||
_currentInspectedItem = null;
|
||||
|
||||
StartCoroutine(DelayedToggleInput(true));
|
||||
}
|
||||
IsInspecting = false;
|
||||
}
|
||||
|
||||
private IEnumerator DelayedToggleInput(bool enable)
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
InputManager.ToggleInput(enable);
|
||||
}
|
||||
|
||||
|
||||
private IEnumerator ListenForInteraction()
|
||||
{
|
||||
yield return null;
|
||||
|
||||
while (IsInspecting)
|
||||
{
|
||||
if (InputManager.Instance.InteractAction.action.WasPressedThisFrame())
|
||||
{
|
||||
StopInspectionInternal();
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
_interactionListener = null;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (IsInspecting && _currentInspectedItem != null)
|
||||
{
|
||||
HandleObjectRotation();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleObjectRotation()
|
||||
{
|
||||
if (InputManager.Instance == null) return;
|
||||
|
||||
Vector2 lookInput = InputManager.Instance.LookInput;
|
||||
|
||||
if (lookInput != Vector2.zero)
|
||||
{
|
||||
if (Gamepad.current != null && Gamepad.current.rightStick.ReadValue().magnitude > 0.01f)
|
||||
_isUsingGamepad = true;
|
||||
else if (Mouse.current != null && Mouse.current.delta.ReadValue().magnitude > 0.01f)
|
||||
_isUsingGamepad = false;
|
||||
|
||||
float deltaMultiplier = _isUsingGamepad ? Time.deltaTime : 0.005f;
|
||||
|
||||
float rotX = lookInput.y * _currentInspectedItem.RotationSpeed * deltaMultiplier;
|
||||
float rotY = lookInput.x * _currentInspectedItem.RotationSpeed * deltaMultiplier;
|
||||
|
||||
_currentInspectedItem.transform.Rotate(transform.up, -rotY, Space.World);
|
||||
_currentInspectedItem.transform.Rotate(transform.right, rotX, Space.World);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 464960797e705e84a9eb87ecce4e9311
|
||||
Reference in New Issue
Block a user