393 lines
13 KiB
C#
393 lines
13 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
} |