392 lines
15 KiB
C#
392 lines
15 KiB
C#
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:先只切层并 Play,idle 用本地材质出首帧后再关 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;
|
||
}
|
||
}
|
||
}
|