chore: Project Restructuring & Theatre
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
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 Depth Sorting
|
||||
/// <summary>
|
||||
/// Sorts words by distance to camera so closer words render on top via sibling index.
|
||||
/// Ghost trails are synced to always sit just below their main word.
|
||||
/// Shared by ObjectDescriptionTrigger and DialogueUI to avoid duplication.
|
||||
/// Sorts a temporary copy to preserve the original list order, which may be index-coupled
|
||||
/// to spawn positions (e.g. _originalSpawnPositions in ObjectDescriptionTrigger).
|
||||
/// </summary>
|
||||
public static void SortByDistance(List<ChoiceFocusHandler> words, Camera camera)
|
||||
{
|
||||
if (words.Count <= 1 || camera == null) return;
|
||||
|
||||
Vector3 cameraPosition = camera.transform.position;
|
||||
|
||||
List<ChoiceFocusHandler> sorted = new List<ChoiceFocusHandler>(words);
|
||||
sorted.Sort((a, b) =>
|
||||
{
|
||||
if (a == null || b == null) return 0;
|
||||
float dA = Vector3.Distance(cameraPosition, a.transform.position);
|
||||
float dB = Vector3.Distance(cameraPosition, b.transform.position);
|
||||
return dA.CompareTo(dB);
|
||||
});
|
||||
|
||||
for (int i = 0; i < sorted.Count; i++)
|
||||
{
|
||||
if (sorted[i] != null)
|
||||
{
|
||||
sorted[i].transform.SetSiblingIndex(i);
|
||||
sorted[i].SyncGhostSiblingIndex(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
#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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user