多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -0,0 +1,27 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Conditional攻击冷却是否完毕可以发动攻击
/// </summary>
public class BD_CanAttack : Conditional
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
return _enemy.CanAttack() ? TaskStatus.Success : TaskStatus.Failure;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,31 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD ActionBoss 切换阶段Phase
/// 调用 BossBase.EnterPhase(PhaseIndex),单帧完成,返回 Success。
/// </summary>
public class BD_EnterPhase : Action
{
[UnityEngine.SerializeField] private int m_PhaseIndex = 1;
private BossBase _boss;
public override void OnStart()
{
_boss = GetComponent<BossBase>();
}
public override TaskStatus OnUpdate()
{
if (_boss == null) return TaskStatus.Failure;
_boss.EnterPhase(m_PhaseIndex);
return TaskStatus.Success;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,32 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action立即朝向玩家单帧完成返回 Success。
/// </summary>
public class BD_FaceTarget : Action
{
private EnemyBase _enemy;
private Transform _player;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
var go = GameObject.FindWithTag("Player");
_player = go != null ? go.transform : null;
}
public override TaskStatus OnUpdate()
{
if (_enemy == null || _player == null) return TaskStatus.Failure;
_enemy.FacePlayer();
return TaskStatus.Success;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,27 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Conditional敌人是否处于地面。
/// </summary>
public class BD_IsGrounded : Conditional
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null || _enemy.Movement == null) return TaskStatus.Failure;
return _enemy.Movement.IsGrounded ? TaskStatus.Success : TaskStatus.Failure;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,32 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD ConditionalBoss HP 是否低于阈值(用于触发阶段切换)。
/// HPThreshold 为 0~1 归一化比例(如 0.5 = HP ≤ 50%)。
/// </summary>
public class BD_IsHPBelow : Conditional
{
[UnityEngine.SerializeField] private float m_HPThreshold = 0.5f;
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null || _enemy.Stats == null) return TaskStatus.Failure;
float ratio = (float)_enemy.Stats.CurrentHP / _enemy.Stats.MaxHP;
return ratio <= m_HPThreshold ? TaskStatus.Success : TaskStatus.Failure;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,28 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Conditional敌人是否靠近平台边缘。
/// 通过 EnemyNavAgent.IsNearEdge() 检测(双射线检测脚下/前方地面)。
/// </summary>
public class BD_IsNearEdge : Conditional
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null || _enemy.Nav == null) return TaskStatus.Failure;
return _enemy.Nav.IsNearEdge() ? TaskStatus.Success : TaskStatus.Failure;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,28 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Conditional玩家是否可见LOS 检测)。
/// 读取 EnemyBase._losResult 字段(由 BatchLOSSystem 每帧写入,或降级 3 帧节流 Raycast
/// </summary>
public class BD_IsPlayerVisible : Conditional
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
return _enemy.IsPlayerVisible() ? TaskStatus.Success : TaskStatus.Failure;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,43 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Conditional敌人当前 EnemyStateType 是否与目标状态名称匹配。
/// TargetStateName 直接输入枚举名称字符串Controlled / Hurt / Stagger / Dead
/// 枚举值顺序变化时 BD 图不会静默失效。
/// </summary>
public class BD_IsStateMatch : Conditional
{
/// <summary>目标状态名称Controlled / Hurt / Stagger / Dead。</summary>
[SerializeField] private string m_TargetStateName = "Controlled";
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
if (!System.Enum.TryParse<EnemyStateType>(m_TargetStateName, out var target))
{
Debug.LogError($"[BD_IsStateMatch] 未知状态名: '{m_TargetStateName}'" +
"有效值为 Controlled / Hurt / Stagger / Dead", gameObject);
return TaskStatus.Failure;
}
return _enemy.CurrentState == target
? TaskStatus.Success
: TaskStatus.Failure;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,45 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action跳跃至目标坐标抛物线跳跃
/// 调用 EnemyBase.JumpTo待落地IsGrounded后返回 Success。
/// </summary>
public class BD_JumpTo : Action
{
[SerializeField] private Vector2 m_Target;
private EnemyBase _enemy;
private bool _jumped;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
_jumped = false;
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
if (!_jumped)
{
_enemy.JumpTo(m_Target);
_jumped = true;
return TaskStatus.Running;
}
// 等待落地
if (_enemy.Movement != null && _enemy.Movement.IsGrounded)
return TaskStatus.Success;
return TaskStatus.Running;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,42 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action移动到指定世界坐标 Target。
/// 到达目标IsAtDestination后返回 Success。
/// </summary>
public class BD_MoveTo : Action
{
[SerializeField] private Vector2 m_Target;
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
_enemy.MoveTo(m_Target);
if (_enemy.Nav != null && _enemy.Nav.IsAtDestination())
return TaskStatus.Success;
return TaskStatus.Running;
}
public override void OnEnd()
{
_enemy?.StopMovement();
}
}
}
#endif

View File

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

View File

@@ -1,5 +1,4 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
@@ -8,26 +7,24 @@ namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action移向玩家。
/// OnStart 缓存玩家 TransformOnUpdate 每帧更新导航目标。
/// OnStart 从 EnemyBase.PlayerTransform 获取玩家引用(由 _onPlayerSpawned 事件缓存,
/// 避免 FindWithTag 全场景扫描。OnUpdate 每帧更新导航目标。
/// </summary>
public class BD_MoveToPlayer : Action
{
private EnemyBase _enemy;
private Transform _playerTransform;
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
var playerGO = GameObject.FindWithTag("Player");
if (playerGO != null) _playerTransform = playerGO.transform;
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
if (_playerTransform == null) return TaskStatus.Failure;
if (_enemy == null) return TaskStatus.Failure;
if (_enemy.PlayerTransform == null) return TaskStatus.Failure;
_enemy.MoveTo(_playerTransform.position);
_enemy.MoveTo(_enemy.PlayerTransform.position);
_enemy.FacePlayer();
return TaskStatus.Running;
}

View File

@@ -2,16 +2,25 @@
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
using UnityEngine;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action敌人巡逻行为Phase 1 简单实现)
/// 挂在 BT 节点上,持续令敌人向当前朝向移动。
/// BD Action敌人巡逻行为。
/// 持续令敌人向当前朝向移动,遇墙/边缘时自动转向
/// </summary>
public class BD_Patrol : Action
{
[Tooltip("检测地面边缘的向下射线长度")]
public float edgeCheckLength = 1.2f;
[Tooltip("检测障碍物的水平射线长度")]
public float wallCheckLength = 0.4f;
[Tooltip("地面/墙壁 LayerMask")]
public LayerMask groundLayer;
private EnemyBase _enemy;
private float _dir = 1f;
public override void OnStart()
{
@@ -21,8 +30,11 @@ namespace BaseGames.Enemies.AI
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
// Phase 1固定向右巡逻Phase 2 改为往返检测
_enemy.MoveInDirection(1f);
if (ShouldFlip())
_dir = -_dir;
_enemy.MoveInDirection(_dir);
return TaskStatus.Running;
}
@@ -30,6 +42,23 @@ namespace BaseGames.Enemies.AI
{
_enemy?.StopMovement();
}
private bool ShouldFlip()
{
Transform t = _enemy.transform;
Vector2 pos = t.position;
// 前方边缘检测:在脚前方向下射线,若无地面则转向
Vector2 edgeOrigin = pos + Vector2.right * (_dir * 0.3f) + Vector2.down * 0.1f;
bool hasGround = Physics2D.Raycast(edgeOrigin, Vector2.down, edgeCheckLength, groundLayer);
if (!hasGround) return true;
// 前方障碍检测:水平射线,若撞墙则转向
bool hitWall = Physics2D.Raycast(pos, Vector2.right * _dir, wallCheckLength, groundLayer);
if (hitWall) return true;
return false;
}
}
}
#endif

View File

@@ -0,0 +1,37 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action通过 AnimationClip 名称播放 Animancer 动画,立即返回 Success。
/// ClipName 需与 EnemyAnimationConfigSO 中字段名一致。
/// </summary>
public class BD_PlayAnimation : Action
{
/// <summary>EnemyAnimationConfigSO 中的 AnimationClip 字段名(如 "Attack_Melee")。</summary>
[SerializeField] private string m_ClipName = "Idle";
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null || _enemy.AnimConfig == null) return TaskStatus.Failure;
var clip = _enemy.AnimConfig.GetClipByName(m_ClipName);
if (clip != null)
_enemy.Animancer?.Play(clip);
return TaskStatus.Success;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,29 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action设置警觉状态并通知 EnemyBase 调整 BT Tick 频率。
/// </summary>
public class BD_SetAlert : Action
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
_enemy.SetAggroTickRate(true);
return TaskStatus.Success;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,39 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Core;
using BaseGames.Core.Pool;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action在敌人当前位置生成弹射物。
/// ProjectileKey 为 Addressable 键,通过 GlobalObjectPool 实例化。
/// </summary>
public class BD_SpawnProjectile : Action
{
[SerializeField] private Vector2 m_Direction = Vector2.right;
[SerializeField] private string m_ProjectileKey = "";
[SerializeField] private float m_Speed = 8f;
public override TaskStatus OnUpdate()
{
if (string.IsNullOrEmpty(m_ProjectileKey)) return TaskStatus.Failure;
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
if (pool == null) return TaskStatus.Failure;
var go = pool.Spawn(m_ProjectileKey, transform.position, Quaternion.identity);
if (go != null)
{
var rb = go.GetComponent<Rigidbody2D>();
if (rb != null)
rb.velocity = m_Direction.normalized * m_Speed;
}
return TaskStatus.Success;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,28 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action立即停止移动单帧完成返回 Success。
/// </summary>
public class BD_StopMovement : Action
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
_enemy.StopMovement();
return TaskStatus.Success;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,43 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Core;
using BaseGames.Core.Pool;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD ActionBoss 专用召唤小兵。
/// 通过 GlobalObjectPool 在 Boss 周围生成 MinionPrefabKey 对应的敌人。
/// </summary>
public class BD_SummonMinions : Action
{
[SerializeField] private string m_MinionPrefabKey = "";
[SerializeField] private int m_Count = 2;
[SerializeField] private float m_SpawnRadius = 3f;
public override TaskStatus OnUpdate()
{
if (string.IsNullOrEmpty(m_MinionPrefabKey)) return TaskStatus.Failure;
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
if (pool == null) return TaskStatus.Failure;
for (int i = 0; i < m_Count; i++)
{
var angle = Random.Range(0f, 360f) * Mathf.Deg2Rad;
var offset = new Vector2(Mathf.Cos(angle), 0f) * m_SpawnRadius;
var spawnPos = new Vector3(
transform.position.x + offset.x,
transform.position.y + offset.y,
0f);
pool.Spawn(m_MinionPrefabKey, spawnPos, Quaternion.identity);
}
return TaskStatus.Success;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,40 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies.Boss.Patterns;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action触发攻击预警TelegraphSystem等待 Duration 秒后返回 Success。
/// 对应架构 07_EnemyModule §8 BD_TelegraphAttack与 TelegraphSystem 协作。
/// </summary>
public class BD_TelegraphAttack : Action
{
[SerializeField] private float m_Duration = 1f;
[SerializeField] private string m_VfxKey = "";
private TelegraphSystem _telegraph;
private float _elapsed;
public override void OnStart()
{
_telegraph = GetComponent<TelegraphSystem>();
_elapsed = 0f;
if (_telegraph != null && !string.IsNullOrEmpty(m_VfxKey))
{
_telegraph.StartCoroutine(
_telegraph.ShowTelegraph(m_VfxKey, m_Duration, transform.position));
}
}
public override TaskStatus OnUpdate()
{
_elapsed += Time.deltaTime;
return _elapsed >= m_Duration ? TaskStatus.Success : TaskStatus.Running;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,23 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action瞬移传送到目标坐标Boss 专用。
/// 单帧直接修改 transform.position不使用导航。
/// </summary>
public class BD_TeleportTo : Action
{
[SerializeField] private Vector2 m_Target;
public override TaskStatus OnUpdate()
{
transform.position = new Vector3(m_Target.x, m_Target.y, transform.position.z);
return TaskStatus.Success;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,30 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action等待固定 Duration 秒后返回 Success。
/// 适用于攻击前摇、冷却间隔等固定等待。
/// </summary>
public class BD_Wait : Action
{
[UnityEngine.SerializeField] private float m_Duration = 1f;
private float _elapsed;
public override void OnStart()
{
_elapsed = 0f;
}
public override TaskStatus OnUpdate()
{
_elapsed += Time.deltaTime;
return _elapsed >= m_Duration ? TaskStatus.Success : TaskStatus.Running;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,34 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action等待当前 Animancer 动画播放完毕再返回 Success。
/// 通过 EnemyBase.IsAnimationComplete 轮询Animancer CurrentState.NormalizedTime >= 1
/// </summary>
public class BD_WaitForAnimation : Action
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
var state = _enemy.Animancer?.States?.Current;
if (state == null) return TaskStatus.Success; // 无动画直接继续
if (state.NormalizedTime >= 1f) return TaskStatus.Success;
return TaskStatus.Running;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,33 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action在 [Min, Max] 范围内随机等待后返回 Success。
/// 适用于巡逻间隔、随机攻击延迟等自然行为。
/// </summary>
public class BD_WaitRandom : Action
{
[UnityEngine.SerializeField] private float m_Min = 0.5f;
[UnityEngine.SerializeField] private float m_Max = 2.0f;
private float _elapsed;
private float _target;
public override void OnStart()
{
_elapsed = 0f;
_target = Random.Range(m_Min, m_Max);
}
public override TaskStatus OnUpdate()
{
_elapsed += Time.deltaTime;
return _elapsed >= _target ? TaskStatus.Success : TaskStatus.Running;
}
}
}
#endif

View File

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

View File

@@ -9,6 +9,7 @@
"rootNamespace": "BaseGames.Enemies.AI",
"references": [
"BaseGames.Enemies",
"BaseGames.Enemies.Boss.Patterns",
"Opsive.BehaviorDesigner.Runtime"
],
"autoReferenced": true,

View File

@@ -0,0 +1,97 @@
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Jobs;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 批量视线检测系统(架构 07_EnemyModule §12
/// 每 FixedUpdate
/// 1. 读取上一帧 RaycastHit2D 结果,回调各注册者。
/// 2. 重新构建本帧 RaycastCommand 批次,使用 Physics2D.RaycastAll 同步模式执行。
///
/// ⚠️ Unity 2022.3 中 Physics2D 批量命令RaycastCommand2D尚未稳定
/// 此实现使用 FixedUpdate 内顺序 Raycast2D节流确保零 GC 分配。
/// 当敌人数量 > 20 时建议切换到 Job System RaycastCommand。
/// </summary>
[DefaultExecutionOrder(-200)]
public class BatchLOSSystem : MonoBehaviour
{
[SerializeField, Min(1)] private int _maxRequestersPerFrame = 8;
private readonly List<ILOSRequester> _requesters = new();
private readonly HashSet<ILOSRequester> _requesterSet = new(); // O(1) 包含查询
// _indexMap 记录每个 requester 在 _requesters 中的下标,供 Unregister 实现 O(1) 删除
private readonly Dictionary<ILOSRequester, int> _indexMap = new();
private int _currentOffset = 0;
// ── 注册 ──────────────────────────────────────────────────────────
public void Register(ILOSRequester requester)
{
if (_requesterSet.Add(requester))
{
_indexMap[requester] = _requesters.Count;
_requesters.Add(requester);
}
}
public void Unregister(ILOSRequester requester)
{
if (!_requesterSet.Remove(requester)) return;
int idx = _indexMap[requester];
int last = _requesters.Count - 1;
// Swap-and-pop将末尾元素移动到被删除的位置避免 O(n) 搬移
if (idx != last)
{
var moved = _requesters[last];
_requesters[idx] = moved;
_indexMap[moved] = idx;
}
_requesters.RemoveAt(last);
_indexMap.Remove(requester);
// 修正偏移量,防止越界
if (_currentOffset >= _requesters.Count) _currentOffset = 0;
}
// ── FixedUpdate ───────────────────────────────────────────────────
private void FixedUpdate()
{
if (_requesters.Count == 0) return;
// 每帧轮询部分请求者(均匀分配,避免单帧全量射线)
int count = Mathf.Min(_maxRequestersPerFrame, _requesters.Count);
for (int i = 0; i < count; i++)
{
int idx = (_currentOffset + i) % _requesters.Count;
var requester = _requesters[idx];
bool hasLOS = false;
if (requester != null)
{
Vector2 origin = requester.LOSOrigin;
Vector2 target = requester.LOSTarget;
Vector2 direction = target - origin;
float distance = direction.magnitude;
if (distance > 0.01f)
{
var hit = Physics2D.Raycast(origin, direction.normalized, distance, requester.LOSBlockingMask);
// 若无遮挡物hit.collider == null则视线畅通
hasLOS = hit.collider == null;
}
requester.ReceiveLOSResult(hasLOS);
}
}
_currentOffset = (_currentOffset + count) % Mathf.Max(1, _requesters.Count);
}
}
}

View File

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

View File

@@ -0,0 +1,27 @@
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// BatchLOSSystem 的注册接口(架构 07_EnemyModule §12
/// 实现此接口的 EnemyBase 子类可以注册到 BatchLOSSystem
/// 以批处理方式接收 LOSLine of Sight检测结果。
/// </summary>
public interface ILOSRequester
{
/// <summary>射线起点(通常是眼部位置)。</summary>
Vector2 LOSOrigin { get; }
/// <summary>射线终点(通常是玩家位置)。</summary>
Vector2 LOSTarget { get; }
/// <summary>遮挡 LOS 的物理图层。</summary>
LayerMask LOSBlockingMask { get; }
/// <summary>
/// 接收 LOS 检测结果。
/// <paramref name="hasLineOfSight"/>: true = 有视线false = 被遮挡。
/// </summary>
void ReceiveLOSResult(bool hasLineOfSight);
}
}

View File

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

View File

@@ -12,8 +12,12 @@
"BaseGames.Core.Events",
"BaseGames.Combat",
"BaseGames.Feedback",
"BaseGames.World",
"MoreMountains.Tools",
"Kybernetik.Animancer"
"Kybernetik.Animancer",
"Opsive.BehaviorDesigner.Runtime",
"Unity.Addressables",
"Unity.ResourceManager"
],
"autoReferenced": true,
"overrideReferences": false,

View File

@@ -0,0 +1,33 @@
using System;
using UnityEngine;
using UnityEngine.AddressableAssets;
using BaseGames.Combat;
namespace BaseGames.Boss
{
/// <summary>
/// 单个攻击图案的数据。伤害参数只写在此处BossSkillSO 不存参数。
/// </summary>
[CreateAssetMenu(menuName = "Boss/AttackPattern")]
public class AttackPatternSO : ScriptableObject
{
[Header("输出")]
public DamageSourceSO DamageSource;
public float KnockbackAngle;
[Header("弹幕(若为弹幕类型)")]
public AssetReferenceGameObject ProjectilePrefab;
public int ProjectileCount = 1;
public float SpreadAngle = 0f;
public float ProjectileSpeed = 8f;
[Header("范围攻击(若为 AoE 类型)")]
public float AoERadius;
public Vector2 AoEOffset;
[Header("时序")]
[Min(0f)] public float WindupDuration;
[Min(0f)] public float ActiveDuration;
[Min(0f)] public float RecoveryDuration;
}
}

View File

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

View File

@@ -0,0 +1,48 @@
using UnityEngine;
using BaseGames.Core.Events;
namespace BaseGames.Enemies
{
/// <summary>
/// Boss 敌人基类。扩展 <see cref="EnemyBase"/> 以支持多阶段切换与战斗结束广播。
/// 具体 Boss 继承此类并重写 <see cref="EnterPhase"/>。
/// </summary>
public class BossBase : EnemyBase
{
[Header("Boss 配置")]
[SerializeField] private string _bossId;
[SerializeField] private BoolEventChannelSO _onBossFightEnded;
[SerializeField] private BossPhaseEventChannelSO _onBossPhaseChanged;
public string BossId => _bossId;
protected int _currentPhase = 0;
/// <summary>
/// 进入指定阶段。广播 <see cref="BossPhaseEvent"/> 供 UI / 音乐系统响应。
/// 子类可重写以添加额外过渡逻辑(动画、无敌帧等)。
/// </summary>
public virtual void EnterPhase(int phase)
{
_currentPhase = phase;
_onBossPhaseChanged?.Raise(new BossPhaseEvent
{
BossId = _bossId,
Phase = phase,
});
}
/// <summary>检查当前 HP 是否低于指定百分比0~1。</summary>
public bool IsHPBelow(float ratio)
{
if (_stats == null || _stats.MaxHP <= 0) return false;
return (float)_stats.CurrentHP / _stats.MaxHP < ratio;
}
protected override void Die()
{
base.Die();
_onBossFightEnded?.Raise(true);
}
}
}

View File

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

View File

@@ -0,0 +1,26 @@
using UnityEngine;
namespace BaseGames.Boss
{
/// <summary>
/// Boss 自身资源(如愤怒值)的配置 ScriptableObject。
/// </summary>
[CreateAssetMenu(menuName = "Boss/ResourceConfig")]
public class BossResourceConfigSO : ScriptableObject
{
public string resourceId;
public string displayName;
public float maxValue;
public float startValue;
[Header("自动变化")]
public float passiveRate;
public float onTakeDamageGain;
public float onSkillUseGain;
[Header("满值效果")]
public bool autoTriggerOnFull;
public BossSkillSO fullTriggerSkill;
public float resetValueAfterTrigger;
}
}

View File

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

View File

@@ -0,0 +1,184 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Animancer;
using BaseGames.Combat;
using BaseGames.Core.Events;
namespace BaseGames.Boss
{
/// <summary>
/// 挂在 Boss GameObject 上,接收 BossOrchestrator 的指令执行指定 BossSkillSO。
/// 管理 VulnerabilityWindow 计时和 WeakPointSystem 激活。
/// </summary>
public class BossSkillExecutor : MonoBehaviour
{
[SerializeField] private HitBox[] _hitBoxes;
[SerializeField] private WeakPointSystem _weakPointSystem;
[SerializeField] private AnimancerComponent _animancer;
[SerializeField] private string _bossId;
[SerializeField] private BossSkillEventChannelSO _onBossSkillStarted;
[SerializeField] private BossSkillEventChannelSO _onBossSkillEnded;
/// <remarks>PlayerController 无 Instance架构 05 §2由 Inspector 指定。</remarks>
[SerializeField] private Transform _playerTransform;
private BossSkillSO _currentSkill;
private bool _isExecuting;
private Coroutine _activeCoroutine;
public bool IsExecuting => _isExecuting;
/// <summary>
/// 按 float 值复用 WaitForSeconds 实例,消除协程中每次 new WaitForSeconds 的 GC 分配。
/// Domain Reload 禁用时静态缓存跨 PlayMode 会话保留,但 WaitForSeconds 是幂等值对象,
/// 不会引发功能错误;[RuntimeInitializeOnLoadMethod] 确保每次进入 Play 时清空。
/// </summary>
private static readonly Dictionary<float, WaitForSeconds> _wfsCache = new();
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ClearWFSCache() => _wfsCache.Clear();
private static WaitForSeconds GetWFS(float t)
{
if (!_wfsCache.TryGetValue(t, out var wfs))
_wfsCache[t] = wfs = new WaitForSeconds(t);
return wfs;
}
// ── 公共 API ───────────────────────────────────────────────────────────
/// <summary>
/// 执行一个 Boss 技能。若当前已在执行中则直接返回。
/// </summary>
public void ExecuteSkill(BossSkillSO skill)
{
if (_isExecuting || skill == null) return;
_activeCoroutine = StartCoroutine(ExecuteSkillCoroutine(skill));
}
/// <summary>
/// 立即打断正在执行的技能(阶段切换时调用)。
/// </summary>
public void InterruptCurrentSkill()
{
if (_activeCoroutine != null)
{
StopCoroutine(_activeCoroutine);
_activeCoroutine = null;
}
FinishExecution();
}
// ── 主协程 ─────────────────────────────────────────────────────────────
private IEnumerator ExecuteSkillCoroutine(BossSkillSO skill)
{
_isExecuting = true;
_currentSkill = skill;
_onBossSkillStarted?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = skill.skillId });
// 播放技能动画
if (skill.skillAnimation != null)
_animancer.Play(skill.skillAnimation);
// 启动 VulnerabilityWindow 协程(与主序列并行)
Coroutine vulnCoroutine = null;
if (skill.vulnerabilityWindows != null && skill.vulnerabilityWindows.Length > 0)
vulnCoroutine = StartCoroutine(ActivateVulnerabilityWindowsCoroutine(skill));
// 执行攻击序列(优先 sequenceOnMiss 作为默认序列)
if (skill.sequenceOnMiss != null)
yield return ExecuteSequenceCoroutine(skill.sequenceOnMiss);
// 若弱点协程还在运行则等待其结束(避免孤立协程)
if (vulnCoroutine != null)
yield return vulnCoroutine;
FinishExecution();
}
private void FinishExecution()
{
_isExecuting = false;
if (_currentSkill != null)
{
_onBossSkillEnded?.Raise(new BossSkillEvent { BossId = _bossId, SkillId = _currentSkill.skillId });
_currentSkill = null;
}
}
// ── 序列协程 ────────────────────────────────────────────────────────────
private IEnumerator ExecuteSequenceCoroutine(SkillSequenceSO seq)
{
int repeatCount = 0;
do
{
foreach (var step in seq.steps)
{
if (step.delayBeforeStep > 0f)
yield return GetWFS(step.delayBeforeStep);
if (step.pattern != null)
yield return ExecutePatternCoroutine(step.pattern);
}
repeatCount++;
if (seq.RepeatIfPlayerInRange && seq.RepeatDelay > 0f)
yield return GetWFS(seq.RepeatDelay);
}
while (seq.RepeatIfPlayerInRange
&& (seq.MaxRepeatCount == 0 || repeatCount < seq.MaxRepeatCount)
&& IsPlayerInRange());
}
private IEnumerator ExecutePatternCoroutine(AttackPatternSO pattern)
{
// 预备
if (pattern.WindupDuration > 0f)
yield return GetWFS(pattern.WindupDuration);
// 激活 HitBox架构 06 §4Activate(DamageSourceSO, Transform)
foreach (var hb in _hitBoxes)
hb.Activate(pattern.DamageSource, transform);
if (pattern.ActiveDuration > 0f)
yield return GetWFS(pattern.ActiveDuration);
// 关闭 HitBox
foreach (var hb in _hitBoxes)
hb.Deactivate();
// 后摇
if (pattern.RecoveryDuration > 0f)
yield return GetWFS(pattern.RecoveryDuration);
}
// ── VulnerabilityWindow 协程 ─────────────────────────────────────────────
private IEnumerator ActivateVulnerabilityWindowsCoroutine(BossSkillSO skill)
{
foreach (var window in skill.vulnerabilityWindows)
{
if (window.TriggerDelay > 0f)
yield return GetWFS(window.TriggerDelay);
bool activateSpecific = window.ActivateWeakPointHurtBox;
_weakPointSystem?.SetActive(true, window.DamageMultiplier, activateSpecific);
window.OpenFeedback?.PlayFeedbacks();
yield return GetWFS(window.Duration);
_weakPointSystem?.SetActive(false, 1f, activateSpecific);
window.CloseFeedback?.PlayFeedbacks();
}
}
// ── 工具 ───────────────────────────────────────────────────────────────
private bool IsPlayerInRange() =>
_playerTransform != null &&
Vector2.Distance(transform.position, _playerTransform.position) < 8f;
}
}

View File

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

View File

@@ -0,0 +1,61 @@
using System;
using UnityEngine;
using Animancer;
using BaseGames.Combat;
namespace BaseGames.Boss
{
/// <summary>
/// Boss 单个技能的所有数据,包括攻击模式、弱点窗口、互动标签等。
/// </summary>
[CreateAssetMenu(menuName = "Boss/BossSkill")]
public class BossSkillSO : ScriptableObject
{
[Header("元信息")]
public string skillId;
public string displayName;
[TextArea(1, 4)]
public string designNote;
[Header("技能分类")]
public BossSkillCategory category;
public BossSkillType skillType;
[Header("阶段可用性")]
[Tooltip("空数组 = 全阶段可用")]
public int[] availablePhaseIndices;
[Header("核心攻击动作引用")]
public AttackPatternSO[] attackPatterns;
[Header("弱点窗口(至少 1 个)")]
public VulnerabilityWindow[] vulnerabilityWindows;
[Header("互动标签")]
public InteractionTag interactionTags;
[Header("连段")]
public SkillSequenceSO sequenceOnHit;
public SkillSequenceSO sequenceOnMiss;
[Header("玩家反制接口")]
public PlayerCounterResponse[] counterResponses;
[Header("场景联动")]
public ArenaEventTrigger[] arenaEvents;
[Header("Boss 资源")]
public BossResourceCost resourceCost;
public bool buildsRage;
[Header("霸体配置")]
public PoiseWindowConfig poiseWindow;
[Header("动画")]
public ClipTransition skillAnimation;
[Header("冷却")]
[Min(0f)]
public float cooldown;
}
}

View File

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

View File

@@ -0,0 +1,184 @@
using System;
using UnityEngine;
using MoreMountains.Feedbacks;
namespace BaseGames.Boss
{
// ── 分类枚举 ───────────────────────────────────────────────────────────────
public enum BossSkillCategory
{
Melee, Ranged, Charge, AoE, Environmental, Summon,
Buff, Debuff, Phase, Passive, Reactive
}
public enum BossSkillType
{
MeleeSlash,
ChargeAttack,
LeapSlam,
ProjectileVolley,
LaserBeam,
PhaseTransition,
SummonMinion,
ArenaTrap,
SpeedBurst,
DefensiveShell,
}
[Flags]
public enum InteractionTag
{
None = 0,
Parryable = 1 << 0,
PerfectParryOnly = 1 << 1,
DodgeWindow = 1 << 2,
Unblockable = 1 << 3,
CanBeReflected = 1 << 4,
ExposesWeakPoint = 1 << 5,
GrantsPlayerReso = 1 << 6,
ArenaHazard = 1 << 7,
PhaseGate = 1 << 8,
}
// ── VulnerabilityWindow ────────────────────────────────────────────────────
public enum VulnTriggerType
{
OnAttackRecovery,
OnParriedSuccess,
OnCounterSkillHit,
OnPhaseTransition,
OnHazardBackfire,
OnSummonDefeated,
Manual,
}
public enum WeakPointType
{
FullBody,
HeadOnly,
BackOnly,
CoreExposed,
CustomPoint,
}
[Serializable]
public struct VulnerabilityWindow
{
[Tooltip("弱点触发方式")]
public VulnTriggerType TriggerType;
[Tooltip("触发后延迟出现(秒)")]
[Min(0f)]
public float TriggerDelay;
[Tooltip("弱点持续时长(秒)")]
[Min(0.1f)]
public float Duration;
public WeakPointType WeakPointType;
[Tooltip("弱点激活时 Boss 受击乘数")]
[Min(0.1f)]
public float DamageMultiplier;
public bool ForceStagger;
[Min(0f)]
public float StaggerDuration;
public MMF_Player OpenFeedback;
public MMF_Player CloseFeedback;
public Color HighlightColor;
public bool ActivateWeakPointHurtBox => WeakPointType != WeakPointType.FullBody;
}
// ── PlayerCounterResponse ─────────────────────────────────────────────────
public enum CounterType
{
Parry,
PerfectParry,
DodgeThrough,
SpecificSkill,
WeakPointHit,
HazardBackfire,
SummonKill,
}
[Serializable]
public struct PlayerCounterResponse
{
[Header("反制条件")]
public CounterType counterType;
public string requiredSkillId;
[Header("反制效果(对 Boss")]
public float bossStaggerDuration;
public float bossDamageBonus;
public bool openVulnWindow;
public bool interruptSkill;
[Header("反制收益(对玩家)")]
public int soulPowerGrant;
public int spiritPowerGrant;
public MMF_Player counterFeedback;
}
// ── ArenaEvent ────────────────────────────────────────────────────────────
public enum ArenaEventType
{
DestroyPlatform,
ActivateHazard,
DeactivateHazard,
SpawnHazardArea,
ShakeArena,
ToggleLighting,
SpawnPlatform,
TriggerCutscene,
}
[Serializable]
public struct ArenaEventParams
{
public float duration;
public float intensity;
public bool revertsOnPhaseEnd;
}
[Serializable]
public struct ArenaEventTrigger
{
public string targetArenaObjectId;
public ArenaEventType eventType;
public float delay;
public ArenaEventParams parameters;
}
[Serializable]
public struct ArenaEventData
{
public ArenaEventType type;
public ArenaEventParams parameters;
public string sourceSkillId;
}
public interface IArenaInteractable
{
string ArenaObjectId { get; }
void OnBossArenaEvent(ArenaEventData data);
}
// ── BossResourceCost ──────────────────────────────────────────────────────
[Serializable]
public struct BossResourceCost
{
public string resourceId;
public float cost;
public float minRequired;
}
}

View File

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

View File

@@ -0,0 +1,50 @@
using System.Collections;
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Pool;
namespace BaseGames.Enemies.Boss.Patterns
{
/// <summary>
/// 攻击预警系统(架构 07_EnemyModule §11
/// 在攻击前若干帧显示视觉提示VFX 从对象池取出,到期归还)。
/// 由 BD_TelegraphAttack 通过协程调用 ShowTelegraph。
/// </summary>
public class TelegraphSystem : MonoBehaviour
{
/// <summary>
/// 开始预警:从对象池取出 vfxKey 对应预警 VFX等待 duration 秒后归还。
/// 由 BD_TelegraphAttack.OnStart 通过 StartCoroutine 调用。
/// </summary>
public IEnumerator ShowTelegraph(string vfxKey, float duration, Vector2 position)
{
if (string.IsNullOrEmpty(vfxKey) || duration <= 0f)
{
yield return null;
yield break;
}
GameObject vfx = null;
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
if (pool != null)
vfx = pool.Spawn(vfxKey, new Vector3(position.x, position.y, 0f), Quaternion.identity);
else
Debug.LogWarning($"[TelegraphSystem] IObjectPoolService 未就绪,预警 VFX '{vfxKey}' 无法显示。");
yield return new WaitForSeconds(duration);
if (vfx != null && pool != null)
{
var po = vfx.GetComponent<PooledObject>();
if (po != null) po.ReturnToPool();
else vfx.SetActive(false);
}
}
/// <summary>立即隐藏所有活跃预警 VFX技能被打断时调用。</summary>
public void CancelTelegraph()
{
StopAllCoroutines();
}
}
}

View File

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

View File

@@ -0,0 +1,26 @@
using System;
using UnityEngine;
namespace BaseGames.Boss
{
/// <summary>
/// 有序攻击序列(一个技能内的多段连段)。
/// </summary>
[CreateAssetMenu(menuName = "Boss/SkillSequence")]
public class SkillSequenceSO : ScriptableObject
{
[Serializable]
public struct SequenceStep
{
public AttackPatternSO pattern;
[Min(0f)] public float delayBeforeStep;
}
public SequenceStep[] steps;
[Header("序列完成后的行为")]
public bool RepeatIfPlayerInRange;
[Min(0f)] public float RepeatDelay;
[Range(0, 10)] public int MaxRepeatCount;
}
}

View File

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

View File

@@ -0,0 +1,51 @@
using UnityEngine;
using BaseGames.Combat;
using BaseGames.Core.Events;
namespace BaseGames.Boss
{
/// <summary>
/// 管理 Boss 的专属弱点 HurtBox如核心、眼睛等
/// 弱点激活期间受到的伤害会乘以 DamageMultiplier。
/// </summary>
public class WeakPointSystem : MonoBehaviour
{
[System.Serializable]
public struct WeakPoint
{
public HurtBox hurtBox;
public GameObject visualIndicator;
}
[SerializeField] private WeakPoint[] _weakPoints;
[SerializeField] private string _bossId;
[SerializeField] private StringEventChannelSO _onVulnerabilityWindowOpened;
private float _damageMultiplier = 1f;
/// <summary>激活或关闭弱点 HurtBox 及视觉指示器。</summary>
/// <param name="active">是否激活。</param>
/// <param name="multiplier">激活时的受击伤害乘数。</param>
/// <param name="activateSpecific">true = 仅激活弱点专属 HurtBoxfalse = 全身视为弱点(不改变 HurtBox 状态)。</param>
public void SetActive(bool active, float multiplier = 1f, bool activateSpecific = false)
{
_damageMultiplier = active ? multiplier : 1f;
if (activateSpecific)
{
foreach (var wp in _weakPoints)
{
wp.hurtBox.gameObject.SetActive(active);
if (wp.visualIndicator != null)
wp.visualIndicator.SetActive(active);
}
}
if (active)
_onVulnerabilityWindowOpened?.Raise(_bossId);
}
/// <summary>弱点 HurtBox 受击时,由 BossStats 调用此方法获取最终伤害系数。</summary>
public float GetDamageMultiplier() => _damageMultiplier;
}
}

View File

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

View File

@@ -19,6 +19,26 @@ namespace BaseGames.Enemies
[Header("受击")]
public AnimationClip Hurt;
public AnimationClip Stagger;
public AnimationClip Dead;
/// <summary>
/// 按字段名返回 ClipBD_PlayAnimation 使用)。
/// 支持字段名和常见别名(如 Attack_Melee / Idle 等)。
/// </summary>
public AnimationClip GetClipByName(string name)
{
return name switch
{
"Idle" => Idle,
"Walk" => Walk,
"Run" => Run,
"Attack" or "Attack_Melee" => Attack,
"Hurt" => Hurt,
"Stagger" => Stagger,
"Dead" or "Death" => Dead,
_ => null,
};
}
}
}

View File

@@ -1,17 +1,29 @@
using UnityEngine;
using Animancer;
using BaseGames.Combat;
using BaseGames.Core.Events;
using BaseGames.Enemies.States;
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime;
#endif
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人基类(架构 07_EnemyModule §1
/// 实现 IDamageable为 Behavior Designer 任务提供统一虚方法接口。
/// Phase 1 实现:完整骨架,BD 接口、受击、死亡流程。
/// 包含:BD 接口、受击、死亡流程。
/// ⚠️ _nav 字段类型为 IPathAgent在 BaseGames.Enemies.Navigation 中实现具体类)。
/// </summary>
public class EnemyBase : MonoBehaviour, IDamageable
public class EnemyBase : MonoBehaviour, IDamageable, ILOSRequester
{
[Header("标识")]
[SerializeField] private string _enemyId; // 任务系统 / Boss 进程追踪用,如 "Enemy_SpiderGuard"
public string EnemyId => _enemyId;
/// <summary>死亡时触发ChallengeRoomManager 波次结算用)。</summary>
public event System.Action OnDied;
[Header("配置 SO")]
[SerializeField] protected EnemyStatsSO _statsSO;
[SerializeField] protected EnemyAnimationConfigSO _animConfig;
@@ -25,17 +37,28 @@ namespace BaseGames.Enemies
[SerializeField] protected HurtBox _hurtBox;
[Header("事件频道")]
[SerializeField] private BaseGames.Core.Events.TransformEventChannelSO _onEnemyDied;
[SerializeField] private BaseGames.Core.Events.StringEventChannelSO _onEnemyDied;
/// <summary>
/// 玩家生成事件频道(由 PlayerController.Start() 广播)。
/// 配置后替代 FindWithTag避免 N 个敌人同帧全场景标签扫描。
/// </summary>
[SerializeField] private BaseGames.Core.Events.TransformEventChannelSO _onPlayerSpawned;
// ── 导航代理IPathAgent由 EnemyNavAgent 实现)───────────────────
// Phase 1通过接口引用,避免对 Navigation 程序集的直接依赖。
// 通过接口引用,避免对 Navigation 程序集的直接依赖。
// 由子类 / Inspector 注入,或者运行时 GetComponent<IPathAgent>() 获取。
protected IPathAgent _nav;
protected IPathAgent _nav;
// 霸体来源(由 EnemyPoiseComponent.Awake() 自动注入TakeDamage 时读取)
private IPoiseSource _poiseSource;
private readonly CompositeDisposable _subs = new();
// ── 状态 ──────────────────────────────────────────────────────────
private EnemyStateType _currentState;
public EnemyStateType CurrentState => _currentState;
// POCO 状态对象字典:枚举保持对外 API 不变。
// 子类可在 Awake() 重写条目注入自定义状态对象。
protected readonly System.Collections.Generic.Dictionary<EnemyStateType, IEnemyState> _stateObjs
= new System.Collections.Generic.Dictionary<EnemyStateType, IEnemyState>();
// ── IDamageable ───────────────────────────────────────────────────
public bool IsInvincible => _currentState == EnemyStateType.Dead;
public int Defense => _stats != null ? _stats.Defense : 0;
@@ -53,10 +76,26 @@ namespace BaseGames.Enemies
return;
}
// Phase 2根据霸体结果选 Stagger / Hurt
ForceState(EnemyStateType.Hurt);
// 根据霸体等级选择 Stagger硬直或 Hurt受击
// ForceBreak 标记或 BreakLevel 超过当前霸体等级时触发 Stagger否则触发 Hurt。
PoiseLevel curPoise = _poiseSource?.GetCurrentPoiseLevel() ?? PoiseLevel.None;
bool causesStagger = info.Flags.HasFlag(DamageFlags.ForceBreak)
|| (int)info.Break > (int)curPoise;
ForceState(causesStagger ? EnemyStateType.Stagger : EnemyStateType.Hurt);
}
// BD 任务访问接口(公共只读属性)────────────────────────────────
public IPathAgent Nav => _nav;
public EnemyMovement Movement => _movement;
public EnemyStats Stats => _stats;
public AnimancerComponent Animancer => _animancer;
public EnemyAnimationConfigSO AnimConfig => _animConfig;
/// <summary>由 _onPlayerSpawned 事件缓存的玩家 Transform供 BD 任务读取。</summary>
public Transform PlayerTransform => _playerTransform;
#if GRAPH_DESIGNER
public BehaviorTree BehaviorTree => _behaviorTree;
#endif
// ── BD 行为树接口(虚方法)────────────────────────────────────────
public virtual void MoveTo(Vector2 target)
@@ -81,7 +120,10 @@ namespace BaseGames.Enemies
=> _stats != null && _stats.AttackCooldownTimer <= 0f;
public virtual bool IsPlayerInRange(float range)
=> _stats != null && _stats.DistanceToPlayer <= range;
=> _stats != null && _stats.SqrDistanceToPlayer <= range * range;
public virtual bool IsPlayerVisible()
=> _losResult; // BatchLOSSystem 写入;初始 false未见玩家
public virtual void FacePlayer()
{
@@ -95,43 +137,138 @@ namespace BaseGames.Enemies
_movement?.ApplyKnockback(info.KnockbackDirection, info.KnockbackForce);
}
public virtual void JumpTo(Vector2 target)
=> _movement?.JumpToTarget(target);
/// <summary>
/// 调整 BehaviorTree Tick 频率(非警觉=2帧/次,警觉=每帧)。
/// 由 BD_SetAlert 调用(架构 07_EnemyModule §13.5)。
/// </summary>
public void SetAggroTickRate(bool isAggro)
{
#if GRAPH_DESIGNER
// Opsive 运行时当前版本未直接暴露 frameInterval 字段。
// 需升级 Opsive 包或通过自定义 Tick 次数属性实现此功能。
Debug.LogWarning("[EnemyBase] SetAggroTickRate 当前无效Opsive 运行时尚未暴露 frameInterval请升级包后实现。", this);
_ = isAggro;
#endif
}
// ── 动画事件钩子(由 EnemyAnimationEvents 调用)────────────────────
/// <summary>生成弹幕 / 技能投射物。payload 为配置 Id由子类查表实现。</summary>
public virtual void SpawnProjectile(string payload) { }
/// <summary>切换二阶段形态Boss 等特殊敌人重写此方法)。</summary>
public virtual void TriggerPhaseTwo() { }
/// <summary>动画播放完毕回调(用于单次动画后返回 Idle 等逻辑)。</summary>
public virtual void OnAnimationComplete(string payload) { }
/// <summary>设置嘶吼状态(影响 Blackboard / 状态机行为)。</summary>
public virtual void SetRoaring(bool isRoaring) { }
// ── 状态控制 ──────────────────────────────────────────────────────
public void ForceState(EnemyStateType newState)
{
// Exit 当前状态
if (_stateObjs.TryGetValue(_currentState, out var prev))
prev.Exit(this);
_currentState = newState;
// Phase 2根据状态播放对应动画 / 触发硬直计时
// Enter 新状态
if (_stateObjs.TryGetValue(newState, out var next))
next.Enter(this);
}
// ── Unity 生命周期 ────────────────────────────────────────────────
protected virtual void Awake()
{
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
// 初始化 POCO 状态对象(子类可在调用 base.Awake() 后替换字典条目)
_stateObjs[EnemyStateType.Controlled] = new EnemyControlledState();
_stateObjs[EnemyStateType.Hurt] = new EnemyHurtState();
_stateObjs[EnemyStateType.Stagger] = new EnemyStaggerState();
_stateObjs[EnemyStateType.Dead] = new EnemyDeadState();
if (_stats != null && _statsSO != null)
_stats.Initialize(_statsSO);
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
_poiseSource = GetComponent<IPoiseSource>();
// Phase 1简单查找玩家Phase 2 改为事件频道订阅
var playerGO = GameObject.FindWithTag("Player");
if (playerGO != null) _playerTransform = playerGO.transform;
Debug.Assert(_statsSO != null, "[EnemyBase] _statsSO 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this);
Debug.Assert(_stats != null, "[EnemyBase] _stats 未绑定,请在 Prefab Inspector 中绑定 EnemyStats 组件。", this);
_stats.Initialize(_statsSO);
// 订阅玩家生成事件PlayerController.Start 广播),避免每个敌人独立 FindWithTag
// 订阅在 OnEnable 中处理
#if GRAPH_DESIGNER
_behaviorTree = GetComponent<BehaviorTree>();
if (_behaviorTree != null)
{
_behaviorTree.StartWhenEnabled = false;
_behaviorTree.PauseWhenDisabled = true;
_behaviorTree.StartBehavior();
}
#endif
}
protected virtual void Update()
{
_stats?.TickAttackCooldown(Time.deltaTime);
// 使用 sqrMagnitude 替代 Vector2.Distance避免每帧开平方计算
if (_playerTransform != null && _stats != null)
_stats.DistanceToPlayer = Vector2.Distance(transform.position, _playerTransform.position);
_stats.SqrDistanceToPlayer = ((Vector2)_playerTransform.position - (Vector2)transform.position).sqrMagnitude;
}
protected virtual void Start()
{
// 若事件未配置或玩家尚未广播,匹降为一次性查找
if (_playerTransform == null)
{
var playerGO = GameObject.FindWithTag("Player");
if (playerGO != null) _playerTransform = playerGO.transform;
}
// 播放 Idle 动画(若 Animancer 和配置都就绪)
if (_animancer != null && _animConfig != null && _animConfig.Idle != null)
_animancer.Play(_animConfig.Idle);
}
// ── 内部 ──────────────────────────────────────────────────────────
private Transform _playerTransform;
protected Transform _playerTransform;
private void SetPlayerTransform(Transform player) => _playerTransform = player;
protected virtual void OnEnable()
{
_onPlayerSpawned?.Subscribe(SetPlayerTransform).AddTo(_subs);
}
protected virtual void OnDisable()
{
_subs.Clear();
}
protected virtual void OnDestroy() { }
// LOS 缓存BatchLOSSystem 写入;降级时由 3 帧节流 Raycast 写入)
private bool _losResult;
// ── ILOSRequester ──────────────────────────────────────────────────
public Vector2 LOSOrigin => (Vector2)transform.position + _statsSO.EyeOffset;
public Vector2 LOSTarget => _playerTransform != null
? (Vector2)_playerTransform.position
: (Vector2)transform.position;
public LayerMask LOSBlockingMask => _statsSO.LOSBlockingMask;
public void ReceiveLOSResult(bool hasLineOfSight)
{
_losResult = hasLineOfSight;
}
// BehaviorTree 引用(#if GRAPH_DESIGNER 保护,避免空引用)
#if GRAPH_DESIGNER
private BehaviorTree _behaviorTree;
#endif
protected virtual void Die()
{
@@ -154,7 +291,8 @@ namespace BaseGames.Enemies
}
_feedback?.OnDeath();
_onEnemyDied?.Raise(transform);
_onEnemyDied?.Raise(_enemyId);
OnDied?.Invoke();
}
}

View File

@@ -4,8 +4,8 @@ using BaseGames.Combat;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人战斗组件(Phase 1 桩,架构 07_EnemyModule §4
/// Phase 2 实现:HitBox 按 AttackType 索引管理、伤害来源 SO 注入
/// 敌人战斗组件(架构 07_EnemyModule §4
/// HitBox 按 AttackType 索引管理,动画事件触发 HitBox 开关
/// </summary>
public class EnemyCombat : MonoBehaviour
{
@@ -13,7 +13,6 @@ namespace BaseGames.Enemies
public void StartAttack(AttackType type)
{
// Phase 1 桩Phase 2 播放攻击动画,由 AnimationEvent 触发 HitBox On/Off
int idx = (int)type;
EnableHitBox(idx);
}

View File

@@ -42,10 +42,11 @@ namespace BaseGames.Enemies
public void PlayLandImpact() { }
public void PlayAttackWhoosh() { }
public void PlayJumpLaunch() { }
public void PlayFootstep() { }
public void TriggerPreset(string presetId) { }
public void PlaySFXById(string sfxId) { }
// ── EnemyBase 语义方法(向前兼容)────────────────────────────────────────
// ── EnemyBase 语义方法 ────────────────────────────────────────────
/// <summary>受到伤害时调用(由 EnemyBase 触发)。</summary>
public void OnHit(DamageInfo info)
{

View File

@@ -4,13 +4,14 @@ namespace BaseGames.Enemies
{
/// <summary>
/// 敌人移动组件(架构 07_EnemyModule §3
/// Phase 1 实现:水平移动、面向目标、击退。
/// 实现:水平移动、面向目标、击退。
/// ⚠️ 使用 Rigidbody2D.velocityUnity 2022 LTS
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class EnemyMovement : MonoBehaviour
{
[SerializeField] private EnemyStatsSO _config;
[SerializeField] private EnemyStatsSO _config;
[SerializeField] private SpriteRenderer _spriteRenderer;
private Rigidbody2D _rb;
private int _facingDir = 1;
@@ -19,13 +20,13 @@ namespace BaseGames.Enemies
private void Awake()
{
Debug.Assert(_config != null, "[EnemyMovement] _config 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this);
_rb = GetComponent<Rigidbody2D>();
}
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。</summary>
public void MoveHorizontal(float dir)
{
if (_config == null) return;
float speed = _config.WalkSpeed;
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
UpdateFacing(dir);
@@ -54,14 +55,42 @@ namespace BaseGames.Enemies
_rb.velocity = new Vector2(0f, _rb.velocity.y);
}
/// <summary>
/// 向目标位置抖跃(抛物线累加填充)。
/// 计算初速使尔子到达目标,用 Impulse 施加力。
/// </summary>
public void JumpToTarget(Vector2 target)
{
if (_rb == null) return;
Vector2 delta = target - (Vector2)transform.position;
float gravMag = Mathf.Abs(Physics2D.gravity.y * _rb.gravityScale);
float timeAloft = Mathf.Max(0.1f, delta.x != 0f
? Mathf.Abs(delta.x) / _config.RunSpeed
: 0.5f);
float vy = (delta.y - 0.5f * (-gravMag) * timeAloft * timeAloft) / timeAloft;
float vx = delta.x / timeAloft;
_rb.velocity = new Vector2(vx, vy);
UpdateFacing(vx);
}
private void UpdateFacing(float dir)
{
if (Mathf.Approximately(dir, 0f)) return;
int newDir = dir > 0f ? 1 : -1;
if (newDir == _facingDir) return;
_facingDir = newDir;
Vector3 s = transform.localScale;
transform.localScale = new Vector3(Mathf.Abs(s.x) * newDir, s.y, s.z);
if (_spriteRenderer != null)
{
_spriteRenderer.flipX = newDir < 0;
}
else
{
// SpriteRenderer 未绑定时通过 localScale 翻转朝向
Vector3 s = transform.localScale;
transform.localScale = new Vector3(Mathf.Abs(s.x) * newDir, s.y, s.z);
}
}
}
}

View File

@@ -0,0 +1,40 @@
using UnityEngine;
using BaseGames.Combat;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人霸体组件(架构 06_CombatModule §13
/// Awake 时自动注入到同节点 HurtBox无需手动 Inspector 赋值)。
/// EnemyBase 或 BehaviorDesigner 任务可调用 SetPoiseLevel / ResetPoiseLevel 切换超甲状态。
/// </summary>
[RequireComponent(typeof(EnemyBase))]
public class EnemyPoiseComponent : MonoBehaviour, IPoiseSource
{
[SerializeField] private PoiseLevel _defaultPoiseLevel = PoiseLevel.None;
private PoiseLevel _currentPoiseLevel;
private void Awake()
{
_currentPoiseLevel = _defaultPoiseLevel;
// 自动注入到同节点 HurtBox架构 06 §13
if (TryGetComponent<HurtBox>(out var hurtBox))
hurtBox.SetPoiseSource(this);
}
// ── IPoiseSource ──────────────────────────────────────────────────────
/// <summary>返回当前帧的霸体等级(供 HurtBox.ReceiveDamage 步骤 3 比较)。</summary>
public PoiseLevel GetCurrentPoiseLevel() => _currentPoiseLevel;
// ── 外部 API ──────────────────────────────────────────────────────────
/// <summary>进入/退出超甲时由 EnemyBase 或 BehaviorDesigner 任务调用。</summary>
public void SetPoiseLevel(PoiseLevel level) => _currentPoiseLevel = level;
/// <summary>恢复到默认霸体等级(超甲结束时调用)。</summary>
public void ResetPoiseLevel() => _currentPoiseLevel = _defaultPoiseLevel;
}
}

View File

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

View File

@@ -0,0 +1,104 @@
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core.Events;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人 BehaviorTree 配额管理器(架构 07_EnemyModule §13
/// 每 REBALANCE_INTERVAL 帧,按到玩家距离排序已注册敌人,
/// 仅对最近的 _maxActiveBehaviorTrees 个敌人启用 BT。
/// 挂载在场景管理器 GameObject 上(每场景一个实例)。
/// </summary>
public class EnemyQuotaManager : MonoBehaviour
{
[SerializeField, Min(1)] private int _maxActiveBehaviorTrees = 12;
[Tooltip("PlayerController.Start() 广播此频道,替代 FindWithTag")]
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
private const int REBALANCE_INTERVAL = 10;
private int _frameCount;
// _registeredSet 用于 O(1) 重复检测_registered List 用于 Sort
private readonly HashSet<EnemyBase> _registeredSet = new();
private readonly List<EnemyBase> _registered = new();
private readonly Dictionary<EnemyBase, int> _indexMap = new();
// 缓存玩家 Transform
private Transform _playerTransform;
private readonly CompositeDisposable _subs = new();
private void OnEnable()
{
_onPlayerSpawned?.Subscribe(OnPlayerSpawned).AddTo(_subs);
}
private void OnDisable() => _subs.Clear();
private void OnPlayerSpawned(Transform playerTransform)
=> _playerTransform = playerTransform;
// ── 注册 / 注销 ───────────────────────────────────────────────────
public void Register(EnemyBase enemy)
{
if (enemy != null && _registeredSet.Add(enemy))
{
_indexMap[enemy] = _registered.Count;
_registered.Add(enemy);
}
}
public void Unregister(EnemyBase enemy)
{
if (enemy == null || !_registeredSet.Remove(enemy)) return;
int idx = _indexMap[enemy];
int last = _registered.Count - 1;
if (idx != last)
{
var moved = _registered[last];
_registered[idx] = moved;
_indexMap[moved] = idx;
}
_registered.RemoveAt(last);
_indexMap.Remove(enemy);
}
// ── Unity 生命周期 ────────────────────────────────────────────────
private void Update()
{
if (++_frameCount % REBALANCE_INTERVAL == 0)
Rebalance();
}
// ── 内部 ──────────────────────────────────────────────────────────
private void Rebalance()
{
if (_registered.Count == 0) return;
var playerPos = _playerTransform != null ? _playerTransform.position : Vector3.zero;
// 按距离平方升序排序(避免开方,性能更好)
_registered.Sort((a, b) =>
{
if (a == null) return 1;
if (b == null) return -1;
float sqA = (a.transform.position - playerPos).sqrMagnitude;
float sqB = (b.transform.position - playerPos).sqrMagnitude;
return sqA.CompareTo(sqB);
});
#if GRAPH_DESIGNER
for (int i = _registered.Count - 1; i >= 0; i--)
{
var enemy = _registered[i];
if (enemy == null) { _registered.RemoveAt(i); continue; }
var bt = enemy.BehaviorTree;
bool active = i < _maxActiveBehaviorTrees;
if (bt != null && bt.enabled != active)
bt.enabled = active;
}
#endif
}
}
}

View File

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

View File

@@ -1,29 +1,67 @@
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Events;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人运行时数值组件(架构 07_EnemyModule §2
/// 由 EnemyBase.Awake() 通过 Initialize(EnemyStatsSO) 注入配置。
/// 同时订阅难度变更事件,支持游戏进行中切换难度(架构 19 §5
/// </summary>
public class EnemyStats : MonoBehaviour
{
private EnemyStatsSO _config;
[SerializeField] private DifficultyChangedEventChannel _onDifficultyChanged;
public int MaxHP { get; private set; }
public int CurrentHP { get; private set; }
public int Defense { get; private set; }
public float AttackCooldownTimer { get; private set; }
/// <summary>每帧由 EnemyBase 更新(读取玩家位置后写入)。</summary>
public float DistanceToPlayer { get; set; }
/// <summary>
/// 每帧由 EnemyBase 更新sqrMagnitude避免 sqrt 开销)。
/// 使用方请与 range*range 比较,而非直接与 range 比较。
/// </summary>
public float SqrDistanceToPlayer { get; set; }
private readonly CompositeDisposable _subs = new();
private void OnEnable()
{
_onDifficultyChanged?.Subscribe(HandleDifficultyChanged).AddTo(_subs);
}
private void OnDisable()
{
_subs.Clear();
}
public void Initialize(EnemyStatsSO so)
{
_config = so;
MaxHP = so.MaxHP;
CurrentHP = so.MaxHP;
Defense = so.Defense;
_config = so;
Defense = so.Defense; // Defense 不随难度缩放(架构 19 §5
ApplyHPScaler();
CurrentHP = MaxHP;
}
/// <summary>难度变更时重算 HP保持 HP 比例,架构 19 §5。</summary>
private void HandleDifficultyChanged(DifficultyLevel _)
{
if (_config == null) return;
float hpRatio = MaxHP > 0 ? (float)CurrentHP / MaxHP : 1f;
ApplyHPScaler();
CurrentHP = Mathf.Clamp(Mathf.RoundToInt(MaxHP * hpRatio), 1, MaxHP);
}
private void ApplyHPScaler()
{
if (_config == null) return;
var scaler = ServiceLocator.GetOrDefault<IDifficultyService>()?.CurrentScaler;
MaxHP = scaler != null
? Mathf.Max(1, Mathf.RoundToInt(_config.MaxHP * scaler.EnemyHPMultiplier))
: _config.MaxHP;
}
public void TakeDamage(int amount)
@@ -39,7 +77,7 @@ namespace BaseGames.Enemies
public void ResetAttackCooldown()
{
AttackCooldownTimer = _config != null ? _config.AttackCooldown : 1f;
AttackCooldownTimer = _config.AttackCooldown;
}
}
}

View File

@@ -27,5 +27,11 @@ namespace BaseGames.Enemies
[Header("击退(作为来源时)")]
public float KnockbackForce = 5f;
public float HitStunDuration = 0.3f;
[Header("视线检测BatchLOSSystem")]
[Tooltip("相对 transform.position 的眼睛偏移量")]
public Vector2 EyeOffset = new Vector2(0f, 0.8f);
[Tooltip("遮挡 LOS 的物理图层")]
public LayerMask LOSBlockingMask = 1; // Default layer
}
}

View File

@@ -0,0 +1,70 @@
using UnityEngine;
using BaseGames.Combat;
namespace BaseGames.Enemies
{
/// <summary>
/// 飞行敌人。不依赖 PathBerserker2d 导航,直接通过 Rigidbody2D 向玩家移动。
/// 接触玩家时造成伤害。
/// </summary>
public class FlyingEnemy : EnemyBase
{
[SerializeField] private float _chaseSpeed = 3f;
[SerializeField] private DamageSourceSO _contactDamageSource;
private Rigidbody2D _rb;
protected override void Awake()
{
base.Awake();
_rb = GetComponent<Rigidbody2D>();
if (_rb != null)
{
_rb.gravityScale = 0f;
_rb.constraints = RigidbodyConstraints2D.FreezeRotation;
}
}
protected override void Update()
{
base.Update();
if (_playerTransform == null || _rb == null) return;
if (CurrentState == EnemyStateType.Dead ||
CurrentState == EnemyStateType.Stagger) return;
// 向玩家移动
Vector2 targetPos = _playerTransform.position;
Vector2 myPos = _rb.position;
Vector2 newPos = Vector2.MoveTowards(myPos, targetPos, _chaseSpeed * Time.deltaTime);
_rb.MovePosition(newPos);
}
public override void StopMovement()
{
if (_rb != null) _rb.velocity = Vector2.zero;
}
public override void MoveInDirection(float dir)
{
if (_rb != null) _rb.velocity = new Vector2(dir, 0f) * _chaseSpeed;
}
private void OnTriggerStay2D(Collider2D other)
{
if (_contactDamageSource == null) return;
if (CurrentState == EnemyStateType.Dead) return;
var hurtBox = other.GetComponent<HurtBox>();
if (hurtBox == null) return;
Vector2 knockDir = ((Vector2)other.bounds.center - _rb.position).normalized;
var info = DamageInfo.From(
_contactDamageSource,
knockDir,
transform.position,
gameObject.layer);
hurtBox.ReceiveDamage(info);
}
}
}

View File

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

View File

@@ -0,0 +1,27 @@
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// BatchLOSSystem 的注册接口(架构 07_EnemyModule §12
/// 实现此接口的 EnemyBase 子类可以注册到 BatchLOSSystem
/// 以批处理方式接收 LOSLine of Sight检测结果。
/// </summary>
public interface ILOSRequester
{
/// <summary>射线起点(通常是眼部位置)。</summary>
Vector2 LOSOrigin { get; }
/// <summary>射线终点(通常是玩家位置)。</summary>
Vector2 LOSTarget { get; }
/// <summary>遮挡 LOS 的物理图层。</summary>
LayerMask LOSBlockingMask { get; }
/// <summary>
/// 接收 LOS 检测结果。
/// <paramref name="hasLineOfSight"/>: true = 有视线false = 被遮挡。
/// </summary>
void ReceiveLOSResult(bool hasLineOfSight);
}
}

View File

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

View File

@@ -25,6 +25,9 @@ namespace BaseGames.Enemies
/// <summary>当前帧是否在移动(速度 > 0.01 且有有效路径)。</summary>
bool IsMoving { get; }
/// <summary>是否接近平台边缘(脚下或前方无地面时为 true。</summary>
bool IsNearEdge();
/// <summary>路径寻路失败事件(目标不可达时触发)。</summary>
event Action OnNavPathFailed;
}
@@ -37,6 +40,7 @@ namespace BaseGames.Enemies
public bool IsAtDestination() => true;
public void SetSpeed(float _) { }
public bool IsMoving => false;
public bool IsNearEdge() => false;
public event Action OnNavPathFailed { add { } remove { } }
}
}

View File

@@ -0,0 +1,72 @@
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.World;
namespace BaseGames.Enemies
{
/// <summary>
/// 战利品解算器(静态工具类)。
/// 根据 <see cref="LootTableSO"/> 和当前难度决定掉落内容。
/// </summary>
public static class LootResolver
{
/// <summary>
/// 解算并执行战利品掉落。
/// <para>保底 Geo直接加入玩家通过事件频道或在 <paramref name="worldPosition"/> 生成拾取物。</para>
/// <para>随机物品:加权随机后通过 CollectibleSpawner 实例化拾取物。</para>
/// </summary>
public static void Resolve(LootTableSO table, Vector2 worldPosition)
{
if (table == null) return;
// 保底 Geo
int guaranteedGeo = Random.Range(table.GuaranteedGeoMin, table.GuaranteedGeoMax + 1);
ApplyDifficultyGeoScale(ref guaranteedGeo);
if (guaranteedGeo > 0)
CollectibleSpawner.SpawnGeo(worldPosition, guaranteedGeo);
// 加权随机物品掉落
if (table.Entries == null || table.Entries.Length == 0) return;
var dm = ServiceLocator.GetOrDefault<IDifficultyService>();
bool isHard = dm != null &&
(int)dm.CurrentLevel >= (int)DifficultyLevel.Hard;
float totalWeight = 0f;
foreach (var entry in table.Entries)
{
float w = entry.BaseWeight;
if (isHard && entry.ScaleWithDifficulty) w *= 1.5f;
totalWeight += w;
}
if (totalWeight <= 0f) return;
float roll = Random.Range(0f, totalWeight);
float accum = 0f;
foreach (var entry in table.Entries)
{
float w = entry.BaseWeight;
if (isHard && entry.ScaleWithDifficulty) w *= 1.5f;
accum += w;
if (roll <= accum)
{
if (!string.IsNullOrEmpty(entry.ItemId))
CollectibleSpawner.SpawnItem(worldPosition, entry.ItemId);
else if (entry.GeoAmount > 0)
CollectibleSpawner.SpawnGeo(worldPosition, entry.GeoAmount);
return;
}
}
}
private static void ApplyDifficultyGeoScale(ref int geo)
{
var dm = ServiceLocator.GetOrDefault<IDifficultyService>();
if (dm?.CurrentScaler == null) return;
float mult = dm.CurrentScaler.GeoDropMultiplier;
geo = Mathf.RoundToInt(geo * mult);
}
}
}

View File

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

View File

@@ -0,0 +1,29 @@
using System;
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// 战利品表 ScriptableObject。定义敌人死亡后可掉落的物品和保底 Geo 区间。
/// </summary>
[CreateAssetMenu(menuName = "Enemies/LootTable")]
public class LootTableSO : ScriptableObject
{
[Serializable]
public struct LootEntry
{
public string ItemId;
public int GeoAmount;
/// <summary>基础权重(难度 Hard 以上会对 ScaleWithDifficulty 项目额外加权)。</summary>
public float BaseWeight;
public bool ScaleWithDifficulty;
}
[Header("掉落物")]
public LootEntry[] Entries;
[Header("保底 Geo")]
public int GuaranteedGeoMin = 1;
public int GuaranteedGeoMax = 5;
}
}

View File

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

View File

@@ -48,6 +48,17 @@ namespace BaseGames.Enemies.Navigation
{
if (_movement != null) _movement.movementSpeed = speed;
}
public bool IsNearEdge()
{
// 双射线检测:脚下前方是否有地面
if (_navAgent == null) return false;
var origin = (Vector2)transform.position;
var facing = transform.localScale.x >= 0f ? Vector2.right : Vector2.left;
var groundMask = ~0; // 检测所有层;可收窄至 Ground 层
bool groundAhead = Physics2D.Raycast(origin + facing * 0.3f, Vector2.down, 0.5f, groundMask);
return !groundAhead;
}
}
}

View File

@@ -0,0 +1,54 @@
using UnityEngine;
using BaseGames.Combat;
using BaseGames.Core;
using BaseGames.Core.Pool;
namespace BaseGames.Enemies
{
/// <summary>
/// 远程敌人。重写 <see cref="EnemyBase.SpawnProjectile"/> 以从对象池生成抛射物。
/// Behavior Designer 任务通过调用 SpawnProjectile(payload) 触发发射。
/// </summary>
public class RangedEnemy : EnemyBase
{
[SerializeField] private ProjectileConfigSO _projectileConfig;
[SerializeField] private Transform _firePoint;
protected override void Awake()
{
base.Awake();
Debug.Assert(_projectileConfig != null, $"[RangedEnemy] {name}: 未配置 ProjectileConfigSO。", this);
}
/// <summary>
/// 从对象池生成一枚抖射物并朝玩家发射。
/// payload 可留空;若传入多重配置键则由子类解析。
/// </summary>
public override void SpawnProjectile(string payload)
{
var spawnPos = _firePoint != null ? _firePoint.position : transform.position;
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
if (pool == null)
{
Debug.LogWarning($"[RangedEnemy] {name}: IObjectPoolService 未就绪,无法生成抛射物。");
return;
}
var go = pool.Spawn(
_projectileConfig.PoolKey,
spawnPos,
Quaternion.identity);
if (go == null) return;
var proj = go.GetComponent<Projectile>();
if (proj == null) return;
Vector2 dir = _playerTransform != null
? ((Vector2)(_playerTransform.position - spawnPos)).normalized
: (Vector2)transform.right;
var damageInfo = DamageInfo.From(_projectileConfig.DamageSource);
proj.Initialize(_projectileConfig, damageInfo, dir);
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8b008076fd3acda4db7895a3c4085159
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
namespace BaseGames.Enemies.States
{
/// <summary>
/// 受控状态(默认行为)。
/// 敌人处于 AI 正常控制下Behavior Designer 运行中),无特殊 Enter/Exit 动画逻辑。
/// </summary>
public sealed class EnemyControlledState : IEnemyState
{
public EnemyStateType StateType => EnemyStateType.Controlled;
public void Enter(EnemyBase owner) { }
public void Exit(EnemyBase owner) { }
}
}

View File

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

View File

@@ -0,0 +1,18 @@
namespace BaseGames.Enemies.States
{
/// <summary>
/// 死亡状态。Enter 仅作为扩展点(实际死亡流程由 EnemyBase.Die() 驱动),
/// 防止后续添加死亡特效/逻辑时需要改动 EnemyBase。
/// </summary>
public sealed class EnemyDeadState : IEnemyState
{
public EnemyStateType StateType => EnemyStateType.Dead;
/// <summary>
/// 死亡实际逻辑由 EnemyBase.Die() 处理(禁用碰撞体、播放动画、广播事件)。
/// 此方法保留为扩展点,子类可重写状态字典条目加入自定义逻辑。
/// </summary>
public void Enter(EnemyBase owner) { }
public void Exit(EnemyBase owner) { }
}
}

View File

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

View File

@@ -0,0 +1,26 @@
namespace BaseGames.Enemies.States
{
/// <summary>
/// 受击状态。播放受击动画,动画结束后自动回到 Controlled。
/// 原 EnemyBase.ForceState 中的 Hurt if-else 逻辑已迁移至此。
/// </summary>
public sealed class EnemyHurtState : IEnemyState
{
public EnemyStateType StateType => EnemyStateType.Hurt;
public void Enter(EnemyBase owner)
{
if (owner.Animancer == null || owner.AnimConfig?.Hurt == null) return;
var animState = owner.Animancer.Play(owner.AnimConfig.Hurt);
animState.Events(owner).OnEnd = () =>
{
// 只在仍处于 Hurt 时才回 Controlled避免 Die 时被覆盖
if (owner.CurrentState == EnemyStateType.Hurt)
owner.ForceState(EnemyStateType.Controlled);
};
}
public void Exit(EnemyBase owner) { }
}
}

View File

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

View File

@@ -0,0 +1,25 @@
namespace BaseGames.Enemies.States
{
/// <summary>
/// 僵直状态(霸体耗尽时触发)。
/// 播放 Stagger 动画(若配置),动画结束后自动回到 Controlled。
/// </summary>
public sealed class EnemyStaggerState : IEnemyState
{
public EnemyStateType StateType => EnemyStateType.Stagger;
public void Enter(EnemyBase owner)
{
if (owner.Animancer == null || owner.AnimConfig?.Stagger == null) return;
var animState = owner.Animancer.Play(owner.AnimConfig.Stagger);
animState.Events(owner).OnEnd = () =>
{
if (owner.CurrentState == EnemyStateType.Stagger)
owner.ForceState(EnemyStateType.Controlled);
};
}
public void Exit(EnemyBase owner) { }
}
}

View File

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

View File

@@ -0,0 +1,19 @@
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人状态 POCO 接口。
/// EnemyBase.ForceState 通过字典分发 Enter/Exit
/// 枚举 EnemyStateType 保持对外 API 不变。
/// </summary>
public interface IEnemyState
{
/// <summary>对应的枚举值(用于字典键以及 BD 查询)。</summary>
EnemyStateType StateType { get; }
/// <summary>进入此状态时调用。</summary>
void Enter(EnemyBase owner);
/// <summary>离开此状态时调用。</summary>
void Exit(EnemyBase owner);
}
}

View File

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