408 lines
No EOL
13 KiB
C#
408 lines
No EOL
13 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.Events;
|
||
using UnityEngine.UI;
|
||
using TMPro;
|
||
using System.Text;
|
||
|
||
[System.Serializable]
|
||
public class WindowUI
|
||
{
|
||
public GameObject rootObject;
|
||
public TextMeshProUGUI dialogueText;
|
||
|
||
[Tooltip("Родительский объект портрета (Background). Его мы будем включать/выключать.")]
|
||
public GameObject portraitContainer;
|
||
|
||
[Tooltip("Объект самой иконки (Child), куда будет подставляться спрайт.")]
|
||
public Image portraitImage;
|
||
|
||
[Tooltip("Если кнопки выбора находятся внутри этого окна, укажите их тут (необязательно)")]
|
||
public GameObject[] choiceButtons;
|
||
}
|
||
|
||
public class MANAGER_Dialogues : MonoBehaviour
|
||
{
|
||
public static MANAGER_Dialogues Instance { get; private set; }
|
||
|
||
private void Awake()
|
||
{
|
||
if (Instance == null) Instance = this;
|
||
else Destroy(gameObject);
|
||
}
|
||
|
||
public UnityEvent onSentenceStart;
|
||
|
||
[Header("Windows Configuration")]
|
||
public WindowUI topWindow;
|
||
public WindowUI bottomWindow;
|
||
|
||
[Header("UI Focus Settings")]
|
||
public Transform uiRoot;
|
||
public List<GameObject> objectsToIgnore = new List<GameObject>();
|
||
private List<GameObject> objectsDisabledByDialogue = new List<GameObject>();
|
||
|
||
[Header("Global Settings")]
|
||
public AudioSource audioSource;
|
||
public Camera mainCamera;
|
||
[Range(0f, 1f)]
|
||
public float screenThreshold = 0.5f;
|
||
|
||
[Header("Emotional Pauses")]
|
||
[Tooltip("Длительность паузы в миллисекундах")]
|
||
public int emotionalPauseDurationMs = 500;
|
||
|
||
[Tooltip("Символы, после которых будет пауза. Многоточие '...' будет считаться за одно срабатывание.")]
|
||
public List<string> emotionalPauseSymbols = new List<string> { "?", "!", "...", ",", ":" };
|
||
|
||
[Header("Shared Choice Buttons")]
|
||
public GameObject[] globalChoiceButtons;
|
||
|
||
private WindowUI activeWindow;
|
||
private Queue<Sentence> sentences;
|
||
private Queue<UnityEvent> sentenceEvents;
|
||
private DATA_Dialogue currentDialogue;
|
||
private Sentence currentSentence;
|
||
|
||
private bool isTyping;
|
||
private bool skipTyping;
|
||
private bool isWaitingForChoice;
|
||
private bool canShowNextSentence = true;
|
||
|
||
|
||
private readonly Dictionary<string, string> _textCache = new Dictionary<string, string>();
|
||
|
||
void Start()
|
||
{
|
||
sentences = new Queue<Sentence>();
|
||
sentenceEvents = new Queue<UnityEvent>();
|
||
|
||
topWindow.rootObject.SetActive(false);
|
||
bottomWindow.rootObject.SetActive(false);
|
||
|
||
if (mainCamera == null) mainCamera = Camera.main;
|
||
|
||
if (!objectsToIgnore.Contains(topWindow.rootObject)) objectsToIgnore.Add(topWindow.rootObject);
|
||
if (!objectsToIgnore.Contains(bottomWindow.rootObject)) objectsToIgnore.Add(bottomWindow.rootObject);
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
bool inputPressed = Input.GetMouseButtonDown(0) || (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began);
|
||
|
||
if (inputPressed && currentDialogue != null && !isWaitingForChoice)
|
||
{
|
||
if (isTyping)
|
||
{
|
||
if (currentSentence != null && currentSentence.isSkipable)
|
||
{
|
||
skipTyping = true;
|
||
}
|
||
}
|
||
else if (canShowNextSentence)
|
||
{
|
||
DisplayNextSentence();
|
||
}
|
||
}
|
||
}
|
||
|
||
public void StartDialogue(DATA_Dialogue dialogue, Transform npcTransform = null, UnityEvent[] events = null)
|
||
{
|
||
currentDialogue = dialogue;
|
||
sentences.Clear();
|
||
sentenceEvents.Clear();
|
||
isWaitingForChoice = false;
|
||
|
||
ToggleBackgroundUI(false);
|
||
SelectActiveWindow(npcTransform);
|
||
|
||
foreach (var sentence in dialogue.sentences) sentences.Enqueue(sentence);
|
||
if (events != null) foreach (var ev in events) sentenceEvents.Enqueue(ev);
|
||
|
||
DisplayNextSentence();
|
||
}
|
||
|
||
private void ToggleBackgroundUI(bool show)
|
||
{
|
||
if (uiRoot == null) return;
|
||
|
||
if (!show)
|
||
{
|
||
objectsDisabledByDialogue.Clear();
|
||
foreach (Transform child in uiRoot)
|
||
{
|
||
if (child.gameObject.activeSelf && !objectsToIgnore.Contains(child.gameObject))
|
||
{
|
||
child.gameObject.SetActive(false);
|
||
objectsDisabledByDialogue.Add(child.gameObject);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
foreach (GameObject obj in objectsDisabledByDialogue)
|
||
{
|
||
if (obj != null) obj.SetActive(true);
|
||
}
|
||
objectsDisabledByDialogue.Clear();
|
||
}
|
||
}
|
||
|
||
private void SelectActiveWindow(Transform npcTransform)
|
||
{
|
||
topWindow.rootObject.SetActive(false);
|
||
bottomWindow.rootObject.SetActive(false);
|
||
|
||
if (npcTransform == null)
|
||
{
|
||
activeWindow = bottomWindow;
|
||
}
|
||
else
|
||
{
|
||
Vector3 viewportPos = mainCamera.WorldToViewportPoint(npcTransform.position);
|
||
activeWindow = (viewportPos.y < screenThreshold) ? topWindow : bottomWindow;
|
||
}
|
||
|
||
activeWindow.rootObject.SetActive(true);
|
||
}
|
||
|
||
public void DisplayNextSentence()
|
||
{
|
||
if (sentences.Count == 0)
|
||
{
|
||
if (!isTyping && !isWaitingForChoice) EndDialogue();
|
||
return;
|
||
}
|
||
|
||
currentSentence = sentences.Dequeue();
|
||
|
||
if (MANAGER_EventsDialogue.Instance != null && !string.IsNullOrEmpty(currentSentence.eventID))
|
||
{
|
||
MANAGER_EventsDialogue.Instance.Execute(currentSentence.eventID);
|
||
}
|
||
|
||
currentSentence.onSentenceStart?.Invoke();
|
||
UnityEvent currentEvent = (sentenceEvents.Count > 0) ? sentenceEvents.Dequeue() : null;
|
||
|
||
UpdatePortrait(currentSentence);
|
||
|
||
StopAllCoroutines();
|
||
StartCoroutine(TypeSentence(currentSentence, currentEvent));
|
||
}
|
||
|
||
private void UpdatePortrait(Sentence sentence)
|
||
{
|
||
if (activeWindow.portraitContainer != null && activeWindow.portraitImage != null)
|
||
{
|
||
if (sentence.characterSprite != null)
|
||
{
|
||
activeWindow.portraitImage.sprite = sentence.characterSprite;
|
||
activeWindow.portraitImage.gameObject.SetActive(true);
|
||
activeWindow.portraitContainer.SetActive(true);
|
||
}
|
||
else
|
||
{
|
||
activeWindow.portraitContainer.SetActive(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
IEnumerator TypeSentence(Sentence sentence, UnityEvent onSentenceEnd)
|
||
{
|
||
isTyping = true;
|
||
skipTyping = false;
|
||
canShowNextSentence = false;
|
||
ClearChoices();
|
||
|
||
string processedText = ReplaceShortcodes(sentence.text);
|
||
activeWindow.dialogueText.text = processedText;
|
||
|
||
activeWindow.dialogueText.ForceMeshUpdate();
|
||
|
||
var textInfo = activeWindow.dialogueText.textInfo;
|
||
int totalVisibleCharacters = textInfo.characterCount;
|
||
activeWindow.dialogueText.maxVisibleCharacters = 0;
|
||
|
||
WaitForSeconds textDelay = new WaitForSeconds(sentence.speed);
|
||
|
||
|
||
WaitForSeconds emotionalPauseDelay = new WaitForSeconds(emotionalPauseDurationMs / 1000f);
|
||
|
||
for (int i = 0; i <= totalVisibleCharacters; i++)
|
||
{
|
||
if (skipTyping)
|
||
{
|
||
activeWindow.dialogueText.maxVisibleCharacters = totalVisibleCharacters;
|
||
break;
|
||
}
|
||
|
||
activeWindow.dialogueText.maxVisibleCharacters = i;
|
||
|
||
if (i > 0)
|
||
{
|
||
char currentCharacter = textInfo.characterInfo[i - 1].character;
|
||
|
||
bool isSpecialChar = char.IsWhiteSpace(currentCharacter) ||
|
||
currentCharacter == ',' ||
|
||
currentCharacter == ' ';
|
||
|
||
if (!isSpecialChar)
|
||
{
|
||
if (sentence.voiceClip != null && audioSource != null)
|
||
{
|
||
audioSource.pitch = Random.Range(sentence.minPitch, sentence.maxPitch);
|
||
audioSource.PlayOneShot(sentence.voiceClip);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
if (i > 0 && i < totalVisibleCharacters && ShouldPause(i, textInfo, totalVisibleCharacters))
|
||
{
|
||
yield return emotionalPauseDelay;
|
||
}
|
||
else
|
||
{
|
||
yield return textDelay;
|
||
}
|
||
}
|
||
|
||
isTyping = false;
|
||
skipTyping = false;
|
||
yield return null;
|
||
canShowNextSentence = true;
|
||
|
||
onSentenceEnd?.Invoke();
|
||
sentence.onSentenceComplete?.Invoke();
|
||
|
||
if (sentence.choices != null && sentence.choices.Length > 0)
|
||
{
|
||
ShowChoices(sentence.choices);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
private bool ShouldPause(int currentIndex, TMP_TextInfo textInfo, int totalChars)
|
||
{
|
||
if (currentIndex <= 0 || currentIndex >= totalChars) return false;
|
||
|
||
int maxMatchLength = 0;
|
||
|
||
foreach (string symbol in emotionalPauseSymbols)
|
||
{
|
||
if (string.IsNullOrEmpty(symbol)) continue;
|
||
int len = symbol.Length;
|
||
if (currentIndex < len) continue;
|
||
|
||
bool match = true;
|
||
for (int j = 0; j < len; j++)
|
||
{
|
||
if (textInfo.characterInfo[currentIndex - len + j].character != symbol[j])
|
||
{
|
||
match = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (match && len > maxMatchLength)
|
||
{
|
||
maxMatchLength = len;
|
||
}
|
||
}
|
||
|
||
if (maxMatchLength > 0)
|
||
{
|
||
|
||
|
||
char nextChar = textInfo.characterInfo[currentIndex].character;
|
||
if (char.IsPunctuation(nextChar))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
private void ShowChoices(Choice[] choices)
|
||
{
|
||
isWaitingForChoice = true;
|
||
GameObject[] buttonsToUse = (activeWindow.choiceButtons != null && activeWindow.choiceButtons.Length > 0)
|
||
? activeWindow.choiceButtons
|
||
: globalChoiceButtons;
|
||
|
||
for (int i = 0; i < buttonsToUse.Length; i++)
|
||
{
|
||
if (i < choices.Length)
|
||
{
|
||
buttonsToUse[i].SetActive(true);
|
||
buttonsToUse[i].GetComponentInChildren<TextMeshProUGUI>().text = choices[i].choiceText;
|
||
|
||
Button btn = buttonsToUse[i].GetComponent<Button>();
|
||
btn.onClick.RemoveAllListeners();
|
||
Choice currentChoice = choices[i];
|
||
btn.onClick.AddListener(() => OnChoiceSelected(currentChoice));
|
||
}
|
||
else
|
||
{
|
||
buttonsToUse[i].SetActive(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OnChoiceSelected(Choice choice)
|
||
{
|
||
if (MANAGER_EventsDialogue.Instance != null) MANAGER_EventsDialogue.Instance.Execute(choice.eventID);
|
||
choice.onChoiceSelected?.Invoke();
|
||
ClearChoices();
|
||
isWaitingForChoice = false;
|
||
|
||
if (choice.nextDialogue != null) StartDialogue(choice.nextDialogue);
|
||
else if (sentences.Count > 0) DisplayNextSentence();
|
||
else EndDialogue();
|
||
}
|
||
|
||
private void ClearChoices()
|
||
{
|
||
foreach (var b in globalChoiceButtons) if(b) b.SetActive(false);
|
||
if (activeWindow != null && activeWindow.choiceButtons != null)
|
||
{
|
||
foreach (var b in activeWindow.choiceButtons) if(b) b.SetActive(false);
|
||
}
|
||
}
|
||
|
||
private string ReplaceShortcodes(string rawText)
|
||
{
|
||
if (string.IsNullOrEmpty(rawText)) return string.Empty;
|
||
|
||
if (_textCache.TryGetValue(rawText, out string cachedResult))
|
||
{
|
||
return cachedResult;
|
||
}
|
||
|
||
StringBuilder sb = new StringBuilder(rawText);
|
||
sb.Replace("[y]", "<color=#FFFF00>");
|
||
sb.Replace("[b]", "<color=#0000FF>");
|
||
sb.Replace("[r]", "<color=#FF0000>");
|
||
sb.Replace("[o]", "<color=#FFA500>");
|
||
sb.Replace("[/c]", "</color>");
|
||
|
||
string result = sb.ToString();
|
||
_textCache[rawText] = result;
|
||
|
||
return result;
|
||
}
|
||
|
||
void EndDialogue()
|
||
{
|
||
topWindow.rootObject.SetActive(false);
|
||
bottomWindow.rootObject.SetActive(false);
|
||
ToggleBackgroundUI(true);
|
||
currentDialogue = null;
|
||
}
|
||
} |