Files
ft/Client/Assets/Scripts/EventBossFight/Panel/MainVideo/EventBossFightTimelineBossCellVideo.cs
2026-06-29 21:18:33 +08:00

392 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>运行时在本物体上自动添加;默认 ChromaKey须与各 Decoder 的 Alpha Type 一致。</summary>
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);
}
/// <summary>
/// 与面板 <c>enter</c> Director 并行:先播本 Cell 的 <see cref="showTimeline"/>(若有),再 <see cref="PlayIdle"/>。
/// </summary>
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);
}
/// <summary>
/// 无 <see cref="PlayableDirector"/> 时(如复盘):激活 idle 层,对 idle 子层级内 <see cref="LeviathanVideoDecoder"/> / <see cref="ZZVideoRawImageDecoder"/> 循环播放 <c>bytesFile</c>。
/// </summary>
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();
}
/// <summary>
/// 在本物体上获取或添加 <see cref="LeviathanVideoSharedMaterialController"/>。
/// </summary>
private void EnsureSharedMaterialHub()
{
if (_sharedMaterialHub != null)
return;
if (!this)
return;
_sharedMaterialHub = GetComponent<LeviathanVideoSharedMaterialController>();
if (_sharedMaterialHub == null)
_sharedMaterialHub = gameObject.AddComponent<LeviathanVideoSharedMaterialController>();
}
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);
}
/// <summary>
/// 入场 show→idle先只切层并 Playidle 用本地材质出首帧后再关 show 并挂共用材质。
/// </summary>
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);
}
/// <summary>
/// 必须先 TryActivate 再 Apply*Timeline Activation Track 可能改写显隐,需 Apply 兜底;
/// 攻击/受击播前保持 idle 开启遮挡,首帧后再 Apply 关 idle减轻白闪。
/// </summary>
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);
}
/// <summary>
/// 按当前播放的 Timeline 把共用材质绑到对应子物体上的 Decoder同时只应有一路在播
/// </summary>
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<LeviathanVideoDecoder>(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<RawImage>(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<RawImage>(true);
if (graphic != null && graphic.material != null && graphic.material.HasProperty(FillPhaseId))
graphic.material.SetFloat(FillPhaseId, 0);
}
/// <summary>
/// 受击点:<paramref name="hurtIndex"/> 会钳制到挂点数组范围内;数组为空或该槽为空时回退 <see cref="bossFollowAnchor"/>。
/// </summary>
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;
}
}
}