diff --git a/Assets/_Game/Scripts/Combat/DamageInfo.cs b/Assets/_Game/Scripts/Combat/DamageInfo.cs index 80b813d..ff6344d 100644 --- a/Assets/_Game/Scripts/Combat/DamageInfo.cs +++ b/Assets/_Game/Scripts/Combat/DamageInfo.cs @@ -34,6 +34,13 @@ namespace BaseGames.Combat /// [System.NonSerialized] public uint HitActivationId; /// + /// 命中点世界坐标。HitBox 结算时写入精确接触点(ClosestPoint); + /// 直接调用 HurtBox.ReceiveDamage 的路径由 HurtBox 以解析后的命中点兜底写入。 + /// 供"击中位置"模式的反馈在该点生成(FeedbackPositionMode.HitPoint)。 + /// [NonSerialized]:运行时逐次命中生成,不需要序列化。 + /// + [System.NonSerialized] public Vector3 HitPoint; + /// /// 攻击来源投射物(仅当攻击方是 Projectile 时非 null)。 /// 用于弹反成功时调用 ReflectBy(parrier) 按弹反者阵营反射(玩家弹反才翻转阵营)。 /// [NonSerialized]:MonoBehaviour 引用不参与 Unity 资产序列化。 diff --git a/Assets/_Game/Scripts/Combat/HitBox.cs b/Assets/_Game/Scripts/Combat/HitBox.cs index 5de485f..88cc09c 100644 --- a/Assets/_Game/Scripts/Combat/HitBox.cs +++ b/Assets/_Game/Scripts/Combat/HitBox.cs @@ -268,15 +268,18 @@ namespace BaseGames.Combat _ownerProjectile); info.HitActivationId = _currentActivationId; + // hitPoint:优先使用触发命中的碰撞体中心在目标表面的最近点; + // 无 sourceCollider(直属碰撞体)时回退到 HitBox 节点坐标。 + // 写入 info.HitPoint 供下游"击中位置"模式的反馈在该点生成。 + Vector2 hitOrigin = sourceCollider != null + ? (Vector2)sourceCollider.bounds.center + : (Vector2)transform.position; + Vector3 hitPoint = other.ClosestPoint(hitOrigin); + info.HitPoint = hitPoint; + // ② 命中 HurtBox if (other.TryGetComponent(out var hurtBox)) { - // hitPoint:优先使用触发命中的碰撞体中心在 HurtBox 表面的最近点; - // 无 sourceCollider(直属碰撞体)时回退到 HitBox 节点坐标。 - Vector2 hitOrigin = sourceCollider != null - ? (Vector2)sourceCollider.bounds.center - : (Vector2)transform.position; - Vector3 hitPoint = other.ClosestPoint(hitOrigin); hurtBox.ReceiveDamage(info, hitPoint); OnHitConfirmed?.Invoke(info); return; diff --git a/Assets/_Game/Scripts/Combat/HurtBox.cs b/Assets/_Game/Scripts/Combat/HurtBox.cs index 5ba3073..72dd7fa 100644 --- a/Assets/_Game/Scripts/Combat/HurtBox.cs +++ b/Assets/_Game/Scripts/Combat/HurtBox.cs @@ -61,6 +61,9 @@ namespace BaseGames.Combat public void ReceiveDamage(DamageInfo info, Vector3? hitPoint = null) { Vector3 resolvedHitPoint = hitPoint ?? transform.position; + // 兜底盖戳:直接调用本方法的路径(陷阱/环境伤害)没有 HitBox 写入的精确接触点, + // 统一以解析后的命中点填充,保证下游"击中位置"模式反馈始终有效 + info.HitPoint = resolvedHitPoint; if (!_isActive || _owner == null) return; // 所有者级去重:同一 HitBox 激活期内多个 HurtBox 子节点只处理首次命中(共享 HP) if (_ownerGuard != null && !_ownerGuard.TryRegisterHit(info.HitActivationId)) return; diff --git a/Assets/_Game/Scripts/Enemies/EnemyFeedback.cs b/Assets/_Game/Scripts/Enemies/EnemyFeedback.cs index 4e3ba76..36861c6 100644 --- a/Assets/_Game/Scripts/Enemies/EnemyFeedback.cs +++ b/Assets/_Game/Scripts/Enemies/EnemyFeedback.cs @@ -15,13 +15,17 @@ namespace BaseGames.Enemies [SerializeField] private MMF_Player _onHitLight; [SerializeField] private MMF_Player _onHitMedium; [SerializeField] private MMF_Player _onHitHeavy; + [Tooltip("命中反馈生成位置:Fixed = 反馈链编排的固定位置;HitPoint = 在命中点生成")] + [SerializeField] private FeedbackPositionMode _hitPositionMode = FeedbackPositionMode.Fixed; [Header("受伤 / 死亡反馈")] [SerializeField] private MMF_Player _onTakeHit; + [Tooltip("受击反馈生成位置:Fixed = 反馈链编排的固定位置;HitPoint = 在受击点生成(血粒子等贴点表现)")] + [SerializeField] private FeedbackPositionMode _takeHitPositionMode = FeedbackPositionMode.Fixed; [SerializeField] private MMF_Player _onDeath; // ── IFeedbackPlayer ────────────────────────────────────────────────────── - public void PlayHit(HitWeight weight) + public void PlayHit(HitWeight weight, Vector3 hitPoint) { var player = weight switch { @@ -30,10 +34,11 @@ namespace BaseGames.Enemies HitWeight.Heavy => _onHitHeavy, _ => _onHitLight, }; - player?.PlayFeedbacks(); + FeedbackPlayback.Play(player, _hitPositionMode, hitPoint); } - public void PlayTakeHit() => _onTakeHit?.PlayFeedbacks(); + public void PlayTakeHit(Vector3 hitPoint) + => FeedbackPlayback.Play(_onTakeHit, _takeHitPositionMode, hitPoint); public void PlayDeath() => _onDeath?.PlayFeedbacks(); // 以下方法对敌人无意义,提供空实现保持接口完整 @@ -51,7 +56,7 @@ namespace BaseGames.Enemies /// 受到伤害时调用(由 EnemyBase 触发)。 public void OnHit(DamageInfo info) { - PlayTakeHit(); + PlayTakeHit(info.HitPoint); } /// 死亡时调用(由 EnemyBase 触发)。 diff --git a/Assets/_Game/Scripts/Feedback/IFeedbackPlayer.cs b/Assets/_Game/Scripts/Feedback/IFeedbackPlayer.cs index 8d38f6d..a858984 100644 --- a/Assets/_Game/Scripts/Feedback/IFeedbackPlayer.cs +++ b/Assets/_Game/Scripts/Feedback/IFeedbackPlayer.cs @@ -1,3 +1,6 @@ +using UnityEngine; +using MoreMountains.Feedbacks; + namespace BaseGames.Feedback { /// @@ -8,15 +11,22 @@ namespace BaseGames.Feedback public interface IFeedbackPlayer { // ── 命中 ────────────────────────────────────────────────────────────────── - /// 对目标造成命中时播放对应力度的反馈(摄像机震屏 + 控制器振动等)。 - void PlayHit(HitWeight weight); + /// + /// 对目标造成命中时播放对应力度的反馈(摄像机震屏 + 控制器振动等)。 + /// hitPoint = 命中点世界坐标(DamageInfo.HitPoint); + /// 实际生成位置取决于实现方各槽位的 FeedbackPositionMode 配置。 + /// + void PlayHit(HitWeight weight, Vector3 hitPoint); /// 成功弹反(Parry)时播放反馈。 void PlayParrySuccess(); // ── 受伤 / 死亡 ────────────────────────────────────────────────────────── - /// 角色受到伤害时播放反馈(闪白 + 轻微震屏等)。 - void PlayTakeHit(); + /// + /// 角色受到伤害时播放反馈(闪白 + 轻微震屏等)。 + /// hitPoint = 受击点世界坐标(DamageInfo.HitPoint)。 + /// + void PlayTakeHit(Vector3 hitPoint); /// 角色死亡时播放反馈(慢动作 + 震屏 + 音效)。 void PlayDeath(); @@ -53,6 +63,34 @@ namespace BaseGames.Feedback /// 命中力度。 public enum HitWeight { Light, Medium, Heavy } + /// + /// 反馈生成位置模式。反馈链本身由开发者在 Inspector 编排, + /// 此模式只决定播放时是否把命中点传入反馈链。 + /// + public enum FeedbackPositionMode + { + /// 固定位置:按反馈链各模块编排时设定的位置播放(不传入命中点)。 + Fixed, + /// 击中位置:把命中点传入反馈链,启用了播放位置选项的模块在命中点生成。 + HitPoint, + } + + /// + /// 反馈播放辅助:统一"按位置模式播放 MMF_Player"的实现,供各 IFeedbackPlayer 实现复用。 + /// + public static class FeedbackPlayback + { + /// 按位置模式播放。HitPoint 模式将命中点传给反馈链(仅启用位置选项的模块生效)。 + public static void Play(MMF_Player player, FeedbackPositionMode mode, Vector3 hitPoint) + { + if (player == null) return; + if (mode == FeedbackPositionMode.HitPoint) + player.PlayFeedbacks(hitPoint); + else + player.PlayFeedbacks(); + } + } + /// /// 空对象模式实现:所有方法均为空操作,用于测试和不需要反馈的实体。 /// @@ -60,9 +98,9 @@ namespace BaseGames.Feedback { public static readonly NullFeedbackPlayer Instance = new NullFeedbackPlayer(); - public void PlayHit(HitWeight weight) { } + public void PlayHit(HitWeight weight, Vector3 hitPoint) { } public void PlayParrySuccess() { } - public void PlayTakeHit() { } + public void PlayTakeHit(Vector3 hitPoint) { } public void PlayDeath() { } public void PlayHeal() { } public void PlayLandImpact() { } @@ -74,4 +112,3 @@ namespace BaseGames.Feedback public void PlayFormSwitch(int formIndex) { } } } - diff --git a/Assets/_Game/Scripts/Feedback/PlayerFeedback.cs b/Assets/_Game/Scripts/Feedback/PlayerFeedback.cs index 4fbe520..6ab66b1 100644 --- a/Assets/_Game/Scripts/Feedback/PlayerFeedback.cs +++ b/Assets/_Game/Scripts/Feedback/PlayerFeedback.cs @@ -15,10 +15,14 @@ namespace BaseGames.Feedback [SerializeField] private MMF_Player _onHitLight; [SerializeField] private MMF_Player _onHitMedium; [SerializeField] private MMF_Player _onHitHeavy; + [Tooltip("命中反馈生成位置:Fixed = 反馈链编排的固定位置;HitPoint = 在命中点生成")] + [SerializeField] private FeedbackPositionMode _hitPositionMode = FeedbackPositionMode.Fixed; [SerializeField] private MMF_Player _onParrySuccess; [Header("受伤 / 死亡反馈")] [SerializeField] private MMF_Player _onTakeHit; + [Tooltip("受击反馈生成位置:Fixed = 反馈链编排的固定位置;HitPoint = 在受击点生成")] + [SerializeField] private FeedbackPositionMode _takeHitPositionMode = FeedbackPositionMode.Fixed; [SerializeField] private MMF_Player _onDeath; [Header("恢复反馈")] @@ -53,7 +57,7 @@ namespace BaseGames.Feedback } // ── IFeedbackPlayer ────────────────────────────────────────────────────── - public void PlayHit(HitWeight weight) + public void PlayHit(HitWeight weight, Vector3 hitPoint) { var player = weight switch { @@ -62,11 +66,12 @@ namespace BaseGames.Feedback HitWeight.Heavy => _onHitHeavy, _ => _onHitLight, }; - player?.PlayFeedbacks(); + FeedbackPlayback.Play(player, _hitPositionMode, hitPoint); } public void PlayParrySuccess() => _onParrySuccess?.PlayFeedbacks(); - public void PlayTakeHit() => _onTakeHit?.PlayFeedbacks(); + public void PlayTakeHit(Vector3 hitPoint) + => FeedbackPlayback.Play(_onTakeHit, _takeHitPositionMode, hitPoint); public void PlayDeath() => _onDeath?.PlayFeedbacks(); public void PlayHeal() => _onHeal?.PlayFeedbacks(); public void PlayLandImpact() => _onLandImpact?.PlayFeedbacks(); diff --git a/Assets/_Game/Scripts/Feedback/WeaponFeedback.cs b/Assets/_Game/Scripts/Feedback/WeaponFeedback.cs index 79b78aa..8df938f 100644 --- a/Assets/_Game/Scripts/Feedback/WeaponFeedback.cs +++ b/Assets/_Game/Scripts/Feedback/WeaponFeedback.cs @@ -14,6 +14,8 @@ namespace BaseGames.Feedback [SerializeField] private MMF_Player _onHitLight; [SerializeField] private MMF_Player _onHitMedium; [SerializeField] private MMF_Player _onHitHeavy; + [Tooltip("命中反馈生成位置:Fixed = 反馈链编排的固定位置;HitPoint = 在命中点生成(命中火花等贴点表现)")] + [SerializeField] private FeedbackPositionMode _hitPositionMode = FeedbackPositionMode.HitPoint; [Header("攻击破风")] [SerializeField] private MMF_Player _onAttackWhoosh; @@ -30,7 +32,7 @@ namespace BaseGames.Feedback // ── IFeedbackPlayer 实现 ────────────────────────────────────────────── - public void PlayHit(HitWeight weight) + public void PlayHit(HitWeight weight, Vector3 hitPoint) { var player = weight switch { @@ -38,7 +40,7 @@ namespace BaseGames.Feedback HitWeight.Heavy => _onHitHeavy, _ => _onHitMedium, }; - player?.PlayFeedbacks(); + FeedbackPlayback.Play(player, _hitPositionMode, hitPoint); } public void PlayAttackWhoosh() => _onAttackWhoosh?.PlayFeedbacks(); @@ -52,7 +54,7 @@ namespace BaseGames.Feedback // ── 武器上不适用的反馈,空实现 ──────────────────────────────────────── public void PlayParrySuccess() { } - public void PlayTakeHit() { } + public void PlayTakeHit(Vector3 hitPoint){ } public void PlayDeath() { } public void PlayHeal() { } public void PlayLandImpact() { } diff --git a/Assets/_Game/Scripts/Player/PlayerCombat.cs b/Assets/_Game/Scripts/Player/PlayerCombat.cs index 8a102df..c92780e 100644 --- a/Assets/_Game/Scripts/Player/PlayerCombat.cs +++ b/Assets/_Game/Scripts/Player/PlayerCombat.cs @@ -92,7 +92,7 @@ namespace BaseGames.Player var weight = info.FinalDamage <= 5 ? HitWeight.Light : info.FinalDamage <= 15 ? HitWeight.Medium : HitWeight.Heavy; - _feedback.PlayHit(weight); + _feedback.PlayHit(weight, info.HitPoint); // 攻击命中反嵈:向攻击反方向施加微小后退冲量,增强打击感 if (_movement?.Rb != null && info.KnockbackDirection.x != 0f) diff --git a/Assets/_Game/Scripts/Player/States/HurtState.cs b/Assets/_Game/Scripts/Player/States/HurtState.cs index 4575c70..ac9d51f 100644 --- a/Assets/_Game/Scripts/Player/States/HurtState.cs +++ b/Assets/_Game/Scripts/Player/States/HurtState.cs @@ -10,14 +10,16 @@ namespace BaseGames.Player.States /// public class HurtState : PlayerStateBase { - private float _timer; - private bool _ended; + private float _timer; + private bool _ended; + private Vector3 _hitPoint; public HurtState(PlayerController owner) : base(owner) { } public void Initialize(BaseGames.Combat.DamageInfo info) { // 由 PlayerController.TakeDamage 传入伤害信息 + _hitPoint = info.HitPoint; if (info.KnockbackForce > 0.01f) Move?.ApplyKnockback(info.KnockbackDirection, info.KnockbackForce); } @@ -28,7 +30,7 @@ namespace BaseGames.Player.States _timer = Owner.AnimConfig?.HurtDuration ?? 0.4f; _ended = false; Stats?.BeginInvincibility(); - Feedback.PlayTakeHit(); + Feedback.PlayTakeHit(_hitPoint); if (AnimCfg?.Hurt != null) { diff --git a/Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs b/Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs index 84404ca..a600c43 100644 --- a/Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs +++ b/Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs @@ -53,7 +53,7 @@ namespace BaseGames.Player var weight = info.FinalDamage <= 5 ? HitWeight.Light : info.FinalDamage <= 15 ? HitWeight.Medium : HitWeight.Heavy; - _feedback.PlayHit(weight); + _feedback.PlayHit(weight, info.HitPoint); OnHitConfirmed?.Invoke(info); if (_activeDir == AttackDirection.Down) @@ -66,7 +66,7 @@ namespace BaseGames.Player /// private void OnAnyBreakableHitConfirmed(DamageInfo info) { - _feedback.PlayHit(HitWeight.Light); + _feedback.PlayHit(HitWeight.Light, info.HitPoint); if (_activeDir == AttackDirection.Down) OnDownHitConfirmed?.Invoke(info); }