添加对话播放速度设置,调整对话以打字机的形式显示
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Definition.Enum;
|
||||
using DG.Tweening;
|
||||
using Definition.Enum;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
@@ -10,6 +11,8 @@ namespace UI
|
||||
{
|
||||
public override DialogFormMode UIMode => DialogFormMode.BottomBox;
|
||||
|
||||
[SerializeField] private GameObject _speakerArea;
|
||||
|
||||
[SerializeField] private TMP_Text _speakerNameText;
|
||||
|
||||
[SerializeField] private TMP_Text _contentText;
|
||||
@@ -22,10 +25,15 @@ namespace UI
|
||||
|
||||
[SerializeField] private int _rightSpritePosition = -450;
|
||||
|
||||
[SerializeField] private float _moveDuration = 0.25f;
|
||||
|
||||
[SerializeField] private Ease _moveEase = Ease.OutCubic;
|
||||
|
||||
private readonly int _singleSpeakerCenterPosition = Screen.width / 2;
|
||||
|
||||
private string _leftSpeakerToken = string.Empty;
|
||||
private string _rightSpeakerToken = string.Empty;
|
||||
private Sequence _layoutSequence;
|
||||
|
||||
public override void StartDialog(DialogFormContext context)
|
||||
{
|
||||
@@ -37,10 +45,11 @@ namespace UI
|
||||
|
||||
_context = context;
|
||||
|
||||
string speakerName = NormalizeValue(context.SpeakerName);
|
||||
if (string.IsNullOrEmpty(speakerName))
|
||||
string speakerName = context.SpeakerName;
|
||||
|
||||
if (_speakerArea != null)
|
||||
{
|
||||
speakerName = NormalizeValue(context.SpeakerId);
|
||||
_speakerArea.SetActive(!string.IsNullOrEmpty(speakerName));
|
||||
}
|
||||
|
||||
if (_speakerNameText != null)
|
||||
@@ -48,15 +57,12 @@ namespace UI
|
||||
_speakerNameText.text = speakerName;
|
||||
}
|
||||
|
||||
if (_contentText != null)
|
||||
{
|
||||
_contentText.text = NormalizeValue(context.Text);
|
||||
}
|
||||
PlayTypewriter(_contentText, context.Text, context.PlayingSpeed);
|
||||
|
||||
if (string.IsNullOrEmpty(speakerName))
|
||||
{
|
||||
ClearSpeakerState();
|
||||
ApplySpeakerLayout(false, false);
|
||||
ApplySpeakerLayout(false, false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,13 +78,14 @@ namespace UI
|
||||
|
||||
bool hasLeftSpeaker = !string.IsNullOrEmpty(_leftSpeakerToken);
|
||||
bool hasRightSpeaker = !string.IsNullOrEmpty(_rightSpeakerToken);
|
||||
ApplySpeakerLayout(hasLeftSpeaker, hasRightSpeaker);
|
||||
ApplySpeakerLayout(hasLeftSpeaker, hasRightSpeaker, false);
|
||||
}
|
||||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
ClearSpeakerState();
|
||||
ApplySpeakerLayout(false, false);
|
||||
KillLayoutTween();
|
||||
ApplySpeakerLayout(false, false, true);
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
||||
@@ -88,35 +95,164 @@ namespace UI
|
||||
_rightSpeakerToken = string.Empty;
|
||||
}
|
||||
|
||||
private void ApplySpeakerLayout(bool hasLeftSpeaker, bool hasRightSpeaker)
|
||||
private void KillLayoutTween()
|
||||
{
|
||||
if (_layoutSequence != null)
|
||||
{
|
||||
_layoutSequence.Kill();
|
||||
_layoutSequence = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySpeakerLayout(bool hasLeftSpeaker, bool hasRightSpeaker, bool instant)
|
||||
{
|
||||
if (_leftSprite == null || _rightSprite == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool leftCurrentlyVisible = _leftSprite.gameObject.activeSelf;
|
||||
bool rightCurrentlyVisible = _rightSprite.gameObject.activeSelf;
|
||||
|
||||
bool leftTargetVisible = hasLeftSpeaker;
|
||||
bool rightTargetVisible = hasRightSpeaker;
|
||||
|
||||
float leftTargetX = GetTargetX(true, hasLeftSpeaker, hasRightSpeaker);
|
||||
float rightTargetX = GetTargetX(false, hasLeftSpeaker, hasRightSpeaker);
|
||||
|
||||
KillLayoutTween();
|
||||
|
||||
PrepareStartState(
|
||||
leftCurrentlyVisible,
|
||||
rightCurrentlyVisible,
|
||||
leftTargetVisible,
|
||||
rightTargetVisible,
|
||||
hasLeftSpeaker,
|
||||
hasRightSpeaker);
|
||||
|
||||
if (instant || _moveDuration <= 0f)
|
||||
{
|
||||
SetSpritePosition(_leftSprite.rectTransform, leftTargetX);
|
||||
SetSpritePosition(_rightSprite.rectTransform, rightTargetX);
|
||||
SetSpriteVisible(_leftSprite, leftTargetVisible);
|
||||
SetSpriteVisible(_rightSprite, rightTargetVisible);
|
||||
return;
|
||||
}
|
||||
|
||||
_layoutSequence = DOTween.Sequence();
|
||||
|
||||
Tween leftTween = CreateMoveTween(_leftSprite.rectTransform, leftTargetX);
|
||||
if (leftTween != null)
|
||||
{
|
||||
_layoutSequence.Join(leftTween);
|
||||
}
|
||||
|
||||
Tween rightTween = CreateMoveTween(_rightSprite.rectTransform, rightTargetX);
|
||||
if (rightTween != null)
|
||||
{
|
||||
_layoutSequence.Join(rightTween);
|
||||
}
|
||||
|
||||
if (_layoutSequence.active && _layoutSequence.Duration(false) > 0f)
|
||||
{
|
||||
_layoutSequence.OnComplete(() =>
|
||||
{
|
||||
SetSpriteVisible(_leftSprite, leftTargetVisible);
|
||||
SetSpriteVisible(_rightSprite, rightTargetVisible);
|
||||
_layoutSequence = null;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
SetSpritePosition(_leftSprite.rectTransform, leftTargetX);
|
||||
SetSpritePosition(_rightSprite.rectTransform, rightTargetX);
|
||||
SetSpriteVisible(_leftSprite, leftTargetVisible);
|
||||
SetSpriteVisible(_rightSprite, rightTargetVisible);
|
||||
_layoutSequence.Kill();
|
||||
_layoutSequence = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareStartState(
|
||||
bool leftCurrentlyVisible,
|
||||
bool rightCurrentlyVisible,
|
||||
bool leftTargetVisible,
|
||||
bool rightTargetVisible,
|
||||
bool hasLeftSpeaker,
|
||||
bool hasRightSpeaker)
|
||||
{
|
||||
if (leftTargetVisible && !leftCurrentlyVisible)
|
||||
{
|
||||
float leftStartX = GetAppearStartX(true, rightCurrentlyVisible, hasLeftSpeaker, hasRightSpeaker);
|
||||
SetSpritePosition(_leftSprite.rectTransform, leftStartX);
|
||||
SetSpriteVisible(_leftSprite, true);
|
||||
}
|
||||
|
||||
if (rightTargetVisible && !rightCurrentlyVisible)
|
||||
{
|
||||
float rightStartX = GetAppearStartX(false, leftCurrentlyVisible, hasLeftSpeaker, hasRightSpeaker);
|
||||
SetSpritePosition(_rightSprite.rectTransform, rightStartX);
|
||||
SetSpriteVisible(_rightSprite, true);
|
||||
}
|
||||
|
||||
if (leftCurrentlyVisible && !leftTargetVisible)
|
||||
{
|
||||
SetSpriteVisible(_leftSprite, true);
|
||||
}
|
||||
|
||||
if (rightCurrentlyVisible && !rightTargetVisible)
|
||||
{
|
||||
SetSpriteVisible(_rightSprite, true);
|
||||
}
|
||||
}
|
||||
|
||||
private float GetAppearStartX(bool isLeft, bool otherCurrentlyVisible, bool hasLeftSpeaker,
|
||||
bool hasRightSpeaker)
|
||||
{
|
||||
if (hasLeftSpeaker && hasRightSpeaker)
|
||||
{
|
||||
SetSpriteVisible(_leftSprite, true);
|
||||
SetSpriteVisible(_rightSprite, true);
|
||||
SetSpritePosition(_leftSprite.rectTransform, _leftSpritePosition);
|
||||
SetSpritePosition(_rightSprite.rectTransform, _rightSpritePosition);
|
||||
return;
|
||||
// single -> multi: hidden side starts from center, then both move to side positions.
|
||||
if (otherCurrentlyVisible)
|
||||
{
|
||||
return _singleSpeakerCenterPosition;
|
||||
}
|
||||
|
||||
return isLeft ? _leftSpritePosition : _rightSpritePosition;
|
||||
}
|
||||
|
||||
if (hasLeftSpeaker)
|
||||
// single appears: active side starts from its side and moves to center.
|
||||
return isLeft ? _leftSpritePosition : _rightSpritePosition;
|
||||
}
|
||||
|
||||
private float GetTargetX(bool isLeft, bool hasLeftSpeaker, bool hasRightSpeaker)
|
||||
{
|
||||
if (hasLeftSpeaker && hasRightSpeaker)
|
||||
{
|
||||
SetSpriteVisible(_leftSprite, true);
|
||||
SetSpriteVisible(_rightSprite, false);
|
||||
SetSpritePosition(_leftSprite.rectTransform, _singleSpeakerCenterPosition);
|
||||
return;
|
||||
return isLeft ? _leftSpritePosition : _rightSpritePosition;
|
||||
}
|
||||
|
||||
if (hasRightSpeaker)
|
||||
if (hasLeftSpeaker || hasRightSpeaker)
|
||||
{
|
||||
SetSpriteVisible(_leftSprite, false);
|
||||
SetSpriteVisible(_rightSprite, true);
|
||||
SetSpritePosition(_rightSprite.rectTransform, -_singleSpeakerCenterPosition);
|
||||
return;
|
||||
// multi -> single: both move to center first, then inactive side hides.
|
||||
return _singleSpeakerCenterPosition;
|
||||
}
|
||||
|
||||
SetSpriteVisible(_leftSprite, false);
|
||||
SetSpriteVisible(_rightSprite, false);
|
||||
return isLeft ? _leftSpritePosition : _rightSpritePosition;
|
||||
}
|
||||
|
||||
private Tween CreateMoveTween(RectTransform rectTransform, float targetX)
|
||||
{
|
||||
if (rectTransform == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Mathf.Abs(rectTransform.anchoredPosition.x - targetX) < 0.01f)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return rectTransform.DOAnchorPosX(targetX, _moveDuration).SetEase(_moveEase);
|
||||
}
|
||||
|
||||
private static void SetSpriteVisible(Image spriteImage, bool visible)
|
||||
@@ -141,4 +277,4 @@ namespace UI
|
||||
rectTransform.anchoredPosition = anchoredPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using System.Collections;
|
||||
using Definition.Enum;
|
||||
using Event;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public abstract class DialogFormBase : UGuiForm
|
||||
{
|
||||
[SerializeField] protected float _playSpeed;
|
||||
|
||||
protected DialogFormContext _context;
|
||||
private Coroutine _typingCoroutine;
|
||||
private TMP_Text _typingTargetText;
|
||||
private bool _isTypewriting;
|
||||
|
||||
public abstract DialogFormMode UIMode { get; }
|
||||
|
||||
@@ -29,12 +32,18 @@ namespace UI
|
||||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
StopTypewriter();
|
||||
_context = null;
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
||||
public void OnClickNextLine()
|
||||
{
|
||||
if (CompleteTypewriterIfRunning())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Fire(this, DialogNextLineRequestEventArgs.Create());
|
||||
}
|
||||
|
||||
@@ -48,24 +57,90 @@ namespace UI
|
||||
GameEntry.Event.Fire(this, DialogStopRequestEventArgs.Create());
|
||||
}
|
||||
|
||||
protected static string NormalizeValue(string value)
|
||||
protected void PlayTypewriter(TMP_Text targetText, string text, float charsPerSecond)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
StopTypewriter();
|
||||
|
||||
if (targetText == null)
|
||||
{
|
||||
return string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(value, "Null", System.StringComparison.OrdinalIgnoreCase))
|
||||
string finalText = text ?? string.Empty;
|
||||
_typingTargetText = targetText;
|
||||
|
||||
if (charsPerSecond <= 0f || string.IsNullOrEmpty(finalText))
|
||||
{
|
||||
return string.Empty;
|
||||
targetText.text = finalText;
|
||||
targetText.maxVisibleCharacters = int.MaxValue;
|
||||
_isTypewriting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(value, "None", System.StringComparison.OrdinalIgnoreCase))
|
||||
_isTypewriting = true;
|
||||
_typingCoroutine = StartCoroutine(TypewriterRoutine(targetText, finalText, charsPerSecond));
|
||||
}
|
||||
|
||||
protected void StopTypewriter()
|
||||
{
|
||||
if (_typingCoroutine != null)
|
||||
{
|
||||
return string.Empty;
|
||||
StopCoroutine(_typingCoroutine);
|
||||
_typingCoroutine = null;
|
||||
}
|
||||
|
||||
return value;
|
||||
_typingTargetText = null;
|
||||
_isTypewriting = false;
|
||||
}
|
||||
|
||||
private bool CompleteTypewriterIfRunning()
|
||||
{
|
||||
if (!_isTypewriting || _typingTargetText == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_typingTargetText.maxVisibleCharacters = int.MaxValue;
|
||||
StopTypewriter();
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerator TypewriterRoutine(TMP_Text targetText, string finalText, float charsPerSecond)
|
||||
{
|
||||
targetText.text = finalText;
|
||||
targetText.ForceMeshUpdate();
|
||||
|
||||
int totalCharacters = targetText.textInfo.characterCount;
|
||||
if (totalCharacters <= 0)
|
||||
{
|
||||
targetText.maxVisibleCharacters = int.MaxValue;
|
||||
_typingCoroutine = null;
|
||||
_typingTargetText = null;
|
||||
_isTypewriting = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
targetText.maxVisibleCharacters = 0;
|
||||
float elapsed = 0f;
|
||||
int visibleCharacters = 0;
|
||||
|
||||
while (visibleCharacters < totalCharacters)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
int nextVisible = Mathf.Min(totalCharacters, Mathf.FloorToInt(elapsed * charsPerSecond));
|
||||
if (nextVisible != visibleCharacters)
|
||||
{
|
||||
visibleCharacters = nextVisible;
|
||||
targetText.maxVisibleCharacters = visibleCharacters;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
targetText.maxVisibleCharacters = int.MaxValue;
|
||||
_typingCoroutine = null;
|
||||
_typingTargetText = null;
|
||||
_isTypewriting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ namespace UI
|
||||
public class MaskDialogForm : DialogFormBase
|
||||
{
|
||||
public override DialogFormMode UIMode => DialogFormMode.Mask;
|
||||
|
||||
|
||||
[SerializeField] private Image _maskImage;
|
||||
|
||||
|
||||
[SerializeField] private TMP_Text _text;
|
||||
|
||||
|
||||
public override void StartDialog(DialogFormContext context)
|
||||
{
|
||||
if (context == null)
|
||||
@@ -29,10 +29,7 @@ namespace UI
|
||||
_maskImage.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
if (_text != null)
|
||||
{
|
||||
_text.text = NormalizeValue(context.Text);
|
||||
}
|
||||
PlayTypewriter(_text, context.Text, context.PlayingSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user