Add WeaponFeedback component and AddressableManagerWindow meta file

- Implemented WeaponFeedback class for handling weapon-related feedbacks such as hit effects and attack sounds.
- Added meta file for AddressableManagerWindow to manage addressable assets.
- Included a new jump.data file for profiler data.
This commit is contained in:
2026-05-22 22:03:32 +08:00
parent 3e1f234ddc
commit b7baf7ad6a
44 changed files with 1783 additions and 1927 deletions

View File

@@ -1,5 +1,6 @@
using UnityEngine;
using BaseGames.Combat;
using BaseGames.Feedback;
namespace BaseGames.Player
{
@@ -14,6 +15,7 @@ namespace BaseGames.Player
private PlayerStats _stats;
private PlayerMovement _movement;
private WeaponHitBoxInstance _currentHitBoxInstance;
private IFeedbackPlayer _feedback;
/// <summary>下劈 HitBox 命中确认事件(供 DownAttackState 订阅 pogo 弹跳逻辑)。</summary>
public event System.Action<DamageInfo> OnDownHitConfirmed;
@@ -22,6 +24,9 @@ namespace BaseGames.Player
{
_stats = GetComponentInParent<PlayerStats>();
_movement = GetComponentInParent<PlayerMovement>();
_feedback = GetComponentInParent<IFeedbackPlayer>()
?? GetComponentInChildren<IFeedbackPlayer>()
?? NullFeedbackPlayer.Instance;
}
private void OnEnable()
@@ -83,6 +88,12 @@ namespace BaseGames.Player
int gain = _weaponManager?.ActiveWeapon?.soulPowerGain ?? 10;
_stats?.AddSoulPower(gain);
// 命中反馈:按伤害量决定力度档位
var weight = info.FinalDamage <= 5 ? HitWeight.Light
: info.FinalDamage <= 15 ? HitWeight.Medium
: HitWeight.Heavy;
_feedback.PlayHit(weight);
// 攻击命中反嵈:向攻击反方向施加微小后退冲量,增强打击感
if (_movement?.Rb != null && info.KnockbackDirection.x != 0f)
_movement.Rb.AddForce(

View File

@@ -46,8 +46,12 @@ namespace BaseGames.Player
private bool _facingLocked; // 为 true 时 UpdateFacing() 不覆盖朝向
private bool _cancelWindowOpen;
private SurfaceType _currentSurface = SurfaceType.Ground;
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
private int _groundHitCount;
private bool _wasGrounded;
// 跳跃/二段跳期间禁用斜坡吸附,防止把起跳判定成斜坡而立即下压
private bool _slopeSnapDisabled;
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
private int _groundHitCount;
private readonly ContactPoint2D[] _slopeContactBuffer = new ContactPoint2D[8];
#if UNITY_EDITOR
// ── 运行时调试Inspector 中可见)───────────────────────────────
@@ -118,17 +122,20 @@ namespace BaseGames.Player
_wallCoyoteTimer = Mathf.Max(0f, _wallCoyoteTimer - Time.fixedDeltaTime);
#if UNITY_EDITOR
_dbg_VelocityX = _rb.velocity.x;
_dbg_VelocityY = _rb.velocity.y;
_dbg_IsGrounded = _isGrounded;
_dbg_OnOneWayPlatform = _onOneWayPlatform;
_dbg_HasCoyoteTime = _coyoteTimer > 0f;
_dbg_HasWallCoyoteTime = _wallCoyoteTimer > 0f;
_dbg_IsWallLeft = _isWallLeft;
_dbg_IsWallRight = _isWallRight;
_dbg_CancelWindowOpen = _cancelWindowOpen;
_dbg_FacingDirection = _facingDirection;
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
// 值类型字段每帧同步(无分配)
_dbg_VelocityX = _rb.velocity.x;
_dbg_VelocityY = _rb.velocity.y;
_dbg_IsGrounded = _isGrounded;
_dbg_OnOneWayPlatform = _onOneWayPlatform;
_dbg_HasCoyoteTime = _coyoteTimer > 0f;
_dbg_HasWallCoyoteTime = _wallCoyoteTimer > 0f;
_dbg_IsWallLeft = _isWallLeft;
_dbg_IsWallRight = _isWallRight;
_dbg_CancelWindowOpen = _cancelWindowOpen;
_dbg_FacingDirection = _facingDirection;
// 字符串格式化限速到 ~10 Hz避免每帧分配
if (Time.frameCount % 6 == 0)
_dbg_Position = $"({transform.position.x:F1}, {transform.position.y:F1})";
#endif
}
@@ -175,6 +182,7 @@ namespace BaseGames.Player
{
_rb.velocity = new Vector2(_rb.velocity.x, _config.JumpForce);
_coyoteTimer = 0f;
_slopeSnapDisabled = true;
}
public void CutJump()
@@ -191,6 +199,7 @@ namespace BaseGames.Player
{
_rb.velocity = new Vector2(_rb.velocity.x, _config.DoubleJumpForce);
_coyoteTimer = 0f;
_slopeSnapDisabled = true;
}
// ── 重力 ──────────────────────────────────────────────────────────────
@@ -379,10 +388,38 @@ namespace BaseGames.Player
{
if (_groundCheck == null) return;
_wasGrounded = _isGrounded;
_groundHitCount = Physics2D.OverlapBoxNonAlloc(
_groundCheck.position, _groundCheckSize, 0f, _groundBuffer, _groundLayer);
_isGrounded = _groundHitCount > 0;
// 斜坡吸附禁用标记:仅在重新落地(从空中→地面)时重置,
// 而非每帧在地面时都重置。
// 这样 Jump() 设置的 _slopeSnapDisabled = true 可以存活到玩家真正离开地面,
// 防止起跳后的首个 FixedUpdate 仍检测到地面时把标记清零,
// 导致紧接着的斜坡吸附把垂直速度归零(即"一直按方向键起跳立即落地"bug
if (_isGrounded && !_wasGrounded)
_slopeSnapDisabled = false;
// 斜坡吸附OverlapBox 是水平矩形,在平地→斜坡转折处可能短暂离地。
// 读取 Rigidbody2D 已有的物理接触点(零额外物理查询开销),
// 接触法线 Y > 0.5 即视为地面接触,保持 IsGrounded 为 true。
if (!_isGrounded && _wasGrounded && !_slopeSnapDisabled
&& Mathf.Abs(_rb.velocity.x) > 0.1f)
{
int contactCount = _rb.GetContacts(_slopeContactBuffer);
for (int i = 0; i < contactCount; i++)
{
if (_slopeContactBuffer[i].normal.y > 0.5f)
{
_isGrounded = true;
_rb.velocity = new Vector2(_rb.velocity.x, 0f);
break;
}
}
}
// 检测是否站在单向平台(含 IDropThrough 组件的碰撞体)
_onOneWayPlatform = false;
for (int i = 0; i < _groundHitCount; i++)

View File

@@ -127,14 +127,19 @@ namespace BaseGames.Player
}
#if UNITY_EDITOR
_dbg_HP = $"{CurrentHP} / {MaxHP}";
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
// 非字符串字段每帧同步(拷贝值,无分配)
_dbg_IsInvincible = IsInvincible;
_dbg_InvincibleTimer = _invincibleTimer;
_dbg_GodMode = _isGodMode;
_dbg_Abilities = _unlockedAbilities == AbilityType.None ? "\u65e0" : _unlockedAbilities.ToString();
// 字符串插值限速到 ~10 Hz避免每帧分配GC
if (Time.frameCount % 6 == 0)
{
_dbg_HP = $"{CurrentHP} / {MaxHP}";
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
_dbg_Abilities = _unlockedAbilities == AbilityType.None ? "\u65e0" : _unlockedAbilities.ToString();
}
#endif
}
// ── 护符修改器 API ─────────────────────────────────────────────────────

View File

@@ -29,6 +29,8 @@ namespace BaseGames.Player
// 物理接触点缓冲区(避免每帧 GC
private Rigidbody2D _rb;
private static readonly ContactPoint2D[] _contactBuffer = new ContactPoint2D[8];
// LayerMask 在 Awake 解析一次,避免 FixedUpdate50Hz每帧字符串查找
private LayerMask _resolvedWallMask;
/// <summary>
/// 指定方向上是否存在墙壁接触(射线命中 OR 物理接触点命中,任一为 true
@@ -43,6 +45,7 @@ namespace BaseGames.Player
{
Debug.Assert(_config != null, "[PlayerWallDetector] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
_rb = GetComponent<Rigidbody2D>();
_resolvedWallMask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Platform");
}
private void FixedUpdate()
@@ -69,9 +72,8 @@ namespace BaseGames.Player
private bool CheckPhysicalContact(int direction)
{
if (_rb == null) return false;
LayerMask mask = _wallLayer != 0 ? _wallLayer : LayerMask.GetMask("Wall", "Platform");
var filter = new ContactFilter2D();
filter.SetLayerMask(mask);
filter.SetLayerMask(_resolvedWallMask);
filter.useTriggers = false;
int count = _rb.GetContacts(filter, _contactBuffer);
@@ -94,7 +96,7 @@ namespace BaseGames.Player
Vector2 center = transform.position;
float len = _config.WallRayLength;
float oy = _config.WallRayOffsetY;
int layer = _wallLayer != 0 ? (int)_wallLayer : LayerMask.GetMask("Wall", "Platform");
int layer = _resolvedWallMask;
bool top = Physics2D.Raycast(center + Vector2.up * oy, dir, len, layer);
bool bot = Physics2D.Raycast(center + Vector2.down * oy, dir, len, layer);

View File

@@ -82,6 +82,8 @@ namespace BaseGames.Player.States
? AnimCfg.DashInvincible
: AnimCfg?.Dash;
if (dashClip != null) Anim?.Play(dashClip);
Feedback.TriggerPreset("dash");
}
public override void OnStateUpdate()

View File

@@ -19,6 +19,8 @@ namespace BaseGames.Player.States
if (Owner.HurtBox != null)
Owner.HurtBox.SetActive(false);
Feedback.PlayDeath();
// 播放死亡动画
if (AnimCfg?.Dead != null)
Anim?.Play(AnimCfg.Dead);

View File

@@ -28,6 +28,7 @@ namespace BaseGames.Player.States
_timer = Owner.AnimConfig?.HurtDuration ?? 0.4f;
_ended = false;
Stats?.BeginInvincibility();
Feedback.PlayTakeHit();
if (AnimCfg?.Hurt != null)
{

View File

@@ -4,6 +4,7 @@ using Animancer;
using BaseGames.Core.Events;
using BaseGames.Input;
using BaseGames.Combat;
using BaseGames.Feedback;
using BaseGames.Parry;
using BaseGames.Skills;
@@ -35,6 +36,7 @@ namespace BaseGames.Player.States
// ── 战斗组件 ──────────────────────────────────────────────────────────
[Header("战斗")]
[SerializeField] private PlayerFeedback _feedback;
[SerializeField] private PlayerCombat _combat;
[SerializeField] private FormController _formController;
[SerializeField] private WeaponManager _weaponManager;
@@ -57,6 +59,8 @@ namespace BaseGames.Player.States
private InputBuffer _inputBuffer;
private bool _missingDependencyLogged;
private bool _dependenciesReady;
// DashState 在 Update 每帧访问TickCooldown + CanDash提前缓存避免重复 Dictionary 查找
private DashState _dashState;
/// <summary>
/// 当前腾空可用的额外跳跃次数(二段跳)。
/// 由 IdleState/RunState.OnStateEnter 落地时通过 ResetAirJumps() 重置;
@@ -128,6 +132,7 @@ namespace BaseGames.Player.States
public InputReaderSO Input => _inputReader;
public InputBuffer Buffer => _inputBuffer;
public IFeedbackPlayer Feedback => _feedback != null ? (IFeedbackPlayer)_feedback : NullFeedbackPlayer.Instance;
public PlayerCombat Combat => _combat;
public FormController Form => _formController;
public WeaponManager Weapon => _weaponManager;
@@ -272,6 +277,7 @@ namespace BaseGames.Player.States
{
_stats?.AddSoul(info.SoulGained);
_shield?.OnParrySuccess();
Feedback.PlayParrySuccess();
}
/// <summary>灵泉输入:地面且有剩余充能时转入 SpringState 使用一次。</summary>
@@ -301,7 +307,7 @@ namespace BaseGames.Player.States
return;
// 冲刺冷却计时
GetState<DashState>()?.TickCooldown(Time.deltaTime);
_dashState?.TickCooldown(Time.deltaTime);
_currentState?.OnStateUpdate();
@@ -309,7 +315,7 @@ namespace BaseGames.Player.States
_dbg_CurrentState = _currentState?.GetType().Name ?? "None";
_dbg_IsGrounded = _movement != null && _movement.IsGrounded;
_dbg_AirJumpsLeft = _airJumpsLeft;
_dbg_CanDash = GetState<DashState>()?.CanDash ?? false;
_dbg_CanDash = _dashState?.CanDash ?? false;
_dbg_IsInvincible = _stats != null && _stats.IsInvincible;
#endif
}
@@ -359,8 +365,9 @@ namespace BaseGames.Player.States
_states[typeof(HurtState)] = new HurtState(this);
_states[typeof(DeadState)] = new DeadState(this);
_states[typeof(SpringState)] = new SpringState(this);
_states[typeof(ParryState)] = new ParryState(this);
_states[typeof(SwimState)] = new SwimState(this);
_states[typeof(ParryState)] = new ParryState(this);
_states[typeof(SwimState)] = new SwimState(this);
_dashState = (DashState)_states[typeof(DashState)];
}
/// <summary>

View File

@@ -1,4 +1,5 @@
using Animancer;
using BaseGames.Feedback;
using BaseGames.Input;
using BaseGames.Player;
@@ -41,6 +42,7 @@ namespace BaseGames.Player.States
protected InputBuffer Buffer => _owner.Buffer;
protected PlayerMovement Move => _owner.Movement;
protected PlayerStats Stats => _owner.Stats;
protected IFeedbackPlayer Feedback => _owner.Feedback;
protected AnimancerComponent Anim => _owner.Animancer;
protected PlayerMovementConfigSO Cfg => _owner.MovConfig;
protected PlayerAnimationConfigSO AnimCfg => _owner.AnimConfig;

View File

@@ -49,8 +49,9 @@ namespace BaseGames.Player.States
private void OnSpringEnd()
{
// 前摇正常结束 → 执行回血
// 前摇正常结束 → 执行回血 + 反馈
Stats?.ApplySpringHeal();
Feedback.PlayHeal();
Owner.TransitionTo(Owner.GetState<IdleState>());
}
}

View File

@@ -1,5 +1,6 @@
using UnityEngine;
using BaseGames.Combat;
using BaseGames.Feedback;
namespace BaseGames.Player
{
@@ -28,6 +29,7 @@ namespace BaseGames.Player
private HitBox[] _allHitBoxes;
private AttackDirection _activeDir;
private IFeedbackPlayer _feedback;
/// <summary>下劈命中确认事件(供 DownAttackState Pogo 逻辑)。</summary>
public event System.Action<DamageInfo> OnDownHitConfirmed;
@@ -40,10 +42,16 @@ namespace BaseGames.Player
_allHitBoxes = GetComponentsInChildren<HitBox>(true);
foreach (var hb in _allHitBoxes)
hb.OnHitConfirmed += OnAnyHitConfirmed;
_feedback = GetComponentInChildren<IFeedbackPlayer>() ?? NullFeedbackPlayer.Instance;
}
private void OnAnyHitConfirmed(DamageInfo info)
{
var weight = info.FinalDamage <= 5 ? HitWeight.Light
: info.FinalDamage <= 15 ? HitWeight.Medium
: HitWeight.Heavy;
_feedback.PlayHit(weight);
OnHitConfirmed?.Invoke(info);
if (_activeDir == AttackDirection.Down)
OnDownHitConfirmed?.Invoke(info);
@@ -59,6 +67,7 @@ namespace BaseGames.Player
string hitBoxId = "")
{
_activeDir = dir;
_feedback.PlayAttackWhoosh();
var hitBox = string.IsNullOrEmpty(hitBoxId)
? GetHitBox(dir)
: (GetHitBoxById(hitBoxId) ?? GetHitBox(dir));

View File

@@ -59,6 +59,9 @@ namespace BaseGames.Player
[Min(0)]
public int soulPowerGain = 10;
[Tooltip("命中敌人时的打击力度反馈档位(影响摄像机震屏和控制器振动强度)。")]
public HitWeight hitWeight = HitWeight.Medium;
// ── 查询 API ──────────────────────────────────────────────────────────
/// <summary>取指定方向、指定段的完整配置,越界自动取最后一个。</summary>