using Cysharp.Threading.Tasks; using DG.Tweening; using System; using System.Threading; using TMPro; using UnityEngine; using UnityEngine.Serialization; namespace CharacterSystem { public class MessageBox : MonoBehaviour { [SerializeField] private Transform _frame; [SerializeField] private Vector2 _localPos; [SerializeField] private Vector3 _visibleScale = Vector3.one; [SerializeField, FormerlySerializedAs("nameLabel")] private TextMeshProUGUI _nameLabel; [SerializeField, FormerlySerializedAs("textField")] private TextMeshProUGUI _textField; [SerializeField] private AudioSource _audioSource; [SerializeField, FormerlySerializedAs("typeClip")] private AudioClip _typeClip; [SerializeField, Min(0.01f)] private float _minimumFontSize = 0.1f; [SerializeField, Min(0.01f)] private float _maximumFontSize = 0.24f; [SerializeField, Range(1, 16)] private int _fontSizeSearchIterations = 10; private CancellationTokenSource _cancellationTokenSource; private bool _isProcessing; private float _lastTypeSoundTime; private float _typeClipLength; private void OnDestroy() => Stop(); public void Initialize(string characterName) { _nameLabel.text = characterName; _nameLabel.gameObject.SetActive(false); _frame.localPosition = _localPos; _frame.localScale = Vector3.zero; _lastTypeSoundTime = 0f; _typeClipLength = _typeClip != null ? _typeClip.length : 0f; } public void PlayReplica(Replica replica, bool force = false, bool showName = false) { if (_isProcessing && force == false) return; if (force) CancelCurrent(); SetOptimalFontSize(replica.text ?? string.Empty); ProcessReplicaAsync(replica, showName).Forget(); } public void Stop() { CancelCurrent(); _cancellationTokenSource = null; _isProcessing = false; _audioSource.Stop(); _frame.DOKill(); _frame.localScale = Vector3.zero; _nameLabel.gameObject.SetActive(false); _textField.text = string.Empty; } private async UniTask ProcessReplicaAsync(Replica replica, bool showName) { var currentTokenSource = new CancellationTokenSource(); _cancellationTokenSource = currentTokenSource; _isProcessing = true; try { await PlayReplicaAsync(replica, showName, currentTokenSource.Token); } catch (OperationCanceledException) { } finally { if (_cancellationTokenSource == currentTokenSource) { _cancellationTokenSource = null; _isProcessing = false; } currentTokenSource.Dispose(); } } private async UniTask PlayReplicaAsync(Replica replica, bool showName, CancellationToken cancellationToken) { _nameLabel.gameObject.SetActive(showName); await GetShowSequence().WithCancellation(cancellationToken); await TypeAsync(replica, cancellationToken); await UniTask.Delay(1000, cancellationToken: cancellationToken); await GetHideSequence().WithCancellation(cancellationToken); _audioSource.Stop(); _nameLabel.gameObject.SetActive(false); _textField.text = string.Empty; } private async UniTask TypeAsync(Replica replica, CancellationToken cancellationToken) { string message = replica.text ?? string.Empty; if (replica.clip != null) { _audioSource.clip = replica.clip; _audioSource.Play(); if (SoundManager.instance != null) { SoundManager.instance.MusicFade( fadeDuration: 0.2f, holdDuration: Mathf.Max(replica.clip.length - 0.2f, 0f), targetVolumePercent: 0.5f); } } float duration = replica.Duration; float elapsedTime = 0f; _textField.text = string.Empty; while (elapsedTime < duration && cancellationToken.IsCancellationRequested == false) { float progress = Mathf.Clamp01(elapsedTime / duration); int symbolsToShow = Mathf.FloorToInt(message.Length * progress); _textField.text = message.Substring(0, symbolsToShow); bool shouldPlayTypeClip = _typeClipLength > 0f && Time.time - _lastTypeSoundTime > _typeClipLength; if (shouldPlayTypeClip) { _audioSource.PlayOneShot(_typeClip); _lastTypeSoundTime = Time.time; } await UniTask.Yield(cancellationToken); elapsedTime += Time.deltaTime; } if (cancellationToken.IsCancellationRequested == false) _textField.text = message; } private void CancelCurrent() { if (_cancellationTokenSource != null && _cancellationTokenSource.IsCancellationRequested == false) _cancellationTokenSource.Cancel(); } private void SetOptimalFontSize(string message) { _textField.enableAutoSizing = false; float minimumFontSize = Mathf.Min(_minimumFontSize, _maximumFontSize); float maximumFontSize = Mathf.Max(_minimumFontSize, _maximumFontSize); if (string.IsNullOrEmpty(message)) { _textField.fontSize = maximumFontSize; return; } Rect fieldRect = _textField.rectTransform.rect; Vector4 margin = _textField.margin; float availableWidth = Mathf.Max(fieldRect.width - margin.x - margin.z, 0.01f); float availableHeight = Mathf.Max(fieldRect.height - margin.y - margin.w, 0.01f); for (int i = 0; i < _fontSizeSearchIterations; i++) { float candidateFontSize = (minimumFontSize + maximumFontSize) * 0.5f; _textField.fontSize = candidateFontSize; Vector2 preferredSize = _textField.GetPreferredValues(message, availableWidth, 0f); if (preferredSize.x <= availableWidth && preferredSize.y <= availableHeight) minimumFontSize = candidateFontSize; else maximumFontSize = candidateFontSize; } _textField.fontSize = minimumFontSize; } private Sequence GetShowSequence() { Sequence sequence = DOTween.Sequence(); sequence.Append(_frame.DOScale(_visibleScale, 0.2f).SetEase(Ease.OutBack)); sequence.PlayForward(); return sequence; } private Sequence GetHideSequence() { Sequence sequence = DOTween.Sequence(); sequence.Append(_frame.DOScale(Vector3.zero, 0.2f).SetEase(Ease.InBack)); sequence.PlayForward(); return sequence; } } }