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;
}
}
}