Files
zeling_v2/Assets/_Game/Scripts/Combat/Projectile.cs
Joywayer 5922ef373d feat(combat): 弹反投射物伤害目标层改为显式配置
ProjectileConfigSO 新增 ReflectedTargetLayers:弹反后写入 HitBox.TargetLayers 的目标层显式配置;留空(Nothing)有明确缺省语义=自动翻转(PlayerHurtBox 位换 EnemyHurtBox 位、其余位保留)。Projectile/ParryableProjectile 两条弹反路径统一走 ApplyReflectedTargetLayers。现有 6 个投射物配置资产已显式配为 EnemyHurtBox。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 10:43:57 +08:00

153 lines
6.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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; // 归还对象池后重置,防止未初始化时自毁
}
}
}