新Feedback系统
This commit is contained in:
@@ -195,6 +195,11 @@ namespace Cielonos.MainGame.Characters
|
||||
_ => 0
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(funcAnimName))
|
||||
{
|
||||
funcAnimName = GetHitFuncAnimName(breakthroughType, direction);
|
||||
}
|
||||
|
||||
if (CheckBreakthrough(breakthroughType))
|
||||
{
|
||||
if (!animationSc.fullBodyFuncAnimSm.Stop(disruptionType))
|
||||
@@ -225,6 +230,7 @@ namespace Cielonos.MainGame.Characters
|
||||
protected virtual string GetHitFuncAnimName(BreakthroughType breakthroughType, Vector3 direction)
|
||||
{
|
||||
string prefix = "GetHitMedium";
|
||||
|
||||
if (breakthroughType >= BreakthroughType.Medium)
|
||||
{
|
||||
prefix = breakthroughType switch
|
||||
@@ -236,7 +242,7 @@ namespace Cielonos.MainGame.Characters
|
||||
_ => "GetHit"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
string directionStr = "Front";
|
||||
if (direction != default)
|
||||
{
|
||||
@@ -256,6 +262,16 @@ namespace Cielonos.MainGame.Characters
|
||||
}
|
||||
|
||||
string fullName = prefix + directionStr;
|
||||
|
||||
if (!animationSc.fullBodyFuncAnimSm.collection.ContainsKey(fullName))
|
||||
{
|
||||
if (prefix == "GetHitForced")
|
||||
{
|
||||
prefix = "GetHitHeavy"; //如果没有专门的“强制等级”受击动画,就使用“重度等级”的动画
|
||||
fullName = prefix + directionStr;
|
||||
}
|
||||
}
|
||||
|
||||
return fullName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,67 +232,12 @@ namespace Cielonos.MainGame.Characters
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void PlayGetHitAnimation(string getHitAnimPrefix, out float animDuration,
|
||||
Vector3 direction = default, string funcAnimName = "")
|
||||
public virtual void PlayGetHitAnimation(string funcAnimName, out float animDuration,
|
||||
Vector3 direction = default)
|
||||
{
|
||||
bool useCustomFuncAnim = !string.IsNullOrEmpty(funcAnimName);
|
||||
int fullBodyActionIndex = animator.GetLayerIndex("FullBodyAction");
|
||||
|
||||
if (useCustomFuncAnim)
|
||||
{
|
||||
fullBodyFuncAnimSm.Play(funcAnimName);
|
||||
animDuration = fullBodyFuncAnimSm.currentClip.length;
|
||||
return;
|
||||
}
|
||||
|
||||
string getHitAnim = getHitAnimPrefix + "Front";
|
||||
string getHitFrontAnim = getHitAnimPrefix + "Front";
|
||||
float normalizedTransitionDuration = 0.1f / animator.GetCurrentAnimatorStateInfo(fullBodyActionIndex).length;
|
||||
animDuration = 0f;
|
||||
if (animator.HasState(fullBodyActionIndex, Animator.StringToHash(getHitAnim)))
|
||||
{
|
||||
direction.y = 0;
|
||||
direction = direction.normalized;
|
||||
float angle = Vector3.SignedAngle(transform.forward, direction, Vector3.up);
|
||||
|
||||
string directionStr = angle switch
|
||||
{
|
||||
> -45f and <= 45f => "Back",
|
||||
> 45f and <= 135f => "Left",
|
||||
> -135f and <= -45f => "Right",
|
||||
_ => "Front"
|
||||
};
|
||||
|
||||
getHitAnim = getHitAnimPrefix + directionStr;
|
||||
if (direction == default || !animator.HasState(fullBodyActionIndex, Animator.StringToHash(getHitAnim)))
|
||||
{
|
||||
animator.CrossFade(getHitFrontAnim, normalizedTransitionDuration, fullBodyActionIndex, 0);
|
||||
animDuration = mapper.GetClip(getHitFrontAnim).length;
|
||||
}
|
||||
else
|
||||
{
|
||||
animator.CrossFade(getHitAnim, normalizedTransitionDuration, fullBodyActionIndex, 0);
|
||||
animDuration = mapper.GetClip(getHitAnim).length;
|
||||
}
|
||||
}
|
||||
else if (animator.HasState(fullBodyActionIndex, Animator.StringToHash(getHitAnimPrefix)))
|
||||
{
|
||||
getHitAnim = getHitAnimPrefix;
|
||||
animator.CrossFade(getHitAnim, normalizedTransitionDuration, fullBodyActionIndex, 0);
|
||||
animDuration = mapper.GetClip(getHitAnim).length;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (animator.HasState(fullBodyActionIndex, Animator.StringToHash("GetHit")))
|
||||
{
|
||||
animator.CrossFade("GetHit", normalizedTransitionDuration, fullBodyActionIndex, 0);
|
||||
animDuration = mapper.GetClip("GetHit").length;
|
||||
}
|
||||
else
|
||||
{
|
||||
animator.CrossFade("Empty", 0.1f, fullBodyActionIndex, 0);
|
||||
}
|
||||
}
|
||||
fullBodyFuncAnimSm.Play(funcAnimName);
|
||||
animDuration = fullBodyFuncAnimSm.currentData.Interval(IntervalType.ActionDisruption).StartTime;
|
||||
//animDuration = fullBodyFuncAnimSm.currentData.Interval(IntervalType.Active).EndTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Effects.Feedback;
|
||||
using Lean.Pool;
|
||||
using MoreMountains.Feedbacks;
|
||||
using MoreMountains.FeedbacksForThirdParty;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.FeelAssistance;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -9,18 +11,114 @@ namespace Cielonos.MainGame.Characters
|
||||
{
|
||||
public partial class FeedbackSubcontroller : SubcontrollerBase<CharacterBase>
|
||||
{
|
||||
// === 旧系统(Feel)—— 保留向后兼容 ===
|
||||
public Dictionary<string, FeedbackUnit> feedbacks;
|
||||
public FeedbackUnit this[string feedbackName] => feedbacks?.GetValueOrDefault(feedbackName, null);
|
||||
|
||||
// === 新系统(Feedback System)===
|
||||
public FeedbackDataCollection feedbackDataCollection;
|
||||
|
||||
private CharacterFeedbackTimeProvider _timeProvider;
|
||||
private readonly List<FeedbackPlayer> _activePlayers = new List<FeedbackPlayer>(8);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
if (owner != null)
|
||||
{
|
||||
_timeProvider = new CharacterFeedbackTimeProvider(owner);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过新系统播放一个 FeedbackData。
|
||||
/// </summary>
|
||||
public FeedbackPlayer PlayFeedback(FeedbackData data, bool stopPrevious = false)
|
||||
{
|
||||
if (data == null) return null;
|
||||
|
||||
if (stopPrevious)
|
||||
{
|
||||
StopFeedback(data);
|
||||
}
|
||||
|
||||
var player = new FeedbackPlayer(data, _timeProvider, owner?.transform);
|
||||
player.Play();
|
||||
_activePlayers.Add(player);
|
||||
return player;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过名称从 FeedbackDataCollection 中查找并播放。
|
||||
/// </summary>
|
||||
public FeedbackPlayer PlayFeedback(string name, bool stopPrevious = false)
|
||||
{
|
||||
if (feedbackDataCollection == null)
|
||||
{
|
||||
Debug.LogWarning($"[FeedbackSubcontroller] feedbackDataCollection is null on {owner?.name}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!feedbackDataCollection.TryGet(name, out FeedbackData data))
|
||||
{
|
||||
Debug.LogWarning($"[FeedbackSubcontroller] FeedbackData '{name}' not found on {owner?.name}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return PlayFeedback(data, stopPrevious);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止指定 FeedbackData 的所有活跃播放器。
|
||||
/// </summary>
|
||||
public void StopFeedback(FeedbackData data)
|
||||
{
|
||||
for (int i = _activePlayers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_activePlayers[i].Data == data)
|
||||
{
|
||||
_activePlayers[i].Stop();
|
||||
_activePlayers.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止所有活跃的新系统播放器。
|
||||
/// </summary>
|
||||
public void StopAllFeedbacks()
|
||||
{
|
||||
for (int i = _activePlayers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_activePlayers[i].Stop();
|
||||
}
|
||||
_activePlayers.Clear();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (feedbacks == null) return;
|
||||
|
||||
foreach (var feedbackUnit in feedbacks.Values)
|
||||
// 旧系统驱动
|
||||
if (feedbacks != null)
|
||||
{
|
||||
float timeScaleMultiplier = owner.selfTimeSm.TimeScale;
|
||||
feedbackUnit.feedback.ExternalTimeScale = timeScaleMultiplier;
|
||||
feedbackUnit.Update();
|
||||
foreach (var feedbackUnit in feedbacks.Values)
|
||||
{
|
||||
float timeScaleMultiplier = owner.selfTimeSm.TimeScale;
|
||||
feedbackUnit.feedback.ExternalTimeScale = timeScaleMultiplier;
|
||||
feedbackUnit.Update();
|
||||
}
|
||||
}
|
||||
|
||||
// 新系统驱动
|
||||
float dt = Time.unscaledDeltaTime;
|
||||
for (int i = _activePlayers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
FeedbackPlayer player = _activePlayers[i];
|
||||
player.Tick(dt);
|
||||
|
||||
if (player.IsCompleted || !player.IsActive)
|
||||
{
|
||||
_activePlayers.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,10 @@ namespace Cielonos.MainGame.Characters
|
||||
public AnimationClip landWalkForward, landWalkBack, landWalkLeft, landWalkRight;
|
||||
public AnimationClip landRunForward, landRunBack, landRunLeft, landRunRight;
|
||||
public AnimationClip jumpStart;
|
||||
[FormerlySerializedAs("jumpInAir")] public AnimationClip inAir;
|
||||
public AnimationClip inAir;
|
||||
public AnimationClip jumpLand;
|
||||
public AnimationClip getHitMediumFront, getHitMediumBack, getHitMediumLeft, getHitMediumRight;
|
||||
public AnimationClip getHitHeavyFront, getHitHeavyBack, getHitHeavyLeft, getHitHeavyRight;
|
||||
public AnimationClip getHitDisruptionFront, getHitDisruptionBack, getHitDisruptionLeft, getHitDisruptionRight;
|
||||
public FuncAnimData getHitMediumFront, getHitMediumBack, getHitMediumLeft, getHitMediumRight;
|
||||
public FuncAnimData getHitHeavyFront, getHitHeavyBack, getHitHeavyLeft, getHitHeavyRight;
|
||||
public AnimationClip riseUpFront, riseUpBack, riseUpLeft, riseUpRight;
|
||||
public AnimationClip incapacitation, death;
|
||||
|
||||
@@ -44,20 +43,28 @@ namespace Cielonos.MainGame.Characters
|
||||
animatorOverride["InAir"] = inAir;
|
||||
animatorOverride["JumpLand"] = jumpLand;
|
||||
|
||||
animatorOverride["GetHitMediumFront"] = getHitMediumFront;
|
||||
animatorOverride["GetHitMediumBack"] = getHitMediumBack;
|
||||
animatorOverride["GetHitMediumLeft"] = getHitMediumLeft;
|
||||
animatorOverride["GetHitMediumRight"] = getHitMediumRight;
|
||||
animatorOverride["GetHitHeavyFront"] = getHitHeavyFront;
|
||||
animatorOverride["GetHitHeavyBack"] = getHitHeavyBack;
|
||||
animatorOverride["GetHitHeavyLeft"] = getHitHeavyLeft;
|
||||
animatorOverride["GetHitHeavyRight"] = getHitHeavyRight;
|
||||
animatorOverride["GetHitDisruptionFront"] = getHitDisruptionFront;
|
||||
animatorOverride["GetHitDisruptionBack"] = getHitDisruptionBack;
|
||||
animatorOverride["GetHitDisruptionLeft"] = getHitDisruptionLeft;
|
||||
animatorOverride["GetHitDisruptionRight"] = getHitDisruptionRight;
|
||||
animatorOverride["GetHitMediumFront"] = getHitMediumFront.animationClip;
|
||||
animatorOverride["GetHitMediumBack"] = getHitMediumBack.animationClip;
|
||||
animatorOverride["GetHitMediumLeft"] = getHitMediumLeft.animationClip;
|
||||
animatorOverride["GetHitMediumRight"] = getHitMediumRight.animationClip;
|
||||
animatorOverride["GetHitHeavyFront"] = getHitHeavyFront.animationClip;
|
||||
animatorOverride["GetHitHeavyBack"] = getHitHeavyBack.animationClip;
|
||||
animatorOverride["GetHitHeavyLeft"] = getHitHeavyLeft.animationClip;
|
||||
animatorOverride["GetHitHeavyRight"] = getHitHeavyRight.animationClip;
|
||||
|
||||
animatorOverride["Dash"] = dash.animationClip;
|
||||
animatorOverride["Dodge"] = dodge.animationClip;
|
||||
}
|
||||
|
||||
animSc.fullBodyFuncAnimSm.Reset(getHitMediumFront);
|
||||
animSc.fullBodyFuncAnimSm.Reset(getHitMediumBack);
|
||||
animSc.fullBodyFuncAnimSm.Reset(getHitMediumLeft);
|
||||
animSc.fullBodyFuncAnimSm.Reset(getHitMediumRight);
|
||||
animSc.fullBodyFuncAnimSm.Reset(getHitHeavyFront);
|
||||
animSc.fullBodyFuncAnimSm.Reset(getHitHeavyBack);
|
||||
animSc.fullBodyFuncAnimSm.Reset(getHitHeavyLeft);
|
||||
animSc.fullBodyFuncAnimSm.Reset(getHitHeavyRight);
|
||||
|
||||
animSc.fullBodyFuncAnimSm.Reset(dash);
|
||||
animSc.fullBodyFuncAnimSm.Reset(dodge);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,24 @@ namespace Cielonos.MainGame.Characters
|
||||
{
|
||||
base.Initialize();
|
||||
upperBodyFuncAnimSm = new FunctionalAnimationSubmodule(this, "UpperBodyAction");
|
||||
player.operationSc.OnDash += (inputDirection, length) => SetupDash(inputDirection, true, length);
|
||||
player.operationSc.OnDodge += SetupDodge;
|
||||
player.operationSc.OnDash += (inputDirection, length) =>
|
||||
{
|
||||
if (player.statusSm.HasStatus(StatusType.Stun))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetupDash(inputDirection, true, length);
|
||||
};
|
||||
player.operationSc.OnDodge += (length)=>
|
||||
{
|
||||
if (player.statusSm.HasStatus(StatusType.Stun))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetupDodge(length);
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -236,12 +236,18 @@ namespace Cielonos.MainGame.Characters.Inventory.Collections
|
||||
|
||||
public override void OnSpecialCPress()
|
||||
{
|
||||
CharacterBase target = BattleManager.EnemySm.GetNearestEnemy(5);
|
||||
if (functionSm["Block"].IsAvailable() && fullBodyFuncAnimSm.Stop(DisruptionType.ForcedAction) &&
|
||||
PlayTargetedAnimation("Block", target, 2f, false, true, upperBodyFuncAnimSm, 1f, 0.1f, true))
|
||||
comboSm.main.Reset();
|
||||
SetBlock();
|
||||
|
||||
if (!player.statusSm.HasStatus(StatusType.Stun))
|
||||
{
|
||||
comboSm.main.Reset();
|
||||
SetBlock();
|
||||
CharacterBase target = BattleManager.EnemySm.GetNearestEnemy(5);
|
||||
if (functionSm["Block"].IsAvailable() &&
|
||||
fullBodyFuncAnimSm.Stop(DisruptionType.ForcedAction) &&
|
||||
PlayTargetedAnimation("Block", target, 2f, false, true, upperBodyFuncAnimSm, 1f, 0.1f, true))
|
||||
{
|
||||
feedbackSc.PlayFeedback("TestFeedBack");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cielonos.MainGame.Effects.Feedback;
|
||||
using MoreMountains.Feedbacks;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.FeelAssistance;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -8,16 +10,115 @@ namespace Cielonos.MainGame.Characters.Inventory
|
||||
{
|
||||
public class FeedbackSubcontroller : SubcontrollerBase<ItemBase>
|
||||
{
|
||||
// === 旧系统(Feel)—— 保留向后兼容 ===
|
||||
public Dictionary<string, FeedbackUnit> feedbacks;
|
||||
public FeedbackUnit this[string feedbackName] => feedbacks.GetValueOrDefault(feedbackName, null);
|
||||
|
||||
// === 新系统(Feedback System)===
|
||||
public FeedbackDataCollection feedbackDataCollection;
|
||||
|
||||
private CharacterFeedbackTimeProvider _timeProvider;
|
||||
private readonly List<FeedbackPlayer> _activePlayers = new List<FeedbackPlayer>(8);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
if (owner?.player != null)
|
||||
{
|
||||
_timeProvider = new CharacterFeedbackTimeProvider(owner.player);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过新系统播放一个 FeedbackData。
|
||||
/// </summary>
|
||||
public FeedbackPlayer PlayFeedback(FeedbackData data, bool stopPrevious = false)
|
||||
{
|
||||
if (data == null) return null;
|
||||
|
||||
if (stopPrevious)
|
||||
{
|
||||
StopFeedback(data);
|
||||
}
|
||||
|
||||
Transform ownerTransform = owner?.player?.transform;
|
||||
var player = new FeedbackPlayer(data, _timeProvider, ownerTransform);
|
||||
player.Play();
|
||||
_activePlayers.Add(player);
|
||||
return player;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过名称从 FeedbackDataCollection 中查找并播放。
|
||||
/// </summary>
|
||||
public FeedbackPlayer PlayFeedback(string name, bool stopPrevious = false)
|
||||
{
|
||||
if (feedbackDataCollection == null)
|
||||
{
|
||||
Debug.LogWarning($"[Item.FeedbackSubcontroller] feedbackDataCollection is null on {owner?.name}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!feedbackDataCollection.TryGet(name, out FeedbackData data))
|
||||
{
|
||||
Debug.LogWarning($"[Item.FeedbackSubcontroller] FeedbackData '{name}' not found on {owner?.name}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return PlayFeedback(data, stopPrevious);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止指定 FeedbackData 的所有活跃播放器。
|
||||
/// </summary>
|
||||
public void StopFeedback(FeedbackData data)
|
||||
{
|
||||
for (int i = _activePlayers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_activePlayers[i].Data == data)
|
||||
{
|
||||
_activePlayers[i].Stop();
|
||||
_activePlayers.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止所有活跃的新系统播放器。
|
||||
/// </summary>
|
||||
public void StopAllFeedbacks()
|
||||
{
|
||||
for (int i = _activePlayers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_activePlayers[i].Stop();
|
||||
}
|
||||
_activePlayers.Clear();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
foreach (var feedbackUnit in feedbacks.Values)
|
||||
// 旧系统驱动
|
||||
if (feedbacks != null)
|
||||
{
|
||||
float timeScaleMultiplier = owner.player.selfTimeSm.TimeScale;
|
||||
feedbackUnit.feedback.ExternalTimeScale = timeScaleMultiplier;
|
||||
feedbackUnit.Update();
|
||||
foreach (var feedbackUnit in feedbacks.Values)
|
||||
{
|
||||
float timeScaleMultiplier = owner.player.selfTimeSm.TimeScale;
|
||||
feedbackUnit.feedback.ExternalTimeScale = timeScaleMultiplier;
|
||||
feedbackUnit.Update();
|
||||
}
|
||||
}
|
||||
|
||||
// 新系统驱动
|
||||
float dt = Time.unscaledDeltaTime;
|
||||
for (int i = _activePlayers.Count - 1; i >= 0; i--)
|
||||
{
|
||||
FeedbackPlayer player = _activePlayers[i];
|
||||
player.Tick(dt);
|
||||
|
||||
if (player.IsCompleted || !player.IsActive)
|
||||
{
|
||||
_activePlayers.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ namespace Cielonos.MainGame.Characters
|
||||
{
|
||||
public partial class Player : CharacterBase
|
||||
{
|
||||
public ICharacterBuffFactory buffFactory;
|
||||
|
||||
[TitleGroup("Data")]
|
||||
public AttributeData globalAttributeData;
|
||||
[TitleGroup("Data")]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 484bc3a7a27bb1945ba1d6c8e2fcea5c
|
||||
guid: 36f55143d14ca3747bbd595e7c6fe163
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
8
Assets/Scripts/MainGame/Effects/Feedbacks/Actions.meta
Normal file
8
Assets/Scripts/MainGame/Effects/Feedbacks/Actions.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf419471c4b289149b78527c147b27f9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 摄像机方向影响设置,嵌入到摄像机类 Action 中。
|
||||
/// 控制最终偏移/振幅是否受摄像机方向和角色朝向影响。
|
||||
/// 此类为可扩展设计:新增字段不会导致已有序列化数据重置。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class CameraDirectionSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否将偏移从本地空间转换到摄像机方向空间。
|
||||
/// 开启后,定义的振幅向量会根据摄像机的朝向进行旋转。
|
||||
/// </summary>
|
||||
[LabelText("Affected by Camera Direction")]
|
||||
[Tooltip("将偏移从本地空间转换到摄像机方向空间")]
|
||||
public bool affectedByCameraDirection;
|
||||
|
||||
/// <summary>
|
||||
/// 是否将偏移从本地空间转换到角色朝向空间。
|
||||
/// 开启后,定义的振幅向量会根据 owner(角色)的 forward 进行旋转。
|
||||
/// </summary>
|
||||
[LabelText("Affected by Character Direction")]
|
||||
[Tooltip("将偏移从本地空间转换到角色朝向空间")]
|
||||
public bool affectedByCharacterDirection;
|
||||
|
||||
// === 以下区域留给未来扩展 ===
|
||||
// 新增字段时请在此区域添加,并提供合理的默认值,
|
||||
// 以确保已有序列化资产不会被重置。
|
||||
// 例如:
|
||||
// public bool affectedByMovementDirection;
|
||||
// public float directionBlendFactor = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// 将给定的本地空间向量根据当前设置转换到世界空间。
|
||||
/// 如果两个方向都开启,角色方向优先。
|
||||
/// </summary>
|
||||
/// <param name="localAmplitude">本地空间下的振幅向量</param>
|
||||
/// <param name="ownerTransform">角色 Transform(可能为 null)</param>
|
||||
/// <returns>经方向变换后的振幅向量</returns>
|
||||
public Vector3 TransformAmplitude(Vector3 localAmplitude, Transform ownerTransform)
|
||||
{
|
||||
if (affectedByCharacterDirection && ownerTransform != null)
|
||||
{
|
||||
return ownerTransform.TransformDirection(localAmplitude);
|
||||
}
|
||||
|
||||
if (affectedByCameraDirection)
|
||||
{
|
||||
Camera mainCamera = Camera.main;
|
||||
if (mainCamera != null)
|
||||
{
|
||||
return mainCamera.transform.TransformDirection(localAmplitude);
|
||||
}
|
||||
}
|
||||
|
||||
return localAmplitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cbd6096fecc72b7448c9314ed8140726
|
||||
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using MoreMountains.Feedbacks;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.Feedback;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 摄像机位移震动反馈,通过 MMCinemachinePositionShakeEvent 触发现有的 Shaker。
|
||||
/// Shaker 负责处理多个震动的叠加混合。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class CameraPositionShakeAction : FeedbackActionBase
|
||||
{
|
||||
public override string DisplayName => "Camera Position Shake";
|
||||
|
||||
/// <summary>
|
||||
/// 震动曲线,定义震动强度随时间的变化。
|
||||
/// </summary>
|
||||
[Title("Position Shake")]
|
||||
[LabelText("Shake Curve")]
|
||||
public AnimationCurve shakeCurve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f),
|
||||
new Keyframe(0.2f, 1f),
|
||||
new Keyframe(1f, 0f)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 最大位移振幅(本地空间)。
|
||||
/// </summary>
|
||||
[LabelText("Amplitude")]
|
||||
public Vector3 positionAmplitude = new Vector3(0.5f, 0.5f, 0f);
|
||||
|
||||
/// <summary>
|
||||
/// 方向影响设置。
|
||||
/// </summary>
|
||||
[Title("Direction")]
|
||||
public CameraDirectionSettings directionSettings = new CameraDirectionSettings();
|
||||
|
||||
/// <summary>
|
||||
/// 距离衰减:根据摄像机与 owner 的距离衰减震动强度。
|
||||
/// </summary>
|
||||
[Title("Distance Attenuation")]
|
||||
[LabelText("Use Attenuation")]
|
||||
public bool useAttenuation;
|
||||
|
||||
/// <summary>
|
||||
/// 全强度的最大距离。
|
||||
/// </summary>
|
||||
[ShowIf("useAttenuation")]
|
||||
[LabelText("Attenuation Range")]
|
||||
public float attenuationRange = 50f;
|
||||
|
||||
/// <summary>
|
||||
/// 距离-强度衰减曲线(0=近处/全强度,1=远处/无强度)。
|
||||
/// </summary>
|
||||
[ShowIf("useAttenuation")]
|
||||
[LabelText("Attenuation Curve")]
|
||||
public AnimationCurve attenuationCurve = new AnimationCurve(
|
||||
new Keyframe(0f, 1f),
|
||||
new Keyframe(1f, 0f)
|
||||
);
|
||||
|
||||
public override void OnStart(FeedbackContext context)
|
||||
{
|
||||
Vector3 finalAmplitude = directionSettings.TransformAmplitude(positionAmplitude, context.owner);
|
||||
float intensityMultiplier = ComputeAttenuation(context);
|
||||
|
||||
MMCinemachinePositionShakeEvent.Trigger(
|
||||
null,
|
||||
shakeCurve,
|
||||
context.duration,
|
||||
finalAmplitude,
|
||||
intensityMultiplier
|
||||
);
|
||||
}
|
||||
|
||||
public override void OnInterrupt(FeedbackContext context)
|
||||
{
|
||||
MMCinemachinePositionShakeEvent.Trigger(
|
||||
null, shakeCurve, 0f, Vector3.zero, 0f,
|
||||
stop: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算距离衰减系数。
|
||||
/// </summary>
|
||||
private float ComputeAttenuation(FeedbackContext context)
|
||||
{
|
||||
if (!useAttenuation || context.owner == null) return 1f;
|
||||
|
||||
Camera mainCamera = Camera.main;
|
||||
if (mainCamera == null) return 1f;
|
||||
|
||||
float distance = Vector3.Distance(context.owner.position, mainCamera.transform.position);
|
||||
float normalizedDistance = Mathf.Clamp01(distance / attenuationRange);
|
||||
return attenuationCurve.Evaluate(normalizedDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8630ea3ff64b8914191a50988d94d665
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using MoreMountains.FeedbacksForThirdParty;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.Feedback;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 摄像机旋转震动反馈,通过 MMCinemachineRotationShakeEvent 触发现有的 Shaker。
|
||||
/// X/Y 作用于 FollowTarget 旋转,Z 作用于 Dutch 倾斜。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class CameraRotationShakeAction : FeedbackActionBase
|
||||
{
|
||||
public override string DisplayName => "Camera Rotation Shake";
|
||||
|
||||
/// <summary>
|
||||
/// 震动曲线,定义震动强度随时间的变化。
|
||||
/// </summary>
|
||||
[Title("Rotation Shake")]
|
||||
[LabelText("Shake Curve")]
|
||||
public AnimationCurve shakeCurve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f),
|
||||
new Keyframe(0.2f, 1f),
|
||||
new Keyframe(1f, 0f)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 最大旋转角度振幅(度)。X/Y -> FollowTarget, Z -> Dutch。
|
||||
/// </summary>
|
||||
[LabelText("Rotation Amplitude")]
|
||||
public Vector3 rotationAmplitude = new Vector3(2f, 2f, 5f);
|
||||
|
||||
/// <summary>
|
||||
/// 方向影响设置。
|
||||
/// </summary>
|
||||
[Title("Direction")]
|
||||
public CameraDirectionSettings directionSettings = new CameraDirectionSettings();
|
||||
|
||||
/// <summary>
|
||||
/// 距离衰减。
|
||||
/// </summary>
|
||||
[Title("Distance Attenuation")]
|
||||
[LabelText("Use Attenuation")]
|
||||
public bool useAttenuation;
|
||||
|
||||
[ShowIf("useAttenuation")]
|
||||
[LabelText("Attenuation Range")]
|
||||
public float attenuationRange = 50f;
|
||||
|
||||
[ShowIf("useAttenuation")]
|
||||
[LabelText("Attenuation Curve")]
|
||||
public AnimationCurve attenuationCurve = new AnimationCurve(
|
||||
new Keyframe(0f, 1f),
|
||||
new Keyframe(1f, 0f)
|
||||
);
|
||||
|
||||
public override void OnStart(FeedbackContext context)
|
||||
{
|
||||
Vector3 finalAmplitude = directionSettings.TransformAmplitude(rotationAmplitude, context.owner);
|
||||
float intensityMultiplier = ComputeAttenuation(context);
|
||||
|
||||
MMCinemachineRotationShakeEvent.Trigger(
|
||||
null,
|
||||
shakeCurve,
|
||||
context.duration,
|
||||
finalAmplitude,
|
||||
0f, 1f, false,
|
||||
intensityMultiplier
|
||||
);
|
||||
}
|
||||
|
||||
public override void OnInterrupt(FeedbackContext context)
|
||||
{
|
||||
MMCinemachineRotationShakeEvent.Trigger(
|
||||
null, shakeCurve, 0f, Vector3.zero,
|
||||
0f, 1f, false,
|
||||
stop: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算距离衰减系数。
|
||||
/// </summary>
|
||||
private float ComputeAttenuation(FeedbackContext context)
|
||||
{
|
||||
if (!useAttenuation || context.owner == null) return 1f;
|
||||
|
||||
Camera mainCamera = Camera.main;
|
||||
if (mainCamera == null) return 1f;
|
||||
|
||||
float distance = Vector3.Distance(context.owner.position, mainCamera.transform.position);
|
||||
float normalizedDistance = Mathf.Clamp01(distance / attenuationRange);
|
||||
return attenuationCurve.Evaluate(normalizedDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33f1efd4ae9710e46bcd50df76f19c8e
|
||||
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using Cielonos;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.Rendering.PostProcessing;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 高级色散反馈动作,通过 PostProcessingManager 驱动 AdvancedChromaticAberration Volume 参数。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ChromaticAberrationAction : CurveShakeAction
|
||||
{
|
||||
public override string DisplayName => "Chromatic Aberration";
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时修改中心点。
|
||||
/// </summary>
|
||||
[Title("Chromatic Aberration Settings")]
|
||||
[LabelText("Modify Center")]
|
||||
public bool modifyCenter;
|
||||
|
||||
[ShowIf("modifyCenter")]
|
||||
[LabelText("Center")]
|
||||
public Vector2 center = new Vector2(0.5f, 0.5f);
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时修改抖动强度。
|
||||
/// </summary>
|
||||
[LabelText("Modify Jitter")]
|
||||
public bool modifyJitter;
|
||||
|
||||
[ShowIf("modifyJitter")]
|
||||
[LabelText("Jitter Curve")]
|
||||
public AnimationCurve jitterCurve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f),
|
||||
new Keyframe(0.5f, 1f),
|
||||
new Keyframe(1f, 0f)
|
||||
);
|
||||
|
||||
[ShowIf("modifyJitter")]
|
||||
[LabelText("Jitter Remap Min")]
|
||||
public float jitterRemapMin;
|
||||
|
||||
[ShowIf("modifyJitter")]
|
||||
[LabelText("Jitter Remap Max")]
|
||||
public float jitterRemapMax = 0.5f;
|
||||
|
||||
[NonSerialized] private AdvancedChromaticAberration _aca;
|
||||
[NonSerialized] private float _initialIntensity;
|
||||
[NonSerialized] private Vector2 _initialCenter;
|
||||
[NonSerialized] private float _initialJitter;
|
||||
[NonSerialized] private bool _resolved;
|
||||
|
||||
public override void OnStart(FeedbackContext context)
|
||||
{
|
||||
_resolved = TryResolveComponent();
|
||||
if (!_resolved) return;
|
||||
|
||||
_initialIntensity = _aca.intensity.value;
|
||||
_initialCenter = _aca.center.value;
|
||||
_initialJitter = _aca.jitterIntensity.value;
|
||||
|
||||
if (modifyCenter)
|
||||
{
|
||||
_aca.center.value = center;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnUpdate(FeedbackContext context, float normalizedTime)
|
||||
{
|
||||
if (!_resolved) return;
|
||||
|
||||
float newIntensity = EvaluateShake(normalizedTime, _initialIntensity);
|
||||
_aca.intensity.value = newIntensity;
|
||||
|
||||
if (modifyJitter)
|
||||
{
|
||||
float jitterValue = jitterCurve.Evaluate(normalizedTime);
|
||||
float mappedJitter = Mathf.LerpUnclamped(jitterRemapMin, jitterRemapMax, jitterValue);
|
||||
_aca.jitterIntensity.value = relativeToInitial ? _initialJitter + mappedJitter : mappedJitter;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnEnd(FeedbackContext context)
|
||||
{
|
||||
RestoreValues();
|
||||
}
|
||||
|
||||
public override void OnInterrupt(FeedbackContext context)
|
||||
{
|
||||
RestoreValues();
|
||||
}
|
||||
|
||||
private bool TryResolveComponent()
|
||||
{
|
||||
if (_aca != null) return true;
|
||||
|
||||
if (PostProcessingManager.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[ChromaticAberrationAction] PostProcessingManager instance not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PostProcessingManager.Instance.GetVolumeComponent(out _aca))
|
||||
{
|
||||
Debug.LogWarning("[ChromaticAberrationAction] AdvancedChromaticAberration not found in Volume Profile.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RestoreValues()
|
||||
{
|
||||
if (!_resolved) return;
|
||||
|
||||
_aca.intensity.value = _initialIntensity;
|
||||
_aca.center.value = _initialCenter;
|
||||
_aca.jitterIntensity.value = _initialJitter;
|
||||
_resolved = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e0386d39ab6252478e73744f7b3b20b
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.Feedback;
|
||||
using Unity.Cinemachine;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// Cinemachine Impulse 反馈,通过 CinemachineImpulseDefinition 直接创建脉冲事件。
|
||||
/// 需要场景中 Cinemachine Camera 上有 CinemachineImpulseListener 组件。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class CinemachineImpulseAction : FeedbackActionBase
|
||||
{
|
||||
public override string DisplayName => "Cinemachine Impulse";
|
||||
|
||||
/// <summary>
|
||||
/// Impulse 定义,包含信号形状、衰减模式、持续时间等。
|
||||
/// </summary>
|
||||
[Title("Impulse Settings")]
|
||||
public CinemachineImpulseDefinition impulseDefinition = new CinemachineImpulseDefinition();
|
||||
|
||||
/// <summary>
|
||||
/// 脉冲速度向量。
|
||||
/// </summary>
|
||||
[LabelText("Velocity")]
|
||||
public Vector3 velocity = new Vector3(5f, 5f, 5f);
|
||||
|
||||
/// <summary>
|
||||
/// Stop 时是否清除所有 impulse。
|
||||
/// </summary>
|
||||
[LabelText("Clear Impulse on Stop")]
|
||||
public bool clearImpulseOnStop;
|
||||
|
||||
/// <summary>
|
||||
/// 方向影响设置。
|
||||
/// </summary>
|
||||
[Title("Direction")]
|
||||
public CameraDirectionSettings directionSettings = new CameraDirectionSettings();
|
||||
|
||||
public override void OnStart(FeedbackContext context)
|
||||
{
|
||||
Vector3 finalVelocity = directionSettings.TransformAmplitude(velocity, context.owner);
|
||||
Vector3 position = context.owner != null ? context.owner.position : Vector3.zero;
|
||||
|
||||
CinemachineImpulseManager.Instance.IgnoreTimeScale = true;
|
||||
impulseDefinition.CreateEvent(position, finalVelocity);
|
||||
}
|
||||
|
||||
public override void OnInterrupt(FeedbackContext context)
|
||||
{
|
||||
if (clearImpulseOnStop)
|
||||
{
|
||||
CinemachineImpulseManager.Instance.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e30410247dced6409fff042f9c8828a
|
||||
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using Cielonos;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.Rendering.PostProcessing;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 径向模糊反馈动作,通过 PostProcessingManager 驱动 RadialBlur Volume 参数。
|
||||
/// 继承 CurveShakeAction 获得曲线采样和初始值管理能力。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class RadialBlurAction : CurveShakeAction
|
||||
{
|
||||
public override string DisplayName => "Radial Blur";
|
||||
|
||||
/// <summary>
|
||||
/// 是否修改模糊中心点。关闭时保持 Volume 当前设置(通常为 0.5, 0.5)。
|
||||
/// </summary>
|
||||
[Title("Radial Blur Settings")]
|
||||
[LabelText("Modify Center")]
|
||||
public bool modifyCenter;
|
||||
|
||||
/// <summary>
|
||||
/// 模糊中心的屏幕坐标 (0-1)。(0.5, 0.5) 为屏幕正中心。
|
||||
/// </summary>
|
||||
[ShowIf("modifyCenter")]
|
||||
[LabelText("Center")]
|
||||
public Vector2 center = new Vector2(0.5f, 0.5f);
|
||||
|
||||
// 运行时缓存
|
||||
[NonSerialized] private RadialBlur _radialBlur;
|
||||
[NonSerialized] private float _initialBlurRadius;
|
||||
[NonSerialized] private float _initialCenterX;
|
||||
[NonSerialized] private float _initialCenterY;
|
||||
[NonSerialized] private bool _resolved;
|
||||
|
||||
public override void OnStart(FeedbackContext context)
|
||||
{
|
||||
_resolved = TryResolveComponent();
|
||||
if (!_resolved) return;
|
||||
|
||||
// 记录初始值用于复位
|
||||
_initialBlurRadius = _radialBlur.blurRadius.value;
|
||||
_initialCenterX = _radialBlur.radialCenterX.value;
|
||||
_initialCenterY = _radialBlur.radialCenterY.value;
|
||||
|
||||
// 设置中心点(整个 Clip 期间保持不变)
|
||||
if (modifyCenter)
|
||||
{
|
||||
_radialBlur.radialCenterX.value = center.x;
|
||||
_radialBlur.radialCenterY.value = center.y;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnUpdate(FeedbackContext context, float normalizedTime)
|
||||
{
|
||||
if (!_resolved) return;
|
||||
|
||||
float newRadius = EvaluateShake(normalizedTime, _initialBlurRadius);
|
||||
_radialBlur.blurRadius.value = newRadius;
|
||||
}
|
||||
|
||||
public override void OnEnd(FeedbackContext context)
|
||||
{
|
||||
RestoreValues();
|
||||
}
|
||||
|
||||
public override void OnInterrupt(FeedbackContext context)
|
||||
{
|
||||
RestoreValues();
|
||||
}
|
||||
|
||||
public override bool Validate(out string error)
|
||||
{
|
||||
if (PostProcessingManager.Instance == null)
|
||||
{
|
||||
error = "PostProcessingManager instance not found in scene.";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从 PostProcessingManager 获取 RadialBlur Volume 组件。
|
||||
/// </summary>
|
||||
private bool TryResolveComponent()
|
||||
{
|
||||
if (_radialBlur != null) return true;
|
||||
|
||||
if (PostProcessingManager.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[RadialBlurAction] PostProcessingManager instance not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PostProcessingManager.Instance.GetVolumeComponent(out _radialBlur))
|
||||
{
|
||||
Debug.LogWarning("[RadialBlurAction] RadialBlur component not found in Volume Profile.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复到 OnStart 时记录的初始值。
|
||||
/// </summary>
|
||||
private void RestoreValues()
|
||||
{
|
||||
if (!_resolved) return;
|
||||
|
||||
_radialBlur.blurRadius.value = _initialBlurRadius;
|
||||
_radialBlur.radialCenterX.value = _initialCenterX;
|
||||
_radialBlur.radialCenterY.value = _initialCenterY;
|
||||
_resolved = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5be6f051b57e9a43ae22b286bc29c95
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using Cielonos;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.Rendering.PostProcessing;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 黑白闪反馈动作,在 Clip 持续时间内开启 StrobeFlash 的 AutoFlash,
|
||||
/// Clip 结束或被打断时自动关闭。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class StrobeFlashAction : FeedbackActionBase
|
||||
{
|
||||
public override string DisplayName => "Strobe Flash";
|
||||
|
||||
/// <summary>
|
||||
/// 是否修改频率和颜色参数。
|
||||
/// </summary>
|
||||
[Title("Strobe Settings")]
|
||||
[LabelText("Modify Extra")]
|
||||
public bool modifyExtra;
|
||||
|
||||
[ShowIf("modifyExtra")]
|
||||
[LabelText("Frequency")]
|
||||
public float frequency = 15f;
|
||||
|
||||
[ShowIf("modifyExtra")]
|
||||
[LabelText("Color High")]
|
||||
public Color colorHigh = Color.white;
|
||||
|
||||
[ShowIf("modifyExtra")]
|
||||
[LabelText("Color Low")]
|
||||
public Color colorLow = Color.black;
|
||||
|
||||
[NonSerialized] private StrobeFlash _strobeFlash;
|
||||
[NonSerialized] private bool _initialEnable;
|
||||
[NonSerialized] private bool _initialAutoFlash;
|
||||
[NonSerialized] private float _initialFrequency;
|
||||
[NonSerialized] private Color _initialColorHigh;
|
||||
[NonSerialized] private Color _initialColorLow;
|
||||
[NonSerialized] private bool _resolved;
|
||||
|
||||
public override void OnStart(FeedbackContext context)
|
||||
{
|
||||
_resolved = TryResolveComponent();
|
||||
if (!_resolved) return;
|
||||
|
||||
_initialEnable = _strobeFlash.enableEffect.value;
|
||||
_initialAutoFlash = _strobeFlash.autoFlash.value;
|
||||
_initialFrequency = _strobeFlash.frequency.value;
|
||||
_initialColorHigh = _strobeFlash.colorHigh.value;
|
||||
_initialColorLow = _strobeFlash.colorLow.value;
|
||||
|
||||
_strobeFlash.enableEffect.value = true;
|
||||
_strobeFlash.autoFlash.value = true;
|
||||
|
||||
if (modifyExtra)
|
||||
{
|
||||
_strobeFlash.frequency.value = frequency;
|
||||
_strobeFlash.colorHigh.value = colorHigh;
|
||||
_strobeFlash.colorLow.value = colorLow;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnUpdate(FeedbackContext context, float normalizedTime)
|
||||
{
|
||||
// StrobeFlash 由 Shader 内部的 _Time 驱动自动闪烁,
|
||||
// Action 只负责开关控制,不需要每帧更新。
|
||||
}
|
||||
|
||||
public override void OnEnd(FeedbackContext context)
|
||||
{
|
||||
RestoreValues();
|
||||
}
|
||||
|
||||
public override void OnInterrupt(FeedbackContext context)
|
||||
{
|
||||
RestoreValues();
|
||||
}
|
||||
|
||||
private bool TryResolveComponent()
|
||||
{
|
||||
if (_strobeFlash != null) return true;
|
||||
|
||||
if (PostProcessingManager.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[StrobeFlashAction] PostProcessingManager instance not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PostProcessingManager.Instance.GetVolumeComponent(out _strobeFlash))
|
||||
{
|
||||
Debug.LogWarning("[StrobeFlashAction] StrobeFlash not found in Volume Profile.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RestoreValues()
|
||||
{
|
||||
if (!_resolved) return;
|
||||
|
||||
_strobeFlash.enableEffect.value = _initialEnable;
|
||||
_strobeFlash.autoFlash.value = _initialAutoFlash;
|
||||
_strobeFlash.frequency.value = _initialFrequency;
|
||||
_strobeFlash.colorHigh.value = _initialColorHigh;
|
||||
_strobeFlash.colorLow.value = _initialColorLow;
|
||||
_resolved = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24d976dc110cab94db2c00346fe0ebc6
|
||||
@@ -0,0 +1,208 @@
|
||||
using System;
|
||||
using Cielonos.MainGame;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.Feedback;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 时间缩放通道的工作模式。
|
||||
/// </summary>
|
||||
public enum TimeScaleMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 固定值模式:在 Clip 期间将时间缩放设为固定值。
|
||||
/// </summary>
|
||||
Fixed,
|
||||
|
||||
/// <summary>
|
||||
/// 动态曲线模式:根据曲线和 Remap 驱动时间缩放。
|
||||
/// </summary>
|
||||
Dynamic
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个时间缩放通道的配置。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class TimeScaleChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否激活此通道。
|
||||
/// </summary>
|
||||
public bool active;
|
||||
|
||||
/// <summary>
|
||||
/// 通道工作模式。
|
||||
/// </summary>
|
||||
[ShowIf("active")]
|
||||
public TimeScaleMode mode = TimeScaleMode.Fixed;
|
||||
|
||||
/// <summary>
|
||||
/// Fixed 模式下的目标值。
|
||||
/// </summary>
|
||||
[ShowIf("@active && mode == TimeScaleMode.Fixed")]
|
||||
[LabelText("Fixed Value")]
|
||||
public float fixedValue;
|
||||
|
||||
/// <summary>
|
||||
/// Dynamic 模式下的变化曲线。
|
||||
/// </summary>
|
||||
[ShowIf("@active && mode == TimeScaleMode.Dynamic")]
|
||||
[LabelText("Curve")]
|
||||
public AnimationCurve curve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f),
|
||||
new Keyframe(0.5f, 1f),
|
||||
new Keyframe(1f, 0f)
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 曲线值 0 映射到的实际值。
|
||||
/// </summary>
|
||||
[ShowIf("@active && mode == TimeScaleMode.Dynamic")]
|
||||
[LabelText("Remap Zero")]
|
||||
public float remapZero;
|
||||
|
||||
/// <summary>
|
||||
/// 曲线值 1 映射到的实际值。
|
||||
/// </summary>
|
||||
[ShowIf("@active && mode == TimeScaleMode.Dynamic")]
|
||||
[LabelText("Remap One")]
|
||||
public float remapOne = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// 根据归一化进度计算当前通道的时间缩放值。
|
||||
/// </summary>
|
||||
public float Evaluate(float normalizedTime)
|
||||
{
|
||||
if (!active) return 1f;
|
||||
|
||||
if (mode == TimeScaleMode.Fixed)
|
||||
{
|
||||
return fixedValue;
|
||||
}
|
||||
|
||||
float curveValue = curve.Evaluate(normalizedTime);
|
||||
return Mathf.LerpUnclamped(remapZero, remapOne, curveValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 时间缩放修改器反馈,直接驱动 TimeManager 的各个通道。
|
||||
///
|
||||
/// 重要:此 Action 只应使用游戏的 unscaledDeltaTime 驱动。
|
||||
/// 不要在包含此 Action 的 Clip 上启用自定义 overrideTimeSettings,
|
||||
/// FeedbackData 的 defaultTimeSettings.useTimeScale 也应保持为 false。
|
||||
/// 我们的自定义时间参数绝不能影响时间缩放修改器本身。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class TimeScaleModifierAction : FeedbackActionBase
|
||||
{
|
||||
public override string DisplayName => "Time Scale Modifier";
|
||||
|
||||
[Title("Global Time Scale")]
|
||||
public TimeScaleChannel globalChannel = new TimeScaleChannel { active = true, fixedValue = 0f };
|
||||
|
||||
[Title("Player Time Scale")]
|
||||
public TimeScaleChannel playerChannel = new TimeScaleChannel();
|
||||
|
||||
[Title("Enemy Time Scale")]
|
||||
public TimeScaleChannel enemyChannel = new TimeScaleChannel();
|
||||
|
||||
[Title("Allied Time Scale")]
|
||||
public TimeScaleChannel alliedChannel = new TimeScaleChannel();
|
||||
|
||||
[Title("Non-Player Time Scale")]
|
||||
public TimeScaleChannel nonPlayerChannel = new TimeScaleChannel();
|
||||
|
||||
[NonSerialized] private float _initialGlobal;
|
||||
[NonSerialized] private float _initialPlayer;
|
||||
[NonSerialized] private float _initialEnemy;
|
||||
[NonSerialized] private float _initialAllied;
|
||||
[NonSerialized] private float _initialNonPlayer;
|
||||
|
||||
public override void OnStart(FeedbackContext context)
|
||||
{
|
||||
if (TimeManager.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[TimeScaleModifierAction] TimeManager instance not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
_initialGlobal = TimeManager.Instance.globalTimeScale.Value;
|
||||
_initialPlayer = TimeManager.Instance.playerTimeScale.Value;
|
||||
_initialEnemy = TimeManager.Instance.enemyTimeScale.Value;
|
||||
_initialAllied = TimeManager.Instance.alliedMinionTimeScale.Value;
|
||||
_initialNonPlayer = TimeManager.Instance.nonPlayerTimeScale.Value;
|
||||
}
|
||||
|
||||
public override void OnUpdate(FeedbackContext context, float normalizedTime)
|
||||
{
|
||||
if (TimeManager.Instance == null) return;
|
||||
|
||||
if (globalChannel.active)
|
||||
TimeManager.Instance.globalTimeScale.Value = globalChannel.Evaluate(normalizedTime);
|
||||
|
||||
if (playerChannel.active)
|
||||
TimeManager.Instance.playerTimeScale.Value = playerChannel.Evaluate(normalizedTime);
|
||||
|
||||
if (enemyChannel.active)
|
||||
TimeManager.Instance.enemyTimeScale.Value = enemyChannel.Evaluate(normalizedTime);
|
||||
|
||||
if (alliedChannel.active)
|
||||
TimeManager.Instance.alliedMinionTimeScale.Value = alliedChannel.Evaluate(normalizedTime);
|
||||
|
||||
if (nonPlayerChannel.active)
|
||||
TimeManager.Instance.nonPlayerTimeScale.Value = nonPlayerChannel.Evaluate(normalizedTime);
|
||||
}
|
||||
|
||||
public override void OnEnd(FeedbackContext context)
|
||||
{
|
||||
RestoreValues();
|
||||
}
|
||||
|
||||
public override void OnInterrupt(FeedbackContext context)
|
||||
{
|
||||
RestoreValues();
|
||||
}
|
||||
|
||||
public override bool Validate(out string error)
|
||||
{
|
||||
// 防呆检查:时间缩放修改器不应受自定义时间缩放影响
|
||||
// 此检查在 Editor 中调用,完整的 Inspector 防呆将在后续版本中添加
|
||||
bool anyActive = globalChannel.active || playerChannel.active ||
|
||||
enemyChannel.active || alliedChannel.active ||
|
||||
nonPlayerChannel.active;
|
||||
|
||||
if (!anyActive)
|
||||
{
|
||||
error = "No time scale channel is active. Enable at least one channel.";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RestoreValues()
|
||||
{
|
||||
if (TimeManager.Instance == null) return;
|
||||
|
||||
if (globalChannel.active)
|
||||
TimeManager.Instance.globalTimeScale.Value = _initialGlobal;
|
||||
|
||||
if (playerChannel.active)
|
||||
TimeManager.Instance.playerTimeScale.Value = _initialPlayer;
|
||||
|
||||
if (enemyChannel.active)
|
||||
TimeManager.Instance.enemyTimeScale.Value = _initialEnemy;
|
||||
|
||||
if (alliedChannel.active)
|
||||
TimeManager.Instance.alliedMinionTimeScale.Value = _initialAllied;
|
||||
|
||||
if (nonPlayerChannel.active)
|
||||
TimeManager.Instance.nonPlayerTimeScale.Value = _initialNonPlayer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c802f3245325e8459d578e1cfd1c68c
|
||||
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using Cielonos;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.Rendering.PostProcessing;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 高级暗角反馈动作,通过 PostProcessingManager 驱动 AdvancedVignette Volume 参数。
|
||||
/// 可用于受击暗角、环境压抑等效果。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class VignetteAction : CurveShakeAction
|
||||
{
|
||||
public override string DisplayName => "Vignette";
|
||||
|
||||
/// <summary>
|
||||
/// 是否修改暗角中心点。
|
||||
/// </summary>
|
||||
[Title("Vignette Settings")]
|
||||
[LabelText("Modify Center")]
|
||||
public bool modifyCenter;
|
||||
|
||||
[ShowIf("modifyCenter")]
|
||||
[LabelText("Center")]
|
||||
public Vector2 center = new Vector2(0.5f, 0.5f);
|
||||
|
||||
/// <summary>
|
||||
/// 是否修改颜色。
|
||||
/// </summary>
|
||||
[LabelText("Modify Colors")]
|
||||
public bool modifyColors;
|
||||
|
||||
[ShowIf("modifyColors")]
|
||||
[LabelText("Color Outer")]
|
||||
public Color colorOuter = Color.black;
|
||||
|
||||
[ShowIf("modifyColors")]
|
||||
[LabelText("Color Inner")]
|
||||
public Color colorInner = Color.black;
|
||||
|
||||
/// <summary>
|
||||
/// 是否修改柔和度和圆度。
|
||||
/// </summary>
|
||||
[LabelText("Modify Shape")]
|
||||
public bool modifyShape;
|
||||
|
||||
[ShowIf("modifyShape")]
|
||||
[LabelText("Smoothness")]
|
||||
[Range(0.01f, 1f)]
|
||||
public float smoothness = 0.5f;
|
||||
|
||||
[ShowIf("modifyShape")]
|
||||
[LabelText("Roundness")]
|
||||
[Range(0f, 1f)]
|
||||
public float roundness = 1f;
|
||||
|
||||
[NonSerialized] private AdvancedVignette _vignette;
|
||||
[NonSerialized] private float _initialIntensity;
|
||||
[NonSerialized] private Vector2 _initialCenter;
|
||||
[NonSerialized] private Color _initialColorOuter;
|
||||
[NonSerialized] private Color _initialColorInner;
|
||||
[NonSerialized] private float _initialSmoothness;
|
||||
[NonSerialized] private float _initialRoundness;
|
||||
[NonSerialized] private bool _resolved;
|
||||
|
||||
public override void OnStart(FeedbackContext context)
|
||||
{
|
||||
_resolved = TryResolveComponent();
|
||||
if (!_resolved) return;
|
||||
|
||||
_initialIntensity = _vignette.intensity.value;
|
||||
_initialCenter = _vignette.center.value;
|
||||
_initialColorOuter = _vignette.colorOuter.value;
|
||||
_initialColorInner = _vignette.colorInner.value;
|
||||
_initialSmoothness = _vignette.smoothness.value;
|
||||
_initialRoundness = _vignette.roundness.value;
|
||||
|
||||
if (modifyCenter)
|
||||
_vignette.center.value = center;
|
||||
|
||||
if (modifyColors)
|
||||
{
|
||||
_vignette.colorOuter.value = colorOuter;
|
||||
_vignette.colorInner.value = colorInner;
|
||||
}
|
||||
|
||||
if (modifyShape)
|
||||
{
|
||||
_vignette.smoothness.value = smoothness;
|
||||
_vignette.roundness.value = roundness;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnUpdate(FeedbackContext context, float normalizedTime)
|
||||
{
|
||||
if (!_resolved) return;
|
||||
|
||||
float newIntensity = EvaluateShake(normalizedTime, _initialIntensity);
|
||||
_vignette.intensity.value = newIntensity;
|
||||
}
|
||||
|
||||
public override void OnEnd(FeedbackContext context)
|
||||
{
|
||||
RestoreValues();
|
||||
}
|
||||
|
||||
public override void OnInterrupt(FeedbackContext context)
|
||||
{
|
||||
RestoreValues();
|
||||
}
|
||||
|
||||
private bool TryResolveComponent()
|
||||
{
|
||||
if (_vignette != null) return true;
|
||||
|
||||
if (PostProcessingManager.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[VignetteAction] PostProcessingManager instance not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PostProcessingManager.Instance.GetVolumeComponent(out _vignette))
|
||||
{
|
||||
Debug.LogWarning("[VignetteAction] AdvancedVignette not found in Volume Profile.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RestoreValues()
|
||||
{
|
||||
if (!_resolved) return;
|
||||
|
||||
_vignette.intensity.value = _initialIntensity;
|
||||
_vignette.center.value = _initialCenter;
|
||||
_vignette.colorOuter.value = _initialColorOuter;
|
||||
_vignette.colorInner.value = _initialColorInner;
|
||||
_vignette.smoothness.value = _initialSmoothness;
|
||||
_vignette.roundness.value = _initialRoundness;
|
||||
_resolved = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 970887032c5ce30478691975811e6af0
|
||||
@@ -0,0 +1,75 @@
|
||||
using Cielonos.MainGame.Characters;
|
||||
using SLSUtilities.Feedback;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// IFeedbackTimeProvider 的游戏层实现,从 SelfTimeSubmodule 和 TimeManager 读取时间缩放。
|
||||
/// 每个角色的 FeedbackSubcontroller 持有一个实例,注入到 FeedbackPlayer 中。
|
||||
/// </summary>
|
||||
public class CharacterFeedbackTimeProvider : IFeedbackTimeProvider
|
||||
{
|
||||
private readonly CharacterBase _character;
|
||||
|
||||
public CharacterFeedbackTimeProvider(CharacterBase character)
|
||||
{
|
||||
_character = character;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局时间缩放(TimeManager.globalTimeScale)。
|
||||
/// </summary>
|
||||
public float GlobalTimeScale =>
|
||||
TimeManager.Instance != null ? TimeManager.Instance.globalTimeScale.Value : 1f;
|
||||
|
||||
/// <summary>
|
||||
/// 分组时间缩放,根据角色 Fraction 返回对应的 TimeManager 通道值。
|
||||
/// </summary>
|
||||
public float GroupTimeScale
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TimeManager.Instance == null || _character == null) return 1f;
|
||||
|
||||
return _character.fraction switch
|
||||
{
|
||||
Fraction.Player => TimeManager.Instance.playerTimeScale.Value,
|
||||
Fraction.AlliedMinion => TimeManager.Instance.alliedMinionTimeScale.Value
|
||||
* TimeManager.Instance.nonPlayerTimeScale.Value,
|
||||
Fraction.Enemy => TimeManager.Instance.enemyTimeScale.Value
|
||||
* TimeManager.Instance.nonPlayerTimeScale.Value,
|
||||
Fraction.Neutral => TimeManager.Instance.nonPlayerTimeScale.Value,
|
||||
_ => 1f
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 角色本地时间缩放(SelfTimeSubmodule.localTimeScale)。
|
||||
/// </summary>
|
||||
public float LocalTimeScale =>
|
||||
_character?.selfTimeSm?.localTimeScale?.Value ?? 1f;
|
||||
|
||||
/// <summary>
|
||||
/// 根据 FeedbackTimeSettings 组合各层级缩放计算实际 deltaTime。
|
||||
/// </summary>
|
||||
public float GetDeltaTime(FeedbackTimeSettings settings)
|
||||
{
|
||||
if (settings == null || !settings.useTimeScale) return Time.unscaledDeltaTime;
|
||||
|
||||
float dt = Time.unscaledDeltaTime;
|
||||
|
||||
if (settings.affectedByGlobalTimeScale)
|
||||
dt *= GlobalTimeScale;
|
||||
|
||||
if (settings.affectedByGroupTimeScale)
|
||||
dt *= GroupTimeScale;
|
||||
|
||||
if (settings.affectedByLocalTimeScale)
|
||||
dt *= LocalTimeScale;
|
||||
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 195308c643432644eb816703b0a2c9a5
|
||||
151
Assets/Scripts/MainGame/Effects/Feedbacks/PlayFeedbackPayload.cs
Normal file
151
Assets/Scripts/MainGame/Effects/Feedbacks/PlayFeedbackPayload.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using Cielonos.MainGame.Characters;
|
||||
using Cielonos.MainGame.Characters.Inventory;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.FunctionalAnimation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// FuncAnim Payload 集成层:在动画事件触发时播放指定的 FeedbackData。
|
||||
/// 放入 FuncAnimData 的 animEvents 中,Invoke() 时自动通过角色/物品的
|
||||
/// FeedbackSubcontroller 播放,获得正确的时间缩放和 owner Transform。
|
||||
///
|
||||
/// 支持两种来源模式:
|
||||
/// 1. Direct:直接引用一个 FeedbackData 资产
|
||||
/// 2. ByName:从执行者的 FeedbackSubcontroller 中按名称查找
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[EventColor(0.4f, 0.8f, 1.0f)]
|
||||
public class PlayFeedbackPayload : FuncAnimPayloadBase
|
||||
{
|
||||
public override string NameForInspector => "Play Feedback";
|
||||
|
||||
public enum SourceMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 直接引用 FeedbackData 资产。
|
||||
/// </summary>
|
||||
Direct,
|
||||
|
||||
/// <summary>
|
||||
/// 从执行者的 FeedbackDataCollection 中按 feedbackName 查找。
|
||||
/// </summary>
|
||||
ByName
|
||||
}
|
||||
|
||||
[Title("Feedback Source")]
|
||||
public SourceMode sourceMode = SourceMode.Direct;
|
||||
|
||||
/// <summary>
|
||||
/// Direct 模式下直接引用的 FeedbackData 资产。
|
||||
/// </summary>
|
||||
[ShowIf("sourceMode", SourceMode.Direct)]
|
||||
[LabelText("Feedback Data")]
|
||||
public FeedbackData feedbackData;
|
||||
|
||||
/// <summary>
|
||||
/// ByName 模式下按名称查找的 feedbackName。
|
||||
/// </summary>
|
||||
[ShowIf("sourceMode", SourceMode.ByName)]
|
||||
[LabelText("Feedback Name")]
|
||||
public string feedbackName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否在播放前停止同名/同 Data 的正在播放的反馈。
|
||||
/// </summary>
|
||||
[Title("Options")]
|
||||
[LabelText("Stop Previous")]
|
||||
public bool stopPrevious;
|
||||
|
||||
public override void Invoke()
|
||||
{
|
||||
FeedbackData data = ResolveFeedbackData();
|
||||
if (data == null)
|
||||
{
|
||||
Debug.LogWarning($"[PlayFeedbackPayload] Cannot resolve FeedbackData. " +
|
||||
$"Mode: {sourceMode}, Name: {feedbackName}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试通过角色/物品的 FeedbackSubcontroller 播放(获得正确的 timeProvider 和 owner)
|
||||
if (TryPlayViaSubcontroller(data)) return;
|
||||
|
||||
// 回退到全局 FeedbackManager
|
||||
if (FeedbackManager.Instance != null)
|
||||
{
|
||||
FeedbackManager.Instance.Play(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[PlayFeedbackPayload] No FeedbackSubcontroller or FeedbackManager available.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 sourceMode 解析实际的 FeedbackData。
|
||||
/// </summary>
|
||||
private FeedbackData ResolveFeedbackData()
|
||||
{
|
||||
if (sourceMode == SourceMode.Direct)
|
||||
{
|
||||
return feedbackData;
|
||||
}
|
||||
|
||||
// ByName 模式:从执行者获取 FeedbackDataCollection 并查找
|
||||
FeedbackDataCollection collection = GetCollectionFromExecutor();
|
||||
if (collection != null && collection.TryGet(feedbackName, out FeedbackData result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试通过角色或物品的 FeedbackSubcontroller 播放。
|
||||
/// </summary>
|
||||
private bool TryPlayViaSubcontroller(FeedbackData data)
|
||||
{
|
||||
if (character == null) return false;
|
||||
|
||||
// 角色层级
|
||||
if (character is CharacterBase characterBase && characterBase.feedbackSc != null)
|
||||
{
|
||||
characterBase.feedbackSc.PlayFeedback(data, stopPrevious);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 物品层级
|
||||
if (character is ItemBase itemBase && itemBase.feedbackSc != null)
|
||||
{
|
||||
itemBase.feedbackSc.PlayFeedback(data, stopPrevious);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从执行者获取 FeedbackDataCollection。
|
||||
/// </summary>
|
||||
private FeedbackDataCollection GetCollectionFromExecutor()
|
||||
{
|
||||
if (character == null) return null;
|
||||
|
||||
if (character is CharacterBase characterBase && characterBase.feedbackSc != null)
|
||||
{
|
||||
return characterBase.feedbackSc.feedbackDataCollection;
|
||||
}
|
||||
|
||||
if (character is ItemBase itemBase && itemBase.feedbackSc != null)
|
||||
{
|
||||
return itemBase.feedbackSc.feedbackDataCollection;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59c46e6d7433a444f9ce44854134e666
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using Cielonos.MainGame;
|
||||
using Cielonos.MainGame.Effects;
|
||||
using NBShader;
|
||||
using SLSUtilities.Feedback;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
Reference in New Issue
Block a user