biography-of-lijie/Assets/GameMain/Scripts/CustomComponent/StoryDirectorComponent.cs

442 lines
16 KiB
C#

using System.Collections;
using System.Collections.Generic;
using Definition.Enum;
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 && config.Slots != null && config.Slots.Count > 0)
{
return new CombineFormContext
{
Slots = CloneSlots(config.Slots),
Parts = CloneParts(config.Parts),
AutoStart = config.AutoStart
};
}
return BuildDefaultCombineContext();
}
private static List<CombineSlotContext> CloneSlots(List<CombineSlotContext> source)
{
var result = new List<CombineSlotContext>();
if (source == null)
{
return result;
}
for (int i = 0; i < source.Count; i++)
{
CombineSlotContext slot = source[i];
if (slot == null)
{
continue;
}
result.Add(new CombineSlotContext
{
RequiredPartType = slot.RequiredPartType,
BuildOrder = slot.BuildOrder,
RequireStrictOrder = slot.RequireStrictOrder,
AnchoredPosition = slot.AnchoredPosition,
SizeDelta = slot.SizeDelta,
MechanicsExplanation = slot.MechanicsExplanation,
MismatchHint = slot.MismatchHint
});
}
return result;
}
private static List<CombinePartContext> CloneParts(List<CombinePartContext> source)
{
var result = new List<CombinePartContext>();
if (source == null)
{
return result;
}
for (int i = 0; i < source.Count; i++)
{
CombinePartContext part = source[i];
if (part == null)
{
continue;
}
result.Add(new CombinePartContext
{
PartType = part.PartType,
PartDisplayName = part.PartDisplayName,
MechanicsExplanation = part.MechanicsExplanation,
LockAfterPlaced = part.LockAfterPlaced
});
}
return result;
}
private static CombineFormContext BuildDefaultCombineContext()
{
List<CombineSlotContext> slots = new List<CombineSlotContext>
{
new CombineSlotContext
{
RequiredPartType = CombinePartType.Dou,
BuildOrder = 0,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-320f, -160f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Dou transfers upper load and works as the base node."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Sheng,
BuildOrder = 1,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-320f, -20f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Sheng raises layer height to form the bracket hierarchy."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Gong,
BuildOrder = 2,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-320f, 120f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Gong spreads force laterally through overhang."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Qiao,
BuildOrder = 3,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(-160f, 120f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Qiao continues force transfer to the outer side."
},
new CombineSlotContext
{
RequiredPartType = CombinePartType.Ang,
BuildOrder = 4,
RequireStrictOrder = true,
AnchoredPosition = new Vector2(0f, 120f),
SizeDelta = new Vector2(120f, 120f),
MechanicsExplanation = "Ang uses leverage to redirect eave load inward."
}
};
List<CombinePartContext> parts = new List<CombinePartContext>
{
new CombinePartContext
{ PartType = CombinePartType.Dou, PartDisplayName = "Dou", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Sheng, PartDisplayName = "Sheng", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Gong, PartDisplayName = "Gong", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Qiao, PartDisplayName = "Qiao", LockAfterPlaced = true },
new CombinePartContext
{ PartType = CombinePartType.Ang, PartDisplayName = "Ang", LockAfterPlaced = true }
};
return new CombineFormContext
{
Slots = slots,
Parts = parts,
AutoStart = true
};
}
}
}