493 lines
16 KiB
C#
493 lines
16 KiB
C#
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<string> NonPortraitSpeakerIds =
|
|
new HashSet<string>(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<Image>().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;
|
|
}
|
|
|
|
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))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int portraitCount = ResolveAvailablePortraitCount(speakerId);
|
|
string portraitAssetName = $"Characters/{speakerId}-{ResolvePortraitIndex(speakerId, lineId, portraitCount)}.png";
|
|
if (GameEntry.SpriteCache == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GameEntry.SpriteCache.GetSprite(portraitAssetName, sprite =>
|
|
{
|
|
if (isRightSpeaker)
|
|
{
|
|
if (requestVersion != _rightPortraitRequestVersion || _rightImage == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (sprite != null)
|
|
{
|
|
_rightImage.sprite = sprite;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (requestVersion != _leftPortraitRequestVersion || _leftImage == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (sprite != null)
|
|
{
|
|
_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 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;
|
|
}
|
|
|
|
}
|
|
}
|