442 lines
14 KiB
C#
442 lines
14 KiB
C#
using System.Collections;
|
|
using DG.Tweening;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.UI;
|
|
|
|
public class UI_CharacterDialogController : MonoBehaviour
|
|
{
|
|
[SerializeField] private string requiredSceneName = "UI_UI";
|
|
[SerializeField] private string characterObjectName = "Image_Character";
|
|
[SerializeField] private string dialogObjectName = "UI_DIALOG";
|
|
|
|
[Header("Enter")]
|
|
[SerializeField] private float enterDuration = 0.3f;
|
|
[SerializeField] private float enterFromOffsetY = 60f;
|
|
[SerializeField] private Ease enterEase = Ease.OutCubic;
|
|
|
|
[Header("Blink Out")]
|
|
[SerializeField] private int blinkCount = 2;
|
|
[SerializeField] private float blinkHalfTime = 0.05f;
|
|
|
|
[Header("Auto Hide")]
|
|
[SerializeField] private float idleHideDelay = 5f;
|
|
[SerializeField] private bool useUnscaledTime = true;
|
|
|
|
private RectTransform dialogRect;
|
|
private CanvasGroup dialogGroup;
|
|
private Vector2 dialogBasePos;
|
|
private bool basePosCached;
|
|
private RectTransform boundCharacterRect;
|
|
private int bindAttemptToken;
|
|
private bool isVisible;
|
|
private Coroutine idleRoutine;
|
|
private Coroutine bindRoutine;
|
|
private Coroutine keepAliveRoutine;
|
|
private Sequence dialogSequence;
|
|
private UI_CharacterDialogClickProxy clickProxy;
|
|
private Button characterButton;
|
|
private bool initialHideDone;
|
|
|
|
private void OnEnable()
|
|
{
|
|
SceneManager.sceneLoaded -= OnSceneLoaded;
|
|
SceneManager.sceneLoaded += OnSceneLoaded;
|
|
SceneManager.activeSceneChanged -= OnActiveSceneChanged;
|
|
SceneManager.activeSceneChanged += OnActiveSceneChanged;
|
|
|
|
RebindSoon();
|
|
if (keepAliveRoutine == null)
|
|
keepAliveRoutine = StartCoroutine(KeepBindingAliveRoutine());
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
SceneManager.sceneLoaded -= OnSceneLoaded;
|
|
SceneManager.activeSceneChanged -= OnActiveSceneChanged;
|
|
|
|
KillTweens();
|
|
if (characterButton != null)
|
|
{
|
|
characterButton.onClick.RemoveListener(OnCharacterClicked);
|
|
characterButton = null;
|
|
}
|
|
if (idleRoutine != null)
|
|
{
|
|
StopCoroutine(idleRoutine);
|
|
idleRoutine = null;
|
|
}
|
|
if (bindRoutine != null)
|
|
{
|
|
StopCoroutine(bindRoutine);
|
|
bindRoutine = null;
|
|
}
|
|
if (keepAliveRoutine != null)
|
|
{
|
|
StopCoroutine(keepAliveRoutine);
|
|
keepAliveRoutine = null;
|
|
}
|
|
|
|
clickProxy = null;
|
|
dialogRect = null;
|
|
dialogGroup = null;
|
|
boundCharacterRect = null;
|
|
basePosCached = false;
|
|
initialHideDone = false;
|
|
}
|
|
|
|
public void RebindSoon()
|
|
{
|
|
bindAttemptToken++;
|
|
if (bindRoutine != null)
|
|
StopCoroutine(bindRoutine);
|
|
bindRoutine = StartCoroutine(BindRoutine(bindAttemptToken));
|
|
}
|
|
|
|
private IEnumerator BindRoutine(int token)
|
|
{
|
|
yield return null;
|
|
TryBind();
|
|
yield return null;
|
|
if (token != bindAttemptToken)
|
|
yield break;
|
|
yield return null;
|
|
TryBind();
|
|
if (token != bindAttemptToken)
|
|
yield break;
|
|
yield return null;
|
|
TryBind();
|
|
if (token != bindAttemptToken)
|
|
yield break;
|
|
yield return new WaitForSecondsRealtime(0.2f);
|
|
TryBind();
|
|
if (token != bindAttemptToken)
|
|
yield break;
|
|
yield return new WaitForSecondsRealtime(0.6f);
|
|
TryBind();
|
|
bindRoutine = null;
|
|
}
|
|
|
|
private void TryBind()
|
|
{
|
|
if (!IsAllowedScene())
|
|
return;
|
|
|
|
Scene scene = gameObject.scene;
|
|
if (!scene.IsValid() || !scene.isLoaded)
|
|
scene = SceneManager.GetActiveScene();
|
|
|
|
Transform dialogTr = FindInSceneByName(scene, dialogObjectName);
|
|
if (dialogTr == null)
|
|
dialogTr = FindInSceneByName(SceneManager.GetActiveScene(), dialogObjectName);
|
|
if (dialogTr != null)
|
|
{
|
|
RectTransform nextDialogRect = dialogTr as RectTransform;
|
|
bool dialogChanged = dialogRect != nextDialogRect;
|
|
dialogRect = nextDialogRect;
|
|
if (dialogRect != null)
|
|
{
|
|
dialogGroup = dialogRect.GetComponent<CanvasGroup>();
|
|
if (dialogGroup == null)
|
|
dialogGroup = dialogRect.gameObject.AddComponent<CanvasGroup>();
|
|
|
|
if (dialogChanged || !basePosCached)
|
|
{
|
|
dialogBasePos = dialogRect.anchoredPosition;
|
|
basePosCached = true;
|
|
}
|
|
}
|
|
|
|
if (!initialHideDone)
|
|
{
|
|
HideImmediate();
|
|
initialHideDone = true;
|
|
}
|
|
}
|
|
|
|
Transform characterTr = FindInSceneByName(scene, characterObjectName);
|
|
if (characterTr == null)
|
|
characterTr = FindInSceneByName(SceneManager.GetActiveScene(), characterObjectName);
|
|
if (characterTr != null)
|
|
{
|
|
boundCharacterRect = characterTr as RectTransform;
|
|
clickProxy = characterTr.GetComponent<UI_CharacterDialogClickProxy>();
|
|
if (clickProxy == null)
|
|
clickProxy = characterTr.gameObject.AddComponent<UI_CharacterDialogClickProxy>();
|
|
clickProxy.Bind(this);
|
|
|
|
// Ensure the image can receive click events.
|
|
Graphic graphic = characterTr.GetComponent<Graphic>();
|
|
if (graphic != null)
|
|
graphic.raycastTarget = true;
|
|
|
|
// Also bind through Button click to avoid missing pointer callbacks.
|
|
Button btn = characterTr.GetComponent<Button>();
|
|
if (btn == null)
|
|
{
|
|
btn = characterTr.gameObject.AddComponent<Button>();
|
|
btn.transition = Selectable.Transition.None;
|
|
if (graphic != null)
|
|
btn.targetGraphic = graphic;
|
|
}
|
|
|
|
if (characterButton != null && characterButton != btn)
|
|
characterButton.onClick.RemoveListener(OnCharacterClicked);
|
|
characterButton = btn;
|
|
characterButton.onClick.RemoveListener(OnCharacterClicked);
|
|
characterButton.onClick.AddListener(OnCharacterClicked);
|
|
}
|
|
else
|
|
{
|
|
boundCharacterRect = null;
|
|
}
|
|
}
|
|
|
|
public void OnCharacterClicked()
|
|
{
|
|
if (!IsBindingValid())
|
|
TryBind();
|
|
|
|
if (dialogRect == null || dialogGroup == null || !basePosCached)
|
|
return;
|
|
|
|
if (isVisible)
|
|
{
|
|
BlinkOut(() => PlayEnter(), deactivateAtEnd: true);
|
|
}
|
|
else
|
|
{
|
|
PlayEnter();
|
|
}
|
|
}
|
|
|
|
private void PlayEnter()
|
|
{
|
|
if (dialogRect == null || dialogGroup == null)
|
|
return;
|
|
|
|
KillTweens();
|
|
dialogRect.gameObject.SetActive(true);
|
|
dialogRect.anchoredPosition = dialogBasePos - new Vector2(0f, Mathf.Abs(enterFromOffsetY));
|
|
dialogGroup.alpha = 0f;
|
|
|
|
dialogSequence = DOTween.Sequence().SetUpdate(useUnscaledTime);
|
|
dialogSequence.Join(dialogRect.DOAnchorPos(dialogBasePos, Mathf.Max(0.01f, enterDuration)).SetEase(enterEase));
|
|
dialogSequence.Join(dialogGroup.DOFade(1f, Mathf.Max(0.01f, enterDuration)).SetEase(Ease.OutCubic));
|
|
dialogSequence.OnComplete(() => dialogSequence = null);
|
|
|
|
isVisible = true;
|
|
RestartIdleTimer();
|
|
}
|
|
|
|
private void BlinkOut(System.Action onComplete, bool deactivateAtEnd)
|
|
{
|
|
if (dialogRect == null || dialogGroup == null)
|
|
{
|
|
onComplete?.Invoke();
|
|
return;
|
|
}
|
|
|
|
KillTweens();
|
|
dialogRect.gameObject.SetActive(true);
|
|
|
|
dialogSequence = DOTween.Sequence().SetUpdate(useUnscaledTime);
|
|
int blinks = Mathf.Max(1, blinkCount);
|
|
float half = Mathf.Max(0.01f, blinkHalfTime);
|
|
for (int i = 0; i < blinks; i++)
|
|
{
|
|
dialogSequence.Append(dialogGroup.DOFade(0f, half).SetEase(Ease.OutQuad));
|
|
dialogSequence.Append(dialogGroup.DOFade(1f, half).SetEase(Ease.OutQuad));
|
|
}
|
|
dialogSequence.Append(dialogGroup.DOFade(0f, half).SetEase(Ease.OutQuad));
|
|
dialogSequence.OnComplete(() =>
|
|
{
|
|
dialogSequence = null;
|
|
isVisible = false;
|
|
if (deactivateAtEnd && dialogRect != null)
|
|
dialogRect.gameObject.SetActive(false);
|
|
onComplete?.Invoke();
|
|
});
|
|
}
|
|
|
|
private void RestartIdleTimer()
|
|
{
|
|
if (idleRoutine != null)
|
|
StopCoroutine(idleRoutine);
|
|
idleRoutine = StartCoroutine(IdleHideRoutine());
|
|
}
|
|
|
|
private IEnumerator IdleHideRoutine()
|
|
{
|
|
float delay = Mathf.Max(0.2f, idleHideDelay);
|
|
float t = 0f;
|
|
while (t < delay)
|
|
{
|
|
t += useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
|
|
yield return null;
|
|
}
|
|
|
|
if (isVisible)
|
|
{
|
|
BlinkOut(onComplete: null, deactivateAtEnd: true);
|
|
}
|
|
idleRoutine = null;
|
|
}
|
|
|
|
private void HideImmediate()
|
|
{
|
|
if (dialogRect == null || dialogGroup == null)
|
|
return;
|
|
|
|
KillTweens();
|
|
dialogGroup.alpha = 0f;
|
|
dialogRect.anchoredPosition = dialogBasePos;
|
|
dialogRect.gameObject.SetActive(false);
|
|
isVisible = false;
|
|
}
|
|
|
|
private void KillTweens()
|
|
{
|
|
if (dialogSequence != null)
|
|
{
|
|
dialogSequence.Kill();
|
|
dialogSequence = null;
|
|
}
|
|
if (dialogRect != null)
|
|
dialogRect.DOKill();
|
|
if (dialogGroup != null)
|
|
dialogGroup.DOKill();
|
|
}
|
|
|
|
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
|
{
|
|
if (string.Equals(scene.name, requiredSceneName, System.StringComparison.OrdinalIgnoreCase))
|
|
RebindSoon();
|
|
}
|
|
|
|
private void OnActiveSceneChanged(Scene from, Scene to)
|
|
{
|
|
if (string.Equals(to.name, requiredSceneName, System.StringComparison.OrdinalIgnoreCase))
|
|
RebindSoon();
|
|
}
|
|
|
|
private IEnumerator KeepBindingAliveRoutine()
|
|
{
|
|
while (enabled)
|
|
{
|
|
if (IsAllowedScene() && !IsBindingValid())
|
|
RebindSoon();
|
|
yield return new WaitForSecondsRealtime(0.5f);
|
|
}
|
|
}
|
|
|
|
private bool IsBindingValid()
|
|
{
|
|
if (!IsAllowedScene())
|
|
return false;
|
|
|
|
if (dialogRect == null || dialogGroup == null || !basePosCached)
|
|
return false;
|
|
if (boundCharacterRect == null)
|
|
return false;
|
|
if (characterButton == null)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool IsAllowedScene()
|
|
{
|
|
if (string.IsNullOrEmpty(requiredSceneName))
|
|
return true;
|
|
|
|
Scene own = gameObject.scene;
|
|
if (own.IsValid() && string.Equals(own.name, requiredSceneName, System.StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
|
|
Scene active = SceneManager.GetActiveScene();
|
|
return active.IsValid() && string.Equals(active.name, requiredSceneName, System.StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private Transform FindInSceneByName(Scene scene, string objectName)
|
|
{
|
|
if (!scene.IsValid() || !scene.isLoaded || string.IsNullOrEmpty(objectName))
|
|
return null;
|
|
|
|
Transform[] all = UnityEngine.Object.FindObjectsByType<Transform>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
for (int i = 0; i < all.Length; i++)
|
|
{
|
|
Transform t = all[i];
|
|
if (t == null || t.gameObject.scene != scene)
|
|
continue;
|
|
if (string.Equals(t.name, objectName, System.StringComparison.OrdinalIgnoreCase))
|
|
return t;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public class UI_CharacterDialogClickProxy : MonoBehaviour, IPointerClickHandler
|
|
{
|
|
private UI_CharacterDialogController controller;
|
|
|
|
public void Bind(UI_CharacterDialogController owner)
|
|
{
|
|
controller = owner;
|
|
}
|
|
|
|
public void OnPointerClick(PointerEventData eventData)
|
|
{
|
|
controller?.OnCharacterClicked();
|
|
}
|
|
}
|
|
|
|
static class UI_CharacterDialogControllerBootstrap
|
|
{
|
|
private const string SceneName = "UI_UI";
|
|
private const string HostName = "__UICharacterDialogControllerHost";
|
|
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
|
private static void Init()
|
|
{
|
|
SceneManager.sceneLoaded -= OnSceneLoaded;
|
|
SceneManager.sceneLoaded += OnSceneLoaded;
|
|
}
|
|
|
|
private static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
|
{
|
|
if (!string.Equals(scene.name, SceneName, System.StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
GameObject host = FindHostInScene(scene, HostName);
|
|
if (host == null)
|
|
{
|
|
host = new GameObject(HostName);
|
|
SceneManager.MoveGameObjectToScene(host, scene);
|
|
}
|
|
|
|
UI_CharacterDialogController controller = host.GetComponent<UI_CharacterDialogController>();
|
|
if (controller == null)
|
|
controller = host.AddComponent<UI_CharacterDialogController>();
|
|
controller.RebindSoon();
|
|
}
|
|
|
|
private static GameObject FindHostInScene(Scene scene, string hostName)
|
|
{
|
|
if (!scene.IsValid() || !scene.isLoaded)
|
|
return null;
|
|
|
|
Transform[] all = UnityEngine.Object.FindObjectsByType<Transform>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
|
for (int i = 0; i < all.Length; i++)
|
|
{
|
|
Transform t = all[i];
|
|
if (t == null || t.gameObject.scene != scene)
|
|
continue;
|
|
if (t.name == hostName)
|
|
return t.gameObject;
|
|
}
|
|
return null;
|
|
}
|
|
}
|