添加对话播放速度设置,调整对话以打字机的形式显示
This commit is contained in:
parent
26b69bc870
commit
74ac8f199d
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue