using System; using System.Threading.Tasks; using System.Threading; using UnityEngine; using UnityEngine.Playables; using UnityEngine.UI; namespace EventBossFight { public class EventBossFightTimelineBossCellVideo : EventBossFightTimelineCellVideo, IEventBossFightBossCell { [Header("OBJ")] private GameObject attack; private GameObject hit; private GameObject hit_02; public PlayableAsset attacked1Timeline; public PlayableAsset attacked2Timeline; public PlayableAsset attackTimeline; [Header("入场 show(与面板 enter Director 并行;播完由本 Cell 切 idle)")] [Tooltip("未配置则入场直接进 idleTimeline")] public PlayableAsset showTimeline; public PlayableDirector BossTimelineDirector => CellPlayableDirector; [Header("boss attack")] [Tooltip("玩家攻击的震动数据,必须有五个")] public EventBossFightShakeData bossAttackShakeData; [Header("boss attacked")] [Tooltip("material _FillPhase(若 RawImage 材质无此属性则跳过)")] [Range(0, 1)] [SerializeField] private float bossAttackedFillPhase = 0.5f; [Tooltip("material 恢复闪白")] [SerializeField] private float bossAttackedRevertTime = 0.5f; [Header("boss attack")] public GameObject playerAttackedFx; [Header("跟随挂点")] [Tooltip("通用跟随(如血条图标等),对应原 BoneFollower 根骨骼")] public RectTransform bossFollowAnchor; [Tooltip("受击特效挂点,数量与顺序由主面板 hurt_fx_position 下子节点自动收集;随机下标在 [0,Length) 内选取。")] public RectTransform[] bossHurtAnchors; /// 运行时在本物体上自动添加;默认 ChromaKey,须与各 Decoder 的 Alpha Type 一致。 private LeviathanVideoSharedMaterialController _sharedMaterialHub; [Header("可选:受击闪白目标")] [SerializeField] private RawImage flashTarget; private static readonly int FillPhaseId = Shader.PropertyToID("_FillPhase"); protected override void Awake() { EnsureSharedMaterialHub(); attack ??= ResolveVideoLayer(transform, "attack"); hit ??= ResolveVideoLayer(transform, "hit"); hit_02 ??= ResolveVideoLayer(transform, "hit_02"); base.Awake(); } public void ShowGameObject() { gameObject.SetActive(true); if (attack) attack.SetActive(false); if (hit) hit.SetActive(false); if (hit_02) hit_02.SetActive(false); if (death) death.SetActive(false); if (idle) idle.SetActive(false); if (show) show.SetActive(true); } /// /// 与面板 enter Director 并行:先播本 Cell 的 (若有),再 。 /// public async Task PlayShowThenIdleAsync() { var dir = CellPlayableDirector; if (!dir) { Debug.LogError($"{name}: 未配置 Cell 上的 PlayableDirector,无法播放入场/待机 Timeline", this); return; } ShowGameObject(); if (showTimeline) await PlayTimelineSegmentOnceAsync(dir, showTimeline); PlayIdle(dir); } /// /// 无 时(如复盘):激活 idle 层,对 idle 子层级内 / 循环播放 bytesFile。 /// public void PlayIdleDirectDecoderLoop() { EnsureSharedMaterialHub(); ApplyIdleLayerVisibilityBoss(); if (show) show.SetActive(false); if (!idle) { Debug.LogError($"{name}: PlayIdleDirectDecoderLoop 需要 idle 节点", this); return; } var decoder = GetDecoderUnder(idle); if (decoder == null) { Debug.LogError($"{name}: idle 子层级未找到 LeviathanVideoDecoder", this); return; } if (decoder.bytesFile == null) { Debug.LogError($"{name}: idle 解码器未配置 bytesFile", this); return; } decoder.Stop(); decoder.SetLooping(true); if (decoder is ZZVideoRawImageDecoder zzDecoder) zzDecoder.PlayVideoFromSerialized(); else { var ret = decoder.PlayVideo(decoder.bytesFile, decoder.AlphaType, true); if (ret != 0) Debug.LogError($"{name}: PlayVideo 失败 ret={ret}", this); } OnReturnedToIdleVisuals(); } /// /// 在本物体上获取或添加 。 /// private void EnsureSharedMaterialHub() { if (_sharedMaterialHub != null) return; if (!this) return; _sharedMaterialHub = GetComponent(); if (_sharedMaterialHub == null) _sharedMaterialHub = gameObject.AddComponent(); } private LeviathanVideoSharedMaterialController SharedMaterialHubOrNull() { EnsureSharedMaterialHub(); return _sharedMaterialHub; } public override void PlayIdle(PlayableDirector director) { if (!director) return; if (!idleTimeline) { Debug.LogError($"{name}: 视频 Boss 必须配置 idleTimeline", this); return; } var ct = StartPlayIdleDeferredCancellation(); PlayIdleDeferredBossVideoAsync(director, ct); } /// /// 入场 show→idle:先只切层并 Play,idle 用本地材质出首帧后再关 show 并挂共用材质。 /// private async void PlayIdleDeferredBossVideoAsync(PlayableDirector director, CancellationToken cancellationToken) { try { ApplyIdleLayerVisibilityBoss(); if (!director) return; director.Play(idleTimeline, DirectorWrapMode.Loop); await WaitPlayIdleFrameAsync(cancellationToken); await WaitPlayIdleFrameAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); if (show) show.SetActive(false); OnReturnedToIdleVisuals(); await WaitPlayIdleFrameAsync(cancellationToken); OnReturnedToIdleVisuals(); } catch (OperationCanceledException) { // 销毁或再次 PlayIdle 时取消 } catch (Exception e) { Debug.LogException(e); } } private void ApplyIdleLayerVisibilityBoss() { if (idle) idle.SetActive(true); if (attack) attack.SetActive(false); if (hit) hit.SetActive(false); if (hit_02) hit_02.SetActive(false); } protected override void OnReturnedToIdleVisuals() { if (!this) return; ApplyIdleLayerVisibilityBoss(); TryActivateSharedMaterialForDecoder(GetDecoderUnder(idle), stopPrevious: true); } /// /// 必须先 TryActivate 再 Apply*:Timeline Activation Track 可能改写显隐,需 Apply 兜底; /// 攻击/受击播前保持 idle 开启遮挡,首帧后再 Apply 关 idle,减轻白闪。 /// protected override void OnAfterDirectorPlayedFirstFrame(PlayableDirector director, PlayableAsset timeline) { TryActivateSharedMaterialForTimeline(timeline); if (showTimeline != null && timeline == showTimeline) { if (show) show.SetActive(true); } else if (attackTimeline != null && timeline == attackTimeline) ApplyAttackLayerState(director); else if (attacked1Timeline != null && timeline == attacked1Timeline) ApplyAttackedLayerState(director, 1); else if (attacked2Timeline != null && timeline == attacked2Timeline) ApplyAttackedLayerState(director, 2); } /// /// 按当前播放的 Timeline 把共用材质绑到对应子物体上的 Decoder(同时只应有一路在播)。 /// private void TryActivateSharedMaterialForTimeline(PlayableAsset timeline) { var hub = SharedMaterialHubOrNull(); if (hub == null || timeline == null) return; var decoder = ResolveDecoderForTimeline(timeline); TryActivateSharedMaterialForDecoder(decoder, stopPrevious: true); } private LeviathanVideoDecoder ResolveDecoderForTimeline(PlayableAsset timeline) { if (showTimeline != null && timeline == showTimeline) return GetDecoderUnder(show); if (idleTimeline != null && timeline == idleTimeline) return GetDecoderUnder(idle); if (attackTimeline != null && timeline == attackTimeline) return GetDecoderUnder(attack); if (attacked1Timeline != null && timeline == attacked1Timeline) return GetDecoderUnder(hit); if (attacked2Timeline != null && timeline == attacked2Timeline) return GetDecoderUnder(hit_02); return null; } private static LeviathanVideoDecoder GetDecoderUnder(GameObject root) { return root ? root.GetComponentInChildren(true) : null; } private void TryActivateSharedMaterialForDecoder(LeviathanVideoDecoder decoder, bool stopPrevious) { var hub = SharedMaterialHubOrNull(); if (hub == null || !decoder) return; hub.ActivateDecoder(decoder, stopPreviousDecoder: stopPrevious); } private void ApplyAttackLayerState(PlayableDirector director) { WarnIfDirectorUnderIdle(director); if (idle) idle.SetActive(false); if (attack) attack.SetActive(true); } private void ApplyAttackedLayerState(PlayableDirector director, int index) { WarnIfDirectorUnderIdle(director); if (idle) idle.SetActive(false); if (attack) attack.SetActive(false); if (hit) hit.SetActive(index == 1); if (hit_02) hit_02.SetActive(index == 2); } private void WarnIfDirectorUnderIdle(PlayableDirector director) { if (!idle || !director) return; if (director.transform.IsChildOf(idle.transform)) Debug.LogWarning( $"{name}: PlayableDirector 在 idle 节点子层级下时,隐藏 idle 会导致 Director 停掉。请把 Director 挂到 idle/attack/hit 的公共父物体上。", this); } public EventBossFightShakeData BossAttackShakeData => bossAttackShakeData; public GameObject PlayerAttackedFx => playerAttackedFx; public async Task PlayTimelineAttack(PlayableDirector bossDirector) { WarnIfDirectorUnderIdle(bossDirector); if (hit) hit.SetActive(false); if (hit_02) hit_02.SetActive(false); if (attack) attack.SetActive(true); // 不提前关 idle,首帧内 Timeline 解码攻击层后由 OnAfterDirectorPlayedFirstFrame 里 ApplyAttackLayerState 关 idle await PlayTimeline(bossDirector, attackTimeline); } public async Task PlayTimelineAttacked(PlayableDirector bossDirector, int index) { _ = PlayFlash(); WarnIfDirectorUnderIdle(bossDirector); if (attack) attack.SetActive(false); if (hit) hit.SetActive(index == 1); if (hit_02) hit_02.SetActive(index == 2); // 同上,由 OnAfterDirectorPlayedFirstFrame 里 ApplyAttackedLayerState 关 idle 并兜底显隐 if (index == 1) await PlayTimeline(bossDirector, attacked1Timeline); else await PlayTimeline(bossDirector, attacked2Timeline); } public override async Task PlayTimelineDeath(PlayableDirector director) { if (attack) attack.SetActive(false); if (hit) hit.SetActive(false); if (hit_02) hit_02.SetActive(false); await base.PlayTimelineDeath(director); } private async Task PlayFlash() { var graphic = flashTarget ? flashTarget : GetComponentInChildren(true); if (graphic != null && graphic.material != null && graphic.material.HasProperty(FillPhaseId)) { graphic.material.SetFloat(FillPhaseId, bossAttackedFillPhase); await Awaiters.Seconds(bossAttackedRevertTime); graphic.material.SetFloat(FillPhaseId, 0); } } public void InitFlash() { var graphic = flashTarget ? flashTarget : GetComponentInChildren(true); if (graphic != null && graphic.material != null && graphic.material.HasProperty(FillPhaseId)) graphic.material.SetFloat(FillPhaseId, 0); } /// /// 受击点: 会钳制到挂点数组范围内;数组为空或该槽为空时回退 。 /// public RectTransform GetHurtAnchor(int hurtIndex) { if (bossHurtAnchors == null || bossHurtAnchors.Length == 0) return bossFollowAnchor; var i = Mathf.Clamp(hurtIndex, 0, bossHurtAnchors.Length - 1); return bossHurtAnchors[i] ? bossHurtAnchors[i] : bossFollowAnchor; } } }