From 09bdda970a1112fcf52b868540d8e08993a92861 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Fri, 12 Jun 2026 11:48:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(feedback):=20=E5=91=BD=E4=B8=AD/=E5=8F=97?= =?UTF-8?q?=E5=87=BB=E5=8F=8D=E9=A6=88=E6=94=AF=E6=8C=81=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E4=B8=8E=E5=87=BB=E4=B8=AD=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E4=B8=A4=E7=A7=8D=E7=94=9F=E6=88=90=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DamageInfo 新增运行时字段 HitPoint:HitBox 结算时写入精确接触点 (ClosestPoint),HurtBox.ReceiveDamage 以解析后命中点兜底盖戳, 陷阱/环境等直调路径同样可用。IFeedbackPlayer.PlayHit/PlayTakeHit 增加命中点参数,全链路(武器实例→PlayerCombat→HurtState→ EnemyFeedback)透传。各 Feedback 组件命中/受击槽位新增 FeedbackPositionMode(Fixed=反馈链编排的固定位置 / HitPoint=传入命中点,启用位置选项的模块在该点生成),由编排 反馈链的开发者在 Inspector 按表现选择;武器命中默认 HitPoint (火花贴点),角色级默认 Fixed(震屏/振动与位置无关)。 Co-Authored-By: Claude Fable 5 --- Assets/_Game/Scripts/Combat/DamageInfo.cs | 7 +++ Assets/_Game/Scripts/Combat/HitBox.cs | 15 +++--- Assets/_Game/Scripts/Combat/HurtBox.cs | 3 ++ Assets/_Game/Scripts/Enemies/EnemyFeedback.cs | 13 +++-- .../_Game/Scripts/Feedback/IFeedbackPlayer.cs | 51 ++++++++++++++++--- .../_Game/Scripts/Feedback/PlayerFeedback.cs | 11 ++-- .../_Game/Scripts/Feedback/WeaponFeedback.cs | 8 +-- Assets/_Game/Scripts/Player/PlayerCombat.cs | 2 +- .../_Game/Scripts/Player/States/HurtState.cs | 8 +-- .../Scripts/Player/WeaponHitBoxInstance.cs | 4 +- 10 files changed, 93 insertions(+), 29 deletions(-) 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); }