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:
@@ -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(
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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 ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace BaseGames.Player
|
||||
// 物理接触点缓冲区(避免每帧 GC)
|
||||
private Rigidbody2D _rb;
|
||||
private static readonly ContactPoint2D[] _contactBuffer = new ContactPoint2D[8];
|
||||
// LayerMask 在 Awake 解析一次,避免 FixedUpdate(50Hz)每帧字符串查找
|
||||
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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace BaseGames.Player.States
|
||||
_timer = Owner.AnimConfig?.HurtDuration ?? 0.4f;
|
||||
_ended = false;
|
||||
Stats?.BeginInvincibility();
|
||||
Feedback.PlayTakeHit();
|
||||
|
||||
if (AnimCfg?.Hurt != null)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -49,8 +49,9 @@ namespace BaseGames.Player.States
|
||||
|
||||
private void OnSpringEnd()
|
||||
{
|
||||
// 前摇正常结束 → 执行回血
|
||||
// 前摇正常结束 → 执行回血 + 反馈
|
||||
Stats?.ApplySpringHeal();
|
||||
Feedback.PlayHeal();
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -59,6 +59,9 @@ namespace BaseGames.Player
|
||||
[Min(0)]
|
||||
public int soulPowerGain = 10;
|
||||
|
||||
[Tooltip("命中敌人时的打击力度反馈档位(影响摄像机震屏和控制器振动强度)。")]
|
||||
public HitWeight hitWeight = HitWeight.Medium;
|
||||
|
||||
// ── 查询 API ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>取指定方向、指定段的完整配置,越界自动取最后一个。</summary>
|
||||
|
||||
Reference in New Issue
Block a user