ProjectileConfigSO 新增 ReflectedTargetLayers:弹反后写入 HitBox.TargetLayers 的目标层显式配置;留空(Nothing)有明确缺省语义=自动翻转(PlayerHurtBox 位换 EnemyHurtBox 位、其余位保留)。Projectile/ParryableProjectile 两条弹反路径统一走 ApplyReflectedTargetLayers。现有 6 个投射物配置资产已显式配为 EnemyHurtBox。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
153 lines
6.5 KiB
C#
153 lines
6.5 KiB
C#
using UnityEngine;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Pool;
|
||
|
||
namespace BaseGames.Combat
|
||
{
|
||
/// <summary>
|
||
/// 抛射物基类。子类通过重写 <see cref="OnInitialized"/> 设定初速度。
|
||
/// 依赖 <see cref="HitBox"/> 子组件进行碰撞伤害检测。
|
||
/// </summary>
|
||
[RequireComponent(typeof(Rigidbody2D), typeof(HitBox))]
|
||
public abstract class Projectile : MonoBehaviour
|
||
{
|
||
[HideInInspector] public DamageInfo DamageInfo;
|
||
[HideInInspector] public Vector2 Direction;
|
||
|
||
protected ProjectileConfigSO _config;
|
||
protected Rigidbody2D _rb;
|
||
protected HitBox _hitBox;
|
||
protected float _aliveTimer;
|
||
// Lifetime 在 Initialize 时缓存,避免 Update 每帧访问 SO 成员并做 null check
|
||
private float _lifetime = float.MaxValue;
|
||
|
||
// 命中预算:剩余可命中次数(<=0 的 _maxHits 表示无限穿透)
|
||
private int _maxHits = 1;
|
||
private int _hitsRemaining;
|
||
// 弹反帧标记:弹反命中不计入命中预算(避免反射后立即被回收)
|
||
private bool _justReflected;
|
||
// 预制体上配置的初始伤害目标层:对象池复用时还原,避免上一发弹反翻转后的掩码污染下一发
|
||
private LayerMask _initialTargetLayers;
|
||
|
||
private PooledObject _pooledObject;
|
||
|
||
protected virtual void Awake()
|
||
{
|
||
_rb = GetComponent<Rigidbody2D>();
|
||
_hitBox = GetComponent<HitBox>();
|
||
_pooledObject = GetComponent<PooledObject>();
|
||
_initialTargetLayers = _hitBox.TargetLayers;
|
||
// 订阅命中确认:按命中预算决定何时回收(穿透 / 命中即消失)
|
||
_hitBox.OnHitConfirmed += HandleHitConfirmed;
|
||
}
|
||
|
||
/// <summary>从对象池取出后的初始化入口。</summary>
|
||
public virtual void Initialize(ProjectileConfigSO config, DamageInfo damageInfo, Vector2 direction, int ownerLayer = 0)
|
||
{
|
||
_config = config;
|
||
_lifetime = config.Lifetime;
|
||
DamageInfo = damageInfo;
|
||
Direction = direction.normalized;
|
||
_aliveTimer = 0f;
|
||
_maxHits = config.MaxHits;
|
||
_hitsRemaining = config.MaxHits;
|
||
_justReflected = false;
|
||
// 还原预制体配置的伤害目标层(清除上一次弹反翻转的影响)
|
||
_hitBox.TargetLayers = _initialTargetLayers;
|
||
|
||
SetFactionLayer(ownerLayer);
|
||
_hitBox.Activate(config.DamageSource);
|
||
OnInitialized();
|
||
}
|
||
|
||
/// <summary>根据发射方所在 Layer 切换到对应的 PlayerProjectile / EnemyProjectile 层。</summary>
|
||
private void SetFactionLayer(int ownerLayer)
|
||
{
|
||
int playerLayer = LayerMask.NameToLayer("Player");
|
||
int playerProjLayer = LayerMask.NameToLayer("PlayerProjectile");
|
||
int enemyProjLayer = LayerMask.NameToLayer("EnemyProjectile");
|
||
if (playerProjLayer < 0 || enemyProjLayer < 0) return; // Layer 尚未创建,保留现有层
|
||
gameObject.layer = (ownerLayer == playerLayer) ? playerProjLayer : enemyProjLayer;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 弹反:将投射物阵营从 EnemyProjectile 切换为 PlayerProjectile。
|
||
/// 反转飞行方向,并重置 HitBox 命中记录使其能够命中新目标(敌人)。
|
||
/// 由 HurtBox.ReceiveDamage() 在弹反成功后调用。
|
||
/// </summary>
|
||
public virtual void ReflectAsPlayerProjectile()
|
||
{
|
||
int playerProjLayer = LayerMask.NameToLayer("PlayerProjectile");
|
||
if (playerProjLayer < 0) return;
|
||
|
||
gameObject.layer = playerProjLayer;
|
||
Direction = -Direction;
|
||
_rb.velocity = -_rb.velocity;
|
||
|
||
// 重置 HitBox 命中记录,确保反射后可命中新目标
|
||
_hitBox.Deactivate();
|
||
_hitBox.Activate(_config?.DamageSource);
|
||
// 伤害目标随阵营切换(优先取配置的显式目标层)
|
||
ApplyReflectedTargetLayers();
|
||
|
||
// 反射后重置命中预算,并跳过本次(弹反)命中的扣减,避免反射后立即被回收
|
||
_hitsRemaining = _maxHits;
|
||
_justReflected = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 弹反换阵营后应用伤害目标层:
|
||
/// 优先使用 <see cref="ProjectileConfigSO.ReflectedTargetLayers"/> 的显式配置;
|
||
/// 未配置(Nothing)时自动翻转——仅交换 PlayerHurtBox→EnemyHurtBox 位,
|
||
/// 保留可破坏物等其他目标层。
|
||
/// </summary>
|
||
protected void ApplyReflectedTargetLayers()
|
||
{
|
||
if (_config != null && _config.ReflectedTargetLayers.value != 0)
|
||
{
|
||
_hitBox.TargetLayers = _config.ReflectedTargetLayers;
|
||
return;
|
||
}
|
||
int playerHurt = LayerMask.NameToLayer("PlayerHurtBox");
|
||
int enemyHurt = LayerMask.NameToLayer("EnemyHurtBox");
|
||
if (playerHurt < 0 || enemyHurt < 0) return; // Layer 尚未创建,保留现有掩码
|
||
int mask = _hitBox.TargetLayers.value;
|
||
mask &= ~(1 << playerHurt);
|
||
mask |= 1 << enemyHurt;
|
||
_hitBox.TargetLayers = mask;
|
||
}
|
||
|
||
/// <summary>HitBox 命中确认回调:按命中预算决定是否回收(穿透 / 命中即消失)。</summary>
|
||
private void HandleHitConfirmed(DamageInfo _)
|
||
{
|
||
if (_maxHits <= 0) return; // 无限穿透:只靠寿命回收
|
||
if (_justReflected) { _justReflected = false; return; } // 弹反命中不计入预算
|
||
if (--_hitsRemaining <= 0) ReturnToPool();
|
||
}
|
||
|
||
protected virtual void OnInitialized() { }
|
||
|
||
protected virtual void Update()
|
||
{
|
||
_aliveTimer += Time.deltaTime;
|
||
if (_aliveTimer >= _lifetime)
|
||
ReturnToPool();
|
||
}
|
||
|
||
/// <summary>停用并归还对象池。</summary>
|
||
protected void ReturnToPool()
|
||
{
|
||
_hitBox.Deactivate();
|
||
gameObject.SetActive(false);
|
||
if (_pooledObject != null && _config != null)
|
||
ServiceLocator.GetOrDefault<IObjectPoolService>()?.Despawn(_config.PoolKey, _pooledObject);
|
||
}
|
||
|
||
protected virtual void OnDisable()
|
||
{
|
||
_aliveTimer = 0f;
|
||
_lifetime = float.MaxValue; // 归还对象池后重置,防止未初始化时自毁
|
||
}
|
||
}
|
||
}
|