324 lines
11 KiB
C#
324 lines
11 KiB
C#
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<StoryDirectiveAsset> _directives = new List<StoryDirectiveAsset>();
|
|
|
|
[SerializeField] private bool _allowRepeatTrigger = false;
|
|
|
|
[SerializeField] private bool _verboseLog = true;
|
|
|
|
[SerializeField] private string _backgroundAssetNamePrefix = string.Empty;
|
|
|
|
private readonly HashSet<string> _consumedDirectiveTokens = new HashSet<string>();
|
|
private readonly HashSet<string> _queuedDirectiveTokens = new HashSet<string>();
|
|
private readonly Queue<PendingDirectiveInvocation> _pendingDirectives =
|
|
new Queue<PendingDirectiveInvocation>();
|
|
|
|
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<Sprite> ClonePartSprites(List<Sprite> source)
|
|
{
|
|
var result = new List<Sprite>();
|
|
if (source == null)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
result.Add(source[i]);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
}
|