Advanced POC - portals, dialogue system + floating words, room loading
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unheimlich.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,214 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
|
||||
namespace Unheimlich.UI
|
||||
{
|
||||
[RequireComponent(typeof(Camera))]
|
||||
public class DialogueBlurEffectURP : MonoBehaviour
|
||||
{
|
||||
public static DialogueBlurEffectURP Instance { get; private set; }
|
||||
|
||||
[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;
|
||||
|
||||
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;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
|
||||
_baseCam = GetComponent<Camera>();
|
||||
|
||||
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()
|
||||
{
|
||||
if (_autoCreateDialogueCamera)
|
||||
SetupDialogueOverlayCamera();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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 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 (_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,326 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using Unheimlich.Data.Dialogues;
|
||||
using Unheimlich.Gameplay.Player;
|
||||
using Unheimlich.Interactions.FloatingWords;
|
||||
using Unheimlich.Interactions.Dialogues;
|
||||
|
||||
namespace Unheimlich.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")]
|
||||
[SerializeField] private GameObject _dialoguePanel;
|
||||
public GameObject ChoiceContainer;
|
||||
|
||||
[Header("Text Fields")]
|
||||
[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 List<ChoiceFocusHandler> _activeChoices = new List<ChoiceFocusHandler>();
|
||||
private float _sortTimer = 0f;
|
||||
private const float SORT_INTERVAL = 0.1f;
|
||||
|
||||
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>();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Player.Instance == null || !ChoiceContainer.activeInHierarchy)
|
||||
return;
|
||||
|
||||
if (_shouldFollowPlayerPosition)
|
||||
ChoiceContainer.transform.position = Player.Instance.transform.position;
|
||||
|
||||
if (_shouldFollowPlayerRotation)
|
||||
{
|
||||
float playerYRotation = Player.Instance.transform.eulerAngles.y;
|
||||
ChoiceContainer.transform.rotation = Quaternion.Euler(0f, playerYRotation, 0f);
|
||||
}
|
||||
|
||||
if (_currentAnimationSettings != null && _currentAnimationSettings.EnableDistanceScaling && !_shouldFollowPlayerPosition)
|
||||
ApplyDistanceScaling();
|
||||
|
||||
SortChoicesByDistance();
|
||||
}
|
||||
|
||||
public void ShowDialogueBox(string speaker, string content)
|
||||
{
|
||||
_speakerText.text = speaker;
|
||||
_contentText.text = content;
|
||||
_dialoguePanel.SetActive(true);
|
||||
|
||||
if (_dialogueCanvasGroup != null)
|
||||
{
|
||||
LeanTween.cancel(_dialoguePanel);
|
||||
_dialogueCanvasGroup.alpha = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
public void HideDialogueBox()
|
||||
{
|
||||
_dialoguePanel.SetActive(false);
|
||||
ChoiceContainer.SetActive(false);
|
||||
HideChoices();
|
||||
}
|
||||
|
||||
public void FadeOutAndHide(System.Action onComplete = null)
|
||||
{
|
||||
if (_dialogueCanvasGroup != null)
|
||||
{
|
||||
LeanTween.cancel(_dialoguePanel);
|
||||
LeanTween.alphaCanvas(_dialogueCanvasGroup, 0f, _fadeOutDuration)
|
||||
.setOnComplete(() =>
|
||||
{
|
||||
HideDialogueBox();
|
||||
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 && Player.Instance != null)
|
||||
{
|
||||
Vector3 playerWorldPos = Player.Instance.transform.position;
|
||||
ChoiceContainer.transform.position = playerWorldPos;
|
||||
}
|
||||
else
|
||||
ChoiceContainer.transform.localPosition = Vector3.zero;
|
||||
|
||||
if (followRotation && Player.Instance != null)
|
||||
{
|
||||
float playerYRotation = Player.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();
|
||||
|
||||
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 offset = index * 0.2f;
|
||||
|
||||
Vector3 startPos = choice.transform.localPosition;
|
||||
|
||||
LeanTween.value(choice, 0f, Mathf.PI * 2f, duration)
|
||||
.setDelay(offset)
|
||||
.setOnUpdate((float val) => {
|
||||
if (choice != null)
|
||||
{
|
||||
Vector3 newPos = startPos;
|
||||
newPos.y += Mathf.Sin(val) * amplitude;
|
||||
choice.transform.localPosition = newPos;
|
||||
}
|
||||
})
|
||||
.setLoopClamp()
|
||||
.setEase(LeanTweenType.linear);
|
||||
}
|
||||
|
||||
public void RotationAnimation(float degreesPerSecond = 60f)
|
||||
{
|
||||
if (degreesPerSecond <= 0f)
|
||||
return;
|
||||
|
||||
float duration = 360f / degreesPerSecond;
|
||||
|
||||
LeanTween.cancel(ChoiceContainer);
|
||||
LeanTween.rotateAroundLocal(ChoiceContainer, _currentRotationAxis, 360f, duration)
|
||||
.setLoopClamp()
|
||||
.setEase(LeanTweenType.linear);
|
||||
}
|
||||
|
||||
/// <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 || Player.Instance == null)
|
||||
return;
|
||||
|
||||
float distance = Vector3.Distance(_currentAnchor.position, Player.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 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();
|
||||
LeanTween.cancel(ChoiceContainer);
|
||||
foreach (Transform child in ChoiceContainer.transform)
|
||||
{
|
||||
LeanTween.cancel(child.gameObject);
|
||||
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 || Player.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
|
||||
Reference in New Issue
Block a user