feat: Round 48 narrative systems improvements

- QuestSO: Add ValidateBranchCycles() DFS detection for branches[].nextQuest loop
- QuestSO: Mark three legacy prerequisite fields with v2.0 removal warning in Tooltip
- IQuestManager: Add QuestLockReason enum + QuestLockInfo struct (strongly-typed lock info)
- IQuestManager: Add GetQuestLockInfo() method to interface; GetQuestLockReason() now delegates to it
- IQuestEventSource: Add OnQuestStateChanged(questId, oldState, newState) unified event
- QuestManager: Implement GetQuestLockInfo(); fire OnQuestStateChanged on all state transitions
- DialogueManager: Add one-frame yield in HandleChoices before ShowChoices (skip-debounce fix)
- DialogueManager: Increment _playbackId in ForceEnd() to invalidate residual choice callbacks
- DialogueSequenceSO: Add UNITY_EDITOR debug log in TryGetActiveVariant on variant match
- WorldStateRegistry: Add OnBatchStateChanged event + BatchMark() batch-write API
- DialogueModule: List badge shows warning indicator for unconditional-shadowing variants
- DialogueModule: BuildVariantsCard shows logic mode (AND/OR) alongside flag conditions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-25 00:05:15 +08:00
parent 446fd5dcd0
commit 6eaa83dc71
72 changed files with 7080 additions and 373 deletions

View File

@@ -18,6 +18,11 @@ namespace BaseGames.EventChain
[Header("所有事件链")]
[SerializeField] private EventChainSO[] _chains;
[Header("执行保护")]
[Tooltip("单个 Action 的最长执行时长unscaled time。超时后强制跳过并记录警告。0 = 不限时。")]
[Min(0f)]
[SerializeField] private float _maxActionTimeout = 30f;
[Header("事件频道(中继)")]
[SerializeField] private StringEventChannelSO _onBossDefeated; // EVT_EnemyDiedbossId
[SerializeField] private StringEventChannelSO _onCollectiblePickedUp; // EVT_CollectiblePickup
@@ -33,6 +38,39 @@ namespace BaseGames.EventChain
public event Action<string> OnDialogueCompleted;
/// <summary>链执行完成时广播 chainId供 ChainCompletedCondition。</summary>
public event Action<string> OnChainCompleted;
/// <summary>
/// 世界标志变更事件(供 FlagSetCondition 事件驱动订阅,避免每帧轮询)。
/// 由 SetFlagAction.ExecuteAsync 在写入标志后调用 NotifyFlagChanged 触发。
/// </summary>
public event Action<string> OnWorldFlagChanged;
/// <summary>通知所有 FlagSetCondition 指定标志已变更,并立即重评估标志相关链条件。</summary>
public void NotifyFlagChanged(string flagId)
{
OnWorldFlagChanged?.Invoke(flagId);
EvaluateForMask(ChainEventMask.FlagChanged);
}
/// <summary>
/// 强制执行指定链,完全跳过条件检查。
/// 用于调试、关卡测试,或 QA 快速验证后续事件逻辑。
/// 仅在 Play Mode 中有效(链执行需要运行时环境)。
/// </summary>
public void ForceExecute(string chainId)
{
if (_chains == null)
{
Debug.LogWarning("[EventChainManager] ForceExecute: _chains 为空,没有可执行的事件链。", this);
return;
}
foreach (var chain in _chains)
{
if (chain == null || chain.chainId != chainId) continue;
StartCoroutine(ExecuteChain(chain));
return;
}
Debug.LogWarning($"[EventChainManager] ForceExecute: 未找到 chainId='{chainId}' 的事件链。", this);
}
#if UNITY_EDITOR
/// <summary>
@@ -46,6 +84,15 @@ namespace BaseGames.EventChain
private readonly HashSet<string> _completedChains = new();
private readonly CompositeDisposable _subs = new();
// 每个链对应的事件掩码OnEnable 后通过 BuildChainMasks 构建
private List<ChainMaskEntry> _chainMasks;
private struct ChainMaskEntry
{
public EventChainSO chain;
public ChainEventMask mask;
}
// ── 生命周期 ──────────────────────────────────────────────────────
private void Awake()
@@ -56,11 +103,11 @@ namespace BaseGames.EventChain
private void OnEnable()
{
_onBossDefeated?.Subscribe(id => { OnBossDefeated?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
_onCollectiblePickedUp?.Subscribe(id => { OnCollectiblePickedUp?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
_onAbilityUnlocked?.Subscribe(id => { OnAbilityUnlocked?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
_onRoomEntered?.Subscribe(id => { OnRoomEntered?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
_onDialogueCompleted?.Subscribe(id => { OnDialogueCompleted?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
_onBossDefeated?.Subscribe(id => { OnBossDefeated?.Invoke(id); EvaluateForMask(ChainEventMask.Boss); }).AddTo(_subs);
_onCollectiblePickedUp?.Subscribe(id => { OnCollectiblePickedUp?.Invoke(id); EvaluateForMask(ChainEventMask.Collectible); }).AddTo(_subs);
_onAbilityUnlocked?.Subscribe(id => { OnAbilityUnlocked?.Invoke(id); EvaluateForMask(ChainEventMask.Ability); }).AddTo(_subs);
_onRoomEntered?.Subscribe(id => { OnRoomEntered?.Invoke(id); EvaluateForMask(ChainEventMask.Room); }).AddTo(_subs);
_onDialogueCompleted?.Subscribe(id => { OnDialogueCompleted?.Invoke(id); EvaluateForMask(ChainEventMask.Dialogue); }).AddTo(_subs);
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
@@ -68,12 +115,26 @@ namespace BaseGames.EventChain
// 先重置 SO 资产上的运行时状态,防止跨 PlayMode 会话或多场景加载时状态残留
if (_chains == null) return;
foreach (var chain in _chains)
{
// 旧版 conditions[] 字段
if (chain?.conditions != null)
foreach (var cond in chain.conditions)
{
cond?.ResetState();
cond?.Register(this);
}
// 新版 conditionGroups[] 字段
if (chain?.conditionGroups != null)
foreach (var group in chain.conditionGroups)
if (group?.conditions != null)
foreach (var cond in group.conditions)
{
cond?.ResetState();
cond?.Register(this);
}
}
BuildChainMasks();
}
private void OnDisable()
@@ -83,9 +144,16 @@ namespace BaseGames.EventChain
if (_chains == null) return;
foreach (var chain in _chains)
{
if (chain?.conditions != null)
foreach (var cond in chain.conditions)
cond?.Unregister(this);
if (chain?.conditionGroups != null)
foreach (var group in chain.conditionGroups)
if (group?.conditions != null)
foreach (var cond in group.conditions)
cond?.Unregister(this);
}
}
// ── ISaveable ─────────────────────────────────────────────────────────
@@ -108,26 +176,98 @@ namespace BaseGames.EventChain
}
// ── 评估逻辑 ──────────────────────────────────────────────────────
/// <summary>收到新事件时立即评估所有链条件(无帧延迟)。</summary>
private void EvaluateAll() => DoEvaluateAll();
private void DoEvaluateAll()
/// <summary>
/// 构建每条链的 ChainEventMask链内所有条件 RelevantEvents 的并集)。
/// OnEnable 在所有条件 Register 完毕后调用,之后事件触发只评估掩码相交的链。
/// </summary>
private void BuildChainMasks()
{
_chainMasks = new List<ChainMaskEntry>(_chains?.Length ?? 0);
if (_chains == null) return;
foreach (var chain in _chains)
{
if (chain == null) continue;
ChainEventMask mask = ChainEventMask.None;
bool hasGroups = chain.conditionGroups != null && chain.conditionGroups.Length > 0;
if (hasGroups)
{
foreach (var group in chain.conditionGroups)
if (group?.conditions != null)
foreach (var cond in group.conditions)
if (cond != null) mask |= cond.RelevantEvents;
}
else if (chain.conditions != null)
{
foreach (var cond in chain.conditions)
if (cond != null) mask |= cond.RelevantEvents;
}
// 无条件链:任何事件均可触发(等同 Any
if (mask == ChainEventMask.None) mask = ChainEventMask.Any;
_chainMasks.Add(new ChainMaskEntry { chain = chain, mask = mask });
}
}
/// <summary>
/// 懒评估:仅评估 mask 与 <paramref name="triggerMask"/> 相交的链。
/// ChainEventMask.Any-1与任何非零 mask 均相交,确保自定义条件链不被跳过。
/// </summary>
private void EvaluateForMask(ChainEventMask triggerMask)
{
if (_chainMasks == null) return;
foreach (var entry in _chainMasks)
{
if ((entry.mask & triggerMask) == 0) continue;
var chain = entry.chain;
if (!chain.repeatable && _completedChains.Contains(chain.chainId)) continue;
bool allMet = true;
if (chain.conditions != null)
foreach (var cond in chain.conditions)
if (cond != null && !cond.IsMet()) { allMet = false; break; }
if (allMet) StartCoroutine(ExecuteChain(chain));
if (EvaluateChainConditions(chain)) StartCoroutine(ExecuteChain(chain));
}
}
/// <summary>
/// 评估链的所有触发条件。
/// 若链配置了 <see cref="EventChainSO.conditionGroups"/>,按组逻辑评估(组内 And/Or组间 And
/// 否则回退到旧版 <see cref="EventChainSO.conditions"/> 隐式 And 逻辑。
/// </summary>
private static bool EvaluateChainConditions(EventChainSO chain)
{
bool hasGroups = chain.conditionGroups != null && chain.conditionGroups.Length > 0;
if (hasGroups)
{
// 组间为 And所有组均须通过
foreach (var group in chain.conditionGroups)
{
if (group?.conditions == null || group.conditions.Length == 0) continue;
if (!EvaluateConditionGroup(group)) return false;
}
return true;
}
// 旧版conditions[] 隐式 And
if (chain.conditions != null)
foreach (var cond in chain.conditions)
if (cond != null && !cond.IsMet()) return false;
return true;
}
/// <summary>按组内 logicAnd/Or评估单个条件组是否通过。</summary>
private static bool EvaluateConditionGroup(ConditionGroup group)
{
if (group.logic == WorldStateFlagLogic.Or)
{
foreach (var cond in group.conditions)
if (cond != null && cond.IsMet()) return true;
return false;
}
// And默认
foreach (var cond in group.conditions)
if (cond != null && !cond.IsMet()) return false;
return true;
}
private IEnumerator ExecuteChain(EventChainSO chain)
{
// 防重入:一次性链立即标记为已完成
@@ -137,7 +277,62 @@ namespace BaseGames.EventChain
foreach (var action in chain.actions)
{
if (action == null) continue;
yield return action.ExecuteAsync(this);
bool actionFailed = false;
if (_maxActionTimeout > 0f)
{
bool finished = false;
Coroutine co = null;
try
{
co = StartCoroutine(SetTrueOnFinish(action.ExecuteAsync(this), () => finished = true));
}
catch (System.Exception ex)
{
Debug.LogError(
$"[EventChainManager] 链 '{chain.chainId}' 动作 '{action.GetType().Name}' 启动异常:{ex.Message}",
this);
actionFailed = true;
}
if (!actionFailed)
{
// 用 realtimeSinceStartup 计时,确保 PlayMode 暂停时超时仍能触发
float deadline = Time.realtimeSinceStartup + _maxActionTimeout;
while (!finished)
{
if (Time.realtimeSinceStartup >= deadline)
{
if (co != null) StopCoroutine(co);
Debug.LogWarning(
$"[EventChainManager] 链 '{chain.chainId}' 的动作 '{action.GetType().Name}' " +
$"执行超时({_maxActionTimeout}s已强制跳过。");
#if UNITY_EDITOR
OnChainExecutedInEditor?.Invoke(chain.chainId,
$"超时跳过:{action.GetType().Name}>{_maxActionTimeout}s");
#endif
break;
}
yield return null;
}
}
}
else
{
bool exceptionThrown = false;
IEnumerator routine = null;
try { routine = action.ExecuteAsync(this); }
catch (System.Exception ex)
{
Debug.LogError(
$"[EventChainManager] 链 '{chain.chainId}' 动作 '{action.GetType().Name}' 异常:{ex.Message}",
this);
exceptionThrown = true;
}
if (!exceptionThrown && routine != null)
yield return routine;
}
if (chain.actionDelay > 0f)
yield return new WaitForSeconds(chain.actionDelay);
}
@@ -148,5 +343,15 @@ namespace BaseGames.EventChain
OnChainExecutedInEditor?.Invoke(chain.chainId, "执行完成");
#endif
}
/// <summary>
/// 将 inner 协程执行完毕后触发 onFinish 回调,供超时保护使用。
/// StopCoroutine 此协程时,内嵌的 inner通过 yield return 直接展开)也会一并停止。
/// </summary>
private static IEnumerator SetTrueOnFinish(IEnumerator inner, Action onFinish)
{
yield return inner;
onFinish();
}
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Dialogue;
using UnityEngine;
namespace BaseGames.EventChain
@@ -26,8 +25,14 @@ namespace BaseGames.EventChain
public float actionDelay = 0f; // 各 action 之间的延迟0 = 紧接着执行
[Header("触发条件(全部满足才触发)")]
[Tooltip("旧版条件列表(隐式 And 逻辑)。新配置推荐改用 conditionGroups。\n" +
"conditionGroups 非空时,此字段被忽略。")]
public ChainCondition[] conditions;
[Tooltip("条件组列表(新版)。每组内部支持 And / Or 逻辑,多组之间为 And 关系(全部组满足才触发)。\n" +
"此字段非空时,旧版 conditions 字段将被忽略。")]
public ConditionGroup[] conditionGroups;
[Header("执行动作(顺序执行)")]
public ChainAction[] actions;
}
@@ -46,12 +51,40 @@ namespace BaseGames.EventChain
public abstract void Register(EventChainManager manager);
public abstract void Unregister(EventChainManager manager);
public abstract bool IsMet();
/// <summary>
/// 重置运行时瞬态状态(每次 EventChainManager.OnEnable 时调用)。
/// ScriptableObject 是资产_met 等字段会跨 PlayMode 会话残留;
/// 显式重置确保每次进入游戏/切换场景时条件均从初始状态开始评估。
/// </summary>
public virtual void ResetState() { }
/// <summary>
/// 声明此条件关心哪类运行时事件。
/// EventChainManager 在构建链桶时使用此掩码,使评估仅在相关事件到来时触发,
/// 跳过无关事件,降低 EvaluateAll 的无效迭代次数。
/// 默认返回 Any适配自定义条件任何事件均触发评估
/// </summary>
public virtual ChainEventMask RelevantEvents => ChainEventMask.Any;
}
/// <summary>
/// 位掩码:标识事件链条件关心的运行时事件类别。
/// 用于 EventChainManager 构建懒评估桶,减少每次事件触发时的无关链扫描。
/// </summary>
[System.Flags]
public enum ChainEventMask
{
None = 0,
Boss = 1 << 0,
Collectible = 1 << 1,
Ability = 1 << 2,
Room = 1 << 3,
Dialogue = 1 << 4,
FlagChanged = 1 << 5,
Chain = 1 << 6,
/// <summary>不区分事件类别;任何事件均触发评估(自定义条件的默认值)。</summary>
Any = -1,
}
// ─── 内置条件 ──────────────────────────────────────────────────────────
@@ -65,6 +98,7 @@ namespace BaseGames.EventChain
public override void Unregister(EventChainManager m) => m.OnBossDefeated -= Check;
public override bool IsMet() => _met;
public override void ResetState() => _met = false;
public override ChainEventMask RelevantEvents => ChainEventMask.Boss;
private void Check(string id) { if (id == bossId) _met = true; }
}
@@ -72,9 +106,47 @@ namespace BaseGames.EventChain
public class FlagSetCondition : ChainCondition
{
public string flagId;
public override void Register(EventChainManager m) { } // 持续轮询,无需订阅
public override void Unregister(EventChainManager m) { }
public override bool IsMet() { var sm = ServiceLocator.GetOrDefault<ISaveService>(); return sm != null && sm.GetFlag(flagId); }
[System.NonSerialized] private bool _met;
[System.NonSerialized] private bool _initialized; // 延迟初始化标记
public override void Register(EventChainManager m)
{
// 订阅事件;实际标志值延迟到首次 IsMet() 调用时从 SaveService 读取,
// 避免 Register 早于 SaveService 注册时读取失败
m.OnWorldFlagChanged += OnFlagChanged;
}
public override void Unregister(EventChainManager m)
{
m.OnWorldFlagChanged -= OnFlagChanged;
}
public override bool IsMet()
{
if (!_initialized)
{
var sm = ServiceLocator.GetOrDefault<ISaveService>();
if (sm != null)
{
_met = sm.GetFlag(flagId);
_initialized = true;
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
else
Debug.LogWarning(
$"[FlagSetCondition] 标志 '{flagId}' 首次 IsMet() 时 ISaveService 尚未注册," +
"返回 false将在 SaveService 注册后通过 OnWorldFlagChanged 更新)。");
#endif
}
return _met;
}
public override void ResetState() { _met = false; _initialized = false; }
public override ChainEventMask RelevantEvents => ChainEventMask.FlagChanged;
private void OnFlagChanged(string changedFlagId)
{
if (changedFlagId != flagId) return;
var sm = ServiceLocator.GetOrDefault<ISaveService>();
if (sm != null) { _met = sm.GetFlag(flagId); _initialized = true; }
}
}
[CreateAssetMenu(menuName = "BaseGames/EventChain/Condition/AbilityUnlocked")]
@@ -86,6 +158,7 @@ namespace BaseGames.EventChain
public override void Unregister(EventChainManager m) => m.OnAbilityUnlocked -= Check;
public override bool IsMet() => _met;
public override void ResetState() => _met = false;
public override ChainEventMask RelevantEvents => ChainEventMask.Ability;
private void Check(string id) { if (id == abilityId) _met = true; }
}
@@ -98,6 +171,7 @@ namespace BaseGames.EventChain
public override void Unregister(EventChainManager m) => m.OnCollectiblePickedUp -= Check;
public override bool IsMet() => _met;
public override void ResetState() => _met = false;
public override ChainEventMask RelevantEvents => ChainEventMask.Collectible;
private void Check(string id) { if (id == itemId) _met = true; }
}
@@ -110,6 +184,7 @@ namespace BaseGames.EventChain
public override void Unregister(EventChainManager m) => m.OnRoomEntered -= Check;
public override bool IsMet() => _met;
public override void ResetState() => _met = false;
public override ChainEventMask RelevantEvents => ChainEventMask.Room;
private void Check(string id) { if (id == sceneName) _met = true; }
}
@@ -122,6 +197,7 @@ namespace BaseGames.EventChain
public override void Unregister(EventChainManager m) => m.OnDialogueCompleted -= Check;
public override bool IsMet() => _met;
public override void ResetState() => _met = false;
public override ChainEventMask RelevantEvents => ChainEventMask.Dialogue;
private void Check(string id) { if (id == npcId) _met = true; }
}
@@ -134,9 +210,29 @@ namespace BaseGames.EventChain
public override void Unregister(EventChainManager m) => m.OnChainCompleted -= Check;
public override bool IsMet() => _met;
public override void ResetState() => _met = false;
public override ChainEventMask RelevantEvents => ChainEventMask.Chain;
private void Check(string id) { if (id == chainId) _met = true; }
}
// =====================================================================
// ConditionGroup ── 支持 And/Or 逻辑的条件分组
// =====================================================================
/// <summary>
/// 条件组:将多个 <see cref="ChainCondition"/> 以 And 或 Or 逻辑组合。
/// 多个条件组之间始终为 And 关系(所有组均须满足)。
/// 在 <see cref="EventChainSO.conditionGroups"/> 中配置,替代旧版隐式 And 的 <c>conditions[]</c>。
/// </summary>
[System.Serializable]
public class ConditionGroup
{
[Tooltip("组内条件的逻辑关系:\n And默认= 全部条件均须满足\n Or = 任意一个条件满足即可触发本组通过")]
public WorldStateFlagLogic logic = WorldStateFlagLogic.And;
[Tooltip("本组的条件列表。")]
public ChainCondition[] conditions;
}
// =====================================================================
// ChainAction 抽象基类 + 内置实现
// =====================================================================
@@ -164,13 +260,22 @@ namespace BaseGames.EventChain
}
}
/// <summary>设置/清除存档标志。</summary>
/// <summary>设置/清除存档标志。设置后通知 EventChainManager 触发条件重评估。</summary>
[CreateAssetMenu(menuName = "BaseGames/EventChain/Action/SetFlag")]
public class SetFlagAction : ChainAction
{
[WorldStateFlag]
[Tooltip("世界状态标志 Key字符串由 ISaveService 持久化。")]
public string flagId;
public bool value = true;
#if UNITY_EDITOR
private void OnValidate()
{
if (string.IsNullOrWhiteSpace(flagId))
Debug.LogWarning(
$"[SetFlagAction] '{name}': flagId 为空,执行时将静默失败。请填写有效的 flagId。", this);
}
#endif
public override IEnumerator ExecuteAsync(MonoBehaviour runner)
{
var saveService = ServiceLocator.GetOrDefault<ISaveService>();
@@ -180,6 +285,11 @@ namespace BaseGames.EventChain
yield break;
}
saveService.SetFlag(flagId, value);
// 通知 EventChainManager 标志已变更,触发 FlagSetCondition 重新评估(事件驱动,无需轮询)
if (runner is EventChainManager ecm)
ecm.NotifyFlagChanged(flagId);
yield break;
}
}
@@ -204,28 +314,67 @@ namespace BaseGames.EventChain
public string cutsceneId;
[SerializeField] private StringEventChannelSO _onPlayCutscene; // → CutsceneManager.PlayById
[SerializeField] private VoidEventChannelSO _onCutsceneEnded; // ← CutsceneManager 播完后 Raise
[Tooltip("等待过场动画结束的超时时间(秒)。超时后记录错误并继续执行事件链,避免链永久挂起。\n0 = 永不超时(不推荐,可能导致链死锁)。")]
[Min(0f)] [SerializeField] private float timeoutSeconds = 60f;
public override IEnumerator ExecuteAsync(MonoBehaviour runner)
{
bool done = false;
// 用 try/finally 确保即使协程被强制停止StopAllCoroutines时也能正确退订
var sub = _onCutsceneEnded != null
? _onCutsceneEnded.Subscribe(() => done = true)
: default(EventSubscription);
_onPlayCutscene?.Raise(cutsceneId);
yield return new WaitUntil(() => done);
sub.Dispose();
try
{
_onPlayCutscene?.Raise(cutsceneId);
if (timeoutSeconds > 0f)
{
// 超时保护:防止 CutsceneManager 未触发 Ended 事件导致链永久挂起
float elapsed = 0f;
while (!done && elapsed < timeoutSeconds)
{
elapsed += UnityEngine.Time.deltaTime;
yield return null;
}
if (!done)
Debug.LogError(
$"[PlayCutsceneAction] 过场动画 '{cutsceneId}' 等待超时({timeoutSeconds}s。" +
"请确认 CutsceneManager 在动画结束后调用了 _onCutsceneEnded.Raise()。");
}
else
{
yield return new WaitUntil(() => done);
}
}
finally
{
sub.Dispose();
}
}
}
/// <summary>切换 NPC 对话(通过 EVT_NPCDialogueChange 广播NPC 自行响应)。</summary>
/// <summary>切换 NPC 对话(通过 EVT_NPCDialogueChange 强类型事件广播NPC 自行响应)。</summary>
[CreateAssetMenu(menuName = "BaseGames/EventChain/Action/ChangeNPCDialogue")]
public class ChangeNPCDialogueAction : ChainAction
{
[Tooltip("目标 NPC 的唯一 ID对应 NpcSO.npcId。")]
public string npcId;
[Tooltip("要切换到的对话序列 ID对应 DialogueSequenceSO.sequenceId。")]
public string newSequenceId;
[SerializeField] private StringEventChannelSO _onNPCDialogueChange; // EVT_NPCDialogueChange → NPC 订阅
/// <summary>
/// 强类型事件频道NpcDialogueChangeEventChannelSO
/// NPC 组件订阅后根据 npcId 字段过滤,无需 Split 字符串。
/// 资产Assets/ScriptableObjects/Events/EVT_NpcDialogueChange.asset
/// </summary>
[SerializeField] private BaseGames.Core.Events.NpcDialogueChangeEventChannelSO _onNPCDialogueChange;
public override IEnumerator ExecuteAsync(MonoBehaviour runner)
{
_onNPCDialogueChange?.Raise($"{npcId}:{newSequenceId}");
_onNPCDialogueChange?.Raise(new BaseGames.Core.Events.NpcDialogueChangeEvent
{
npcId = npcId,
newSequenceId = newSequenceId
});
yield break;
}
}