203 lines
7.3 KiB
C#

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;
}
}
}