- 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.
120 lines
3.9 KiB
C#
120 lines
3.9 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using BaseGames.Combat;
|
||
|
||
namespace BaseGames.Enemies.Abilities
|
||
{
|
||
/// <summary>
|
||
/// 近战连击能力。
|
||
/// 按 EnemyAbilitySO.attackSequence 顺序播放动画段,每段在 hitBoxEnterT~hitBoxExitT
|
||
/// 之间激活对应槽位的 HitBox。
|
||
///
|
||
/// 设计:HitBox 通过命名槽位绑定,避免对 EnemyCombat 的硬依赖;同一敌人多个近战
|
||
/// 能力可共享同一 HitBox 槽位(如不同段使用同一把武器)。
|
||
/// </summary>
|
||
public sealed class MeleeAttackAbility : EnemyAbilityBase
|
||
{
|
||
[System.Serializable]
|
||
public struct HitBoxSlot
|
||
{
|
||
public string slotName;
|
||
public HitBox hitBox;
|
||
}
|
||
|
||
[Header("HitBox 槽位(按名字索引)")]
|
||
[SerializeField] private HitBoxSlot[] _hitBoxSlots;
|
||
|
||
[Header("行为")]
|
||
[SerializeField] private bool _faceTargetOnStart = true;
|
||
|
||
private Dictionary<string, HitBox> _slotMap;
|
||
|
||
protected override void Awake()
|
||
{
|
||
base.Awake();
|
||
_slotMap = new Dictionary<string, HitBox>(_hitBoxSlots?.Length ?? 0);
|
||
if (_hitBoxSlots != null)
|
||
{
|
||
for (int i = 0; i < _hitBoxSlots.Length; i++)
|
||
{
|
||
var s = _hitBoxSlots[i];
|
||
if (s.hitBox != null && !string.IsNullOrEmpty(s.slotName))
|
||
_slotMap[s.slotName] = s.hitBox;
|
||
}
|
||
}
|
||
}
|
||
|
||
protected override IEnumerator ExecuteCoroutine()
|
||
{
|
||
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 PlayAttackStep(atk);
|
||
if (atk.postDelay > 0f)
|
||
yield return EnemyAbilityWaits.Get(atk.postDelay);
|
||
}
|
||
}
|
||
|
||
private IEnumerator PlayAttackStep(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;
|
||
}
|
||
|
||
HitBox hb = null;
|
||
if (!string.IsNullOrEmpty(atk.hitBoxSlot))
|
||
_slotMap.TryGetValue(atk.hitBoxSlot, out hb);
|
||
|
||
float enterAbs = atk.hitBoxEnterT * duration;
|
||
float exitAbs = atk.hitBoxExitT * duration;
|
||
float t = 0f;
|
||
bool active = false;
|
||
|
||
while (t < duration)
|
||
{
|
||
t += Time.deltaTime;
|
||
if (hb != null)
|
||
{
|
||
if (!active && t >= enterAbs)
|
||
{
|
||
hb.Activate(atk.damageSource, _transform);
|
||
active = true;
|
||
}
|
||
else if (active && t >= exitAbs)
|
||
{
|
||
hb.Deactivate();
|
||
active = false;
|
||
}
|
||
}
|
||
yield return null;
|
||
}
|
||
|
||
if (active && hb != null) hb.Deactivate();
|
||
}
|
||
|
||
protected override void OnInterrupted(InterruptReason reason)
|
||
{
|
||
// 确保 HitBox 被关闭,防止中断时残留激活
|
||
if (_hitBoxSlots == null) return;
|
||
for (int i = 0; i < _hitBoxSlots.Length; i++)
|
||
{
|
||
var hb = _hitBoxSlots[i].hitBox;
|
||
if (hb != null && hb.IsActive) hb.Deactivate();
|
||
}
|
||
}
|
||
}
|
||
}
|