Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-05-26 13:04:38 +08:00
parent f74d7f1877
commit 5a0f1548ea
53 changed files with 4853 additions and 163 deletions

View File

@@ -0,0 +1,88 @@
using System.Collections;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 带动画的天花板跌落能力:播放 Fall 动画 + 切换物理体为 Dynamic 自由下落,落地后启用接触伤害并恢复巡逻。
/// 取代 sealed 的 CeilingDropAbility动画所有权完整封装在本能力中。
/// 可复用于任何需要从天花板落下并造成接触伤害的敌人。
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class AnimatedCeilingDropAbility : EnemyAbilityBase
{
[Header("动画")]
[Tooltip("下落循环动画(旋转下落姿态,循环播放直到落地)")]
[SerializeField] private ClipTransition _fallLoopClip;
[Header("物理下落")]
[Tooltip("切换 Dynamic 后使用的重力倍率")]
[SerializeField] private float _fallGravityScale = 3.5f;
[Tooltip("下落超时保护(秒),超时后强制继续执行")]
[SerializeField] private float _maxFallTime = 3f;
[SerializeField] private LayerMask _groundMask;
[Header("落地后")]
[Tooltip("落地后恢复帧延迟(秒),之后切换为 Patrol 阶段")]
[SerializeField] private float _recoveryTime = 0.1f;
[SerializeField] private BodyContactDamage _contactDamage;
private Rigidbody2D _rb;
protected override void Awake()
{
base.Awake();
_rb = GetComponentInParent<Rigidbody2D>();
}
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
// 播放下落动画(能力脚本负责动画所有权)
if (_fallLoopClip.Clip != null)
_animancer.Play(_fallLoopClip);
// 切换物理Kinematic → Dynamic + 重力
var origBodyType = _rb.bodyType;
var origGravScale = _rb.gravityScale;
_rb.bodyType = RigidbodyType2D.Dynamic;
_rb.gravityScale = _fallGravityScale;
_rb.velocity = Vector2.zero;
// 等待落地(超时保护)
float elapsed = 0f;
while (elapsed < _maxFallTime)
{
elapsed += Time.fixedDeltaTime;
yield return new WaitForFixedUpdate();
if (elapsed > 0.05f && IsGrounded()) break;
}
_rb.velocity = Vector2.zero;
// 落地后启用接触伤害
if (_contactDamage != null)
_contactDamage.enabled = true;
yield return EnemyAbilityWaits.Get(_recoveryTime);
// SetAiPhase(Patrol) 自动播放 AnimConfig.Walk地面移动动画
_enemy.SetAiPhase(AiPhase.Patrol);
}
private bool IsGrounded()
{
var hit = Physics2D.Raycast(_rb.position, Vector2.down, 0.6f, _groundMask);
return hit.collider != null;
}
protected override void OnInterrupted(InterruptReason reason)
{
if (_rb != null)
_rb.velocity = Vector2.zero;
if (_contactDamage != null)
_contactDamage.enabled = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 76c9c3969851d084396edd617bac8ef0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
using System.Collections;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 出场演出能力:播放出场动画后切换到 Combat 阶段适用于小Boss或需出场动画的精英怪
/// 可复用于任何"播出场动画→进入战斗"的敌人。
/// </summary>
public class AppearAbility : EnemyAbilityBase
{
[SerializeField] private ClipTransition _appearClip;
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
if (_appearClip.Clip == null) yield break;
_animancer.Play(_appearClip);
yield return EnemyAbilityWaits.Get(_appearClip.Clip.length);
_enemy.SetAiPhase(AiPhase.Combat);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 419edb00543abc047a8e7601cf0f3ac4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,69 @@
using System.Collections;
using Animancer;
using BaseGames.Combat;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 天花板三段攻击能力:出击 → 脆弱悬挂窗口 → 收回。
/// 悬挂阶段_loopClip为脆弱窗口HurtBox 激活,等待 _hangDuration 后结束。
/// 可复用于任何固定位置的天花板攻击型敌人。
/// </summary>
public class CeilingHangStrikeAbility : EnemyAbilityBase
{
[Header("动画")]
[SerializeField] private ClipTransition _strikeClip;
[SerializeField] private ClipTransition _loopClip;
[SerializeField] private ClipTransition _endClip;
[Header("碰撞")]
[SerializeField] private HitBox _attackHitBox;
[SerializeField] private HurtBox _hurtBox;
[Header("行为")]
[Tooltip("脆弱悬挂窗口持续时间(秒)")]
[SerializeField] private float _hangDuration = 2f;
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
// 出击阶段:播放攻击动画并激活 HitBox
if (_strikeClip.Clip != null)
{
_animancer.Play(_strikeClip);
var dmgSrc = _config?.attackSequence?.Length > 0 ? _config.attackSequence[0].damageSource : null;
_attackHitBox?.Activate(dmgSrc, _transform);
yield return EnemyAbilityWaits.Get(_strikeClip.Clip.length);
}
_attackHitBox?.Deactivate();
// 脆弱悬挂阶段HurtBox 激活为玩家反击窗口
if (_loopClip.Clip != null)
_animancer.Play(_loopClip);
if (_hurtBox != null)
_hurtBox.enabled = true;
yield return EnemyAbilityWaits.Get(_hangDuration);
if (_hurtBox != null)
_hurtBox.enabled = false;
// 收回阶段
if (_endClip.Clip != null)
{
_animancer.Play(_endClip);
yield return EnemyAbilityWaits.Get(_endClip.Clip.length);
}
}
protected override void OnInterrupted(InterruptReason reason)
{
_attackHitBox?.Deactivate();
if (_hurtBox != null)
_hurtBox.enabled = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5615d1d3eb70f414fb137428a3f7f962
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,65 @@
using System.Collections;
using Animancer;
using UnityEngine;
using BaseGames.Enemies.Perception;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 循环追击 + 体接触伤害能力。
/// 追击期间每帧更新 MoveTo失去感知后收招退出并恢复巡逻状态。
/// 可复用于任何需要"追击+接触伤害循环"的敌人。
/// </summary>
public class ContactChaseAbility : EnemyAbilityBase
{
[Header("动画")]
[SerializeField] private ClipTransition _loopClip;
[SerializeField] private ClipTransition _endClip;
[Header("感知与接触伤害")]
[SerializeField] private BodyContactDamage _contactDamage;
[SerializeField] private EnemySensorHub _sensorHub;
[Tooltip("用于追击感知判断的传感器槽位名,通常为 \"aggro\"")]
[SerializeField] private string _aggroSlotName = "aggro";
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
if (_loopClip.Clip != null)
_animancer.Play(_loopClip);
if (_contactDamage != null)
_contactDamage.enabled = true;
while (true)
{
if (_enemy.PlayerTransform == null) break;
if (_sensorHub != null && !_sensorHub.IsDetecting(_aggroSlotName, _enemy.PlayerTransform.gameObject))
break;
_enemy.MoveTo(_enemy.PlayerTransform.position);
yield return null;
}
if (_contactDamage != null)
_contactDamage.enabled = false;
_enemy.StopMovement();
if (_endClip.Clip != null)
{
_animancer.Play(_endClip);
yield return EnemyAbilityWaits.Get(_endClip.Clip.length);
}
_enemy.SetAiPhase(AiPhase.Patrol);
}
protected override void OnInterrupted(InterruptReason reason)
{
if (_contactDamage != null)
_contactDamage.enabled = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7bfd6f44ebdb5bf489ab6703b1ee429b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
using System.Collections;
using System.Linq;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 面向玩家能力:朝向玩家后播放转身/翻转动画并等待完成。
/// CanUse 重写:仅在无其他技能运行且玩家在背后时可用。
/// 可复用于任何需要"检测背后玩家并翻转"的敌人。
/// </summary>
public class FacePlayerAbility : EnemyAbilityBase
{
[SerializeField] private ClipTransition _faceClip;
public override bool CanUse =>
base.CanUse
&& !_enemy.Abilities.All.Any(a => a != this && a.IsRunning)
&& IsPlayerBehind();
private bool IsPlayerBehind()
{
if (_enemy.PlayerTransform == null) return false;
float dx = _enemy.PlayerTransform.position.x - _enemy.transform.position.x;
int facing = _enemy.Movement?.FacingDirection ?? 1;
return (facing > 0 && dx < 0) || (facing < 0 && dx > 0);
}
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
_enemy.Movement?.FaceTarget(_enemy.PlayerTransform.position);
if (_faceClip.Clip == null) yield break;
_animancer.Play(_faceClip);
yield return EnemyAbilityWaits.Get(_faceClip.Clip.length);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c97df5c477ad0b488171b2b39530b08
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,81 @@
using System.Collections;
using Animancer;
using BaseGames.Combat;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 近战攻击带后摇脆弱窗口能力:攻击动画期间按时间比例激活 HitBox
/// 攻击完成后开放 HurtBox脆弱窗口窗口结束后恢复正常。
/// 可复用于任何"攻击后有反击窗口"的敌人技能。
/// </summary>
public class MeleeVulnerabilityAbility : EnemyAbilityBase
{
[Header("动画")]
[SerializeField] private ClipTransition _attackClip;
[Header("碰撞")]
[SerializeField] private HitBox _hitBox;
[SerializeField] private HurtBox _hurtBox;
[Header("打击时间0~1 归一化,基于 Clip 时长)")]
[SerializeField, Range(0f, 1f)] private float _hitEnterT = 0.30f;
[SerializeField, Range(0f, 1f)] private float _hitExitT = 0.60f;
[Header("后摇脆弱窗口")]
[Tooltip("HurtBox 激活时间(秒)")]
[SerializeField] private float _staggerDuration = 1.0f;
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
if (_attackClip.Clip == null) yield break;
float len = _attackClip.Clip.length;
float t = 0f;
bool active = false;
var dmgSrc = _config?.attackSequence?.Length > 0 ? _config.attackSequence[0].damageSource : null;
_animancer.Play(_attackClip);
while (t < len)
{
t += Time.deltaTime;
if (!active && t >= len * _hitEnterT)
{
_hitBox?.Activate(dmgSrc, _transform);
active = true;
}
if (active && t >= len * _hitExitT)
{
_hitBox?.Deactivate();
active = false;
}
yield return null;
}
if (active)
_hitBox?.Deactivate();
// 后摇脆弱窗口
if (_hurtBox != null)
_hurtBox.enabled = true;
yield return EnemyAbilityWaits.Get(_staggerDuration);
if (_hurtBox != null)
_hurtBox.enabled = false;
}
protected override void OnInterrupted(InterruptReason reason)
{
_hitBox?.Deactivate();
if (_hurtBox != null)
_hurtBox.enabled = false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5fc3af69bdd62a418fe6e2ce136c9a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
using System.Collections;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 播放单个动画 Clip 并等待完成后退出(无副作用)。
/// 可复用于任何需要"播一段动画即完成"的能力(如出场激活、台词演出等)。
/// </summary>
public class PlayClipAbility : EnemyAbilityBase
{
[SerializeField] private ClipTransition _clip;
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
if (_clip.Clip == null) yield break;
_animancer.Play(_clip);
yield return EnemyAbilityWaits.Get(_clip.Clip.length);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a26fca0fa72894a4da1a5a58ee023154
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,75 @@
using System.Collections;
using Animancer;
using BaseGames.Combat;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 多段砸地能力:起手 → 循环砸地N次带 HitBox 激活)→ 收招。
/// 支持霸体ABL SO 配置 interruptOnHurt=false后摇时间可配置。
/// 可复用于任何多段地面攻击型敌人。
/// </summary>
public class RepeatSlamAbility : EnemyAbilityBase
{
[Header("动画")]
[SerializeField] private ClipTransition _startClip;
[SerializeField] private ClipTransition _loopClip;
[SerializeField] private ClipTransition _endClip;
[Header("打击")]
[SerializeField] private HitBox _hitBox;
[Header("行为配置")]
[Tooltip("每次砸地时 HitBox 激活时长(秒)")]
[SerializeField] private float _hitActiveTime = 0.15f;
[Tooltip("砸地次数")]
[SerializeField] private int _slamCount = 2;
[Tooltip("最后一次砸地收招后额外等待时间(秒)")]
[SerializeField] private float _staggerDuration = 1.2f;
protected override IEnumerator ExecuteCoroutine()
{
Phase = AbilityRunState.Active;
if (_startClip.Clip != null)
{
_animancer.Play(_startClip);
yield return EnemyAbilityWaits.Get(_startClip.Clip.length);
}
for (int i = 0; i < _slamCount; i++)
{
if (_loopClip.Clip != null)
{
_animancer.Play(_loopClip);
float preHit = Mathf.Max(0f, _loopClip.Clip.length - _hitActiveTime - 0.05f);
yield return EnemyAbilityWaits.Get(preHit);
}
var dmgSrc = _config?.attackSequence?.Length > 0 ? _config.attackSequence[0].damageSource : null;
_hitBox?.Activate(dmgSrc, _transform);
yield return EnemyAbilityWaits.Get(_hitActiveTime);
_hitBox?.Deactivate();
if (i < _slamCount - 1)
yield return EnemyAbilityWaits.Get(0.1f);
}
if (_endClip.Clip != null)
{
_animancer.Play(_endClip);
yield return EnemyAbilityWaits.Get(_endClip.Clip.length + _staggerDuration);
}
else
{
yield return EnemyAbilityWaits.Get(_staggerDuration);
}
}
protected override void OnInterrupted(InterruptReason reason)
{
_hitBox?.Deactivate();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ff71de2a91f535d488c76df6395f1d78
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: