修复已知功能

1. 添加章节开始 UI 介绍
2. 清理不使用的资源
3. 补充 AIChat 入口 UI
4. 接上所有的正常对话流程
This commit is contained in:
2026-04-21 10:36:47 +08:00
parent ef68ccc9e9
commit a1b6b4a823
137 changed files with 27967 additions and 26332 deletions
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using DataTable;
using Definition;
@@ -14,22 +15,45 @@ namespace CustomComponent
[DisallowMultipleComponent]
public class DialogComponent : GameFrameworkComponent
{
[Serializable]
private sealed class SpeakerPortraitBinding
{
[SerializeField] private string _speakerId = string.Empty;
[SerializeField] private List<Sprite> _portraits = new List<Sprite>();
public string SpeakerId => _speakerId;
public List<Sprite> Portraits => _portraits;
}
#region Property
private const int DialogChapterDivisor = 1000;
private const int LineChapterDivisor = 100000000;
private const int LineDialogDivisor = 100000;
[Header("角色立绘配置(拖拽 Sprite)")]
[SerializeField] private List<SpeakerPortraitBinding> _speakerPortraitBindings = new List<SpeakerPortraitBinding>();
private readonly Dictionary<int, DRDialog> _dialogMap = new();
private readonly Dictionary<int, List<DRDialogLine>> _dialogLinesMap = new();
private readonly Dictionary<int, int> _dialogFirstLineIdMap = new();
private static readonly HashSet<string> NonPortraitSpeakerIds = new(System.StringComparer.OrdinalIgnoreCase)
{
"Other",
"Subtitle",
"Time"
};
private IDataTable<DRDialog> _dtDialog;
private IDataTable<DRDialogLine> _dtDialogLine;
private DialogFormController _formController;
private DialogFormContext _formContext;
private int _pendingStartDialogId;
private int _activeChapterTitleRequestId;
private int _nextChapterTitleRequestId = 1;
private bool _chapterTitleShownAfterInit;
private int _currentChapterId;
private int _currentLineIndex = -1;
@@ -46,6 +70,31 @@ namespace CustomComponent
public int CurrentLineId => _formContext != null ? _formContext.CurrentLineId : 0;
public int GetSpeakerPortraitCount(string speakerId)
{
if (string.IsNullOrWhiteSpace(speakerId))
{
return 0;
}
SpeakerPortraitBinding binding = FindSpeakerPortraitBinding(speakerId);
if (binding == null || binding.Portraits == null)
{
return 0;
}
int validCount = 0;
for (int i = 0; i < binding.Portraits.Count; i++)
{
if (binding.Portraits[i] != null)
{
validCount++;
}
}
return validCount;
}
#endregion
@@ -54,6 +103,7 @@ namespace CustomComponent
GameEntry.Event.Subscribe(DialogNextLineRequestEventArgs.EventId, OnDialogNextLineRequest);
GameEntry.Event.Subscribe(DialogSkipRequestEventArgs.EventId, OnDialogSkipRequest);
GameEntry.Event.Subscribe(DialogStopRequestEventArgs.EventId, OnDialogStopRequest);
GameEntry.Event.Subscribe(ChapterTitleCompletedEventArgs.EventId, OnChapterTitleCompleted);
}
private void OnDestroy()
@@ -61,6 +111,7 @@ namespace CustomComponent
GameEntry.Event.Unsubscribe(DialogNextLineRequestEventArgs.EventId, OnDialogNextLineRequest);
GameEntry.Event.Unsubscribe(DialogSkipRequestEventArgs.EventId, OnDialogSkipRequest);
GameEntry.Event.Unsubscribe(DialogStopRequestEventArgs.EventId, OnDialogStopRequest);
GameEntry.Event.Unsubscribe(ChapterTitleCompletedEventArgs.EventId, OnChapterTitleCompleted);
}
public bool Init(int chapterId)
@@ -159,10 +210,14 @@ namespace CustomComponent
return false;
}
PreloadChapterPortraitSprites();
EnsureFormController();
_formContext = null;
_currentChapterId = chapterId;
_currentLineIndex = -1;
_pendingStartDialogId = 0;
_activeChapterTitleRequestId = 0;
_chapterTitleShownAfterInit = false;
_isInitialized = true;
_isPlaying = false;
return true;
@@ -196,6 +251,24 @@ namespace CustomComponent
return false;
}
if (_activeChapterTitleRequestId > 0)
{
_pendingStartDialogId = dialogId;
return true;
}
if (!_chapterTitleShownAfterInit)
{
_chapterTitleShownAfterInit = true;
_pendingStartDialogId = dialogId;
if (TryOpenChapterTitle(dialogRow))
{
return true;
}
_pendingStartDialogId = 0;
}
if (_isPlaying)
{
StopDialog();
@@ -267,6 +340,21 @@ namespace CustomComponent
public void StopDialog()
{
_pendingStartDialogId = 0;
if (_activeChapterTitleRequestId > 0)
{
_activeChapterTitleRequestId = 0;
if (GameEntry.UI != null)
{
UGuiForm chapterTitleForm = GameEntry.UI.GetUIForm(UIFormId.ChapterTitleForm);
if (chapterTitleForm != null)
{
GameEntry.UI.CloseUIForm(chapterTitleForm.UIForm);
}
}
}
if (!_isPlaying)
{
return;
@@ -286,6 +374,9 @@ namespace CustomComponent
_formContext = null;
_currentChapterId = 0;
_currentLineIndex = -1;
_pendingStartDialogId = 0;
_activeChapterTitleRequestId = 0;
_chapterTitleShownAfterInit = false;
_isInitialized = false;
_isPlaying = false;
}
@@ -368,6 +459,138 @@ namespace CustomComponent
_formContext.IsLastLine = lineIndex >= totalLines - 1;
}
private void PreloadChapterPortraitSprites()
{
if (GameEntry.SpriteCache == null)
{
return;
}
HashSet<string> speakerIds = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase);
foreach (KeyValuePair<int, List<DRDialogLine>> pair in _dialogLinesMap)
{
List<DRDialogLine> lines = pair.Value;
if (lines == null)
{
continue;
}
for (int i = 0; i < lines.Count; i++)
{
DRDialogLine line = lines[i];
if (line == null || !IsHumanSpeakerId(line.SpeakerId))
{
continue;
}
speakerIds.Add(line.SpeakerId);
}
}
if (speakerIds.Count == 0)
{
return;
}
foreach (string speakerId in speakerIds)
{
RegisterSpeakerPortraitsToCache(speakerId);
}
}
private void RegisterSpeakerPortraitsToCache(string speakerId)
{
SpeakerPortraitBinding binding = FindSpeakerPortraitBinding(speakerId);
if (binding == null || binding.Portraits == null || binding.Portraits.Count == 0)
{
Log.Warning("Portrait preload skipped. Missing speaker portrait binding. speakerId='{0}'.", speakerId);
return;
}
int portraitIndex = 1;
for (int i = 0; i < binding.Portraits.Count; i++)
{
Sprite portrait = binding.Portraits[i];
if (portrait == null)
{
continue;
}
string cacheKey = $"Characters/{speakerId}-{portraitIndex}.png";
GameEntry.SpriteCache.RegisterSprite(cacheKey, portrait, true);
portraitIndex++;
}
}
private SpeakerPortraitBinding FindSpeakerPortraitBinding(string speakerId)
{
if (string.IsNullOrWhiteSpace(speakerId) || _speakerPortraitBindings == null)
{
return null;
}
for (int i = 0; i < _speakerPortraitBindings.Count; i++)
{
SpeakerPortraitBinding binding = _speakerPortraitBindings[i];
if (binding == null || string.IsNullOrWhiteSpace(binding.SpeakerId))
{
continue;
}
if (string.Equals(binding.SpeakerId.Trim(), speakerId.Trim(), StringComparison.OrdinalIgnoreCase))
{
return binding;
}
}
return null;
}
private static bool IsHumanSpeakerId(string speakerId)
{
if (string.IsNullOrWhiteSpace(speakerId))
{
return false;
}
return !NonPortraitSpeakerIds.Contains(speakerId);
}
private bool TryOpenChapterTitle(DRDialog dialogRow)
{
if (GameEntry.UI == null)
{
return false;
}
int requestId = _nextChapterTitleRequestId++;
string subtitle = dialogRow == null || string.IsNullOrWhiteSpace(dialogRow.Title)
? string.Empty
: dialogRow.Title.Trim();
ChapterTitleFormContext context = new ChapterTitleFormContext
{
ChapterId = _currentChapterId,
RequestId = requestId,
Title = BuildChapterTitle(_currentChapterId),
Subtitle = subtitle
};
int? formSerialId = GameEntry.UI.OpenUIForm(UIFormId.ChapterTitleForm, context);
if (!formSerialId.HasValue)
{
Log.Warning("Open chapter title form failed. chapterId='{0}'.", _currentChapterId.ToString());
return false;
}
_activeChapterTitleRequestId = requestId;
return true;
}
private static string BuildChapterTitle(int chapterId)
{
return chapterId > 0 ? $"第{chapterId}章" : string.Empty;
}
private static int ParseChapterIdFromDialogId(int dialogId)
{
return dialogId / DialogChapterDivisor;
@@ -415,6 +638,29 @@ namespace CustomComponent
StopDialog();
}
private void OnChapterTitleCompleted(object sender, GameEventArgs e)
{
if (!(e is ChapterTitleCompletedEventArgs args))
{
return;
}
if (args.RequestId <= 0 || args.RequestId != _activeChapterTitleRequestId)
{
return;
}
_activeChapterTitleRequestId = 0;
int pendingDialogId = _pendingStartDialogId;
_pendingStartDialogId = 0;
if (pendingDialogId <= 0 || !_isInitialized)
{
return;
}
StartDialog(pendingDialogId);
}
#endregion
}
}
@@ -14,59 +14,192 @@ namespace CustomComponent
[SerializeField] private Vector2 _defaultPivot = new(0.5f, 0.5f);
private Dictionary<string, Sprite> _spriteCache;
private Dictionary<string, List<Action<Sprite>>> _pendingCallbacks;
private HashSet<string> _loadingSpriteAssets;
private ResourceComponent _resource;
void Start()
{
_spriteCache = new Dictionary<string, Sprite>();
_resource = GameEntry.Resource;
EnsureRuntimeCaches();
}
private void EnsureRuntimeCaches()
{
_spriteCache ??= new Dictionary<string, Sprite>();
_pendingCallbacks ??= new Dictionary<string, List<Action<Sprite>>>();
_loadingSpriteAssets ??= new HashSet<string>();
_resource ??= GameEntry.Resource;
}
public bool TryGetCachedSprite(string assetName, out Sprite sprite)
{
EnsureRuntimeCaches();
if (string.IsNullOrWhiteSpace(assetName))
{
sprite = null;
return false;
}
return _spriteCache.TryGetValue(assetName, out sprite);
}
public void GetSprite(string assetName, Action<Sprite> callback)
{
EnsureRuntimeCaches();
if (string.IsNullOrWhiteSpace(assetName))
{
callback?.Invoke(null);
return;
}
if (_spriteCache.TryGetValue(assetName, out var sprite))
{
callback?.Invoke(sprite);
return;
}
else
if (callback != null)
{
_resource.LoadAsset
(
AssetUtility.GetUISpriteAsset(assetName),
Constant.AssetPriority.UIFormAsset,
new LoadAssetCallbacks(
(resourcePath, asset, duration, userData) =>
AddPendingCallback(assetName, callback);
}
if (_loadingSpriteAssets.Contains(assetName))
{
return;
}
_loadingSpriteAssets.Add(assetName);
StartLoadSprite(assetName);
}
public void PreloadSprite(string assetName)
{
EnsureRuntimeCaches();
if (string.IsNullOrWhiteSpace(assetName) || _spriteCache.ContainsKey(assetName) ||
_loadingSpriteAssets.Contains(assetName))
{
return;
}
_loadingSpriteAssets.Add(assetName);
StartLoadSprite(assetName);
}
public void PreloadSprites(IEnumerable<string> assetNames)
{
EnsureRuntimeCaches();
if (assetNames == null)
{
return;
}
foreach (string assetName in assetNames)
{
PreloadSprite(assetName);
}
}
public void RegisterSprite(string assetName, Sprite sprite, bool overwrite = true)
{
EnsureRuntimeCaches();
if (string.IsNullOrWhiteSpace(assetName) || sprite == null)
{
return;
}
if (!overwrite && _spriteCache.ContainsKey(assetName))
{
return;
}
_spriteCache[assetName] = sprite;
InvokeAndClearPendingCallbacks(assetName, sprite);
_loadingSpriteAssets.Remove(assetName);
}
private void StartLoadSprite(string assetName)
{
EnsureRuntimeCaches();
if (_resource == null)
{
InvokeAndClearPendingCallbacks(assetName, null);
_loadingSpriteAssets.Remove(assetName);
return;
}
_resource.LoadAsset
(
AssetUtility.GetUISpriteAsset(assetName),
Constant.AssetPriority.UIFormAsset,
new LoadAssetCallbacks(
(resourcePath, asset, duration, userData) =>
{
Texture2D texture = asset as Texture2D;
Sprite loadedSprite = null;
if (texture != null)
{
Log.Debug(resourcePath);
Texture2D texture = asset as Texture2D;
if (texture != null)
{
Sprite newSprite = Sprite.Create(
texture,
new Rect(0, 0, texture.width, texture.height),
_defaultPivot,
_pixelsPerUnit);
_spriteCache.Add(assetName, newSprite);
callback?.Invoke(newSprite);
}
},
(resourcePath, status, errorMessage, userData) =>
{
Log.Error("Can not load icon '{0}' from '{1}' with error message '{2}'.",
assetName,
resourcePath,
errorMessage);
loadedSprite = Sprite.Create(
texture,
new Rect(0, 0, texture.width, texture.height),
_defaultPivot,
_pixelsPerUnit);
_spriteCache[assetName] = loadedSprite;
}
)
);
else
{
Log.Warning("Load sprite succeeded but asset is not Texture2D. AssetName='{0}', Path='{1}'.",
assetName, resourcePath);
}
InvokeAndClearPendingCallbacks(assetName, loadedSprite);
_loadingSpriteAssets.Remove(assetName);
},
(resourcePath, status, errorMessage, userData) =>
{
Log.Error("Can not load icon '{0}' from '{1}' with error message '{2}'.",
assetName,
resourcePath,
errorMessage);
InvokeAndClearPendingCallbacks(assetName, null);
_loadingSpriteAssets.Remove(assetName);
}
)
);
}
private void AddPendingCallback(string assetName, Action<Sprite> callback)
{
EnsureRuntimeCaches();
if (!_pendingCallbacks.TryGetValue(assetName, out List<Action<Sprite>> callbackList))
{
callbackList = new List<Action<Sprite>>();
_pendingCallbacks.Add(assetName, callbackList);
}
callbackList.Add(callback);
}
private void InvokeAndClearPendingCallbacks(string assetName, Sprite sprite)
{
EnsureRuntimeCaches();
if (!_pendingCallbacks.TryGetValue(assetName, out List<Action<Sprite>> callbackList))
{
return;
}
_pendingCallbacks.Remove(assetName);
for (int i = 0; i < callbackList.Count; i++)
{
callbackList[i]?.Invoke(sprite);
}
}
private void OnDestroy()
{
_spriteCache.Clear();
_spriteCache?.Clear();
_pendingCallbacks?.Clear();
_loadingSpriteAssets?.Clear();
_resource = null;
}
}
}
}