using System.Collections.Generic; using System.Linq; using DG.Tweening; using Definition.Enum; using TMPro; using UnityEngine; using UnityEngine.Serialization; using UnityEngine.UI; using UnityGameFramework.Runtime; namespace UI { public class BottomDialogForm : DialogFormBase { public override DialogFormMode UIMode => DialogFormMode.BottomBox; [SerializeField] private GameObject _speakerArea; [SerializeField] private TMP_Text _speakerNameText; [SerializeField] private TMP_Text _contentText; [FormerlySerializedAs("_leftSprite")] [SerializeField] private Image _leftImage; [FormerlySerializedAs("_rightSprite")] [SerializeField] private Image _rightImage; [SerializeField] private int _leftSpritePosition = 450; [SerializeField] private int _rightSpritePosition = -450; [SerializeField] private float _moveDuration = 0.25f; [SerializeField] private Ease _moveEase = Ease.OutCubic; [SerializeField] private Image[] _dialogBgImages; [SerializeField] private GameObject _speakerNameArea; private string _leftSpeakerToken = string.Empty; private string _rightSpeakerToken = string.Empty; private Sequence _layoutSequence; private DialogWindowAlpha _currentWindowAlpha = DialogWindowAlpha.Medium; private float _currentPlayingSpeed = 10f; private int _leftPortraitRequestVersion; private int _rightPortraitRequestVersion; private static readonly HashSet NonPortraitSpeakerIds = new HashSet(System.StringComparer.OrdinalIgnoreCase) { "Other", "Subtitle", "Time" }; public override void StartDialog(DialogFormContext context) { if (context == null) { Log.Warning("BottomDialogForm start failed. context is null."); return; } _context = context; if (_context.DialogWindowAlpha != this._currentWindowAlpha) { _currentWindowAlpha = _context.DialogWindowAlpha; float targetAlpha = _currentWindowAlpha switch { DialogWindowAlpha.None => 1, DialogWindowAlpha.Low => 0.95f, DialogWindowAlpha.Medium => 0.8f, DialogWindowAlpha.High => 0.65f, _ => 0.9f }; if (_dialogBgImages.Length == 0) { //TODO:一个很奇怪的问题,在 prefab 里赋好的值实例化出来就没了,只能先这样赋值 _dialogBgImages = GetComponentsInChildren().Where(image => image.name == "bg").ToArray(); } foreach (Image image in _dialogBgImages) { Color color = image.color; color.a = targetAlpha; image.color = color; } } if ((int)(_context.PlayingSpeed + 1) * 5 != (int)_currentPlayingSpeed) { _currentPlayingSpeed = 5f * (int)(_context.PlayingSpeed + 1); } string speakerName = _context.SpeakerName; bool isHumanSpeaker = IsHumanSpeaker(_context.SpeakerId); if (_speakerArea != null) { _speakerArea.SetActive(!string.IsNullOrEmpty(speakerName)); } if (_speakerNameArea != null) { _speakerNameArea.SetActive(!string.IsNullOrEmpty(speakerName) && isHumanSpeaker); } if (_speakerNameText != null) { _speakerNameText.text = speakerName; } PlayTypewriter(_contentText, _context.Text, _currentPlayingSpeed); MainOverlayForm.DispatchCue(_context.Emphasis); if (string.IsNullOrEmpty(speakerName)) { ClearSpeakerState(); ApplySpeakerLayout(false, false, true); return; } if (!isHumanSpeaker) { ClearSpeakerState(); ApplySpeakerLayout(false, false, true); return; } if (!HasSpeakerPortrait(_context.SpeakerId)) { ClearSpeakerState(); ApplySpeakerLayout(false, false, true); return; } bool isRightSpeaker = context.Direction > 0; if (isRightSpeaker) { _rightSpeakerToken = speakerName; } else { _leftSpeakerToken = speakerName; } bool hasLeftSpeaker = !string.IsNullOrEmpty(_leftSpeakerToken); bool hasRightSpeaker = !string.IsNullOrEmpty(_rightSpeakerToken); UpdateSpeakerPortrait(isRightSpeaker ? _rightImage : _leftImage, _context.SpeakerId, _context.CurrentLineId, isRightSpeaker); ApplySpeakerLayout(hasLeftSpeaker, hasRightSpeaker, false); } protected override void OnOpen(object userData) { ResetSpeakerVisualState(); base.OnOpen(userData); } protected override void OnClose(bool isShutdown, object userData) { ResetSpeakerVisualState(); base.OnClose(isShutdown, userData); } private void ClearSpeakerState() { if (_leftImage != null) { _leftImage.sprite = null; } if (_rightImage != null) { _rightImage.sprite = null; } _leftSpeakerToken = string.Empty; _rightSpeakerToken = string.Empty; _leftPortraitRequestVersion++; _rightPortraitRequestVersion++; } private void KillLayoutTween() { if (_layoutSequence != null) { _layoutSequence.Kill(); _layoutSequence = null; } if (_leftImage != null && _leftImage.rectTransform != null) { _leftImage.rectTransform.DOKill(); } if (_rightImage != null && _rightImage.rectTransform != null) { _rightImage.rectTransform.DOKill(); } } private void ApplySpeakerLayout(bool hasLeftSpeaker, bool hasRightSpeaker, bool instant) { if (_leftImage == null || _rightImage == null) { return; } bool leftCurrentlyVisible = _leftImage.gameObject.activeSelf; bool rightCurrentlyVisible = _rightImage.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(_leftImage.rectTransform, leftTargetX); SetSpritePosition(_rightImage.rectTransform, rightTargetX); SetSpriteVisible(_leftImage, leftTargetVisible); SetSpriteVisible(_rightImage, rightTargetVisible); return; } _layoutSequence = DOTween.Sequence(); Tween leftTween = CreateMoveTween(_leftImage.rectTransform, leftTargetX); if (leftTween != null) { _layoutSequence.Join(leftTween); } Tween rightTween = CreateMoveTween(_rightImage.rectTransform, rightTargetX); if (rightTween != null) { _layoutSequence.Join(rightTween); } if (_layoutSequence.active && _layoutSequence.Duration(false) > 0f) { _layoutSequence.OnComplete(() => { SetSpriteVisible(_leftImage, leftTargetVisible); SetSpriteVisible(_rightImage, rightTargetVisible); _layoutSequence = null; }); } else { SetSpritePosition(_leftImage.rectTransform, leftTargetX); SetSpritePosition(_rightImage.rectTransform, rightTargetX); SetSpriteVisible(_leftImage, leftTargetVisible); SetSpriteVisible(_rightImage, 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(_leftImage.rectTransform, leftStartX); SetSpriteVisible(_leftImage, true); } if (rightTargetVisible && !rightCurrentlyVisible) { float rightStartX = GetAppearStartX(false, leftCurrentlyVisible, hasLeftSpeaker, hasRightSpeaker); SetSpritePosition(_rightImage.rectTransform, rightStartX); SetSpriteVisible(_rightImage, true); } if (leftCurrentlyVisible && !leftTargetVisible) { SetSpriteVisible(_leftImage, true); } if (rightCurrentlyVisible && !rightTargetVisible) { SetSpriteVisible(_rightImage, true); } } private float GetAppearStartX(bool isLeft, bool otherCurrentlyVisible, bool hasLeftSpeaker, bool hasRightSpeaker) { if (hasLeftSpeaker && hasRightSpeaker) { // single -> multi: hidden side starts from center, then both move to side positions. if (otherCurrentlyVisible) { return GetSingleSpeakerCenterPosition(isLeft); } return isLeft ? _leftSpritePosition : _rightSpritePosition; } // 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) { return isLeft ? _leftSpritePosition : _rightSpritePosition; } if (hasLeftSpeaker || hasRightSpeaker) { // multi -> single: both move to center first, then inactive side hides. return GetSingleSpeakerCenterPosition(isLeft); } 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) { if (spriteImage == null) { return; } spriteImage.gameObject.SetActive(visible); } private static void SetSpritePosition(RectTransform rectTransform, float xPosition) { if (rectTransform == null) { return; } Vector2 anchoredPosition = rectTransform.anchoredPosition; anchoredPosition.x = xPosition; rectTransform.anchoredPosition = anchoredPosition; } private void UpdateSpeakerPortrait(Image portraitImage, string speakerId, int lineId, bool isRightSpeaker) { if (portraitImage == null) { return; } int requestVersion = isRightSpeaker ? ++_rightPortraitRequestVersion : ++_leftPortraitRequestVersion; if (string.IsNullOrWhiteSpace(speakerId) || NonPortraitSpeakerIds.Contains(speakerId)) { portraitImage.sprite = null; return; } int portraitCount = ResolveAvailablePortraitCount(speakerId); if (portraitCount <= 0) { portraitImage.sprite = null; return; } string portraitAssetName = $"Characters/{speakerId}-{ResolvePortraitIndex(speakerId, lineId, portraitCount)}.png"; if (GameEntry.SpriteCache == null) { portraitImage.sprite = null; return; } GameEntry.SpriteCache.GetSprite(portraitAssetName, sprite => { if (isRightSpeaker) { if (requestVersion != _rightPortraitRequestVersion || _rightImage == null) { return; } _rightImage.sprite = sprite; return; } if (requestVersion != _leftPortraitRequestVersion || _leftImage == null) { return; } _leftImage.sprite = sprite; }); } private static int ResolvePortraitIndex(string speakerId, int lineId, int portraitCount) { int safePortraitCount = portraitCount > 0 ? portraitCount : 3; unchecked { int hash = 17; hash = hash * 31 + lineId; hash = hash * 31 + (string.IsNullOrEmpty(speakerId) ? 0 : System.StringComparer.OrdinalIgnoreCase.GetHashCode(speakerId)); int mod = hash % safePortraitCount; if (mod < 0) { mod += safePortraitCount; } return mod + 1; } } private static int ResolveAvailablePortraitCount(string speakerId) { if (GameEntry.Dialog == null) { return 0; } return GameEntry.Dialog.GetSpeakerPortraitCount(speakerId); } private static bool HasSpeakerPortrait(string speakerId) { return ResolveAvailablePortraitCount(speakerId) > 0; } private static bool IsHumanSpeaker(string speakerId) { if (string.IsNullOrWhiteSpace(speakerId)) { return false; } return !NonPortraitSpeakerIds.Contains(speakerId); } private void ResetSpeakerVisualState() { KillLayoutTween(); ClearSpeakerState(); ApplySpeakerLayout(false, false, true); } private float GetSingleSpeakerCenterPosition(bool isLeft) { RectTransform rectTransform = isLeft ? _leftImage != null ? _leftImage.rectTransform : null : _rightImage != null ? _rightImage.rectTransform : null; return GetCenterAnchoredX(rectTransform); } private static float GetCenterAnchoredX(RectTransform rectTransform) { if (rectTransform == null) { return 0f; } if (!(rectTransform.parent is RectTransform parent)) { return 0f; } float anchorX = rectTransform.anchorMin.x; return (0.5f - anchorX) * parent.rect.width; } } }