- Add RoomStreamingManager to manage room loading and unloading based on player proximity. - Create StreamingBudgetConfigSO for memory and performance budgeting of the streaming system. - Introduce TransitionDirector to handle seamless and atmospheric fade transitions between rooms. - Develop WorldGraph to represent room connectivity and facilitate neighbor queries and distance calculations. - Implement RoomNode and RoomEdge classes to structure room data and connections.
127 lines
4.5 KiB
C#
127 lines
4.5 KiB
C#
using System.Collections;
|
|
using UnityEngine;
|
|
using BaseGames.Combat;
|
|
using BaseGames.Core;
|
|
using BaseGames.Core.Pool;
|
|
|
|
namespace BaseGames.Enemies.Abilities
|
|
{
|
|
/// <summary>
|
|
/// 远程射弹能力(扇形 / 弹幕)。
|
|
/// 每段攻击按 projectileFireT 触发,从池中生成 projectileCount 个抛射物,
|
|
/// 按 spreadAngleDeg 均布角度。
|
|
/// </summary>
|
|
public sealed class ProjectileAttackAbility : EnemyAbilityBase
|
|
{
|
|
[Header("发射点(为空则使用本物体位置)")]
|
|
[SerializeField] private Transform _muzzle;
|
|
|
|
[Header("行为")]
|
|
[SerializeField] private bool _faceTargetOnStart = true;
|
|
|
|
private IObjectPoolService _pool;
|
|
|
|
protected override void Awake()
|
|
{
|
|
base.Awake();
|
|
if (_muzzle == null) _muzzle = _transform;
|
|
}
|
|
|
|
protected override IEnumerator ExecuteCoroutine()
|
|
{
|
|
_pool ??= ServiceLocator.GetOrDefault<IObjectPoolService>();
|
|
var seq = _config != null ? _config.attackSequence : null;
|
|
if (seq == null || seq.Length == 0) yield break;
|
|
|
|
if (_faceTargetOnStart && _enemy != null && _enemy.PlayerTransform != null)
|
|
FaceTarget(_enemy.PlayerTransform);
|
|
|
|
for (int i = 0; i < seq.Length; i++)
|
|
{
|
|
var atk = seq[i];
|
|
if (atk == null) continue;
|
|
yield return PlayShot(atk);
|
|
if (atk.postDelay > 0f) yield return EnemyAbilityWaits.Get(atk.postDelay);
|
|
}
|
|
}
|
|
|
|
private IEnumerator PlayShot(EnemyAttackSO atk)
|
|
{
|
|
Phase = AbilityRunState.Active;
|
|
float duration = atk.fallbackDuration;
|
|
if (atk.clip != null && _animancer != null)
|
|
{
|
|
var state = _animancer.Play(atk.clip);
|
|
if (state != null && state.Length > 0f) duration = state.Length;
|
|
}
|
|
|
|
float fireAbs = atk.projectileFireT * duration;
|
|
float t = 0f;
|
|
bool fired = false;
|
|
|
|
while (t < duration)
|
|
{
|
|
t += Time.deltaTime;
|
|
if (!fired && t >= fireAbs)
|
|
{
|
|
SpawnVolley(atk);
|
|
fired = true;
|
|
}
|
|
yield return null;
|
|
}
|
|
if (!fired) SpawnVolley(atk);
|
|
}
|
|
|
|
private void SpawnVolley(EnemyAttackSO atk)
|
|
{
|
|
if (_pool == null || atk.projectileConfig == null) return;
|
|
var cfg = atk.projectileConfig;
|
|
if (string.IsNullOrEmpty(cfg.PoolKey)) return;
|
|
|
|
Vector2 baseDir = GetAimDirection();
|
|
int count = Mathf.Max(1, atk.projectileCount);
|
|
float spread = atk.spreadAngleDeg;
|
|
float startAngle = -spread * 0.5f;
|
|
float step = count > 1 ? spread / (count - 1) : 0f;
|
|
|
|
var src = atk.damageSource != null ? atk.damageSource : cfg.DamageSource;
|
|
int layer = gameObject.layer;
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
float angle = startAngle + step * i;
|
|
Vector2 dir = Rotate(baseDir, angle);
|
|
|
|
var go = _pool.Spawn(cfg.PoolKey, _muzzle.position, Quaternion.identity);
|
|
if (go == null)
|
|
{
|
|
Debug.LogWarning($"[ProjectileAttackAbility] 对象池 '{cfg.PoolKey}' 无法获取弹体,请检查池配置或容量。", this);
|
|
continue;
|
|
}
|
|
var proj = go.GetComponent<Projectile>();
|
|
if (proj == null)
|
|
{
|
|
Debug.LogWarning($"[ProjectileAttackAbility] 弹体 '{go.name}' 缺少 Projectile 组件,请检查 Prefab 配置。", this);
|
|
continue;
|
|
}
|
|
var info = DamageInfo.From(src, dir, _muzzle.position, layer, proj);
|
|
proj.Initialize(cfg, info, dir, layer);
|
|
}
|
|
}
|
|
|
|
private Vector2 GetAimDirection()
|
|
{
|
|
if (_enemy != null && _enemy.PlayerTransform != null)
|
|
return ((Vector2)_enemy.PlayerTransform.position - (Vector2)_muzzle.position).normalized;
|
|
return _transform.localScale.x >= 0f ? Vector2.right : Vector2.left;
|
|
}
|
|
|
|
private static Vector2 Rotate(Vector2 v, float degrees)
|
|
{
|
|
float rad = degrees * Mathf.Deg2Rad;
|
|
float c = Mathf.Cos(rad), s = Mathf.Sin(rad);
|
|
return new Vector2(v.x * c - v.y * s, v.x * s + v.y * c);
|
|
}
|
|
}
|
|
}
|