From 74ac8f199d5893946da5e4fbfd9460949aff5883 Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Mon, 9 Feb 2026 13:31:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=E8=AF=9D=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E9=80=9F=E5=BA=A6=E8=AE=BE=E7=BD=AE=EF=BC=8C=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E5=AF=B9=E8=AF=9D=E4=BB=A5=E6=89=93=E5=AD=97=E6=9C=BA?= =?UTF-8?q?=E7=9A=84=E5=BD=A2=E5=BC=8F=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/GameMain/DataTables/DialogLine.txt | 2 +- .../CustomComponent/DialogComponent.cs | 8 +- .../UI/Dialog/Context/DialogFormContext.cs | 2 + .../UI/Dialog/View/BottomDialogForm.cs | 196 +++++++++++++++--- .../Scripts/UI/Dialog/View/DialogFormBase.cs | 95 ++++++++- .../Scripts/UI/Dialog/View/MaskDialogForm.cs | 11 +- .../UI/UIForms/BottomDialogForm.prefab | 9 +- Assets/Launcher.unity | 1 + 8 files changed, 272 insertions(+), 52 deletions(-) diff --git a/Assets/GameMain/DataTables/DialogLine.txt b/Assets/GameMain/DataTables/DialogLine.txt index 567ba54..901aa3f 100644 --- a/Assets/GameMain/DataTables/DialogLine.txt +++ b/Assets/GameMain/DataTables/DialogLine.txt @@ -2,7 +2,7 @@ # Id SpeakerId Expression SpeakerName Direction Text Emphasis ChapterId DialogId # int string ExpressionType string int string EmphasisType int int # 对话行编号 策划备注 说话人Id 表情 显示人名 说话朝向 说话内容 演出效果 章节Id 对话Id - 100100001 Id规则为 Null None Null 0 相传。 None 1.00100001 1001.00001 + 100100001 Id规则为 Null None Null 0 相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。相传。 None 1.00100001 1001.00001 100100002 第1位数为章节Id Null None Null 0 Mask。 None 1.00100002 1001.00002 100100003 第2-4位数为对话Id Null None Null 0 很好。 None 1.00100003 1001.00003 100200001 第5-9位数为对话行Id Xu Normal 徐晟壹 0 你好,王。 None 1.00200001 1002.00001 diff --git a/Assets/GameMain/Scripts/CustomComponent/DialogComponent.cs b/Assets/GameMain/Scripts/CustomComponent/DialogComponent.cs index 6cf6f39..0581337 100644 --- a/Assets/GameMain/Scripts/CustomComponent/DialogComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/DialogComponent.cs @@ -15,6 +15,8 @@ namespace CustomComponent { #region Property + [SerializeField] private float _playingSpeed = 1.0f; + private const int DialogChapterDivisor = 1000; private const int LineChapterDivisor = 100000000; private const int LineDialogDivisor = 100000; @@ -35,6 +37,8 @@ namespace CustomComponent private bool _isInitialized; private bool _isPlaying; + public float PlayingSpeed => _playingSpeed; + public bool IsInitialized => _isInitialized; public bool IsPlaying => _isPlaying; @@ -211,6 +215,7 @@ namespace CustomComponent _formContext.DialogId = dialogRow.Id; _formContext.DialogTitle = dialogRow.Title; _formContext.DialogUIMode = dialogRow.UIMode; + _formContext.PlayingSpeed = Mathf.Max(0f, _playingSpeed); _currentLineIndex = 0; ApplyLineToContext(dialogLines[_currentLineIndex], _currentLineIndex, dialogLines.Count); @@ -350,6 +355,7 @@ namespace CustomComponent _formContext.Direction = lineRow.Direction; _formContext.Text = lineRow.Text; _formContext.Emphasis = lineRow.Emphasis; + _formContext.PlayingSpeed = Mathf.Max(0f, _playingSpeed); _formContext.LineIndex = lineIndex; _formContext.TotalLines = totalLines; _formContext.IsLastLine = lineIndex >= totalLines - 1; @@ -404,4 +410,4 @@ namespace CustomComponent #endregion } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/UI/Dialog/Context/DialogFormContext.cs b/Assets/GameMain/Scripts/UI/Dialog/Context/DialogFormContext.cs index 9f1eb9c..46ec048 100644 --- a/Assets/GameMain/Scripts/UI/Dialog/Context/DialogFormContext.cs +++ b/Assets/GameMain/Scripts/UI/Dialog/Context/DialogFormContext.cs @@ -4,6 +4,8 @@ namespace UI { public class DialogFormContext : UIContext { + public float PlayingSpeed = 1f; + public int ChapterId = 0; public int DialogId = 0; public string DialogTitle = string.Empty; diff --git a/Assets/GameMain/Scripts/UI/Dialog/View/BottomDialogForm.cs b/Assets/GameMain/Scripts/UI/Dialog/View/BottomDialogForm.cs index 1c53f1a..72128ae 100644 --- a/Assets/GameMain/Scripts/UI/Dialog/View/BottomDialogForm.cs +++ b/Assets/GameMain/Scripts/UI/Dialog/View/BottomDialogForm.cs @@ -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; } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/UI/Dialog/View/DialogFormBase.cs b/Assets/GameMain/Scripts/UI/Dialog/View/DialogFormBase.cs index edfab3b..6d35785 100644 --- a/Assets/GameMain/Scripts/UI/Dialog/View/DialogFormBase.cs +++ b/Assets/GameMain/Scripts/UI/Dialog/View/DialogFormBase.cs @@ -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; } } } diff --git a/Assets/GameMain/Scripts/UI/Dialog/View/MaskDialogForm.cs b/Assets/GameMain/Scripts/UI/Dialog/View/MaskDialogForm.cs index 68fd91a..0673233 100644 --- a/Assets/GameMain/Scripts/UI/Dialog/View/MaskDialogForm.cs +++ b/Assets/GameMain/Scripts/UI/Dialog/View/MaskDialogForm.cs @@ -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); } } } diff --git a/Assets/GameMain/UI/UIForms/BottomDialogForm.prefab b/Assets/GameMain/UI/UIForms/BottomDialogForm.prefab index ad6f356..021bc84 100644 --- a/Assets/GameMain/UI/UIForms/BottomDialogForm.prefab +++ b/Assets/GameMain/UI/UIForms/BottomDialogForm.prefab @@ -116,7 +116,7 @@ MonoBehaviour: m_SelectedTrigger: Selected m_DisabledTrigger: Disabled m_Interactable: 1 - m_TargetGraphic: {fileID: 0} + m_TargetGraphic: {fileID: 3708131469420921886} m_OnClick: m_PersistentCalls: m_Calls: @@ -321,7 +321,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!224 &4594737505273760298 RectTransform: m_ObjectHideFlags: 0 @@ -679,12 +679,15 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _playSpeed: 0 + _speakerArea: {fileID: 3736358320082617150} _speakerNameText: {fileID: 2470970474825277305} _contentText: {fileID: 6431296888118130931} _leftSprite: {fileID: 7945103967507868302} _rightSprite: {fileID: 5385698520020721016} _leftSpritePosition: 450 _rightSpritePosition: -450 + _moveDuration: 0.25 + _moveEase: 9 --- !u!1 &7381337123922490182 GameObject: m_ObjectHideFlags: 0 @@ -837,7 +840,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!224 &1023330169278438415 RectTransform: m_ObjectHideFlags: 0 diff --git a/Assets/Launcher.unity b/Assets/Launcher.unity index 720e3fc..0e353b2 100644 --- a/Assets/Launcher.unity +++ b/Assets/Launcher.unity @@ -841,6 +841,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: d6174838c30e460429e5628757bbf015, type: 3} m_Name: m_EditorClassIdentifier: + _playingSpeed: 10 --- !u!1 &513208572 GameObject: m_ObjectHideFlags: 0