修复已知功能
1. 添加章节开始 UI 介绍 2. 清理不使用的资源 3. 补充 AIChat 入口 UI 4. 接上所有的正常对话流程
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
namespace UI
|
||||
{
|
||||
public static class AIChatEntryRuntime
|
||||
{
|
||||
private static AIChatEntryFormController s_entryController;
|
||||
private static AIChatEntryFormContext s_entryContext;
|
||||
|
||||
public static void EnsureOpen()
|
||||
{
|
||||
if (GameEntry.UI == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_entryController == null)
|
||||
{
|
||||
s_entryController = new AIChatEntryFormController();
|
||||
}
|
||||
|
||||
if (s_entryContext == null)
|
||||
{
|
||||
s_entryContext = new AIChatEntryFormContext();
|
||||
}
|
||||
|
||||
s_entryController.OpenUI(s_entryContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f5f8c4efc7e47a8b0354c7f89fef0b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace UI
|
||||
{
|
||||
public sealed class AIChatEntryFormContext : UIContext
|
||||
{
|
||||
public AIChatEntryFormController Controller;
|
||||
public bool IsChatOpened;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b5fd3ce4a3e4e7a8ed2ab9f9472e0a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace UI
|
||||
{
|
||||
public class ChapterTitleFormContext : UIContext
|
||||
{
|
||||
public int ChapterId = 0;
|
||||
public int RequestId = 0;
|
||||
public string Title = string.Empty;
|
||||
public string Subtitle = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2f2414b0ec34d2cb7a64c46c18cb48c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,188 @@
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.Event;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public sealed class AIChatEntryFormController : IFormController<AIChatEntryFormContext>
|
||||
{
|
||||
private AIChatEntryFormContext _entryContext;
|
||||
private AIChatEntryForm _entryForm;
|
||||
private int? _entryFormSerialId;
|
||||
private bool _pendingEntryRefresh;
|
||||
|
||||
private AIChatFormController _chatFormController;
|
||||
private AIChatFormContext _chatContext;
|
||||
|
||||
public AIChatEntryFormController()
|
||||
{
|
||||
GameEntry.Event.Subscribe(OpenUIFormSuccessEventArgs.EventId, OnOpenUIFormSuccess);
|
||||
GameEntry.Event.Subscribe(CloseUIFormCompleteEventArgs.EventId, OnCloseUIFormComplete);
|
||||
}
|
||||
|
||||
~AIChatEntryFormController()
|
||||
{
|
||||
GameEntry.Event.Unsubscribe(OpenUIFormSuccessEventArgs.EventId, OnOpenUIFormSuccess);
|
||||
GameEntry.Event.Unsubscribe(CloseUIFormCompleteEventArgs.EventId, OnCloseUIFormComplete);
|
||||
}
|
||||
|
||||
public int? OpenUI(AIChatEntryFormContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
Log.Warning("AIChatEntryFormController open failed. context is null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
context.Controller = this;
|
||||
_entryContext = context;
|
||||
SyncEntryContextState();
|
||||
|
||||
if (_entryForm != null)
|
||||
{
|
||||
_entryForm.RefreshUI(_entryContext);
|
||||
return _entryFormSerialId;
|
||||
}
|
||||
|
||||
UGuiForm openedForm = GameEntry.UI.GetUIForm(UIFormId.AIChatEntryForm);
|
||||
if (openedForm is AIChatEntryForm existingForm)
|
||||
{
|
||||
_entryForm = existingForm;
|
||||
_entryFormSerialId = existingForm.UIForm.SerialId;
|
||||
_entryForm.RefreshUI(_entryContext);
|
||||
return _entryFormSerialId;
|
||||
}
|
||||
|
||||
_pendingEntryRefresh = true;
|
||||
_entryFormSerialId = GameEntry.UI.OpenUIForm(UIFormId.AIChatEntryForm, _entryContext);
|
||||
return _entryFormSerialId;
|
||||
}
|
||||
|
||||
public void CloseUI()
|
||||
{
|
||||
_pendingEntryRefresh = false;
|
||||
|
||||
if (_entryFormSerialId.HasValue)
|
||||
{
|
||||
GameEntry.UI.CloseUIForm(_entryFormSerialId.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entryForm != null)
|
||||
{
|
||||
_entryForm.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleAIChatForm()
|
||||
{
|
||||
UGuiForm openedChatForm = GameEntry.UI.GetUIForm(UIFormId.AIChatForm);
|
||||
if (openedChatForm != null)
|
||||
{
|
||||
if (_chatFormController != null)
|
||||
{
|
||||
_chatFormController.CloseUI();
|
||||
}
|
||||
else
|
||||
{
|
||||
GameEntry.UI.CloseUIForm(openedChatForm.UIForm);
|
||||
}
|
||||
|
||||
RefreshEntryState();
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureChatContext();
|
||||
_chatContext.Controller = _chatFormController;
|
||||
_chatFormController.OpenUI(_chatContext);
|
||||
RefreshEntryState();
|
||||
}
|
||||
|
||||
private void EnsureChatContext()
|
||||
{
|
||||
if (_chatFormController == null)
|
||||
{
|
||||
_chatFormController = new AIChatFormController();
|
||||
}
|
||||
|
||||
if (_chatContext == null)
|
||||
{
|
||||
_chatContext = new AIChatFormContext
|
||||
{
|
||||
ClearHistoryOnOpen = false,
|
||||
LanguageMode = 0,
|
||||
Messages = new List<AIChatMessageContext>()
|
||||
};
|
||||
}
|
||||
|
||||
_chatContext.Controller = _chatFormController;
|
||||
_chatContext.Messages ??= new List<AIChatMessageContext>();
|
||||
}
|
||||
|
||||
private void SyncEntryContextState()
|
||||
{
|
||||
if (_entryContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_entryContext.IsChatOpened = GameEntry.UI.GetUIForm(UIFormId.AIChatForm) != null;
|
||||
}
|
||||
|
||||
private void RefreshEntryState()
|
||||
{
|
||||
SyncEntryContextState();
|
||||
|
||||
if (_entryForm == null || _entryContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_entryForm.SetChatOpened(_entryContext.IsChatOpened);
|
||||
}
|
||||
|
||||
private void OnOpenUIFormSuccess(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is OpenUIFormSuccessEventArgs args))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entryFormSerialId.HasValue &&
|
||||
args.UIForm != null &&
|
||||
args.UIForm.SerialId == _entryFormSerialId.Value &&
|
||||
args.UserData == _entryContext)
|
||||
{
|
||||
_entryForm = args.UIForm.Logic as AIChatEntryForm;
|
||||
if (_entryForm == null)
|
||||
{
|
||||
Log.Warning("AIChatEntryFormController open success but form logic is invalid.");
|
||||
}
|
||||
else if (_pendingEntryRefresh)
|
||||
{
|
||||
_entryForm.RefreshUI(_entryContext);
|
||||
_pendingEntryRefresh = false;
|
||||
}
|
||||
}
|
||||
|
||||
RefreshEntryState();
|
||||
}
|
||||
|
||||
private void OnCloseUIFormComplete(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CloseUIFormCompleteEventArgs args))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.SerialId == _entryFormSerialId)
|
||||
{
|
||||
_entryForm = null;
|
||||
_entryFormSerialId = null;
|
||||
_pendingEntryRefresh = false;
|
||||
}
|
||||
|
||||
RefreshEntryState();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f9ad5dc60834e1cb1aa5d0f04d2756c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -17,6 +17,7 @@ namespace UI
|
||||
private static TMP_FontAsset _mainTMPFont = null;
|
||||
private Canvas _cachedCanvas = null;
|
||||
private CanvasGroup _canvasGroup = null;
|
||||
private Coroutine _canvasFadeCoroutine = null;
|
||||
private List<Canvas> _cachedCanvasContainer = new List<Canvas>();
|
||||
|
||||
public int OriginalDepth { get; private set; }
|
||||
@@ -30,7 +31,7 @@ namespace UI
|
||||
|
||||
public void Close(bool ignoreFade)
|
||||
{
|
||||
StopAllCoroutines();
|
||||
StopCanvasFadeCoroutine();
|
||||
|
||||
if (ignoreFade)
|
||||
{
|
||||
@@ -38,7 +39,7 @@ namespace UI
|
||||
}
|
||||
else
|
||||
{
|
||||
StartCoroutine(CloseCo(FadeTime));
|
||||
_canvasFadeCoroutine = StartCoroutine(CloseCo(FadeTime));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,8 +137,7 @@ namespace UI
|
||||
base.OnOpen(userData);
|
||||
|
||||
_canvasGroup.alpha = 0f;
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(_canvasGroup.FadeToAlpha(1f, FadeTime));
|
||||
PlayCanvasFade(1f, FadeTime);
|
||||
}
|
||||
|
||||
#if UNITY_2017_3_OR_NEWER
|
||||
@@ -146,6 +146,7 @@ namespace UI
|
||||
protected internal override void OnClose(bool isShutdown, object userData)
|
||||
#endif
|
||||
{
|
||||
StopCanvasFadeCoroutine();
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
||||
@@ -167,8 +168,7 @@ namespace UI
|
||||
base.OnResume();
|
||||
|
||||
_canvasGroup.alpha = 0f;
|
||||
StopAllCoroutines();
|
||||
StartCoroutine(_canvasGroup.FadeToAlpha(1f, FadeTime));
|
||||
PlayCanvasFade(1f, FadeTime);
|
||||
}
|
||||
|
||||
#if UNITY_2017_3_OR_NEWER
|
||||
@@ -229,7 +229,31 @@ namespace UI
|
||||
private IEnumerator CloseCo(float duration)
|
||||
{
|
||||
yield return _canvasGroup.FadeToAlpha(0f, duration);
|
||||
_canvasFadeCoroutine = null;
|
||||
GameEntry.UI.CloseUIForm(this);
|
||||
}
|
||||
|
||||
private void PlayCanvasFade(float alpha, float duration)
|
||||
{
|
||||
StopCanvasFadeCoroutine();
|
||||
_canvasFadeCoroutine = StartCoroutine(FadeCanvasCo(alpha, duration));
|
||||
}
|
||||
|
||||
private IEnumerator FadeCanvasCo(float alpha, float duration)
|
||||
{
|
||||
yield return _canvasGroup.FadeToAlpha(alpha, duration);
|
||||
_canvasFadeCoroutine = null;
|
||||
}
|
||||
|
||||
private void StopCanvasFadeCoroutine()
|
||||
{
|
||||
if (_canvasFadeCoroutine == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopCoroutine(_canvasFadeCoroutine);
|
||||
_canvasFadeCoroutine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public sealed class AIChatEntryForm : UGuiForm
|
||||
{
|
||||
[SerializeField] private Button _entryButton;
|
||||
[SerializeField] private TMP_Text _entryLabel;
|
||||
[SerializeField] private string _openLabel = "问 AI";
|
||||
[SerializeField] private string _closeLabel = "关闭 AI";
|
||||
|
||||
private AIChatEntryFormController _controller;
|
||||
|
||||
protected override void OnOpen(object userData)
|
||||
{
|
||||
base.OnOpen(userData);
|
||||
BindEntryButton();
|
||||
|
||||
if (!(userData is AIChatEntryFormContext context))
|
||||
{
|
||||
Log.Error("AIChatEntryFormContext is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshUI(context);
|
||||
}
|
||||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
UnbindEntryButton();
|
||||
_controller = null;
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
||||
public void RefreshUI(AIChatEntryFormContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controller = context.Controller;
|
||||
SetChatOpened(context.IsChatOpened);
|
||||
}
|
||||
|
||||
public void SetChatOpened(bool isChatOpened)
|
||||
{
|
||||
if (_entryLabel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_entryLabel.text = isChatOpened ? _closeLabel : _openLabel;
|
||||
}
|
||||
|
||||
public void OnEntryButtonClick()
|
||||
{
|
||||
_controller?.ToggleAIChatForm();
|
||||
}
|
||||
|
||||
private void BindEntryButton()
|
||||
{
|
||||
if (_entryButton == null)
|
||||
{
|
||||
Log.Warning("AIChatEntryForm entry button is not assigned.");
|
||||
return;
|
||||
}
|
||||
|
||||
_entryButton.onClick.RemoveListener(OnEntryButtonClick);
|
||||
_entryButton.onClick.AddListener(OnEntryButtonClick);
|
||||
}
|
||||
|
||||
private void UnbindEntryButton()
|
||||
{
|
||||
if (_entryButton == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_entryButton.onClick.RemoveListener(OnEntryButtonClick);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 17d9f89cf8b5472e8f167d90f9d2621c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -57,6 +57,7 @@ namespace UI
|
||||
}
|
||||
|
||||
int requestId = context.TransitionRequestId;
|
||||
bool shouldFadeTransition = _currentSprite != null;
|
||||
|
||||
if (context.Sprite != null)
|
||||
{
|
||||
@@ -87,7 +88,7 @@ namespace UI
|
||||
}
|
||||
|
||||
int version = ++_refreshVersion;
|
||||
FadeToBlack(() =>
|
||||
TweenCallback loadAndApplySprite = () =>
|
||||
{
|
||||
if (version != _refreshVersion)
|
||||
{
|
||||
@@ -115,9 +116,25 @@ namespace UI
|
||||
Log.Warning("BgForm refresh failed. Can not load sprite '{0}'.", targetAssetName);
|
||||
}
|
||||
|
||||
FadeFromBlack(() => { PublishTransitionCompleted(requestId); });
|
||||
if (shouldFadeTransition)
|
||||
{
|
||||
FadeFromBlack(() => { PublishTransitionCompleted(requestId); });
|
||||
}
|
||||
else
|
||||
{
|
||||
PublishTransitionCompleted(requestId);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (shouldFadeTransition)
|
||||
{
|
||||
FadeToBlack(loadAndApplySprite);
|
||||
}
|
||||
else
|
||||
{
|
||||
loadAndApplySprite.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshWithSprite(Sprite sprite, string backgroundAssetName, int requestId)
|
||||
@@ -137,8 +154,9 @@ namespace UI
|
||||
return;
|
||||
}
|
||||
|
||||
bool shouldFadeTransition = _currentSprite != null;
|
||||
int version = ++_refreshVersion;
|
||||
FadeToBlack(() =>
|
||||
TweenCallback applySprite = () =>
|
||||
{
|
||||
if (version != _refreshVersion)
|
||||
{
|
||||
@@ -153,8 +171,25 @@ namespace UI
|
||||
{
|
||||
_bgImage.sprite = sprite;
|
||||
}
|
||||
FadeFromBlack(() => { PublishTransitionCompleted(requestId); });
|
||||
});
|
||||
|
||||
if (shouldFadeTransition)
|
||||
{
|
||||
FadeFromBlack(() => { PublishTransitionCompleted(requestId); });
|
||||
}
|
||||
else
|
||||
{
|
||||
PublishTransitionCompleted(requestId);
|
||||
}
|
||||
};
|
||||
|
||||
if (shouldFadeTransition)
|
||||
{
|
||||
FadeToBlack(applySprite);
|
||||
}
|
||||
else
|
||||
{
|
||||
applySprite.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private void FadeToBlack(TweenCallback onComplete)
|
||||
|
||||
@@ -4,6 +4,7 @@ using DG.Tweening;
|
||||
using Definition.Enum;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
@@ -19,9 +20,9 @@ namespace UI
|
||||
|
||||
[SerializeField] private TMP_Text _contentText;
|
||||
|
||||
[SerializeField] private Image _leftSprite;
|
||||
[FormerlySerializedAs("_leftSprite")] [SerializeField] private Image _leftImage;
|
||||
|
||||
[SerializeField] private Image _rightSprite;
|
||||
[FormerlySerializedAs("_rightSprite")] [SerializeField] private Image _rightImage;
|
||||
|
||||
[SerializeField] private int _leftSpritePosition = 450;
|
||||
|
||||
@@ -35,8 +36,6 @@ namespace UI
|
||||
|
||||
[SerializeField] private GameObject _speakerNameArea;
|
||||
|
||||
private readonly int _singleSpeakerCenterPosition = Screen.width / 2;
|
||||
|
||||
private string _leftSpeakerToken = string.Empty;
|
||||
private string _rightSpeakerToken = string.Empty;
|
||||
private Sequence _layoutSequence;
|
||||
@@ -95,6 +94,7 @@ namespace UI
|
||||
}
|
||||
|
||||
string speakerName = _context.SpeakerName;
|
||||
bool isHumanSpeaker = IsHumanSpeaker(_context.SpeakerId);
|
||||
|
||||
if (_speakerArea != null)
|
||||
{
|
||||
@@ -103,7 +103,7 @@ namespace UI
|
||||
|
||||
if (_speakerNameArea != null)
|
||||
{
|
||||
_speakerNameArea.SetActive(!string.IsNullOrEmpty(speakerName) && IsHumanSpeaker(_context.SpeakerId));
|
||||
_speakerNameArea.SetActive(!string.IsNullOrEmpty(speakerName) && isHumanSpeaker);
|
||||
}
|
||||
|
||||
if (_speakerNameText != null)
|
||||
@@ -112,6 +112,7 @@ namespace UI
|
||||
}
|
||||
|
||||
PlayTypewriter(_contentText, _context.Text, _currentPlayingSpeed);
|
||||
MainOverlayForm.DispatchCue(_context.Emphasis);
|
||||
|
||||
if (string.IsNullOrEmpty(speakerName))
|
||||
{
|
||||
@@ -120,6 +121,13 @@ namespace UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isHumanSpeaker)
|
||||
{
|
||||
ClearSpeakerState();
|
||||
ApplySpeakerLayout(false, false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
bool isRightSpeaker = context.Direction > 0;
|
||||
if (isRightSpeaker)
|
||||
{
|
||||
@@ -133,21 +141,35 @@ namespace UI
|
||||
bool hasLeftSpeaker = !string.IsNullOrEmpty(_leftSpeakerToken);
|
||||
bool hasRightSpeaker = !string.IsNullOrEmpty(_rightSpeakerToken);
|
||||
|
||||
UpdateSpeakerPortrait(isRightSpeaker ? _rightSprite : _leftSprite, _context.SpeakerId,
|
||||
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)
|
||||
{
|
||||
ClearSpeakerState();
|
||||
KillLayoutTween();
|
||||
ApplySpeakerLayout(false, false, true);
|
||||
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++;
|
||||
@@ -161,17 +183,27 @@ namespace UI
|
||||
_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 (_leftSprite == null || _rightSprite == null)
|
||||
if (_leftImage == null || _rightImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool leftCurrentlyVisible = _leftSprite.gameObject.activeSelf;
|
||||
bool rightCurrentlyVisible = _rightSprite.gameObject.activeSelf;
|
||||
bool leftCurrentlyVisible = _leftImage.gameObject.activeSelf;
|
||||
bool rightCurrentlyVisible = _rightImage.gameObject.activeSelf;
|
||||
|
||||
bool leftTargetVisible = hasLeftSpeaker;
|
||||
bool rightTargetVisible = hasRightSpeaker;
|
||||
@@ -191,22 +223,22 @@ namespace UI
|
||||
|
||||
if (instant || _moveDuration <= 0f)
|
||||
{
|
||||
SetSpritePosition(_leftSprite.rectTransform, leftTargetX);
|
||||
SetSpritePosition(_rightSprite.rectTransform, rightTargetX);
|
||||
SetSpriteVisible(_leftSprite, leftTargetVisible);
|
||||
SetSpriteVisible(_rightSprite, rightTargetVisible);
|
||||
SetSpritePosition(_leftImage.rectTransform, leftTargetX);
|
||||
SetSpritePosition(_rightImage.rectTransform, rightTargetX);
|
||||
SetSpriteVisible(_leftImage, leftTargetVisible);
|
||||
SetSpriteVisible(_rightImage, rightTargetVisible);
|
||||
return;
|
||||
}
|
||||
|
||||
_layoutSequence = DOTween.Sequence();
|
||||
|
||||
Tween leftTween = CreateMoveTween(_leftSprite.rectTransform, leftTargetX);
|
||||
Tween leftTween = CreateMoveTween(_leftImage.rectTransform, leftTargetX);
|
||||
if (leftTween != null)
|
||||
{
|
||||
_layoutSequence.Join(leftTween);
|
||||
}
|
||||
|
||||
Tween rightTween = CreateMoveTween(_rightSprite.rectTransform, rightTargetX);
|
||||
Tween rightTween = CreateMoveTween(_rightImage.rectTransform, rightTargetX);
|
||||
if (rightTween != null)
|
||||
{
|
||||
_layoutSequence.Join(rightTween);
|
||||
@@ -216,17 +248,17 @@ namespace UI
|
||||
{
|
||||
_layoutSequence.OnComplete(() =>
|
||||
{
|
||||
SetSpriteVisible(_leftSprite, leftTargetVisible);
|
||||
SetSpriteVisible(_rightSprite, rightTargetVisible);
|
||||
SetSpriteVisible(_leftImage, leftTargetVisible);
|
||||
SetSpriteVisible(_rightImage, rightTargetVisible);
|
||||
_layoutSequence = null;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
SetSpritePosition(_leftSprite.rectTransform, leftTargetX);
|
||||
SetSpritePosition(_rightSprite.rectTransform, rightTargetX);
|
||||
SetSpriteVisible(_leftSprite, leftTargetVisible);
|
||||
SetSpriteVisible(_rightSprite, rightTargetVisible);
|
||||
SetSpritePosition(_leftImage.rectTransform, leftTargetX);
|
||||
SetSpritePosition(_rightImage.rectTransform, rightTargetX);
|
||||
SetSpriteVisible(_leftImage, leftTargetVisible);
|
||||
SetSpriteVisible(_rightImage, rightTargetVisible);
|
||||
_layoutSequence.Kill();
|
||||
_layoutSequence = null;
|
||||
}
|
||||
@@ -243,25 +275,25 @@ namespace UI
|
||||
if (leftTargetVisible && !leftCurrentlyVisible)
|
||||
{
|
||||
float leftStartX = GetAppearStartX(true, rightCurrentlyVisible, hasLeftSpeaker, hasRightSpeaker);
|
||||
SetSpritePosition(_leftSprite.rectTransform, leftStartX);
|
||||
SetSpriteVisible(_leftSprite, true);
|
||||
SetSpritePosition(_leftImage.rectTransform, leftStartX);
|
||||
SetSpriteVisible(_leftImage, true);
|
||||
}
|
||||
|
||||
if (rightTargetVisible && !rightCurrentlyVisible)
|
||||
{
|
||||
float rightStartX = GetAppearStartX(false, leftCurrentlyVisible, hasLeftSpeaker, hasRightSpeaker);
|
||||
SetSpritePosition(_rightSprite.rectTransform, rightStartX);
|
||||
SetSpriteVisible(_rightSprite, true);
|
||||
SetSpritePosition(_rightImage.rectTransform, rightStartX);
|
||||
SetSpriteVisible(_rightImage, true);
|
||||
}
|
||||
|
||||
if (leftCurrentlyVisible && !leftTargetVisible)
|
||||
{
|
||||
SetSpriteVisible(_leftSprite, true);
|
||||
SetSpriteVisible(_leftImage, true);
|
||||
}
|
||||
|
||||
if (rightCurrentlyVisible && !rightTargetVisible)
|
||||
{
|
||||
SetSpriteVisible(_rightSprite, true);
|
||||
SetSpriteVisible(_rightImage, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +305,7 @@ namespace UI
|
||||
// single -> multi: hidden side starts from center, then both move to side positions.
|
||||
if (otherCurrentlyVisible)
|
||||
{
|
||||
return _singleSpeakerCenterPosition;
|
||||
return GetSingleSpeakerCenterPosition(isLeft);
|
||||
}
|
||||
|
||||
return isLeft ? _leftSpritePosition : _rightSpritePosition;
|
||||
@@ -293,7 +325,7 @@ namespace UI
|
||||
if (hasLeftSpeaker || hasRightSpeaker)
|
||||
{
|
||||
// multi -> single: both move to center first, then inactive side hides.
|
||||
return _singleSpeakerCenterPosition;
|
||||
return GetSingleSpeakerCenterPosition(isLeft);
|
||||
}
|
||||
|
||||
return isLeft ? _leftSpritePosition : _rightSpritePosition;
|
||||
@@ -350,7 +382,8 @@ namespace UI
|
||||
return;
|
||||
}
|
||||
|
||||
string portraitAssetName = $"Characters/{speakerId}-{ResolvePortraitIndex(speakerId, lineId)}.png";
|
||||
int portraitCount = ResolveAvailablePortraitCount(speakerId);
|
||||
string portraitAssetName = $"Characters/{speakerId}-{ResolvePortraitIndex(speakerId, lineId, portraitCount)}.png";
|
||||
if (GameEntry.SpriteCache == null)
|
||||
{
|
||||
return;
|
||||
@@ -360,32 +393,33 @@ namespace UI
|
||||
{
|
||||
if (isRightSpeaker)
|
||||
{
|
||||
if (requestVersion != _rightPortraitRequestVersion || _rightSprite == null)
|
||||
if (requestVersion != _rightPortraitRequestVersion || _rightImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sprite != null)
|
||||
{
|
||||
_rightSprite.sprite = sprite;
|
||||
_rightImage.sprite = sprite;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestVersion != _leftPortraitRequestVersion || _leftSprite == null)
|
||||
if (requestVersion != _leftPortraitRequestVersion || _leftImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sprite != null)
|
||||
{
|
||||
_leftSprite.sprite = sprite;
|
||||
_leftImage.sprite = sprite;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static int ResolvePortraitIndex(string speakerId, int lineId)
|
||||
private static int ResolvePortraitIndex(string speakerId, int lineId, int portraitCount)
|
||||
{
|
||||
int safePortraitCount = portraitCount > 0 ? portraitCount : 3;
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
@@ -393,16 +427,26 @@ namespace UI
|
||||
hash = hash * 31 + (string.IsNullOrEmpty(speakerId)
|
||||
? 0
|
||||
: System.StringComparer.OrdinalIgnoreCase.GetHashCode(speakerId));
|
||||
int mod = hash % 3;
|
||||
int mod = hash % safePortraitCount;
|
||||
if (mod < 0)
|
||||
{
|
||||
mod += 3;
|
||||
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))
|
||||
@@ -412,5 +456,37 @@ namespace UI
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
using System.Collections;
|
||||
using Event;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class ChapterTitleForm : UGuiForm
|
||||
{
|
||||
[SerializeField] private CanvasGroup _contentGroup;
|
||||
[SerializeField] private Image _dimImage;
|
||||
[SerializeField] private TMP_Text _titleText;
|
||||
[SerializeField] private TMP_Text _subtitleText;
|
||||
[SerializeField] private float _fadeInDuration = 0.3f;
|
||||
[SerializeField] private float _holdDuration = 1.2f;
|
||||
[SerializeField] private float _fadeOutDuration = 0.3f;
|
||||
|
||||
private Coroutine _playCoroutine;
|
||||
private int _requestId;
|
||||
private int _chapterId;
|
||||
private bool _hasPublishedCompletion;
|
||||
|
||||
protected override void OnOpen(object userData)
|
||||
{
|
||||
base.OnOpen(userData);
|
||||
EnsureView();
|
||||
ApplyContext(userData as ChapterTitleFormContext);
|
||||
StartPlay();
|
||||
}
|
||||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
StopPlay();
|
||||
SetContentAlpha(0f);
|
||||
if (!isShutdown)
|
||||
{
|
||||
PublishCompletionIfNeeded();
|
||||
}
|
||||
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
||||
private void EnsureView()
|
||||
{
|
||||
RectTransform rootRect = transform as RectTransform;
|
||||
if (rootRect == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_contentGroup == null)
|
||||
{
|
||||
_contentGroup = CreateContentGroup(rootRect);
|
||||
}
|
||||
|
||||
RectTransform contentRect = _contentGroup != null && _contentGroup.transform is RectTransform contentTransform
|
||||
? contentTransform
|
||||
: rootRect;
|
||||
|
||||
if (_dimImage == null)
|
||||
{
|
||||
_dimImage = CreateImage("Dim", contentRect, new Color(0f, 0f, 0f, 0.72f), true);
|
||||
}
|
||||
|
||||
if (_titleText == null)
|
||||
{
|
||||
_titleText = CreateText("Title", contentRect, 108f, FontStyles.Bold, TextAlignmentOptions.Center);
|
||||
RectTransform titleRect = _titleText.rectTransform;
|
||||
titleRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
titleRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
titleRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
titleRect.sizeDelta = new Vector2(2200f, 220f);
|
||||
titleRect.anchoredPosition = new Vector2(0f, 40f);
|
||||
}
|
||||
|
||||
if (_subtitleText == null)
|
||||
{
|
||||
_subtitleText = CreateText("Subtitle", contentRect, 42f, FontStyles.Normal, TextAlignmentOptions.Center);
|
||||
RectTransform subtitleRect = _subtitleText.rectTransform;
|
||||
subtitleRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
subtitleRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
subtitleRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
subtitleRect.sizeDelta = new Vector2(2200f, 120f);
|
||||
subtitleRect.anchoredPosition = new Vector2(0f, -60f);
|
||||
}
|
||||
|
||||
if (_dimImage != null)
|
||||
{
|
||||
_dimImage.transform.SetAsFirstSibling();
|
||||
}
|
||||
|
||||
if (_subtitleText != null)
|
||||
{
|
||||
_subtitleText.transform.SetAsLastSibling();
|
||||
}
|
||||
|
||||
if (_titleText != null)
|
||||
{
|
||||
_titleText.transform.SetAsLastSibling();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyContext(ChapterTitleFormContext context)
|
||||
{
|
||||
_hasPublishedCompletion = false;
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
_requestId = 0;
|
||||
_chapterId = 0;
|
||||
SetTitleText(string.Empty, string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
_requestId = context.RequestId;
|
||||
_chapterId = context.ChapterId;
|
||||
|
||||
string title = string.IsNullOrWhiteSpace(context.Title)
|
||||
? BuildDefaultChapterTitle(_chapterId)
|
||||
: context.Title.Trim();
|
||||
string subtitle = string.IsNullOrWhiteSpace(context.Subtitle) ? string.Empty : context.Subtitle.Trim();
|
||||
SetTitleText(title, subtitle);
|
||||
}
|
||||
|
||||
private void SetTitleText(string title, string subtitle)
|
||||
{
|
||||
if (_titleText != null)
|
||||
{
|
||||
_titleText.text = title;
|
||||
}
|
||||
|
||||
if (_subtitleText != null)
|
||||
{
|
||||
_subtitleText.text = subtitle;
|
||||
_subtitleText.gameObject.SetActive(!string.IsNullOrEmpty(subtitle));
|
||||
}
|
||||
}
|
||||
|
||||
private void StartPlay()
|
||||
{
|
||||
StopPlay();
|
||||
_playCoroutine = StartCoroutine(PlayRoutine());
|
||||
}
|
||||
|
||||
private void StopPlay()
|
||||
{
|
||||
if (_playCoroutine == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StopCoroutine(_playCoroutine);
|
||||
_playCoroutine = null;
|
||||
}
|
||||
|
||||
private IEnumerator PlayRoutine()
|
||||
{
|
||||
SetContentAlpha(0f);
|
||||
yield return FadeAlpha(0f, 1f, _fadeInDuration);
|
||||
yield return WaitUnscaled(_holdDuration);
|
||||
yield return FadeAlpha(1f, 0f, _fadeOutDuration);
|
||||
_playCoroutine = null;
|
||||
|
||||
PublishCompletionIfNeeded();
|
||||
if (GameEntry.UI != null && UIForm != null)
|
||||
{
|
||||
GameEntry.UI.CloseUIForm(UIForm);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator FadeAlpha(float from, float to, float duration)
|
||||
{
|
||||
float safeDuration = Mathf.Max(0f, duration);
|
||||
if (safeDuration <= 0f)
|
||||
{
|
||||
SetContentAlpha(to);
|
||||
yield break;
|
||||
}
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < safeDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / safeDuration);
|
||||
SetContentAlpha(Mathf.Lerp(from, to, t));
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetContentAlpha(to);
|
||||
}
|
||||
|
||||
private static IEnumerator WaitUnscaled(float duration)
|
||||
{
|
||||
float safeDuration = Mathf.Max(0f, duration);
|
||||
if (safeDuration <= 0f)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < safeDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetContentAlpha(float alpha)
|
||||
{
|
||||
if (_contentGroup == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_contentGroup.alpha = Mathf.Clamp01(alpha);
|
||||
}
|
||||
|
||||
private void PublishCompletionIfNeeded()
|
||||
{
|
||||
if (_hasPublishedCompletion || _requestId <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_hasPublishedCompletion = true;
|
||||
GameEntry.Event.Fire(this, ChapterTitleCompletedEventArgs.Create(_chapterId, _requestId));
|
||||
}
|
||||
|
||||
private static string BuildDefaultChapterTitle(int chapterId)
|
||||
{
|
||||
return chapterId > 0 ? $"第{chapterId}章" : string.Empty;
|
||||
}
|
||||
|
||||
private static CanvasGroup CreateContentGroup(RectTransform parent)
|
||||
{
|
||||
GameObject go = new GameObject("ContentRoot", typeof(RectTransform), typeof(CanvasGroup));
|
||||
RectTransform rect = go.GetComponent<RectTransform>();
|
||||
rect.SetParent(parent, false);
|
||||
rect.anchorMin = Vector2.zero;
|
||||
rect.anchorMax = Vector2.one;
|
||||
rect.offsetMin = Vector2.zero;
|
||||
rect.offsetMax = Vector2.zero;
|
||||
return go.GetComponent<CanvasGroup>();
|
||||
}
|
||||
|
||||
private static Image CreateImage(string name, RectTransform parent, Color color, bool raycastTarget)
|
||||
{
|
||||
GameObject go = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
RectTransform rect = go.GetComponent<RectTransform>();
|
||||
rect.SetParent(parent, false);
|
||||
rect.anchorMin = Vector2.zero;
|
||||
rect.anchorMax = Vector2.one;
|
||||
rect.offsetMin = Vector2.zero;
|
||||
rect.offsetMax = Vector2.zero;
|
||||
|
||||
Image image = go.GetComponent<Image>();
|
||||
image.color = color;
|
||||
image.raycastTarget = raycastTarget;
|
||||
return image;
|
||||
}
|
||||
|
||||
private static TMP_Text CreateText(string name, RectTransform parent, float fontSize, FontStyles style,
|
||||
TextAlignmentOptions alignment)
|
||||
{
|
||||
GameObject go = new GameObject(name, typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
|
||||
RectTransform rect = go.GetComponent<RectTransform>();
|
||||
rect.SetParent(parent, false);
|
||||
|
||||
TextMeshProUGUI text = go.GetComponent<TextMeshProUGUI>();
|
||||
if (TMP_Settings.defaultFontAsset != null)
|
||||
{
|
||||
text.font = TMP_Settings.defaultFontAsset;
|
||||
}
|
||||
|
||||
text.text = string.Empty;
|
||||
text.fontSize = fontSize;
|
||||
text.fontStyle = style;
|
||||
text.alignment = alignment;
|
||||
text.color = new Color(0.96f, 0.94f, 0.88f, 1f);
|
||||
text.raycastTarget = false;
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6bb48f2d6a2e4df6a8360f0f84c1f58a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,268 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Definition.Enum;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class MainOverlayForm : UGuiForm
|
||||
{
|
||||
[SerializeField] private Image _cueImage;
|
||||
[SerializeField] private List<Sprite> _cueBindings = new List<Sprite>();
|
||||
[SerializeField] private float _showDuration = 0.2f;
|
||||
[SerializeField] private float _hideDuration = 0.2f;
|
||||
|
||||
private static int _dispatchCount;
|
||||
private Coroutine _transitionCoroutine;
|
||||
private bool _isClosingByCue;
|
||||
private Vector3 _baseScale = Vector3.one;
|
||||
private bool _baseScaleInitialized;
|
||||
|
||||
public static void DispatchCue(EmphasisType emphasis)
|
||||
{
|
||||
if (GameEntry.UI == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UGuiForm existingForm = GameEntry.UI.GetUIForm(UIFormId.MainOverlayForm);
|
||||
if (emphasis == EmphasisType.EntityCueClose)
|
||||
{
|
||||
if (existingForm is MainOverlayForm overlay)
|
||||
{
|
||||
overlay.PlayHideAndClose();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (emphasis != EmphasisType.EntityCue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingForm is MainOverlayForm overlayForm)
|
||||
{
|
||||
overlayForm.ShowNextCue();
|
||||
return;
|
||||
}
|
||||
|
||||
int? formSerialId = GameEntry.UI.OpenUIForm(UIFormId.MainOverlayForm);
|
||||
if (!formSerialId.HasValue)
|
||||
{
|
||||
Log.Warning("MainOverlayForm open failed. Check UIForm.txt config and prefab asset.");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnOpen(object userData)
|
||||
{
|
||||
base.OnOpen(userData);
|
||||
EnsureBaseScaleInitialized();
|
||||
ShowNextCue();
|
||||
}
|
||||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
StopTransition();
|
||||
HideCueImage(clearSprite: true);
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
||||
private void ShowNextCue()
|
||||
{
|
||||
if (_cueImage == null)
|
||||
{
|
||||
Log.Warning("MainOverlayForm cue image is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cueBindings == null || _cueBindings.Count == 0)
|
||||
{
|
||||
HideCueImage(clearSprite: false);
|
||||
Log.Warning("MainOverlayForm cue bindings are empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
int index = _dispatchCount % _cueBindings.Count;
|
||||
_dispatchCount++;
|
||||
|
||||
Sprite sprite = _cueBindings[index];
|
||||
if (sprite == null)
|
||||
{
|
||||
HideCueImage(clearSprite: false);
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureBaseScaleInitialized();
|
||||
StopTransition();
|
||||
_transitionCoroutine = StartCoroutine(PlayShowRoutine(sprite));
|
||||
}
|
||||
|
||||
private void PlayHideAndClose()
|
||||
{
|
||||
if (_isClosingByCue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cueImage == null)
|
||||
{
|
||||
if (GameEntry.UI != null && UIForm != null)
|
||||
{
|
||||
GameEntry.UI.CloseUIForm(UIForm);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
StopTransition();
|
||||
_transitionCoroutine = StartCoroutine(PlayHideAndCloseRoutine());
|
||||
}
|
||||
|
||||
private IEnumerator PlayShowRoutine(Sprite sprite)
|
||||
{
|
||||
_cueImage.sprite = sprite;
|
||||
_cueImage.SetNativeSize();
|
||||
_cueImage.gameObject.SetActive(true);
|
||||
|
||||
SetCueVisual(0f, 0f);
|
||||
yield return PlayLerp(0f, 1f, 0f, 0.6f, _showDuration);
|
||||
_transitionCoroutine = null;
|
||||
}
|
||||
|
||||
private IEnumerator PlayHideAndCloseRoutine()
|
||||
{
|
||||
_isClosingByCue = true;
|
||||
float fromAlpha = _cueImage.color.a;
|
||||
float fromScale = GetCurrentScale01();
|
||||
yield return PlayLerp(fromAlpha, 0f, fromScale, 0f, _hideDuration);
|
||||
HideCueImage(clearSprite: true);
|
||||
_isClosingByCue = false;
|
||||
_transitionCoroutine = null;
|
||||
|
||||
if (GameEntry.UI != null && UIForm != null)
|
||||
{
|
||||
GameEntry.UI.CloseUIForm(UIForm);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator PlayLerp(float fromAlpha, float toAlpha, float fromScale, float toScale, float duration)
|
||||
{
|
||||
if (_cueImage == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
float safeDuration = Mathf.Max(0f, duration);
|
||||
if (safeDuration <= 0f)
|
||||
{
|
||||
SetCueVisual(toAlpha, toScale);
|
||||
yield break;
|
||||
}
|
||||
|
||||
float elapsed = 0f;
|
||||
while (elapsed < safeDuration)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
float t = Mathf.Clamp01(elapsed / safeDuration);
|
||||
float alpha = Mathf.Lerp(fromAlpha, toAlpha, t);
|
||||
float scale = Mathf.Lerp(fromScale, toScale, t);
|
||||
SetCueVisual(alpha, scale);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
SetCueVisual(toAlpha, toScale);
|
||||
}
|
||||
|
||||
private void SetCueVisual(float alpha, float scale01)
|
||||
{
|
||||
if (_cueImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Color color = _cueImage.color;
|
||||
color.a = Mathf.Clamp01(alpha);
|
||||
_cueImage.color = color;
|
||||
|
||||
RectTransform rectTransform = _cueImage.rectTransform;
|
||||
if (rectTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float safeScale = Mathf.Clamp01(scale01);
|
||||
rectTransform.localScale = _baseScale * safeScale;
|
||||
}
|
||||
|
||||
private float GetCurrentScale01()
|
||||
{
|
||||
if (_cueImage == null)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
EnsureBaseScaleInitialized();
|
||||
RectTransform rectTransform = _cueImage.rectTransform;
|
||||
if (rectTransform == null)
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
float ratioX = Mathf.Abs(_baseScale.x) > 0.0001f ? rectTransform.localScale.x / _baseScale.x : 1f;
|
||||
float ratioY = Mathf.Abs(_baseScale.y) > 0.0001f ? rectTransform.localScale.y / _baseScale.y : 1f;
|
||||
return Mathf.Clamp01((ratioX + ratioY) * 0.5f);
|
||||
}
|
||||
|
||||
private void EnsureBaseScaleInitialized()
|
||||
{
|
||||
if (_baseScaleInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cueImage == null || _cueImage.rectTransform == null)
|
||||
{
|
||||
_baseScale = Vector3.one;
|
||||
_baseScaleInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
_baseScale = _cueImage.rectTransform.localScale;
|
||||
if (_baseScale == Vector3.zero)
|
||||
{
|
||||
_baseScale = Vector3.one;
|
||||
}
|
||||
|
||||
_baseScaleInitialized = true;
|
||||
}
|
||||
|
||||
private void StopTransition()
|
||||
{
|
||||
if (_transitionCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_transitionCoroutine);
|
||||
_transitionCoroutine = null;
|
||||
}
|
||||
|
||||
_isClosingByCue = false;
|
||||
}
|
||||
|
||||
private void HideCueImage(bool clearSprite)
|
||||
{
|
||||
if (_cueImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cueImage.gameObject.SetActive(false);
|
||||
SetCueVisual(0f, 0f);
|
||||
if (clearSprite)
|
||||
{
|
||||
_cueImage.sprite = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4e1c3f77c2743b7a1d7d2f44f5eb9aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user