修复已知功能

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
-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 20cdfc6b68f9f074a88435387f8f30a3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,51 +0,0 @@
using UnityEngine;
namespace Components
{
public class InputComponent : MonoBehaviour
{
private PlayerInputActions _inputActions;
private bool _isListening = false;
private Vector3 _direction = Vector3.zero;
public Vector3 Direction => _direction;
public void OnInit()
{
_inputActions = new();
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (_isListening && _inputActions != null)
{
ReadInput();
}
}
public void OnReset()
{
_inputActions = null;
_isListening = false;
}
private void ReadInput()
{
Vector2 rawDir = _inputActions.Game.Move.ReadValue<Vector2>();
_direction = new Vector3(rawDir.x, 0, rawDir.y);
}
public void SetListening(bool isListening)
{
_isListening = isListening;
if (_isListening)
{
_inputActions.Enable();
}
else
{
_inputActions.Disable();
}
}
}
}
@@ -1,41 +0,0 @@
using UnityEngine;
namespace Components
{
public class MovementComponent : MonoBehaviour
{
private bool _isMoving = false;
private Vector3 _direction = Vector3.forward;
private float _speed = 0;
private Transform _cachedTransform;
public void OnInit(float speed, Transform transform)
{
_speed = speed;
_cachedTransform = transform;
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (_isMoving && _cachedTransform != null)
{
Move(elapseSeconds);
}
}
public void OnReset()
{
_speed = 0;
_cachedTransform = null;
_isMoving = false;
}
private void Move(float deltaTime = 0)
{
_cachedTransform.Translate(_direction * _speed * deltaTime);
}
public void SetMove(bool isMoving) => _isMoving = isMoving;
public void SetDirection(Vector3 direction) => _direction = direction;
}
}
@@ -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;
}
}
}
}
@@ -9,5 +9,7 @@ namespace Definition.Enum
None,
Shake,
Blink,
EntityCue,
EntityCueClose,
}
}
}
@@ -1,14 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Definition.Enum
{
public enum ExpressionType
{
None,
Normal,
Shock,
Angry,
}
}
@@ -59,5 +59,9 @@ namespace UI
MainOverlayForm = 107,
AIChatForm = 108,
ChapterTitleForm = 109,
AIChatEntryForm = 110,
}
}
-9
View File
@@ -1,9 +0,0 @@
fileFormatVersion: 2
guid: f7c4b051e63f0c2418a55e8817bf4ba9
folderAsset: yes
timeCreated: 1528026151
licenseType: Pro
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -1,9 +0,0 @@
fileFormatVersion: 2
guid: 58efe0589d7e90f498c8bcbbf94c6be4
folderAsset: yes
timeCreated: 1528026151
licenseType: Pro
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -1,62 +0,0 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using System;
using UnityEngine;
namespace Entity.EntityData
{
[Serializable]
public abstract class EntityDataBase
{
[SerializeField]
private int m_Id = 0;
[SerializeField]
private int m_TypeId = 0;
[SerializeField]
private Vector3 m_Position = Vector3.zero;
[SerializeField]
private Quaternion m_Rotation = Quaternion.identity;
public EntityDataBase(int entityId, int typeId)
{
m_Id = entityId;
m_TypeId = typeId;
}
/// <summary>
/// 实体编号。
/// </summary>
public int Id => m_Id;
/// <summary>
/// 实体类型编号。
/// </summary>
public int TypeId => m_TypeId;
/// <summary>
/// 实体位置。
/// </summary>
public Vector3 Position
{
get => m_Position;
set => m_Position = value;
}
/// <summary>
/// 实体朝向。
/// </summary>
public Quaternion Rotation
{
get => m_Rotation;
set => m_Rotation = value;
}
}
}
@@ -1,70 +0,0 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using GameFramework.DataTable;
using System;
using CustomUtility;
using DataTable;
using Entity;
using Entity.EntityData;
using UnityGameFramework.Runtime;
namespace StarForce
{
public static class EntityExtension
{
private static int s_SerialId = 0;
public static EntityBase GetGameEntity(this EntityComponent entityComponent, int entityId)
{
UnityGameFramework.Runtime.Entity entity = entityComponent.GetEntity(entityId);
if (entity == null)
{
return null;
}
return (EntityBase)entity.Logic;
}
public static void HideEntity(this EntityComponent entityComponent, EntityBase entity)
{
entityComponent.HideEntity(entity.Entity);
}
public static void AttachEntity(this EntityComponent entityComponent, EntityBase entityBase, int ownerId,
string parentTransformPath = null, object userData = null)
{
entityComponent.AttachEntity(entityBase.Entity, ownerId, parentTransformPath, userData);
}
private static void ShowEntity(this EntityComponent entityComponent, Type logicType, string entityGroup,
int priority, EntityDataBase data)
{
if (data == null)
{
Log.Warning("Data is invalid.");
return;
}
IDataTable<DREntity> dtEntity = GameEntry.DataTable.GetDataTable<DREntity>();
DREntity drEntity = dtEntity.GetDataRow(data.TypeId);
if (drEntity == null)
{
Log.Warning("Can not load entity id '{0}' from data table.", data.TypeId.ToString());
return;
}
entityComponent.ShowEntity(data.Id, logicType, AssetUtility.GetEntityAsset(drEntity.AssetName), entityGroup,
priority, data);
}
public static int GenerateSerialId(this EntityComponent entityComponent)
{
return --s_SerialId;
}
}
}
@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: 626632079980ad34595c3b43a062a664
timeCreated: 1528026153
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,9 +0,0 @@
fileFormatVersion: 2
guid: 74fbdf043ed3bc141a0a0b09756bc7d2
folderAsset: yes
timeCreated: 1528026153
licenseType: Pro
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -1,122 +0,0 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using Entity.EntityData;
using GameFramework;
using StarForce;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Entity
{
public abstract class EntityBase : EntityLogic
{
[SerializeField] private EntityDataBase _entityData = null;
public int Id => Entity.Id;
public Animation CachedAnimation
{
get;
private set;
}
#if UNITY_2017_3_OR_NEWER
protected override void OnInit(object userData)
#else
protected internal override void OnInit(object userData)
#endif
{
base.OnInit(userData);
CachedAnimation = GetComponent<Animation>();
}
#if UNITY_2017_3_OR_NEWER
protected override void OnRecycle()
#else
protected internal override void OnRecycle()
#endif
{
base.OnRecycle();
}
#if UNITY_2017_3_OR_NEWER
protected override void OnShow(object userData)
#else
protected internal override void OnShow(object userData)
#endif
{
base.OnShow(userData);
_entityData = userData as EntityDataBase;
if (_entityData == null)
{
Log.Error("Entity data is invalid.");
return;
}
Name = Utility.Text.Format("[Entity {0}]", Id);
CachedTransform.localPosition = _entityData.Position;
CachedTransform.localRotation = _entityData.Rotation;
CachedTransform.localScale = Vector3.one;
}
#if UNITY_2017_3_OR_NEWER
protected override void OnHide(bool isShutdown, object userData)
#else
protected internal override void OnHide(bool isShutdown, object userData)
#endif
{
base.OnHide(isShutdown, userData);
}
#if UNITY_2017_3_OR_NEWER
protected override void OnAttached(EntityLogic childEntity, Transform parentTransform, object userData)
#else
protected internal override void OnAttached(EntityLogic childEntity, Transform parentTransform, object userData)
#endif
{
base.OnAttached(childEntity, parentTransform, userData);
}
#if UNITY_2017_3_OR_NEWER
protected override void OnDetached(EntityLogic childEntity, object userData)
#else
protected internal override void OnDetached(EntityLogic childEntity, object userData)
#endif
{
base.OnDetached(childEntity, userData);
}
#if UNITY_2017_3_OR_NEWER
protected override void OnAttachTo(EntityLogic parentEntity, Transform parentTransform, object userData)
#else
protected internal override void OnAttachTo(EntityLogic parentEntity, Transform parentTransform, object userData)
#endif
{
base.OnAttachTo(parentEntity, parentTransform, userData);
}
#if UNITY_2017_3_OR_NEWER
protected override void OnDetachFrom(EntityLogic parentEntity, object userData)
#else
protected internal override void OnDetachFrom(EntityLogic parentEntity, object userData)
#endif
{
base.OnDetachFrom(parentEntity, userData);
}
#if UNITY_2017_3_OR_NEWER
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
#else
protected internal override void OnUpdate(float elapseSeconds, float realElapseSeconds)
#endif
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
}
}
}
@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: 878fbbe82b4bde944812bcb7c964cff8
timeCreated: 1528026154
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,35 @@
using GameFramework;
using GameFramework.Event;
namespace Event
{
public class ChapterTitleCompletedEventArgs : GameEventArgs
{
public static readonly int EventId = typeof(ChapterTitleCompletedEventArgs).GetHashCode();
public override int Id => EventId;
public int ChapterId { get; private set; }
public int RequestId { get; private set; }
public ChapterTitleCompletedEventArgs()
{
ChapterId = 0;
RequestId = 0;
}
public static ChapterTitleCompletedEventArgs Create(int chapterId, int requestId)
{
ChapterTitleCompletedEventArgs args = ReferencePool.Acquire<ChapterTitleCompletedEventArgs>();
args.ChapterId = chapterId;
args.RequestId = requestId;
return args;
}
public override void Clear()
{
ChapterId = 0;
RequestId = 0;
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 9baf591df06b9bb44ba5313457ba84b3
guid: 2de1f87831fa4a81ad93a2f5277cfaa2
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -23,7 +23,6 @@ namespace Procedure
{
public static readonly string[] DataTableNames = new string[]
{
"Entity",
"BGM",
"Scene",
"UIForm",
@@ -31,6 +31,7 @@ namespace Procedure
protected override void OnEnter(ProcedureOwner procedureOwner)
{
base.OnEnter(procedureOwner);
AIChatEntryRuntime.EnsureOpen();
GameEntry.Dialog.Init(1);
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using GameFramework.Fsm;
using GameFramework.Procedure;
using UnityEngine;
using UI;
namespace Procedure
{
@@ -13,6 +14,7 @@ namespace Procedure
protected override void OnEnter(IFsm<IProcedureManager> procedureOwner)
{
base.OnEnter(procedureOwner);
AIChatEntryRuntime.EnsureOpen();
GameEntry.Dialog.Init(1);
GameEntry.Dialog.StartDialog(1001);
@@ -61,6 +61,7 @@ namespace Procedure
protected override void OnEnter(IFsm<IProcedureManager> procedureOwner)
{
base.OnEnter(procedureOwner);
AIChatEntryRuntime.EnsureOpen();
var e = GameEntry.Event;
e.Subscribe(MenuStartEventArgs.EventId, MenuStart);
-9
View File
@@ -1,9 +0,0 @@
fileFormatVersion: 2
guid: 2ff69ece58f4d3f4e8cf9a40f813e745
folderAsset: yes
timeCreated: 1528026159
licenseType: Pro
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -1,30 +0,0 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using Entity;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace StarForce
{
public class HideByBoundary : MonoBehaviour
{
private void OnTriggerExit(Collider other)
{
GameObject go = other.gameObject;
EntityBase entity = go.GetComponent<EntityBase>();
if (entity == null)
{
Log.Warning("Unknown GameObject '{0}', you must use entity only.", go.name);
Destroy(go);
return;
}
GameEntry.Entity.HideEntity(entity);
}
}
}
@@ -1,12 +0,0 @@
fileFormatVersion: 2
guid: 649621152579e224a955822793b63aca
timeCreated: 1528026159
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,7 +1,6 @@
using CustomUtility;
using DataTable;
using Definition;
using Entity;
using GameFramework.DataTable;
using GameFramework.Sound;
using UnityGameFramework.Runtime;
@@ -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);
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: f754cc5f339dcfa4aaec3aa32260c4b8
guid: 8f5f8c4efc7e47a8b0354c7f89fef0b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,8 @@
namespace UI
{
public sealed class AIChatEntryFormContext : UIContext
{
public AIChatEntryFormController Controller;
public bool IsChatOpened;
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 174e046d822a14d49843dc415067f13c
guid: 0b5fd3ce4a3e4e7a8ed2ab9f9472e0a1
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -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;
}
}
@@ -1,8 +1,7 @@
fileFormatVersion: 2
guid: afb91fe3ed8105e4a96b7bd5917ecc2b
timeCreated: 1528026152
licenseType: Pro
guid: d2f2414b0ec34d2cb7a64c46c18cb48c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
@@ -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:
+31 -7
View File
@@ -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:
+41 -6
View File
@@ -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: