Complete reorga
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes UI element always face the main camera.
|
||||
/// Commonly used for world-space UI elements like floating words.
|
||||
/// </summary>
|
||||
public class BillboardUI : MonoBehaviour
|
||||
{
|
||||
private Transform _cameraTransform;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_cameraTransform = Camera.main.transform;
|
||||
|
||||
if (_cameraTransform == null)
|
||||
Debug.LogWarning($"BillboardUI on {gameObject.name}: Main camera not found!");
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_cameraTransform != null)
|
||||
{
|
||||
transform.LookAt(transform.position + _cameraTransform.rotation * Vector3.forward,
|
||||
_cameraTransform.rotation * Vector3.up);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 222282a8dbdde4d418b8c85126583bf1
|
||||
@@ -0,0 +1,567 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using System.Collections.Generic;
|
||||
using Unheimlich.Features.Dialogue.Data;
|
||||
using Unheimlich.Features.Dialogue;
|
||||
using Unheimlich.Features.Player;
|
||||
using Unheimlich.Core.Utils;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.UI
|
||||
{
|
||||
/// <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?.GetLocalizedChoiceText() ?? 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 = 120f;
|
||||
[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 float _originalTextScale = 1f;
|
||||
private int _textScaleTweenId = -1;
|
||||
private bool _positionCaptured = false;
|
||||
|
||||
private Vector3 _shakeOffset = Vector3.zero;
|
||||
private Vector3 _shakeTarget = Vector3.zero;
|
||||
private float _shakeTimer = 0f;
|
||||
private bool _isShaking = false;
|
||||
private float _shakeSpeedMultiplier = 1f;
|
||||
|
||||
private bool _disableFloatingEffects = false;
|
||||
|
||||
private Queue<Vector3> _positionHistory = new Queue<Vector3>();
|
||||
private float _historyTimer = 0f;
|
||||
private const float HISTORY_SAMPLE_RATE = 0.016f;
|
||||
|
||||
private SketchFadeEffect _sketchFadeEffect;
|
||||
|
||||
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;
|
||||
|
||||
_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() * _shakeSpeedMultiplier;
|
||||
|
||||
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 SetShakeSpeedMultiplier(float multiplier)
|
||||
{
|
||||
_shakeSpeedMultiplier = Mathf.Max(0f, multiplier);
|
||||
}
|
||||
|
||||
public void SetTextScale(float targetScale, float duration = 0.3f)
|
||||
{
|
||||
if (_choiceTextComponent == null)
|
||||
return;
|
||||
|
||||
TweenHelper.CancelTween(ref _textScaleTweenId);
|
||||
|
||||
_textScaleTweenId = TweenHelper.ScaleLocal(
|
||||
_choiceTextComponent.gameObject,
|
||||
Vector3.one * targetScale,
|
||||
duration,
|
||||
null,
|
||||
LeanTweenType.easeOutQuad
|
||||
);
|
||||
}
|
||||
|
||||
public void ResetTextScale(float duration = 0.3f)
|
||||
{
|
||||
SetTextScale(_originalTextScale, duration);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
float textScaleMultiplier = settings != null ? settings.FocusedTextScaleMultiplier : 0.9f;
|
||||
float textScaleDuration = settings != null ? settings.FocusedTextScaleDuration : 0.3f;
|
||||
SetTextScale(_originalTextScale * textScaleMultiplier, textScaleDuration);
|
||||
}
|
||||
|
||||
// transform.localScale = Vector3.one * Mathf.Lerp(_originalScale, MAX_FOCUS_SCALE, progress);
|
||||
|
||||
bool shouldMoveToPlayer = GetMoveTowardPlayerOnFocus();
|
||||
|
||||
if (shouldMoveToPlayer && PlayerController.Instance != null)
|
||||
{
|
||||
float distance = GetFocusDistanceFromPlayer();
|
||||
Vector3 targetPos = PlayerController.Instance.transform.position
|
||||
+ PlayerController.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;
|
||||
|
||||
if (_choiceTextComponent != null)
|
||||
_originalTextScale = _choiceTextComponent.transform.localScale.x;
|
||||
|
||||
LeanTween.cancel(gameObject);
|
||||
|
||||
SetupGhostTrail();
|
||||
|
||||
if (_ghostTextComponent != null)
|
||||
_ghostTextComponent.text = GetChoiceText();
|
||||
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
if (settings != null && settings.EnableSketchFadeOnFocus && settings.SketchMaterial != null)
|
||||
{
|
||||
_sketchFadeEffect = gameObject.AddComponent<SketchFadeEffect>();
|
||||
_sketchFadeEffect.Initialize(_choiceTextComponent, _ghostTextComponent, settings.SketchMaterial);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
TweenHelper.CancelTween(ref _textScaleTweenId);
|
||||
|
||||
StopOrganicShake();
|
||||
|
||||
ResetSketchFade();
|
||||
|
||||
TweenHelper.MoveLocal(gameObject, _startLocalPos, duration, () => {
|
||||
_positionCaptured = false;
|
||||
|
||||
if (DialogueManager.Instance != null)
|
||||
DialogueManager.Instance.UnlockFocus();
|
||||
|
||||
StartOrganicShake();
|
||||
}, LeanTweenType.easeOutQuad);
|
||||
|
||||
transform.localScale = Vector3.one * _originalScale;
|
||||
|
||||
if (_choiceTextComponent != null)
|
||||
{
|
||||
_choiceTextComponent.transform.localScale = Vector3.one * _originalTextScale;
|
||||
_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;
|
||||
}
|
||||
|
||||
public void ApplySketchFade()
|
||||
{
|
||||
DialogueAnimationSettings settings = GetAnimationSettings();
|
||||
if (settings == null || !settings.EnableSketchFadeOnFocus || _sketchFadeEffect == null)
|
||||
return;
|
||||
|
||||
_sketchFadeEffect.PlaySketchFadeEffect(
|
||||
settings.SketchDuration,
|
||||
settings.FadeOutDuration,
|
||||
settings.FinalFadeAlpha
|
||||
);
|
||||
}
|
||||
|
||||
public void ResetSketchFade()
|
||||
{
|
||||
if (_sketchFadeEffect != null)
|
||||
_sketchFadeEffect.ResetEffect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abd0f63a44b2272448eb41767c2ce44e
|
||||
@@ -0,0 +1,35 @@
|
||||
using UnityEngine;
|
||||
using Unheimlich.Core;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.UI
|
||||
{
|
||||
public class CrosshairController : Singleton<CrosshairController>
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private GameObject _crosshairObject;
|
||||
|
||||
private bool _isVisible = true;
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
if (_crosshairObject != null)
|
||||
_crosshairObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void Show()
|
||||
{
|
||||
if (_crosshairObject != null)
|
||||
_crosshairObject.SetActive(true);
|
||||
_isVisible = true;
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
if (_crosshairObject != null)
|
||||
_crosshairObject.SetActive(false);
|
||||
_isVisible = false;
|
||||
}
|
||||
|
||||
public bool IsVisible => _isVisible;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9adc6263b2493341b02087fb5a00404
|
||||
@@ -0,0 +1,265 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using Unheimlich.Core;
|
||||
using Unheimlich.Features.Player;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.UI
|
||||
{
|
||||
public class DialogueBlurEffectURP : Singleton<DialogueBlurEffectURP>
|
||||
{
|
||||
|
||||
[Header("Volume Settings")]
|
||||
[SerializeField] private Volume _globalVolume;
|
||||
|
||||
[Header("Depth of Field (Gaussian)")]
|
||||
[SerializeField] private float _gaussianStart = 0f;
|
||||
[SerializeField] private float _gaussianEnd = 15f;
|
||||
[SerializeField] private float _maxGaussianRadius = 4f;
|
||||
|
||||
[Header("Brightness (Color Adjustments)")]
|
||||
[SerializeField] private float _startPostExposure = 0f;
|
||||
[SerializeField] private float _targetPostExposure = 0.3f;
|
||||
|
||||
[Header("Animation Settings")]
|
||||
[SerializeField] private float _fadeDuration = 1.0f;
|
||||
[SerializeField] private AnimationCurve _fadeCurve;
|
||||
|
||||
[Header("UI Configuration")]
|
||||
[Tooltip("Layer(s) for dialogue (World Space UI) to render without blur")]
|
||||
[SerializeField] private LayerMask _dialogueLayers;
|
||||
[SerializeField] private bool _autoCreateDialogueCamera = true;
|
||||
|
||||
[Header("Hands Configuration")]
|
||||
[Tooltip("Layer for player hands to render on top of dialogue")]
|
||||
[SerializeField] private LayerMask _handsLayer;
|
||||
[SerializeField] private bool _autoCreateHandsCamera = true;
|
||||
|
||||
private DepthOfField _dof;
|
||||
private ColorAdjustments _color;
|
||||
private VolumeProfile _profile;
|
||||
|
||||
private float _current = 0f;
|
||||
private float _target = 0f;
|
||||
private float _start = 0f;
|
||||
private float _timer = 0f;
|
||||
private bool _fading = false;
|
||||
|
||||
private Camera _baseCam;
|
||||
private Camera _dialogueCam;
|
||||
private Camera _handsCam;
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
|
||||
|
||||
if (_fadeCurve == null || _fadeCurve.keys.Length == 0)
|
||||
_fadeCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||||
|
||||
if (_globalVolume == null)
|
||||
{
|
||||
Debug.LogError("[DialogueBlur] ERROR: Global Volume NOT ASSIGNED in inspector!");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_globalVolume.profile == null)
|
||||
{
|
||||
Debug.LogError("[DialogueBlur] ERROR: Global Volume has NO VolumeProfile assigned!");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_profile = _globalVolume.profile;
|
||||
|
||||
if (!_profile.TryGet(out _dof))
|
||||
{
|
||||
Debug.LogError("[DialogueBlur] ERROR: Depth Of Field NOT FOUND in Volume Profile!");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_profile.TryGet(out _color))
|
||||
{
|
||||
Debug.LogWarning("[DialogueBlur] WARNING: Color Adjustments not found (not critical)");
|
||||
}
|
||||
|
||||
_dof.mode.Override(DepthOfFieldMode.Gaussian);
|
||||
_dof.gaussianStart.Override(_gaussianStart);
|
||||
_dof.gaussianEnd.Override(_gaussianEnd);
|
||||
_dof.gaussianMaxRadius.Override(0f);
|
||||
_dof.active = false;
|
||||
|
||||
if (_color != null)
|
||||
{
|
||||
_color.postExposure.Override(_startPostExposure);
|
||||
_color.active = false;
|
||||
}
|
||||
|
||||
_globalVolume.weight = 0f;
|
||||
_globalVolume.enabled = true;
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
_baseCam = PlayerController.Instance.PlayerCamera;
|
||||
if (_autoCreateDialogueCamera)
|
||||
SetupDialogueOverlayCamera();
|
||||
if (_autoCreateHandsCamera)
|
||||
SetupHandsOverlayCamera();
|
||||
}
|
||||
|
||||
void SetupDialogueOverlayCamera()
|
||||
{
|
||||
var baseData = _baseCam.GetComponent<UniversalAdditionalCameraData>();
|
||||
if (baseData == null)
|
||||
baseData = _baseCam.gameObject.AddComponent<UniversalAdditionalCameraData>();
|
||||
|
||||
baseData.renderType = CameraRenderType.Base;
|
||||
baseData.renderPostProcessing = true;
|
||||
|
||||
if (_dialogueLayers.value == 0)
|
||||
Debug.LogWarning("[DialogueBlur] WARNING: dialogueLayers is EMPTY! Ghost words will be blurred!");
|
||||
|
||||
_baseCam.cullingMask &= ~_dialogueLayers;
|
||||
if (_handsLayer.value != 0)
|
||||
_baseCam.cullingMask &= ~_handsLayer;
|
||||
|
||||
var go = new GameObject("Dialogue Camera (Overlay)");
|
||||
go.transform.SetParent(_baseCam.transform);
|
||||
go.transform.localPosition = Vector3.zero;
|
||||
go.transform.localRotation = Quaternion.identity;
|
||||
|
||||
_dialogueCam = go.AddComponent<Camera>();
|
||||
_dialogueCam.CopyFrom(_baseCam);
|
||||
_dialogueCam.clearFlags = CameraClearFlags.Depth;
|
||||
_dialogueCam.cullingMask = _dialogueLayers;
|
||||
|
||||
var overlayData = go.AddComponent<UniversalAdditionalCameraData>();
|
||||
overlayData.renderType = CameraRenderType.Overlay;
|
||||
overlayData.renderPostProcessing = false;
|
||||
|
||||
baseData.cameraStack.Add(_dialogueCam);
|
||||
}
|
||||
|
||||
void SetupHandsOverlayCamera()
|
||||
{
|
||||
if (_handsLayer.value == 0)
|
||||
{
|
||||
Debug.LogWarning("[DialogueBlur] WARNING: handsLayer is EMPTY! Hands camera will not be created.");
|
||||
return;
|
||||
}
|
||||
|
||||
var baseData = _baseCam.GetComponent<UniversalAdditionalCameraData>();
|
||||
if (baseData == null)
|
||||
{
|
||||
Debug.LogError("[DialogueBlur] ERROR: Base camera has no UniversalAdditionalCameraData!");
|
||||
return;
|
||||
}
|
||||
|
||||
var go = new GameObject("Hands Camera (Overlay)");
|
||||
go.transform.SetParent(_baseCam.transform);
|
||||
go.transform.localPosition = Vector3.zero;
|
||||
go.transform.localRotation = Quaternion.identity;
|
||||
|
||||
_handsCam = go.AddComponent<Camera>();
|
||||
_handsCam.CopyFrom(_baseCam);
|
||||
_handsCam.clearFlags = CameraClearFlags.Depth;
|
||||
_handsCam.cullingMask = _handsLayer;
|
||||
|
||||
var overlayData = go.AddComponent<UniversalAdditionalCameraData>();
|
||||
overlayData.renderType = CameraRenderType.Overlay;
|
||||
overlayData.renderPostProcessing = false;
|
||||
|
||||
baseData.cameraStack.Add(_handsCam);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!_fading) return;
|
||||
|
||||
_timer += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(_timer / Mathf.Max(0.0001f, _fadeDuration));
|
||||
float cv = _fadeCurve.Evaluate(t);
|
||||
|
||||
_current = Mathf.Lerp(_start, _target, cv);
|
||||
|
||||
ApplyEffects(_current);
|
||||
|
||||
if (t >= 1f)
|
||||
{
|
||||
_current = _target;
|
||||
ApplyEffects(_current);
|
||||
_fading = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyEffects(float s)
|
||||
{
|
||||
s = Mathf.Clamp01(s);
|
||||
bool active = s > 0.01f;
|
||||
|
||||
_globalVolume.weight = s;
|
||||
|
||||
if (active)
|
||||
{
|
||||
_dof.active = true;
|
||||
_dof.gaussianStart.value = _gaussianStart;
|
||||
_dof.gaussianEnd.value = _gaussianEnd;
|
||||
float radius = Mathf.Lerp(0f, _maxGaussianRadius, s);
|
||||
_dof.gaussianMaxRadius.value = radius;
|
||||
|
||||
if (_color != null)
|
||||
{
|
||||
_color.active = true;
|
||||
_color.postExposure.value = Mathf.Lerp(_startPostExposure, _targetPostExposure, s);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_dof.gaussianMaxRadius.value = 0f;
|
||||
_dof.active = false;
|
||||
if (_color != null)
|
||||
{
|
||||
_color.postExposure.value = _startPostExposure;
|
||||
_color.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowBlur()
|
||||
{
|
||||
if (_dialogueCam == null && _autoCreateDialogueCamera)
|
||||
{
|
||||
if (_baseCam == null && PlayerController.Instance != null)
|
||||
_baseCam = PlayerController.Instance.PlayerCamera;
|
||||
|
||||
if (_baseCam != null)
|
||||
{
|
||||
SetupDialogueOverlayCamera();
|
||||
if (_autoCreateHandsCamera)
|
||||
SetupHandsOverlayCamera();
|
||||
}
|
||||
else
|
||||
Debug.LogWarning("[DialogueBlur] WARNING: Cannot create dialogue camera - PlayerCamera not ready!");
|
||||
}
|
||||
|
||||
if (_fading && _target == 1f) return;
|
||||
|
||||
_start = _current;
|
||||
_target = 1f;
|
||||
_timer = 0f;
|
||||
_fading = true;
|
||||
}
|
||||
|
||||
public void HideBlur()
|
||||
{
|
||||
if (_fading && _target == 0f) return;
|
||||
|
||||
_start = _current;
|
||||
_target = 0f;
|
||||
_timer = 0f;
|
||||
_fading = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ed4b22b4be6bbe49a67cbe552c633e1
|
||||
@@ -0,0 +1,79 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using Unheimlich.Core;
|
||||
using Unheimlich.Core.Utils;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.UI
|
||||
{
|
||||
public class DialogueScreenUI : Singleton<DialogueScreenUI>
|
||||
{
|
||||
|
||||
[Header("UI References")]
|
||||
[SerializeField] private GameObject _dialoguePanel;
|
||||
[SerializeField] private TextMeshProUGUI _speakerText;
|
||||
[SerializeField] private TextMeshProUGUI _dialogueText;
|
||||
|
||||
[Header("Fade Settings")]
|
||||
[SerializeField] private float _fadeOutDuration = 0.5f;
|
||||
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
|
||||
if (_dialoguePanel != null)
|
||||
{
|
||||
_canvasGroup = _dialoguePanel.GetComponent<CanvasGroup>();
|
||||
|
||||
if (_canvasGroup == null)
|
||||
_canvasGroup = _dialoguePanel.AddComponent<CanvasGroup>();
|
||||
}
|
||||
}
|
||||
|
||||
public void Show(string speaker, string dialogue)
|
||||
{
|
||||
if (_speakerText != null)
|
||||
_speakerText.text = speaker;
|
||||
|
||||
if (_dialogueText != null)
|
||||
_dialogueText.text = dialogue;
|
||||
|
||||
if (_dialoguePanel != null)
|
||||
{
|
||||
_dialoguePanel.SetActive(true);
|
||||
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
LeanTween.cancel(_dialoguePanel);
|
||||
_canvasGroup.alpha = 1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
if (_dialoguePanel != null)
|
||||
_dialoguePanel.SetActive(false);
|
||||
}
|
||||
|
||||
public void FadeOutAndHide(System.Action onComplete = null)
|
||||
{
|
||||
if (_canvasGroup != null && _dialoguePanel != null)
|
||||
{
|
||||
TweenHelper.FadeCanvasGroup(
|
||||
_canvasGroup,
|
||||
0f,
|
||||
_fadeOutDuration,
|
||||
() => {
|
||||
Hide();
|
||||
onComplete.SafeInvoke();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Hide();
|
||||
onComplete.SafeInvoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc4c44e24ba19844b822608a46bde428
|
||||
@@ -0,0 +1,393 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using Unheimlich.Features.Dialogue.Data;
|
||||
using Unheimlich.Features.Player;
|
||||
using Unheimlich.Features.Dialogue.UI;
|
||||
using Unheimlich.Core.Utils;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages dialogue UI display and choice button spawning with configurable animations.
|
||||
/// Supports multiple spawn modes, distance scaling, player following, and visual effects.
|
||||
/// </summary>
|
||||
public class DialogueUI : MonoBehaviour
|
||||
{
|
||||
[Header("Panels - World Mode")]
|
||||
[SerializeField] private GameObject _dialoguePanel;
|
||||
public GameObject ChoiceContainer;
|
||||
|
||||
[Header("Text Fields - World Mode")]
|
||||
[SerializeField] private TextMeshProUGUI _speakerText;
|
||||
[SerializeField] private TextMeshProUGUI _contentText;
|
||||
|
||||
[Header("Prefabs")]
|
||||
public ChoiceFocusHandler ChoiceButtonPrefab;
|
||||
|
||||
[Header("Default Animation Settings")]
|
||||
[SerializeField] private float _defaultRotationSpeed = 36f;
|
||||
|
||||
[Header("Fade Settings")]
|
||||
[SerializeField] private float _fadeOutDuration = 0.5f;
|
||||
[SerializeField] private CanvasGroup _dialogueCanvasGroup;
|
||||
|
||||
private DialogueAnimationSettings _currentAnimationSettings;
|
||||
private Vector3 _currentRotationAxis = Vector3.forward;
|
||||
private bool _shouldFollowPlayerPosition = false;
|
||||
private bool _shouldFollowPlayerRotation = false;
|
||||
private Transform _currentAnchor;
|
||||
private Vector3 _baseScale;
|
||||
private Vector3 _basePanelScale;
|
||||
private List<ChoiceFocusHandler> _activeChoices = new List<ChoiceFocusHandler>();
|
||||
private float _sortTimer = 0f;
|
||||
private const float SORT_INTERVAL = 0.1f;
|
||||
|
||||
private DialogueDisplayMode _currentDisplayMode = DialogueDisplayMode.World;
|
||||
|
||||
private int _fadeCanvasTweenId = -1;
|
||||
private int _rotationTweenId = -1;
|
||||
private List<int> _floatingTweenIds = new List<int>();
|
||||
|
||||
private static readonly Vector3 ViewportCenter = new Vector3(0.5f, 0.5f, 0f);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_dialogueCanvasGroup == null && _dialoguePanel != null)
|
||||
{
|
||||
_dialogueCanvasGroup = _dialoguePanel.GetComponent<CanvasGroup>();
|
||||
|
||||
if (_dialogueCanvasGroup == null)
|
||||
_dialogueCanvasGroup = _dialoguePanel.AddComponent<CanvasGroup>();
|
||||
}
|
||||
|
||||
if (_dialoguePanel != null)
|
||||
{
|
||||
_basePanelScale = _dialoguePanel.transform.localScale;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (PlayerController.Instance == null || !ChoiceContainer.activeInHierarchy)
|
||||
return;
|
||||
|
||||
if (_shouldFollowPlayerPosition)
|
||||
ChoiceContainer.transform.position = PlayerController.Instance.transform.position;
|
||||
|
||||
if (_shouldFollowPlayerRotation)
|
||||
{
|
||||
float playerYRotation = PlayerController.Instance.transform.eulerAngles.y;
|
||||
ChoiceContainer.transform.rotation = Quaternion.Euler(0f, playerYRotation, 0f);
|
||||
}
|
||||
|
||||
if (_currentAnimationSettings != null && _currentAnimationSettings.EnableDistanceScaling && !_shouldFollowPlayerPosition)
|
||||
{
|
||||
ApplyDistanceScaling();
|
||||
ApplyDistanceScalingToPanel();
|
||||
}
|
||||
|
||||
SortChoicesByDistance();
|
||||
}
|
||||
|
||||
public void ShowDialogueBox(string speaker, string content, DialogueDisplayMode displayMode = DialogueDisplayMode.World)
|
||||
{
|
||||
_currentDisplayMode = displayMode;
|
||||
|
||||
if (displayMode == DialogueDisplayMode.World)
|
||||
{
|
||||
if (DialogueScreenUI.Instance != null)
|
||||
DialogueScreenUI.Instance.Hide();
|
||||
|
||||
_speakerText.text = speaker;
|
||||
_contentText.text = content;
|
||||
_dialoguePanel.SetActive(true);
|
||||
|
||||
if (_dialogueCanvasGroup != null)
|
||||
{
|
||||
TweenHelper.CancelTween(ref _fadeCanvasTweenId);
|
||||
_dialogueCanvasGroup.alpha = 1f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_dialoguePanel != null)
|
||||
_dialoguePanel.SetActive(false);
|
||||
|
||||
if (DialogueScreenUI.Instance != null)
|
||||
DialogueScreenUI.Instance.Show(speaker, content);
|
||||
else
|
||||
Debug.LogWarning("[DialogueUI] DialogueScreenUI.Instance is null! Make sure DialogueScreenUI component exists in the scene.");
|
||||
}
|
||||
}
|
||||
|
||||
public void HideDialogueBox()
|
||||
{
|
||||
if (_dialoguePanel != null)
|
||||
_dialoguePanel.SetActive(false);
|
||||
|
||||
if (DialogueScreenUI.Instance != null)
|
||||
DialogueScreenUI.Instance.Hide();
|
||||
|
||||
ChoiceContainer.SetActive(false);
|
||||
HideChoices();
|
||||
}
|
||||
|
||||
public void FadeOutAndHide(System.Action onComplete = null)
|
||||
{
|
||||
if (_currentDisplayMode == DialogueDisplayMode.World)
|
||||
{
|
||||
if (_dialogueCanvasGroup != null && _dialoguePanel != null)
|
||||
{
|
||||
TweenHelper.CancelTween(ref _fadeCanvasTweenId);
|
||||
_fadeCanvasTweenId = TweenHelper.FadeCanvasGroup(
|
||||
_dialogueCanvasGroup,
|
||||
0f,
|
||||
_fadeOutDuration,
|
||||
() =>
|
||||
{
|
||||
HideDialogueBox();
|
||||
onComplete?.Invoke();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
HideDialogueBox();
|
||||
onComplete?.Invoke();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (DialogueScreenUI.Instance != null)
|
||||
{
|
||||
DialogueScreenUI.Instance.FadeOutAndHide(() =>
|
||||
{
|
||||
HideChoices();
|
||||
ChoiceContainer.SetActive(false);
|
||||
onComplete?.Invoke();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
HideDialogueBox();
|
||||
onComplete?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool AreChoicesVisible()
|
||||
{
|
||||
return ChoiceContainer.transform.childCount > 0;
|
||||
}
|
||||
|
||||
public void ShowChoices(List<DialogueChoiceData> nodeChoices, Transform anchorTransform, DialogueAnimationSettings animationSettings = null)
|
||||
{
|
||||
HideChoices();
|
||||
|
||||
_currentAnimationSettings = animationSettings;
|
||||
_currentAnchor = anchorTransform;
|
||||
|
||||
if (anchorTransform != null)
|
||||
{
|
||||
ChoiceContainer.transform.SetParent(anchorTransform);
|
||||
|
||||
bool followPosition = animationSettings != null && animationSettings.FollowPlayerPosition;
|
||||
bool followRotation = animationSettings != null && animationSettings.FollowPlayerRotation;
|
||||
|
||||
_shouldFollowPlayerPosition = followPosition;
|
||||
_shouldFollowPlayerRotation = followRotation;
|
||||
|
||||
if (followPosition && PlayerController.Instance != null)
|
||||
{
|
||||
Vector3 playerWorldPos = PlayerController.Instance.transform.position;
|
||||
ChoiceContainer.transform.position = playerWorldPos;
|
||||
}
|
||||
else
|
||||
ChoiceContainer.transform.localPosition = Vector3.zero;
|
||||
|
||||
if (followRotation && PlayerController.Instance != null)
|
||||
{
|
||||
float playerYRotation = PlayerController.Instance.transform.eulerAngles.y;
|
||||
ChoiceContainer.transform.rotation = Quaternion.Euler(0f, playerYRotation, 0f);
|
||||
}
|
||||
else
|
||||
ChoiceContainer.transform.localRotation = Quaternion.identity;
|
||||
}
|
||||
|
||||
ChoiceContainer.SetActive(true);
|
||||
ChoiceContainer.transform.localScale = Vector3.one;
|
||||
_baseScale = Vector3.one;
|
||||
|
||||
int numChoices = nodeChoices.Count;
|
||||
if (numChoices == 0) return;
|
||||
|
||||
SpawnChoices(nodeChoices, animationSettings);
|
||||
|
||||
if (animationSettings != null && animationSettings.EnableDistanceScaling && !_shouldFollowPlayerPosition)
|
||||
{
|
||||
ApplyDistanceScaling();
|
||||
ApplyDistanceScalingToPanel();
|
||||
}
|
||||
|
||||
bool shouldRotate = animationSettings == null || animationSettings.EnableRotation;
|
||||
if (shouldRotate)
|
||||
{
|
||||
float rotationSpeed = animationSettings != null ? animationSettings.NormalRotationSpeed : _defaultRotationSpeed;
|
||||
RotationAnimation(rotationSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnChoices(List<DialogueChoiceData> nodeChoices, DialogueAnimationSettings settings)
|
||||
{
|
||||
_currentRotationAxis = GetRotationAxisVector(settings != null ? settings.RotationAxis : RotationAxis.Forward);
|
||||
|
||||
if (settings == null) return;
|
||||
|
||||
Vector3[] originalPositions;
|
||||
List<ChoiceFocusHandler> spawnedChoices = FloatingWordsSpawner.SpawnByMode(
|
||||
nodeChoices,
|
||||
ChoiceButtonPrefab,
|
||||
ChoiceContainer.transform,
|
||||
settings,
|
||||
out originalPositions
|
||||
);
|
||||
|
||||
for (int i = 0; i < spawnedChoices.Count; i++)
|
||||
{
|
||||
_activeChoices.Add(spawnedChoices[i]);
|
||||
|
||||
if (settings.EnableFloating)
|
||||
StartFloatingAnimation(spawnedChoices[i].gameObject, i);
|
||||
}
|
||||
}
|
||||
|
||||
private void StartFloatingAnimation(GameObject choice, int index)
|
||||
{
|
||||
if (_currentAnimationSettings == null) return;
|
||||
|
||||
float amplitude = _currentAnimationSettings.FloatingAmplitude;
|
||||
float duration = _currentAnimationSettings.FloatingDuration;
|
||||
float delay = index * 0.2f;
|
||||
|
||||
Vector3 startPos = choice.transform.localPosition;
|
||||
|
||||
int tweenId = TweenHelper.FloatAnimation(choice, startPos, amplitude, duration, delay, Axis.Y);
|
||||
_floatingTweenIds.Add(tweenId);
|
||||
}
|
||||
|
||||
public void RotationAnimation(float degreesPerSecond = 60f)
|
||||
{
|
||||
TweenHelper.CancelTween(ref _rotationTweenId);
|
||||
_rotationTweenId = TweenHelper.RotateAround(ChoiceContainer, _currentRotationAxis, degreesPerSecond);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dynamically scales the choice container based on distance from player to anchor.
|
||||
/// Provides visual feedback for spatial awareness in 3D dialogue interactions.
|
||||
/// </summary>
|
||||
private void ApplyDistanceScaling()
|
||||
{
|
||||
if (_currentAnchor == null || PlayerController.Instance == null)
|
||||
return;
|
||||
|
||||
float distance = Vector3.Distance(_currentAnchor.position, PlayerController.Instance.transform.position);
|
||||
|
||||
float normalizedDistance = Mathf.InverseLerp(_currentAnimationSettings.MinDistance, _currentAnimationSettings.MaxDistance, distance);
|
||||
|
||||
float finalScale = Mathf.Lerp(_currentAnimationSettings.MinScaleMultiplier, _currentAnimationSettings.MaxScaleMultiplier, normalizedDistance);
|
||||
|
||||
ChoiceContainer.transform.localScale = _baseScale * finalScale;
|
||||
}
|
||||
|
||||
private void ApplyDistanceScalingToPanel()
|
||||
{
|
||||
if (_currentAnchor == null || PlayerController.Instance == null || _dialoguePanel == null)
|
||||
return;
|
||||
|
||||
float distance = Vector3.Distance(_currentAnchor.position, PlayerController.Instance.transform.position);
|
||||
|
||||
float normalizedDistance = Mathf.InverseLerp(_currentAnimationSettings.MinDistance, _currentAnimationSettings.MaxDistance, distance);
|
||||
|
||||
float finalScale = Mathf.Lerp(_currentAnimationSettings.MinScaleMultiplier, _currentAnimationSettings.MaxScaleMultiplier, normalizedDistance);
|
||||
|
||||
_dialoguePanel.transform.localScale = _basePanelScale * finalScale;
|
||||
}
|
||||
|
||||
private 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.forward;
|
||||
}
|
||||
}
|
||||
|
||||
public void HideChoices()
|
||||
{
|
||||
_shouldFollowPlayerPosition = false;
|
||||
_shouldFollowPlayerRotation = false;
|
||||
_currentAnchor = null;
|
||||
_currentAnimationSettings = null;
|
||||
_activeChoices.Clear();
|
||||
|
||||
TweenHelper.CancelTween(ref _rotationTweenId);
|
||||
foreach (int tweenId in _floatingTweenIds)
|
||||
{
|
||||
int id = tweenId;
|
||||
TweenHelper.CancelTween(ref id);
|
||||
}
|
||||
_floatingTweenIds.Clear();
|
||||
|
||||
foreach (Transform child in ChoiceContainer.transform)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts choice buttons by distance to player camera.
|
||||
/// Closer choices are rendered on top by being placed last in hierarchy (higher sibling index).
|
||||
/// Uses interval-based sorting to reduce performance overhead.
|
||||
/// </summary>
|
||||
private void SortChoicesByDistance()
|
||||
{
|
||||
if (_activeChoices.Count <= 1 || PlayerController.Instance == null)
|
||||
return;
|
||||
|
||||
_sortTimer += Time.deltaTime;
|
||||
if (_sortTimer < SORT_INTERVAL)
|
||||
return;
|
||||
|
||||
_sortTimer = 0f;
|
||||
|
||||
Camera mainCamera = Camera.main;
|
||||
if (mainCamera == null)
|
||||
return;
|
||||
|
||||
Vector3 cameraPosition = mainCamera.transform.position;
|
||||
|
||||
_activeChoices.Sort((a, b) =>
|
||||
{
|
||||
if (a == null || b == null)
|
||||
return 0;
|
||||
|
||||
float distanceA = Vector3.Distance(cameraPosition, a.transform.position);
|
||||
float distanceB = Vector3.Distance(cameraPosition, b.transform.position);
|
||||
|
||||
return distanceB.CompareTo(distanceA);
|
||||
});
|
||||
|
||||
for (int i = 0; i < _activeChoices.Count; i++)
|
||||
{
|
||||
if (_activeChoices[i] != null)
|
||||
_activeChoices[i].transform.SetSiblingIndex(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90f07eb59fe4c5b49b180cceacb3f24c
|
||||
@@ -0,0 +1,405 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using Unheimlich.Features.Dialogue.Data;
|
||||
using Unheimlich.Features.Player;
|
||||
using Unheimlich.Core.Utils;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.UI
|
||||
{
|
||||
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;
|
||||
public float DefaultScaleMultiplier;
|
||||
public bool EnableScaleVariation;
|
||||
public float MinScaleVariation;
|
||||
public float MaxScaleVariation;
|
||||
}
|
||||
|
||||
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,
|
||||
DefaultScaleMultiplier = 1f
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
DefaultScaleMultiplier = settings.DefaultScaleMultiplier,
|
||||
EnableScaleVariation = settings.EnableScaleVariation,
|
||||
MinScaleVariation = settings.MinScaleVariation,
|
||||
MaxScaleVariation = settings.MaxScaleVariation
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
float scaleMultiplier = config.DefaultScaleMultiplier;
|
||||
if (config.EnableScaleVariation)
|
||||
{
|
||||
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
|
||||
}
|
||||
word.transform.localScale = Vector3.one * scaleMultiplier;
|
||||
|
||||
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;
|
||||
|
||||
float scaleMultiplier = config.DefaultScaleMultiplier;
|
||||
if (config.EnableScaleVariation)
|
||||
{
|
||||
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
|
||||
}
|
||||
word.transform.localScale = Vector3.one * scaleMultiplier;
|
||||
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 (PlayerController.Instance == null) return words;
|
||||
|
||||
Vector3 playerForward = PlayerController.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 = PlayerController.Instance.transform.position + offset;
|
||||
word.transform.LookAt(PlayerController.Instance.transform);
|
||||
|
||||
float scaleMultiplier = config.DefaultScaleMultiplier;
|
||||
if (config.EnableScaleVariation)
|
||||
{
|
||||
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
|
||||
}
|
||||
word.transform.localScale = Vector3.one * scaleMultiplier;
|
||||
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 (PlayerController.Instance == null) return words;
|
||||
|
||||
parent.position = PlayerController.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(PlayerController.Instance.transform);
|
||||
|
||||
float scaleMultiplier = config.DefaultScaleMultiplier;
|
||||
if (config.EnableScaleVariation)
|
||||
{
|
||||
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
|
||||
}
|
||||
word.transform.localScale = Vector3.one * scaleMultiplier;
|
||||
|
||||
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)
|
||||
{
|
||||
TweenHelper.FadeCanvasGroup(canvasGroup, 0f, config.MergeDuration, null, LeanTweenType.easeInQuad);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (words.Count > 0 && words[0] != null)
|
||||
{
|
||||
TweenHelper.ScaleLocal(
|
||||
words[0].gameObject,
|
||||
Vector3.one * config.MergeScalePulse,
|
||||
config.MergeDuration / 2f,
|
||||
() =>
|
||||
{
|
||||
if (words.Count > 0 && words[0] != null)
|
||||
{
|
||||
TweenHelper.ScaleLocal(
|
||||
words[0].gameObject,
|
||||
Vector3.one,
|
||||
config.MergeDuration / 2f,
|
||||
() => onMergeComplete.SafeInvoke(),
|
||||
LeanTweenType.easeInBack
|
||||
);
|
||||
}
|
||||
},
|
||||
LeanTweenType.easeOutBack
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
words[i].gameObject.CancelAllTweens();
|
||||
CanvasGroup canvasGroup = words[i].GetComponent<CanvasGroup>();
|
||||
if (canvasGroup != null)
|
||||
{
|
||||
TweenHelper.FadeCanvasGroup(canvasGroup, 1f, config.MergeDuration, null, LeanTweenType.easeOutQuad);
|
||||
}
|
||||
words[i].transform.localScale = Vector3.one;
|
||||
}
|
||||
}
|
||||
|
||||
if (words.Count > 0 && words[0] != null)
|
||||
{
|
||||
words[0].gameObject.CancelAllTweens();
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b52b115a50463d46b645f5579cf22fd
|
||||
@@ -0,0 +1,215 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using Unheimlich.Core.Utils;
|
||||
|
||||
namespace Unheimlich.Features.Dialogue.UI
|
||||
{
|
||||
public class SketchFadeEffect : MonoBehaviour
|
||||
{
|
||||
private TextMeshProUGUI _textComponent;
|
||||
private TextMeshProUGUI _ghostTextComponent;
|
||||
private Image _scribbleOverlay;
|
||||
private Material _scribbleMaterialInstance;
|
||||
private Material _scribbleMaterialTemplate;
|
||||
|
||||
private static readonly int ProgressID = Shader.PropertyToID("_Progress");
|
||||
|
||||
private bool _isEffectActive = false;
|
||||
private bool _overlayCreated = false;
|
||||
private Color _originalTextColor;
|
||||
private Color _originalGhostColor;
|
||||
|
||||
private int _scribbleTweenId = -1;
|
||||
private int _fadeTweenId = -1;
|
||||
|
||||
public void Initialize(TextMeshProUGUI textComponent, TextMeshProUGUI ghostComponent, Material scribbleMaterial)
|
||||
{
|
||||
_textComponent = textComponent;
|
||||
_ghostTextComponent = ghostComponent;
|
||||
_scribbleMaterialTemplate = scribbleMaterial;
|
||||
_originalTextColor = _textComponent.color;
|
||||
|
||||
if (_ghostTextComponent != null)
|
||||
_originalGhostColor = _ghostTextComponent.color;
|
||||
}
|
||||
|
||||
private void CreateScribbleOverlay()
|
||||
{
|
||||
if (_overlayCreated || _scribbleMaterialTemplate == null || _textComponent == null)
|
||||
return;
|
||||
|
||||
GameObject overlayObject = new GameObject("ScribbleOverlay");
|
||||
overlayObject.transform.SetParent(_textComponent.transform, false);
|
||||
|
||||
_scribbleOverlay = overlayObject.AddComponent<Image>();
|
||||
_scribbleOverlay.raycastTarget = false;
|
||||
|
||||
RectTransform overlayRect = overlayObject.GetComponent<RectTransform>();
|
||||
RectTransform textRect = _textComponent.GetComponent<RectTransform>();
|
||||
|
||||
_textComponent.ForceMeshUpdate();
|
||||
|
||||
Vector2 textSize = _textComponent.GetRenderedValues(false);
|
||||
|
||||
float horizontalPadding = 1.2f;
|
||||
float verticalPadding = 0.8f;
|
||||
Vector2 paddedSize = new Vector2(
|
||||
textSize.x + horizontalPadding,
|
||||
textSize.y + verticalPadding
|
||||
);
|
||||
|
||||
overlayRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
overlayRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
overlayRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
|
||||
float leftOffset = -horizontalPadding * 0.9f;
|
||||
overlayRect.anchoredPosition = new Vector2(leftOffset, 0f);
|
||||
overlayRect.sizeDelta = paddedSize;
|
||||
|
||||
overlayRect.localScale = Vector3.one;
|
||||
|
||||
_scribbleMaterialInstance = new Material(_scribbleMaterialTemplate);
|
||||
_scribbleMaterialInstance.SetFloat(ProgressID, 0f);
|
||||
_scribbleOverlay.material = _scribbleMaterialInstance;
|
||||
|
||||
_scribbleOverlay.gameObject.SetActive(false);
|
||||
|
||||
_overlayCreated = true;
|
||||
}
|
||||
|
||||
public void PlaySketchFadeEffect(float scribbleDuration, float fadeDuration, float finalAlpha)
|
||||
{
|
||||
if (_isEffectActive || _textComponent == null)
|
||||
return;
|
||||
|
||||
CancelActiveTweens();
|
||||
|
||||
CreateScribbleOverlay();
|
||||
|
||||
if (_scribbleOverlay == null || _scribbleMaterialInstance == null)
|
||||
return;
|
||||
|
||||
_scribbleOverlay.gameObject.SetActive(true);
|
||||
|
||||
_isEffectActive = true;
|
||||
|
||||
_scribbleTweenId = LeanTween.value(gameObject, 0f, 1f, scribbleDuration)
|
||||
.setEase(LeanTweenType.easeInOutQuad)
|
||||
.setOnUpdate((float val) => {
|
||||
if (_scribbleMaterialInstance != null)
|
||||
_scribbleMaterialInstance.SetFloat(ProgressID, val);
|
||||
})
|
||||
.setOnComplete(() => {
|
||||
_scribbleTweenId = -1;
|
||||
|
||||
float ghostAlphaRatio = _ghostTextComponent != null ? (_originalGhostColor.a / _originalTextColor.a) : 0f;
|
||||
|
||||
_fadeTweenId = LeanTween.value(gameObject, _originalTextColor.a, finalAlpha, fadeDuration)
|
||||
.setEase(LeanTweenType.easeInQuad)
|
||||
.setOnUpdate((float val) => {
|
||||
if (_textComponent != null)
|
||||
{
|
||||
_textComponent.color = _textComponent.color.WithAlpha(val);
|
||||
}
|
||||
|
||||
if (_ghostTextComponent != null)
|
||||
{
|
||||
_ghostTextComponent.color = _ghostTextComponent.color.WithAlpha(val * ghostAlphaRatio);
|
||||
}
|
||||
|
||||
if (_scribbleOverlay != null)
|
||||
{
|
||||
_scribbleOverlay.color = _scribbleOverlay.color.WithAlpha(val);
|
||||
}
|
||||
})
|
||||
.setOnComplete(() => {
|
||||
_fadeTweenId = -1;
|
||||
_isEffectActive = false;
|
||||
}).id;
|
||||
}).id;
|
||||
}
|
||||
|
||||
public void ResetEffect()
|
||||
{
|
||||
if (!_overlayCreated || _scribbleOverlay == null)
|
||||
{
|
||||
_isEffectActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
bool overlayIsActive = _scribbleOverlay.gameObject.activeSelf;
|
||||
|
||||
if (!overlayIsActive)
|
||||
{
|
||||
_isEffectActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
CancelActiveTweens();
|
||||
_isEffectActive = false;
|
||||
|
||||
if (_scribbleMaterialInstance != null)
|
||||
{
|
||||
float currentProgress = _scribbleMaterialInstance.GetFloat(ProgressID);
|
||||
|
||||
_scribbleTweenId = LeanTween.value(gameObject, currentProgress, 0f, 0.3f)
|
||||
.setEase(LeanTweenType.easeOutQuad)
|
||||
.setOnUpdate((float val) => {
|
||||
if (_scribbleMaterialInstance != null)
|
||||
_scribbleMaterialInstance.SetFloat(ProgressID, val);
|
||||
})
|
||||
.setOnComplete(() => {
|
||||
_scribbleTweenId = -1;
|
||||
if (_scribbleOverlay != null)
|
||||
_scribbleOverlay.gameObject.SetActive(false);
|
||||
}).id;
|
||||
}
|
||||
|
||||
if (_textComponent != null)
|
||||
{
|
||||
float currentAlpha = _textComponent.color.a;
|
||||
float ghostAlphaRatio = _ghostTextComponent != null ? (_originalGhostColor.a / _originalTextColor.a) : 0f;
|
||||
|
||||
_fadeTweenId = LeanTween.value(gameObject, currentAlpha, _originalTextColor.a, 0.3f)
|
||||
.setEase(LeanTweenType.easeOutQuad)
|
||||
.setOnUpdate((float val) => {
|
||||
if (_textComponent != null)
|
||||
{
|
||||
_textComponent.color = _textComponent.color.WithAlpha(val);
|
||||
}
|
||||
|
||||
if (_ghostTextComponent != null)
|
||||
{
|
||||
_ghostTextComponent.color = _ghostTextComponent.color.WithAlpha(val * ghostAlphaRatio);
|
||||
}
|
||||
|
||||
if (_scribbleOverlay != null)
|
||||
{
|
||||
_scribbleOverlay.color = _scribbleOverlay.color.WithAlpha(val);
|
||||
}
|
||||
})
|
||||
.setOnComplete(() => {
|
||||
_fadeTweenId = -1;
|
||||
}).id;
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelActiveTweens()
|
||||
{
|
||||
TweenHelper.CancelTween(ref _scribbleTweenId);
|
||||
TweenHelper.CancelTween(ref _fadeTweenId);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
CancelActiveTweens();
|
||||
|
||||
if (_scribbleMaterialInstance != null)
|
||||
Destroy(_scribbleMaterialInstance);
|
||||
|
||||
if (_scribbleOverlay != null)
|
||||
Destroy(_scribbleOverlay.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ee81e39cd832e34d9219099af39f4bb
|
||||
Reference in New Issue
Block a user