做不出来
This commit is contained in:
8
Assets/Scripts/MainGame/Effects/AfterImage.meta
Normal file
8
Assets/Scripts/MainGame/Effects/AfterImage.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea89affa681d8784eb0146116baeae51
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Cielonos.MainGame.Characters;
|
||||
|
||||
namespace Cielonos.MainGame.Effects
|
||||
{
|
||||
public enum AfterImageSpawnMode
|
||||
{
|
||||
TimeInterval,
|
||||
DistanceInterval
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 残影发生器,挂载在角色身上,用于在高速移动时动态生成姿态残影。
|
||||
/// </summary>
|
||||
public class AfterImageGenerator : MonoBehaviour
|
||||
{
|
||||
private CharacterBase character;
|
||||
private List<SkinnedMeshRenderer> skinnedRenderers = new List<SkinnedMeshRenderer>();
|
||||
private bool isSpawning = false;
|
||||
private float timer;
|
||||
private Vector3 lastSpawnPos;
|
||||
|
||||
[Header("AfterImage Settings")]
|
||||
[Tooltip("残影所使用的菲涅尔发光材质")]
|
||||
public Material afterImageMaterial;
|
||||
[Tooltip("单个残影的持续消失时间(秒)")]
|
||||
public float afterImageDuration = 0.3f;
|
||||
|
||||
[Tooltip("残影生成模式:时间间隔 或 距离间隔")]
|
||||
public AfterImageSpawnMode spawnMode = AfterImageSpawnMode.TimeInterval;
|
||||
|
||||
[Tooltip("生成残影的时间间隔(秒,TimeInterval 模式)")]
|
||||
public float spawnInterval = 0.03f;
|
||||
[Tooltip("生成残影的距离间隔(米,DistanceInterval 模式)")]
|
||||
public float spawnDistance = 0.8f;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
character = GetComponent<CharacterBase>();
|
||||
GetComponentsInChildren(true, skinnedRenderers);
|
||||
}
|
||||
|
||||
public void StartSpawning()
|
||||
{
|
||||
isSpawning = true;
|
||||
timer = 0f; // 时间模式下启动时立即生成一个
|
||||
lastSpawnPos = transform.position; // 距离模式下重置起始点
|
||||
|
||||
if (spawnMode == AfterImageSpawnMode.DistanceInterval)
|
||||
{
|
||||
CreateAfterImageSnapshot(); // 距离模式启动时也立即生成第一个
|
||||
}
|
||||
}
|
||||
|
||||
public void StopSpawning()
|
||||
{
|
||||
isSpawning = false;
|
||||
}
|
||||
|
||||
#region Dynamic Configuration APIs
|
||||
|
||||
public void SetSpawnMode(AfterImageSpawnMode mode) => spawnMode = mode;
|
||||
public void SetSpawnInterval(float interval) => spawnInterval = interval;
|
||||
public void SetSpawnDistance(float distance) => spawnDistance = distance;
|
||||
public void SetAfterImageDuration(float duration) => afterImageDuration = duration;
|
||||
public void SetMaterial(Material mat) => afterImageMaterial = mat;
|
||||
|
||||
#endregion
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isSpawning || character == null) return;
|
||||
|
||||
if (spawnMode == AfterImageSpawnMode.TimeInterval)
|
||||
{
|
||||
timer -= character.selfTimeSm.DeltaTime;
|
||||
if (timer <= 0f)
|
||||
{
|
||||
CreateAfterImageSnapshot();
|
||||
timer = spawnInterval;
|
||||
}
|
||||
}
|
||||
else if (spawnMode == AfterImageSpawnMode.DistanceInterval)
|
||||
{
|
||||
Vector3 currentPos = transform.position;
|
||||
float dist = Vector3.Distance(lastSpawnPos, currentPos);
|
||||
if (dist >= spawnDistance)
|
||||
{
|
||||
CreateAfterImageSnapshot();
|
||||
lastSpawnPos = currentPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动生成一个当前姿态的残影
|
||||
/// </summary>
|
||||
public void CreateAfterImageSnapshot()
|
||||
{
|
||||
if (afterImageMaterial == null) return;
|
||||
|
||||
// 创建残影根容器
|
||||
GameObject afterImageObj = new GameObject($"{gameObject.name}_AfterImage");
|
||||
|
||||
List<MeshFilter> filters = new List<MeshFilter>();
|
||||
List<MeshRenderer> renderers = new List<MeshRenderer>();
|
||||
|
||||
foreach (var skinnedRenderer in skinnedRenderers)
|
||||
{
|
||||
// 仅烘焙当前处于显示状态的蒙皮部件
|
||||
if (skinnedRenderer == null || !skinnedRenderer.gameObject.activeInHierarchy || !skinnedRenderer.enabled)
|
||||
continue;
|
||||
|
||||
// 创建对应蒙皮部件的静态 Mesh 副本
|
||||
GameObject subObj = new GameObject(skinnedRenderer.name);
|
||||
subObj.transform.SetParent(afterImageObj.transform);
|
||||
subObj.transform.position = skinnedRenderer.transform.position;
|
||||
subObj.transform.rotation = skinnedRenderer.transform.rotation;
|
||||
subObj.transform.localScale = skinnedRenderer.transform.lossyScale;
|
||||
|
||||
// 烘焙蒙皮网格的当前姿态
|
||||
Mesh bakedMesh = new Mesh();
|
||||
skinnedRenderer.BakeMesh(bakedMesh);
|
||||
|
||||
MeshFilter filter = subObj.AddComponent<MeshFilter>();
|
||||
filter.mesh = bakedMesh;
|
||||
filters.Add(filter);
|
||||
|
||||
MeshRenderer renderer = subObj.AddComponent<MeshRenderer>();
|
||||
renderers.Add(renderer);
|
||||
}
|
||||
|
||||
// 如果没有成功烘焙出任何网格,直接销毁根容器
|
||||
if (filters.Count == 0)
|
||||
{
|
||||
Destroy(afterImageObj);
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化渐隐控制
|
||||
AfterImageItem afterImageItem = afterImageObj.AddComponent<AfterImageItem>();
|
||||
afterImageItem.Initialize(filters.ToArray(), renderers.ToArray(), afterImageMaterial, afterImageDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 757d4da67572fb847bf547e39d362e9e
|
||||
57
Assets/Scripts/MainGame/Effects/AfterImage/AfterImageItem.cs
Normal file
57
Assets/Scripts/MainGame/Effects/AfterImage/AfterImageItem.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个残影个体生命周期与渐隐控制器。
|
||||
/// </summary>
|
||||
public class AfterImageItem : MonoBehaviour
|
||||
{
|
||||
private MeshRenderer[] renderers;
|
||||
private MaterialPropertyBlock propBlock;
|
||||
private float fadeSpeed;
|
||||
private float currentFade = 1f;
|
||||
|
||||
public void Initialize(MeshFilter[] filters, MeshRenderer[] renderers, Material afterImageMat, float duration)
|
||||
{
|
||||
this.renderers = renderers;
|
||||
this.fadeSpeed = 1f / duration;
|
||||
this.propBlock = new MaterialPropertyBlock();
|
||||
|
||||
foreach (var renderer in renderers)
|
||||
{
|
||||
if (renderer != null)
|
||||
{
|
||||
renderer.sharedMaterial = afterImageMat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
currentFade -= Time.deltaTime * fadeSpeed;
|
||||
if (currentFade <= 0f)
|
||||
{
|
||||
// 必须手动销毁运行时烘焙出的 Mesh 资源,防止显存泄漏 (GC 不会自动回收 BakeMesh 产生的网格)
|
||||
foreach (var filter in GetComponentsInChildren<MeshFilter>())
|
||||
{
|
||||
if (filter != null && filter.sharedMesh != null)
|
||||
{
|
||||
Destroy(filter.sharedMesh);
|
||||
}
|
||||
}
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
propBlock.SetFloat("_Fade", currentFade);
|
||||
foreach (var renderer in renderers)
|
||||
{
|
||||
if (renderer != null)
|
||||
{
|
||||
renderer.SetPropertyBlock(propBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3cb1f0acf837a945ab000963b8c1541
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.Feedback;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
/// <summary>
|
||||
/// 摄像机轨道旋转反馈动作,通过 CameraOrbitEvent 触发 CameraOrbitShaker。
|
||||
/// 支持两种模式:
|
||||
/// - Additive:曲线值作为偏移量叠加到初始角度上
|
||||
/// - TargetValue:从当前值平滑运动到指定的目标角度
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[FeedbackActionColor(0.3f, 0.7f, 0.95f)]
|
||||
public class CameraOrbitAction : CinemachineActionBase
|
||||
{
|
||||
public override string DisplayName => "Camera Orbit";
|
||||
|
||||
public enum OrbitMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 曲线输出值作为角度偏移量,叠加到触发时的初始角度上。
|
||||
/// </summary>
|
||||
Additive,
|
||||
|
||||
/// <summary>
|
||||
/// 类似 DoTween:指定目标角度 (endValue),通过缓动曲线从当前位置平滑运动到目标。
|
||||
/// </summary>
|
||||
TargetValue
|
||||
}
|
||||
|
||||
[TitleGroup("生效相机设置")]
|
||||
[LabelText("在 FreeLook (自由视角) 相机下生效")]
|
||||
public bool enableInFreeLook = true;
|
||||
|
||||
[LabelText("在 LockTarget (锁定视角) 相机下生效")]
|
||||
public bool enableInLockTarget = true;
|
||||
|
||||
// ===== 水平公转设置 (Yaw) =====
|
||||
[TitleGroup("水平公转设置 (Yaw)")]
|
||||
[LabelText("启用水平公转")]
|
||||
public bool enableYaw = true;
|
||||
|
||||
[LabelText("水平公转模式")]
|
||||
[ShowIf("enableYaw")]
|
||||
public OrbitMode yawMode = OrbitMode.Additive;
|
||||
|
||||
// Yaw Additive 参数
|
||||
[LabelText("水平公转曲线 (Yaw)")]
|
||||
[ShowIf("@enableYaw && yawMode == OrbitMode.Additive")]
|
||||
public FloatCurveChannel horizontalCurve = FloatCurveChannel.CreateDefault(0f, 360f);
|
||||
|
||||
// Yaw TargetValue 参数
|
||||
[LabelText("目标角度 (Yaw)")]
|
||||
[Tooltip("目标水平公转角度。0° = 玩家正面,180° = 玩家背面。可以超过 360°。")]
|
||||
[ShowIf("@enableYaw && yawMode == OrbitMode.TargetValue")]
|
||||
public float endHorizontalValue = 0f;
|
||||
|
||||
[LabelText("缓动曲线 (Yaw)")]
|
||||
[Tooltip("水平公转缓动曲线:归一化时间 [0,1] 映射到插值进度 [0,1]。")]
|
||||
[ShowIf("@enableYaw && yawMode == OrbitMode.TargetValue")]
|
||||
[ShakeCurvePreset]
|
||||
public AnimationCurve yawEaseCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
|
||||
|
||||
[LabelText("使用最短路径")]
|
||||
[Tooltip("开启时,相机会选择最近的旋转方向移向目标角度(防止 360 度穿帮旋转);关闭时,将按照绝对数值插值。")]
|
||||
[ShowIf("@enableYaw && yawMode == OrbitMode.TargetValue")]
|
||||
public bool shortestPath = true;
|
||||
|
||||
|
||||
// ===== 垂直公转设置 (Pitch) =====
|
||||
[TitleGroup("垂直公转设置 (Pitch)")]
|
||||
[LabelText("启用垂直公转")]
|
||||
public bool enablePitch = true;
|
||||
|
||||
[LabelText("垂直公转模式")]
|
||||
[ShowIf("enablePitch")]
|
||||
public OrbitMode pitchMode = OrbitMode.Additive;
|
||||
|
||||
// Pitch Additive 参数
|
||||
[LabelText("垂直公转曲线 (Pitch)")]
|
||||
[ShowIf("@enablePitch && pitchMode == OrbitMode.Additive")]
|
||||
public FloatCurveChannel verticalCurve = FloatCurveChannel.CreateDefault(0f, 0f);
|
||||
|
||||
// Pitch TargetValue 参数
|
||||
[LabelText("目标角度 (Pitch)")]
|
||||
[Tooltip("目标垂直公转角度。硬锁相机下为相对锁定仰角的偏移量;自由相机下为绝对仰角。")]
|
||||
[ShowIf("@enablePitch && pitchMode == OrbitMode.TargetValue")]
|
||||
public float endVerticalValue = 0f;
|
||||
|
||||
[LabelText("缓动曲线 (Pitch)")]
|
||||
[Tooltip("垂直公转缓动曲线:归一化时间 [0,1] 映射到插值进度 [0,1]。")]
|
||||
[ShowIf("@enablePitch && pitchMode == OrbitMode.TargetValue")]
|
||||
[ShakeCurvePreset]
|
||||
public AnimationCurve pitchEaseCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
|
||||
|
||||
|
||||
protected override void TriggerEvent(FeedbackContext context)
|
||||
{
|
||||
CameraOrbitEvent.Trigger(context, this);
|
||||
}
|
||||
|
||||
protected override void StopEvent(FeedbackContext context)
|
||||
{
|
||||
CameraOrbitEvent.Trigger(context, this, true);
|
||||
}
|
||||
|
||||
public override bool Validate(out string error)
|
||||
{
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 085667c17a7812b4ca6e0a4e53d7957d
|
||||
@@ -0,0 +1,326 @@
|
||||
using System.Collections.Generic;
|
||||
using SLSUtilities.Feedback;
|
||||
using Unity.Cinemachine;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Cielonos.MainGame.Effects.Feedback
|
||||
{
|
||||
public class CameraOrbitShakeInstance : ShakeInstanceBase
|
||||
{
|
||||
// Yaw 设置
|
||||
public bool enableYaw;
|
||||
public CameraOrbitAction.OrbitMode yawMode;
|
||||
public FloatCurveChannel horizontalCurve;
|
||||
public float startHorizontalValue;
|
||||
public float worldEndHorizontalValue;
|
||||
public AnimationCurve yawEaseCurve;
|
||||
|
||||
// Pitch 设置
|
||||
public bool enablePitch;
|
||||
public CameraOrbitAction.OrbitMode pitchMode;
|
||||
public FloatCurveChannel verticalCurve;
|
||||
public float startVerticalValue;
|
||||
public float worldEndVerticalValue;
|
||||
public AnimationCurve pitchEaseCurve;
|
||||
|
||||
public CameraOrbitShakeInstance(FeedbackTimeSettings timeSettings, IFeedbackTimeProvider timeProvider,
|
||||
CameraOrbitAction action, float startH, float startV, float worldEndH, float worldEndV, float duration)
|
||||
: base(timeSettings, timeProvider, duration)
|
||||
{
|
||||
this.enableYaw = action.enableYaw;
|
||||
this.yawMode = action.yawMode;
|
||||
this.horizontalCurve = action.horizontalCurve;
|
||||
this.startHorizontalValue = startH;
|
||||
this.worldEndHorizontalValue = worldEndH;
|
||||
this.yawEaseCurve = action.yawEaseCurve;
|
||||
|
||||
this.enablePitch = action.enablePitch;
|
||||
this.pitchMode = action.pitchMode;
|
||||
this.verticalCurve = action.verticalCurve;
|
||||
this.startVerticalValue = startV;
|
||||
this.worldEndVerticalValue = worldEndV;
|
||||
this.pitchEaseCurve = action.pitchEaseCurve;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在 TargetValue 模式下,根据归一化时间计算当前水平角度。
|
||||
/// </summary>
|
||||
public float EvaluateHorizontal(float normalizedTime)
|
||||
{
|
||||
if (yawMode == CameraOrbitAction.OrbitMode.Additive)
|
||||
{
|
||||
return horizontalCurve.Evaluate(normalizedTime);
|
||||
}
|
||||
|
||||
float t = yawEaseCurve != null ? yawEaseCurve.Evaluate(Mathf.Clamp01(normalizedTime)) : normalizedTime;
|
||||
return Mathf.LerpUnclamped(startHorizontalValue, worldEndHorizontalValue, t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在 TargetValue 模式下,根据归一化时间计算当前垂直角度。
|
||||
/// </summary>
|
||||
public float EvaluateVertical(float normalizedTime)
|
||||
{
|
||||
if (pitchMode == CameraOrbitAction.OrbitMode.Additive)
|
||||
{
|
||||
return verticalCurve.Evaluate(normalizedTime);
|
||||
}
|
||||
|
||||
float t = pitchEaseCurve != null ? pitchEaseCurve.Evaluate(Mathf.Clamp01(normalizedTime)) : normalizedTime;
|
||||
return Mathf.LerpUnclamped(startVerticalValue, worldEndVerticalValue, t);
|
||||
}
|
||||
}
|
||||
|
||||
public struct CameraOrbitEvent
|
||||
{
|
||||
private static event ShakeDelegate OnEvent;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void RuntimeInitialization() { OnEvent = null; }
|
||||
|
||||
public delegate void ShakeDelegate(
|
||||
FeedbackContext feedbackContext,
|
||||
CameraOrbitAction action,
|
||||
bool stop
|
||||
);
|
||||
|
||||
public static void Register(ShakeDelegate callback) { OnEvent += callback; }
|
||||
public static void Unregister(ShakeDelegate callback) { OnEvent -= callback; }
|
||||
|
||||
public static void Trigger(
|
||||
FeedbackContext feedbackContext,
|
||||
CameraOrbitAction action,
|
||||
bool stop = false)
|
||||
{
|
||||
OnEvent?.Invoke(feedbackContext, action, stop);
|
||||
}
|
||||
}
|
||||
|
||||
[AddComponentMenu("Cielonos/Feedback Shakers/Camera Orbit Shaker")]
|
||||
[RequireComponent(typeof(CinemachineCamera))]
|
||||
[RequireComponent(typeof(CinemachineOrbitalFollow))]
|
||||
public class CameraOrbitShaker : MonoBehaviour
|
||||
{
|
||||
private CinemachineCamera _camera;
|
||||
private CinemachineOrbitalFollow _orbitalFollow;
|
||||
private CinemachineInputAxisController _inputController;
|
||||
|
||||
[Tooltip("目标参考的 Transform。当 CameraOrbitAction 处于 TargetValue 模式时,将以此为基准转换 endHorizontalValue 为世界空间角度。" +
|
||||
"如果为空,则默认使用 MainGameManager.Player.transform,若仍为空则回退到相机的 Follow 目标。")]
|
||||
[SerializeField] private Transform targetReference;
|
||||
|
||||
private float _baseHorizontalValue;
|
||||
private float _baseVerticalValue;
|
||||
private bool _hasBaseValuesCaptured;
|
||||
|
||||
private readonly List<CameraOrbitShakeInstance> _activeShakes = new List<CameraOrbitShakeInstance>();
|
||||
|
||||
public float CurrentHorizontalOffset { get; private set; }
|
||||
public float CurrentVerticalOffset { get; private set; }
|
||||
public bool HasActiveShakes => _activeShakes.Count > 0;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_camera = GetComponent<CinemachineCamera>();
|
||||
_orbitalFollow = GetComponent<CinemachineOrbitalFollow>();
|
||||
_inputController = GetComponent<CinemachineInputAxisController>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
CameraOrbitEvent.Register(OnShakeEvent);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
CameraOrbitEvent.Unregister(OnShakeEvent);
|
||||
StopAll();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_orbitalFollow == null) return;
|
||||
|
||||
if (_activeShakes.Count == 0)
|
||||
{
|
||||
CurrentHorizontalOffset = 0f;
|
||||
CurrentVerticalOffset = 0f;
|
||||
|
||||
if (_hasBaseValuesCaptured)
|
||||
{
|
||||
// 所有 Shake 结束:恢复玩家输入控制器
|
||||
if (_inputController != null && !_inputController.enabled)
|
||||
{
|
||||
_inputController.enabled = true;
|
||||
}
|
||||
_hasBaseValuesCaptured = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 首次触发时捕获当前相机角度作为基准
|
||||
if (!_hasBaseValuesCaptured)
|
||||
{
|
||||
_baseHorizontalValue = _orbitalFollow.HorizontalAxis.Value;
|
||||
_baseVerticalValue = _orbitalFollow.VerticalAxis.Value;
|
||||
_hasBaseValuesCaptured = true;
|
||||
}
|
||||
|
||||
// 区分模式与轨道进行独立计算
|
||||
float finalHorizontal = _baseHorizontalValue;
|
||||
float finalVertical = _baseVerticalValue;
|
||||
|
||||
for (int i = _activeShakes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
CameraOrbitShakeInstance shake = _activeShakes[i];
|
||||
shake.Tick();
|
||||
float normalizedTime = shake.timer / shake.duration;
|
||||
|
||||
// 独立水平轴计算
|
||||
if (shake.enableYaw)
|
||||
{
|
||||
if (shake.yawMode == CameraOrbitAction.OrbitMode.Additive)
|
||||
{
|
||||
finalHorizontal += shake.EvaluateHorizontal(normalizedTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
finalHorizontal = shake.EvaluateHorizontal(normalizedTime);
|
||||
}
|
||||
}
|
||||
|
||||
// 独立垂直轴计算
|
||||
if (shake.enablePitch)
|
||||
{
|
||||
if (shake.pitchMode == CameraOrbitAction.OrbitMode.Additive)
|
||||
{
|
||||
finalVertical += shake.EvaluateVertical(normalizedTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
finalVertical = shake.EvaluateVertical(normalizedTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (shake.IsFinished)
|
||||
{
|
||||
_activeShakes.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
// 管理玩家输入控制器状态(有任何活跃 Orbit Shake 时必定禁用输入)
|
||||
if (_inputController != null && _inputController.enabled)
|
||||
{
|
||||
_inputController.enabled = false;
|
||||
}
|
||||
|
||||
// 计算提供给锁锁相机的当前偏差量
|
||||
CurrentHorizontalOffset = finalHorizontal - _baseHorizontalValue;
|
||||
CurrentVerticalOffset = finalVertical - _baseVerticalValue;
|
||||
|
||||
// 检查当前是否在使用锁定相机的硬锁状态,若是,则由 LockTargetSubmodule 接管赋值,避免冲突
|
||||
bool isLocking = MainGameManager.Player != null &&
|
||||
MainGameManager.Player.viewSc.lockTargetModule != null &&
|
||||
MainGameManager.Player.viewSc.lockTargetModule.isUsingLockTargetCamera &&
|
||||
gameObject == MainGameManager.Player.viewSc.lockingTargetCamera.gameObject;
|
||||
|
||||
if (!isLocking)
|
||||
{
|
||||
// 非锁定状态下直接赋值
|
||||
_orbitalFollow.HorizontalAxis.Value = finalHorizontal;
|
||||
_orbitalFollow.VerticalAxis.Value = finalVertical;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShakeEvent(
|
||||
FeedbackContext feedbackContext,
|
||||
CameraOrbitAction action,
|
||||
bool stop)
|
||||
{
|
||||
if (stop) { StopAll(); return; }
|
||||
|
||||
// 仅在当前组件附加的虚拟相机是当前激活相机时,才响应 Orbit 动作,防止后台相机数据被污染
|
||||
bool isActiveCamera = MainGameManager.Player != null &&
|
||||
MainGameManager.Player.viewSc.currentCamera != null &&
|
||||
gameObject == MainGameManager.Player.viewSc.currentCamera.gameObject;
|
||||
|
||||
if (!isActiveCamera) return;
|
||||
|
||||
// 检查当前是否在使用锁定相机的硬锁状态
|
||||
bool isLocking = MainGameManager.Player != null &&
|
||||
MainGameManager.Player.viewSc.lockTargetModule != null &&
|
||||
MainGameManager.Player.viewSc.lockTargetModule.isUsingLockTargetCamera &&
|
||||
gameObject == MainGameManager.Player.viewSc.lockingTargetCamera.gameObject;
|
||||
|
||||
// 根据当前相机状态与配置决定是否生效
|
||||
if (isLocking && !action.enableInLockTarget) return;
|
||||
if (!isLocking && !action.enableInFreeLook) return;
|
||||
|
||||
// 获取当前轴值作为 TargetValue 模式的起始值
|
||||
float currentH = _orbitalFollow != null ? _orbitalFollow.HorizontalAxis.Value : 0f;
|
||||
float currentV = _orbitalFollow != null ? _orbitalFollow.VerticalAxis.Value : 0f;
|
||||
|
||||
// 解析水平目标值与垂直目标值
|
||||
float worldEndH = currentH;
|
||||
float worldEndV = action.endVerticalValue;
|
||||
|
||||
// 水平 (Yaw) 解析
|
||||
if (action.enableYaw && action.yawMode == CameraOrbitAction.OrbitMode.TargetValue)
|
||||
{
|
||||
Transform reference = targetReference != null ? targetReference : (MainGameManager.Player != null ? MainGameManager.Player.transform : _camera?.Follow);
|
||||
float playerYaw = reference != null ? reference.eulerAngles.y : 0f;
|
||||
float targetAngle = playerYaw + action.endHorizontalValue;
|
||||
|
||||
if (action.shortestPath)
|
||||
{
|
||||
worldEndH = currentH + Mathf.DeltaAngle(currentH, targetAngle);
|
||||
}
|
||||
else
|
||||
{
|
||||
worldEndH = targetAngle;
|
||||
}
|
||||
}
|
||||
|
||||
// 垂直 (Pitch) 解析
|
||||
if (action.enablePitch && action.pitchMode == CameraOrbitAction.OrbitMode.TargetValue)
|
||||
{
|
||||
if (isLocking)
|
||||
{
|
||||
// 硬锁相机下,垂直仰角(Pitch)的 TargetValue 是相对于触发时的基准仰角的相对偏移
|
||||
worldEndV = currentV + action.endVerticalValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
worldEndV = action.endVerticalValue;
|
||||
}
|
||||
}
|
||||
|
||||
var instance = new CameraOrbitShakeInstance(
|
||||
feedbackContext.timeSettings,
|
||||
feedbackContext.player.TimeProvider,
|
||||
action,
|
||||
currentH,
|
||||
currentV,
|
||||
worldEndH,
|
||||
worldEndV,
|
||||
feedbackContext.duration
|
||||
);
|
||||
_activeShakes.Add(instance);
|
||||
}
|
||||
|
||||
private void StopAll()
|
||||
{
|
||||
_activeShakes.Clear();
|
||||
if (_hasBaseValuesCaptured && _orbitalFollow != null)
|
||||
{
|
||||
_orbitalFollow.HorizontalAxis.Value = _baseHorizontalValue;
|
||||
_orbitalFollow.VerticalAxis.Value = _baseVerticalValue;
|
||||
}
|
||||
if (_inputController != null)
|
||||
{
|
||||
_inputController.enabled = true;
|
||||
}
|
||||
_hasBaseValuesCaptured = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 646548dbf6035f04a8a01e875996d327
|
||||
8
Assets/Scripts/MainGame/Effects/Ghost.meta
Normal file
8
Assets/Scripts/MainGame/Effects/Ghost.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be266a922a64f794d9b0b85a84e87c4c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user