chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,517 @@
# 13 · 弹射物系统与对象池
> **命名空间** `BaseGames.Combat`(扩展)· `BaseGames.Core`ObjectPool
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
> **依赖** `BaseGames.Core.Events` · `BaseGames.Combat`DamageInfo / HurtBox· `BaseGames.Parry`(弹反流程)
---
## 目录
1. [系统总览](#1-系统总览)
2. [Projectile 组件](#2-projectile-组件)
3. [弹射物类型](#3-弹射物类型)
4. [可弹反弹射物ParryableProjectile](#4-可弹反弹射物parryableprojectile)
5. [GlobalObjectPool — 对象池架构](#5-globalobjectpool--对象池架构)
6. [ProjectileConfigSO](#6-projectileconfigso)
7. [BD_SpawnProjectile Task 实现](#7-bd_spawnprojectile-task-实现)
8. [碰撞处理流程](#8-碰撞处理流程)
9. [跨场景 Pool 回收](#9-跨场景-pool-回收)
10. [事件频道](#10-事件频道)
11. [编辑器友好设计](#11-编辑器友好设计)
---
## 1. 系统总览
弹射物系统解决以下问题:
| 问题 | 方案 |
|------|------|
| 敌人频繁射击产生大量 GameObject | `GlobalObjectPool` 对象池复用,避免 GC |
| 弹射物需携带完整伤害信息 | `Projectile` 持有 `DamageInfo` 结构体 |
| 弹反机制需要反弹弹射物 | `ParryableProjectile` 扩展类,`OnParried()` 反转方向 |
| 不同弹射物行为差异大 | 类型枚举 + 配置 SO同一组件支持多种轨迹 |
| 场景切换时池对象泄漏 | `SceneLoader` 触发 Pool 回收(见 §9|
---
## 2. Projectile 组件
`Projectile` 是所有弹射物的基类,挂载在弹射物 Prefab 根对象上:
```csharp
namespace BaseGames.Combat
{
[RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))]
public class Projectile : MonoBehaviour
{
// --- 运行时数据(从 ProjectileConfigSO 注入)---
[HideInInspector] public DamageInfo DamageInfo; // 由 BD_SpawnProjectile 注入
[HideInInspector] public Vector2 Direction; // 归一化发射方向
protected ProjectileConfigSO _config;
protected Rigidbody2D _rb;
protected float _aliveTimer;
// 初始化(对象池取出时调用,替代 Awake/Start
public virtual void Initialize(ProjectileConfigSO config, DamageInfo damageInfo, Vector2 direction)
{
_config = config;
DamageInfo = damageInfo;
Direction = direction.normalized;
_aliveTimer = 0f;
_rb = GetComponent<Rigidbody2D>();
OnInitialized();
}
protected virtual void OnInitialized() { }
protected virtual void Update()
{
_aliveTimer += Time.deltaTime;
if (_aliveTimer >= _config.lifetime)
ReturnToPool();
}
protected void ReturnToPool()
=> GlobalObjectPool.Instance.Return(_config.poolKey, gameObject);
// 碰撞在子类或此处处理(见 §8
protected virtual void OnTriggerEnter2D(Collider2D other) { }
}
}
```
---
## 3. 弹射物类型
通过 `ProjectileConfig.moveType` 枚举决定运动方式,同一个 `Projectile` 类支持三种轨迹:
### 3.1 LinearProjectile直线
```csharp
public class LinearProjectile : Projectile
{
protected override void OnInitialized()
=> _rb.velocity = Direction * _config.speed;
// 速度由物理引擎维持,不需要每帧更新
}
```
用途:弓箭手普通箭矢、法师小型法球。
### 3.2 ArcProjectile抛物线
```csharp
public class ArcProjectile : Projectile
{
protected override void OnInitialized()
{
// 给定角度和速度,分解为 X/Y 分量
float angle = _config.launchAngleDeg * Mathf.Deg2Rad;
_rb.velocity = new Vector2(
Direction.x * _config.speed * Mathf.Cos(angle),
_config.speed * Mathf.Sin(angle)
);
_rb.gravityScale = _config.gravityScale; // 受重力影响
}
}
```
用途:投石、毒液弹、炸弹投掷。
### 3.3 HomingProjectile追踪
> **【违规修复】** 原实现使用 `GameObject.FindGameObjectWithTag("Player")`,违反零耦合原则(见 `00_Overview §2.1`)。
> 已修正为通过 `TransformEventChannelSO` 事件频道注入玩家 Transform。
```csharp
public class HomingProjectile : Projectile
{
[SerializeField] TransformEventChannelSO _onPlayerSpawned; // 注入依赖SO 引用)
Transform _target;
void OnEnable()
=> _onPlayerSpawned.OnEventRaised += SetTarget;
void OnDisable()
=> _onPlayerSpawned.OnEventRaised -= SetTarget;
void SetTarget(Transform t) => _target = t;
protected override void OnInitialized()
{
// 若 _target 仍为 null极少数情况玩家未在事件频道广播时已存在
// ProjectileManager 在发射时传入缓存值(见下方 ProjectileManager 说明)
_rb.velocity = Direction * _config.speed;
}
protected override void Update()
{
base.Update();
if (_target == null) return;
Vector2 toTarget = ((Vector2)_target.position - (Vector2)transform.position).normalized;
Vector2 newVel = Vector2.MoveTowards(
_rb.velocity.normalized, toTarget,
_config.homingStrength * Time.deltaTime) * _config.speed;
_rb.velocity = newVel;
}
}
```
**`ProjectileManager` 辅助缓存**(常驻 Persistent 场景,挂载于 GlobalObjectPool 同级):
```csharp
public class ProjectileManager : MonoBehaviour
{
[SerializeField] TransformEventChannelSO _onPlayerSpawned;
Transform _playerTransform;
void OnEnable() => _onPlayerSpawned.OnEventRaised += t => _playerTransform = t;
void OnDisable() => _onPlayerSpawned.OnEventRaised -= t => _playerTransform = t;
/// <summary>
/// 发射追踪弹时,由发射方调用,传入已缓存的玩家 Transform。
/// 保证即使玩家在本帧出生HomingProjectile 也能立即锁定目标。
/// </summary>
public void LaunchHoming(HomingProjectile proj, Transform origin, Vector2 direction)
{
proj.SetTarget(_playerTransform); // 主动注入缓存值
proj.Initialize(direction, origin);
}
public Transform PlayerTransform => _playerTransform;
}
```
**`PlayerController.Awake` 中广播**
```csharp
// PlayerController.cs
[SerializeField] TransformEventChannelSO _onPlayerSpawned;
void Awake()
{
// ... 其他初始化
_onPlayerSpawned.Raise(transform); // 广播自身 Transform供 HomingProjectile 订阅
}
```
用途追踪蜂群、Boss 阶段特殊弹。追踪强度 `homingStrength`(弧度/秒)由 SO 配置。
---
## 4. 可弹反弹射物ParryableProjectile
弹反机制的核心爽点之一:玩家成功弹反标有 `CanBeParried` 的弹射物后,弹射物反向飞回攻击者:
```csharp
public class ParryableProjectile : LinearProjectile
{
bool _isParried = false;
// ParrySystem 在弹反判定成功时调用此方法
public void OnParried(Transform parryer)
{
if (_isParried) return; // 防止重复弹反
_isParried = true;
// 1. 方向反转
Direction = -Direction;
_rb.velocity = Direction * _config.speed * _config.parrySpeedMultiplier; // 弹反后速度可提升(默认 1.2×
// 2. 更新 DamageInfo攻击者变为玩家伤害变为反弹伤害
DamageInfo = new DamageInfo(
attacker: parryer,
receiver: null, // 待碰撞时确定
damage: Mathf.RoundToInt(DamageInfo.Damage * _config.parryDamageMultiplier),
direction: Direction,
flags: DamageFlags.IsProjectile | DamageFlags.IsParried,
soulGain: 0
);
// 3. 切换碰撞层:弹射物现在只伤害敌人,不再伤害玩家
gameObject.layer = LayerMask.NameToLayer("PlayerProjectile");
// 4. 视觉/音效反馈
_onParryableProjectileParried.Raise(); // 订阅者Feel MMF_Flash 金色闪光
}
}
```
### ParrySystem 的弹反检测扩展
`ParrySystem` 已有弹反窗口检测(见 [05_ParrySystem.md](./05_ParrySystem.md)),在 `OnParrySuccess` 中增加弹射物检测:
```csharp
// ParrySystem.OnTriggerEnter2DParryHitBox 激活时):
if (other.TryGetComponent<ParryableProjectile>(out var proj))
{
proj.OnParried(transform);
_onPlayerParrySuccess.Raise(/* DamageInfo */);
// 触发子弹时间、音效、特效(与普通弹反相同流程)
}
```
---
## 5. GlobalObjectPool — 对象池架构
`GlobalObjectPool` 常驻 Persistent 场景,管理全游戏的对象池:
```csharp
namespace BaseGames.Core
{
public class GlobalObjectPool : MonoBehaviour
{
public static GlobalObjectPool Instance { get; private set; }
// Key: PoolKeySO 资产引用,唯一标识), Value: 对象队列
readonly Dictionary<PoolKeySO, Queue<GameObject>> _pools = new();
// 从 Inspector 配置预热清单
[SerializeField] PoolPrewarmEntry[] _prewarmEntries;
async void Awake()
{
Instance = this;
foreach (var entry in _prewarmEntries)
await PrewarmAsync(entry.key, entry.prefabRef, entry.count);
}
async Task PrewarmAsync(PoolKeySO key, AssetReferenceGameObject prefabRef, int count)
{
// 通过 Addressables 加载 Prefab 资源Handle 保留,池销毁时 Release
var handle = Addressables.LoadAssetAsync<GameObject>(prefabRef);
await handle.Task;
var prefab = handle.Result;
_handles[key] = handle; // 记录 handle 以便最终 Release
if (!_pools.ContainsKey(key)) _pools[key] = new Queue<GameObject>();
for (int i = 0; i < count; i++)
{
var obj = Object.Instantiate(prefab, transform);
obj.SetActive(false);
_pools[key].Enqueue(obj);
}
}
public GameObject Get(PoolKeySO key, GameObject prefab)
{
if (!_pools.ContainsKey(key)) _pools[key] = new Queue<GameObject>();
var pool = _pools[key];
if (pool.Count > 0)
{
var obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
return Object.Instantiate(prefab); // 池耗尽时动态扩容
}
public void Return(PoolKeySO key, GameObject obj)
{
obj.SetActive(false);
obj.transform.SetParent(transform);
if (!_pools.ContainsKey(key)) _pools[key] = new Queue<GameObject>();
_pools[key].Enqueue(obj);
}
// 场景切换时回收指定 key 的所有活跃对象(见 §9
public void RecycleAll(PoolKeySO key) { ... }
}
}
```
### 预热清单Inspector 配置)
| PoolKey | Prefab | 预热数量 | 说明 |
|---------|--------|---------|------|
| `Pool_Projectile_Arrow` | Arrow Prefab | 10 | 弓箭手弹射物 |
| `Pool_Projectile_MagicBall` | MagicBall Prefab | 8 | 法师弹射物 |
| `Pool_Collectible_GeoSmall` | GeoSmall Prefab | 20 | 小额 Geo |
| `Pool_Collectible_GeoMedium` | GeoMedium Prefab | 10 | 中额 Geo |
| `Pool_FX_HitSpark` | HitSpark Prefab | 15 | 命中粒子特效 |
| `Pool_UI_FloatingText` | FloatingText Prefab | 10 | 浮动伤害数字 |
### PoolKeySO
使用 `ScriptableObject` 作为字典 Key避免字符串 Key 的拼写错误风险:
```csharp
[CreateAssetMenu(menuName = "Pool/PoolKey")]
public class PoolKeySO : ScriptableObject { }
// 每种对象类型创建一个 PoolKey.asset在 Inspector 中拖拽引用
```
---
## 6. ProjectileConfigSO
每种弹射物类型一个 SO 资产:
```csharp
[CreateAssetMenu(menuName = "Combat/ProjectileConfig")]
public class ProjectileConfigSO : ScriptableObject
{
[Header("基础属性")]
public float speed = 8f;
public float lifetime = 5f; // 超时自动回池
public int damage = 10;
public bool canBeParried = false;
[Header("弹射物类型")]
public ProjectileMoveType moveType = ProjectileMoveType.Linear;
[Header("Linear 参数")]
// LinearProjectile 使用 speed 即可)
[Header("Arc 参数")]
public float launchAngleDeg = 45f;
public float gravityScale = 1f;
[Header("Homing 参数")]
public float homingStrength = 180f; // 度/秒
[Header("弹反参数")]
public float parrySpeedMultiplier = 1.2f;
public float parryDamageMultiplier = 1.5f;
[Header("对象池")]
public PoolKeySO poolKey;
public AssetReferenceGameObject prefabRef; // Addressable 引用,替代裸 GameObject
[Header("视觉")]
public AssetReferenceGameObject hitFXPrefabRef; // 命中时特效Addressable也走对象池
}
```
资产路径:`Assets/ScriptableObjects/Config/Combat/PC_{ProjectileName}.asset`
---
## 7. BD_SpawnProjectile Task 实现
BehaviorDesigner Action Task敌人 AI 中调用:
```csharp
namespace BaseGames.Enemies.AI
{
public class BD_SpawnProjectile : ActionTask
{
[SerializeField] SharedTransform spawnPoint; // 发射点 Transform
[SerializeField] ProjectileConfigSO projectileConfig;
[SerializeField] SharedBool aimAtPlayer = true;
[SerializeField] SharedVector2 fixedDirection; // aimAtPlayer=false 时使用固定方向
EnemyStatsSO _stats;
public override void OnStart()
{
_stats = (GetDefaultGameObject().GetComponent<EnemyBase>()).Stats;
// 计算发射方向
Vector2 direction = aimAtPlayer.Value
? ((Vector2)(Owner.transform.position - spawnPoint.Value.position)).normalized * -1f
: fixedDirection.Value.normalized;
// 注:方向从敌人指向玩家
// 构建 DamageInfo
var dmgInfo = new DamageInfo(
attacker: Owner.transform,
receiver: null,
damage: projectileConfig.damage,
direction: direction,
flags: DamageFlags.IsProjectile,
soulGain: 0
);
// 从对象池取出弹射物
var obj = GlobalObjectPool.Instance.Get(projectileConfig.poolKey, projectileConfig.prefab);
obj.transform.position = spawnPoint.Value.position;
obj.GetComponent<Projectile>().Initialize(projectileConfig, dmgInfo, direction);
}
public override TaskStatus OnUpdate() => TaskStatus.Success;
}
}
```
---
## 8. 碰撞处理流程
`Projectile.OnTriggerEnter2D` 统一处理命中:
```csharp
protected override void OnTriggerEnter2D(Collider2D other)
{
// 1. 检查碰撞层(使用 LayerMask 过滤)
if (!_config.hitLayers.Contains(other.gameObject.layer)) return;
// 2. 尝试获取 HurtBox
if (other.TryGetComponent<HurtBox>(out var hurtBox))
{
DamageInfo.Receiver = other.transform; // 填充接收方
hurtBox.TakeDamage(DamageInfo);
}
// 3. 命中特效(从对象池取出 HitFX播放完毕后自动回池
if (_config.hitFXPrefab != null)
{
var fx = GlobalObjectPool.Instance.Get(_config.hitFXPool, _config.hitFXPrefab);
fx.transform.position = transform.position;
// FX Prefab 自带 AutoReturn 组件,粒子播放完毕后调用 ReturnToPool()
}
// 4. 回池(弹射物消失)
// 特殊情况穿透型弹射物_config.penetrating=true不立即回池
if (!_config.penetrating)
ReturnToPool();
}
```
### 碰撞层矩阵规则
| 弹射物层 | 可命中层 | 说明 |
|---------|---------|------|
| `EnemyProjectile` | `Player``Ground` | 敌人弹射物 |
| `PlayerProjectile` | `Enemy``Ground` | 弹反后的弹射物 |
---
## 9. 跨场景 Pool 回收
房间切换Unload 旧场景)时,场景内弹射物的目标(敌人/玩家)已被销毁,需要回收所有活跃弹射物:
```csharp
// SceneLoader.LoadRoom() 在 Unload 旧场景前:
GlobalObjectPool.Instance.RecycleAll(Pool_Projectile_Arrow);
GlobalObjectPool.Instance.RecycleAll(Pool_Projectile_MagicBall);
// ... 其他弹射物类型
// RecycleAll 实现找到所有活跃的该类型对象SetActive(false) 并入队
```
**替代方案**Projectile 在 `OnDisable()` 中自动检测 return若不是主动回池而是被场景卸载触发 OnDisable则不操作让场景卸载直接清理。由于弹射物的 Pool 位于 Persistent 场景,不会随房间卸载,需要在 SceneLoader 中显式回收。
---
## 10. 事件频道
| 资产名 | 类型 | 用途 |
|--------|------|------|
| `OnParryableProjectileParried.asset` | `VoidEventChannelSO` | 弹反弹射物时触发,启动 Feel 特效 |
---
## 11. 编辑器友好设计
- `ProjectileConfigSO` Custom Inspector预览弹射物轨迹根据 `moveType` 在 Inspector 底部绘制轨迹示意图)
- `GlobalObjectPool` Inspector实时显示每个 Pool Key 的队列大小 + 已借出数量
- 弹射物 Prefab Gizmo在 Scene View 显示 HitBox 碰撞体范围(黄色矩形)
- `BD_SpawnProjectile` 在 BT 节点上显示弹射物名称标签(自定义 BD NodeView