using System.Collections; using System.Collections.Generic; using Event; using UI; using UnityEngine; using UnityGameFramework.Runtime; namespace CustomComponent { [DisallowMultipleComponent] public class StoryDirectorComponent : GameFrameworkComponent { [SerializeField] [Tooltip("按顺序执行。每个元素是一个 StoryDirectiveAsset 资产引用。")] private List _directives = new List(); [SerializeField] private bool _allowRepeatTrigger = false; [SerializeField] private bool _verboseLog = true; [SerializeField] private string _backgroundAssetNamePrefix = string.Empty; private readonly HashSet _consumedDirectiveTokens = new HashSet(); private readonly HashSet _queuedDirectiveTokens = new HashSet(); private readonly Queue _pendingDirectives = new Queue(); private BgFormController _bgFormController; private Coroutine _directiveExecutionCoroutine; private int _nextBackgroundRequestId = 1; private int _lastBackgroundRequestId; private int _completedBackgroundRequestId; private struct PendingDirectiveInvocation { public StoryDirectiveAsset Directive; public StoryTriggerType TriggerType; public int TriggerId; public string DirectiveToken; } private void Start() { GameEntry.Event.Subscribe(DialogCompletedEventArgs.EventId, OnDialogCompleted); GameEntry.Event.Subscribe(CombineCompletedEventArgs.EventId, OnCombineCompleted); GameEntry.Event.Subscribe(BgTransitionCompletedEventArgs.EventId, OnBgTransitionCompleted); } private void OnDestroy() { GameEntry.Event.Unsubscribe(DialogCompletedEventArgs.EventId, OnDialogCompleted); GameEntry.Event.Unsubscribe(CombineCompletedEventArgs.EventId, OnCombineCompleted); GameEntry.Event.Unsubscribe(BgTransitionCompletedEventArgs.EventId, OnBgTransitionCompleted); if (_directiveExecutionCoroutine != null) { StopCoroutine(_directiveExecutionCoroutine); _directiveExecutionCoroutine = null; } _bgFormController?.CloseUI(); _bgFormController = null; } public void ResetConsumedDirectives() { _consumedDirectiveTokens.Clear(); _queuedDirectiveTokens.Clear(); _pendingDirectives.Clear(); } private void OnDialogCompleted(object sender, GameFramework.Event.GameEventArgs e) { if (!(e is DialogCompletedEventArgs args)) { return; } ExecuteDirectives(StoryTriggerType.DialogCompleted, args.DialogId); } private void OnCombineCompleted(object sender, GameFramework.Event.GameEventArgs e) { if (!(e is CombineCompletedEventArgs)) { return; } ExecuteDirectives(StoryTriggerType.CombineCompleted, 0); } private void ExecuteDirectives(StoryTriggerType triggerType, int triggerId) { bool hasEnqueuedDirective = false; for (int i = 0; i < _directives.Count; i++) { StoryDirectiveAsset directive = _directives[i]; if (directive == null || !directive.IsMatch(triggerType, triggerId)) { continue; } string directiveToken = BuildDirectiveToken(directive, i); if (!_allowRepeatTrigger && (_consumedDirectiveTokens.Contains(directiveToken) || _queuedDirectiveTokens.Contains(directiveToken))) { continue; } _pendingDirectives.Enqueue(new PendingDirectiveInvocation { Directive = directive, TriggerType = triggerType, TriggerId = triggerId, DirectiveToken = directiveToken }); hasEnqueuedDirective = true; if (!_allowRepeatTrigger) { _queuedDirectiveTokens.Add(directiveToken); } } if (!hasEnqueuedDirective || _directiveExecutionCoroutine != null) { return; } _directiveExecutionCoroutine = StartCoroutine(ProcessDirectiveQueue()); } private static string BuildDirectiveToken(StoryDirectiveAsset directive, int directiveIndex) { return $"{directive.GetInstanceID()}:{directiveIndex}"; } private void ExecuteDirective(StoryDirectiveAsset directive, StoryTriggerType triggerType, int triggerId) { directive.Execute(this); if (_verboseLog) { Log.Info("StoryDirector executed action '{0}' by trigger '{1}' ({2}).", directive.ActionName, triggerType.ToString(), triggerId.ToString()); } } private IEnumerator ProcessDirectiveQueue() { while (_pendingDirectives.Count > 0) { PendingDirectiveInvocation invocation = _pendingDirectives.Dequeue(); if (!_allowRepeatTrigger) { _queuedDirectiveTokens.Remove(invocation.DirectiveToken); } ExecuteDirective(invocation.Directive, invocation.TriggerType, invocation.TriggerId); if (invocation.Directive.RequiresCompletion) { yield return WaitForDirectiveCompletion(invocation.Directive); } if (!_allowRepeatTrigger) { _consumedDirectiveTokens.Add(invocation.DirectiveToken); } } _directiveExecutionCoroutine = null; } private IEnumerator WaitForDirectiveCompletion(StoryDirectiveAsset directive) { if (!(directive is StoryChangeBackgroundDirectiveAsset)) { yield break; } int requestId = _lastBackgroundRequestId; if (requestId <= 0) { yield break; } const float timeoutSeconds = 10f; float elapsed = 0f; while (_completedBackgroundRequestId < requestId) { elapsed += Time.unscaledDeltaTime; if (elapsed >= timeoutSeconds) { Log.Warning("StoryDirector wait background transition timeout. requestId={0}", requestId.ToString()); break; } yield return null; } } public void ExecuteStartDialog(int dialogId) { if (dialogId <= 0) { Log.Warning("StoryDirector start dialog skipped. dialogId must be positive."); return; } if (GameEntry.Dialog == null) { Log.Warning("StoryDirector start dialog failed. Dialog component is missing."); return; } GameEntry.Dialog.StartDialog(dialogId); } public void ExecuteStartCombine(StoryCombineConfig config) { if (GameEntry.Combine == null) { Log.Warning("StoryDirector start combine failed. Combine component is missing."); return; } CombineFormContext context = BuildCombineFormContext(config); int? formSerialId = GameEntry.Combine.StartLevel(context); if (!formSerialId.HasValue) { Log.Warning("StoryDirector start combine failed. Open combine UI failed."); return; } } public void ExecuteChangeBackground(string backgroundAssetName) { _lastBackgroundRequestId = 0; string assetName = string.IsNullOrWhiteSpace(backgroundAssetName) ? string.Empty : backgroundAssetName.Trim(); if (string.IsNullOrEmpty(assetName)) { Log.Warning("StoryDirector change background skipped. assetName is empty."); return; } if (_bgFormController == null) { _bgFormController = new BgFormController(); } int requestId = _nextBackgroundRequestId++; _lastBackgroundRequestId = requestId; int? formSerialId = _bgFormController.OpenUI(new BgFormContext { BackgroundAssetName = _backgroundAssetNamePrefix + assetName, TransitionRequestId = requestId }); if (!formSerialId.HasValue) { Log.Warning("StoryDirector change background failed. BgForm open returned null."); _completedBackgroundRequestId = Mathf.Max(_completedBackgroundRequestId, requestId); } } public void ExecuteEndChapter(int chapterId) { int finalChapterId = chapterId > 0 ? chapterId : (GameEntry.Dialog != null ? GameEntry.Dialog.CurrentChapterId : 0); GameEntry.Event.Fire(this, StoryChapterEndedEventArgs.Create(finalChapterId)); } private void OnBgTransitionCompleted(object sender, GameFramework.Event.GameEventArgs e) { if (!(e is BgTransitionCompletedEventArgs args)) { return; } if (args.RequestId <= 0) { return; } _completedBackgroundRequestId = Mathf.Max(_completedBackgroundRequestId, args.RequestId); } private static CombineFormContext BuildCombineFormContext(StoryCombineConfig config) { if (config == null) { return null; } return new CombineFormContext { PartSprites = ClonePartSprites(config.PartSprites), AutoStart = config.AutoStart }; } private static List ClonePartSprites(List source) { var result = new List(); if (source == null) { return result; } for (int i = 0; i < source.Count; i++) { result.Add(source[i]); } return result; } } }