摄像机区域的架构改动

This commit is contained in:
2026-05-15 14:47:24 +08:00
parent 1b37297585
commit f264329751
3591 changed files with 1687228 additions and 446503 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 458bf9c7d1eae52438922d9630862656
folderAsset: yes
DefaultImporter:
externalObjects: {}
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 Action发动攻击。
/// CanAttack() 检查通过后调用 BeginAttack立即返回 Success。
/// </summary>
public class BD_Attack : Action
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null || !_enemy.CanAttack())
return TaskStatus.Failure;
_enemy.BeginAttack(AttackType.Melee);
return TaskStatus.Success;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6129e08d8bc28a24ebfb02c246e2e744
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_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,31 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Conditionals;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Conditional检查玩家是否在指定范围内。
/// 成功/失败直接驱动 BT 分支选择Selector / Sequence 节点)。
/// </summary>
public class BD_IsPlayerInRange : Conditional
{
/// <summary>检测范围Inspector 可配置,默认 6 米)。</summary>
public float Range = 6f;
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
return _enemy.IsPlayerInRange(Range) ? TaskStatus.Success : TaskStatus.Failure;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9d176aa20179c6b43a6798432eb073a4
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

@@ -0,0 +1,38 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action移向玩家。
/// OnStart 从 EnemyBase.PlayerTransform 获取玩家引用(由 _onPlayerSpawned 事件缓存,
/// 避免 FindWithTag 全场景扫描。OnUpdate 每帧更新导航目标。
/// </summary>
public class BD_MoveToPlayer : Action
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
if (_enemy.PlayerTransform == null) return TaskStatus.Failure;
_enemy.MoveTo(_enemy.PlayerTransform.position);
_enemy.FacePlayer();
return TaskStatus.Running;
}
public override void OnEnd()
{
_enemy?.StopMovement();
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,64 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
using UnityEngine;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// 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()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
if (ShouldFlip())
_dir = -_dir;
_enemy.MoveInDirection(_dir);
return TaskStatus.Running;
}
public override void OnEnd()
{
_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,11 @@
fileFormatVersion: 2
guid: b0fd85d7c7ce18f4193bb3b70707d055
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

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

@@ -0,0 +1,21 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Enemies.AI",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Enemies.AI",
"references": [
"BaseGames.Core",
"BaseGames.Core.Events",
"BaseGames.Enemies",
"BaseGames.Enemies.Boss.Patterns",
"Opsive.BehaviorDesigner.Runtime",
"Kybernetik.Animancer"
],
"autoReferenced": true,
"overrideReferences": false,
"includePlatforms": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c2f7ab9cf613d0c4fb44cdd94fea551f
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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,25 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Enemies",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Enemies",
"references": [
"BaseGames.Core",
"BaseGames.Core.Events",
"BaseGames.Combat",
"BaseGames.Feedback",
"BaseGames.World",
"MoreMountains.Tools",
"Kybernetik.Animancer",
"Opsive.BehaviorDesigner.Runtime",
"Unity.Addressables",
"Unity.ResourceManager"
],
"autoReferenced": true,
"overrideReferences": false,
"includePlatforms": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8ecc7c40216aa154f81faf49c6d655de
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
using BaseGames.Combat;
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// 接触伤害:组件启用时持续激活 HitBox令敌人对接触到的目标定期造成伤害。
/// 适用于无攻击动画的简单障碍物、环境危险或测试场景。
/// </summary>
[RequireComponent(typeof(HitBox))]
public class BodyContactDamage : MonoBehaviour
{
[SerializeField] private float _repeatInterval = 0.5f;
private HitBox _hitBox;
private float _timer;
private void Awake() => _hitBox = GetComponent<HitBox>();
private void OnEnable() { _hitBox?.Activate(); _timer = 0f; }
private void OnDisable() => _hitBox?.Deactivate();
private void Update()
{
_timer += Time.deltaTime;
if (_timer >= _repeatInterval)
{
_timer = 0f;
_hitBox.Activate();
}
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
using System;
using UnityEngine;
using UnityEngine.AddressableAssets;
using BaseGames.Combat;
namespace BaseGames.Boss
{
/// <summary>
/// 单个攻击图案的数据。伤害参数只写在此处BossSkillSO 不存参数。
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/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 = "BaseGames/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?.Play();
yield return GetWFS(window.Duration);
_weakPointSystem?.SetActive(false, 1f, activateSpecific);
window.CloseFeedback?.Play();
}
}
// ── 工具 ───────────────────────────────────────────────────────────────
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 = "BaseGames/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 BaseGames.Feedback;
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 SceneFeedback OpenFeedback;
public SceneFeedback 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 SceneFeedback 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,8 @@
fileFormatVersion: 2
guid: 6d8f5f23ee1dde046b1a7361ac1b6386
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Enemies.Boss.Patterns",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Enemies.Boss.Patterns",
"references": [
"BaseGames.Core",
"BaseGames.Core.Events",
"BaseGames.Enemies",
"BaseGames.Combat"
],
"autoReferenced": true,
"overrideReferences": false,
"includePlatforms": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8bc3529e552a34a45998814c7cd056e6
AssemblyDefinitionImporter:
externalObjects: {}
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 = "BaseGames/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

@@ -0,0 +1,44 @@
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人动画配置 SO架构 07_EnemyModule §5
/// 所有字段为 AnimationClip由 Animancer 直接播放。
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/Enemies/AnimationConfig")]
public class EnemyAnimationConfigSO : ScriptableObject
{
[Header("基础")]
public AnimationClip Idle;
public AnimationClip Walk;
public AnimationClip Run;
[Header("战斗")]
public AnimationClip Attack;
[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

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

View File

@@ -0,0 +1,353 @@
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 任务提供统一虚方法接口。
/// 包含BD 接口、受击、死亡流程。
/// ⚠️ _nav 字段类型为 IPathAgent在 BaseGames.Enemies.Navigation 中实现具体类)。
/// </summary>
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;
[Header("子组件Prefab Inspector 绑定)")]
[SerializeField] protected EnemyStats _stats;
[SerializeField] protected EnemyMovement _movement;
[SerializeField] protected EnemyCombat _combat;
[SerializeField] protected AnimancerComponent _animancer;
[SerializeField] protected EnemyFeedback _feedback;
[SerializeField] protected HurtBox _hurtBox;
[Header("事件频道")]
[SerializeField] private BaseGames.Core.Events.StringEventChannelSO _onEnemyDied;
/// <summary>
/// 玩家生成事件频道(由 PlayerController.Start() 广播)。
/// 配置后替代 FindWithTag避免 N 个敌人同帧全场景标签扫描。
/// </summary>
[SerializeField] private BaseGames.Core.Events.TransformEventChannelSO _onPlayerSpawned;
// ── 导航代理IPathAgent由 EnemyNavAgent 实现)───────────────────
// 通过接口引用,避免对 Navigation 程序集的直接依赖。
// 由子类 / Inspector 注入,或者运行时 GetComponent<IPathAgent>() 获取。
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 IsAlive => _currentState != EnemyStateType.Dead;
public bool IsInvincible => _currentState == EnemyStateType.Dead;
public int Defense => _stats != null ? _stats.Defense : 0;
public void TakeDamage(DamageInfo info)
{
if (_currentState == EnemyStateType.Dead) return;
_stats?.TakeDamage(info.FinalDamage);
_feedback?.OnHit(info);
if (_stats != null && _stats.CurrentHP <= 0)
{
Die();
return;
}
// 根据霸体等级选择 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)
=> _nav?.RequestMoveTo(target);
public virtual void MoveInDirection(float dir)
=> _movement?.MoveHorizontal(dir);
public virtual void StopMovement()
{
_nav?.StopNavigation();
_movement?.StopHorizontal();
}
public virtual void BeginAttack(AttackType type)
{
_combat?.StartAttack(type);
_stats?.ResetAttackCooldown();
}
public virtual bool CanAttack()
=> _stats != null && _stats.AttackCooldownTimer <= 0f;
public virtual bool IsPlayerInRange(float range)
=> _stats != null && _stats.SqrDistanceToPlayer <= range * range;
public virtual bool IsPlayerVisible()
=> _losResult; // BatchLOSSystem 写入;初始 false未见玩家
public virtual void FacePlayer()
{
if (_playerTransform != null)
_movement?.FaceTarget(_playerTransform.position);
}
public virtual void Knockback(DamageInfo info)
{
if (info.Flags.HasFlag(DamageFlags.NoKnockback)) return;
_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;
// Enter 新状态
if (_stateObjs.TryGetValue(newState, out var next))
next.Enter(this);
}
// ── Unity 生命周期 ────────────────────────────────────────────────
protected virtual void Awake()
{
// 初始化 POCO 状态对象(子类可在调用 base.Awake() 后替换字典条目)
_stateObjs[EnemyStateType.Controlled] = new EnemyControlledState();
_stateObjs[EnemyStateType.Hurt] = new EnemyHurtState();
_stateObjs[EnemyStateType.Stagger] = new EnemyStaggerState();
_stateObjs[EnemyStateType.Dead] = new EnemyDeadState();
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
_poiseSource = GetComponent<IPoiseSource>();
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.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);
}
// ── 内部 ──────────────────────────────────────────────────────────
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()
{
if (_currentState == EnemyStateType.Dead) return;
ForceState(EnemyStateType.Dead);
// 禁用所有碰撞体
foreach (var col in GetComponentsInChildren<Collider2D>())
col.enabled = false;
// 播放死亡动画
if (_animancer != null && _animConfig != null && _animConfig.Dead != null)
{
var state = _animancer.Play(_animConfig.Dead);
state.Events(this).OnEnd = () => Destroy(gameObject);
}
else
{
Destroy(gameObject, 1.5f);
}
_feedback?.OnDeath();
_onEnemyDied?.Raise(_enemyId);
OnDied?.Invoke();
}
private void OnDrawGizmos()
{
#if UNITY_EDITOR
if (_statsSO == null) return;
// ── 侦测范围(淡橙圆,始终可见)────────────────────────────
Gizmos.color = new Color(1f, 0.6f, 0.1f, 0.18f);
HitBox.DrawWireCircle2D(transform.position, _statsSO.DetectRange);
// ── 攻击范围(淡红圆)───────────────────────────────────────
Gizmos.color = new Color(1f, 0.15f, 0.15f, 0.22f);
HitBox.DrawWireCircle2D(transform.position, _statsSO.AttackRange);
// ── 运行时LOS 连线 ────────────────────────────────────────
if (!Application.isPlaying || _playerTransform == null) return;
Vector3 eyeWorld = transform.position + new Vector3(_statsSO.EyeOffset.x, _statsSO.EyeOffset.y, 0f);
Vector3 playerPos = _playerTransform.position;
float sqrDist = (playerPos - transform.position).sqrMagnitude;
bool inRange = sqrDist <= _statsSO.DetectRange * _statsSO.DetectRange;
// 眼睛位置小圆点(金黄)
Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.85f);
HitBox.DrawWireCircle2D(eyeWorld, 0.07f, 8);
if (inRange || _losResult)
{
// 有 LOS → 橙色实线;在范围内但遮挡 → 灰色虚感(透明度低)
Gizmos.color = _losResult
? new Color(1f, 0.5f, 0f, 0.85f)
: new Color(0.6f, 0.6f, 0.6f, 0.25f);
Gizmos.DrawLine(eyeWorld, playerPos);
}
#endif
}
private void OnDrawGizmosSelected()
{
#if UNITY_EDITOR
if (_statsSO == null) return;
// 选中时加亮范围圆
Gizmos.color = new Color(1f, 0.6f, 0.1f, 0.6f);
HitBox.DrawWireCircle2D(transform.position, _statsSO.DetectRange);
Gizmos.color = new Color(1f, 0.2f, 0.2f, 0.7f);
HitBox.DrawWireCircle2D(transform.position, _statsSO.AttackRange);
#endif
}
}
// ── 枚举(架构 07 §1────────────────────────────────────────────────
public enum EnemyStateType { Controlled, Hurt, Stagger, Dead }
public enum AttackType { Melee, Ranged, Special }
}

View File

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

View File

@@ -0,0 +1,38 @@
using UnityEngine;
using BaseGames.Combat;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人战斗组件(架构 07_EnemyModule §4
/// HitBox 按 AttackType 索引管理,动画事件触发 HitBox 开关。
/// </summary>
public class EnemyCombat : MonoBehaviour
{
[SerializeField] private HitBox[] _hitBoxes; // Inspector 按 AttackType 索引绑定
public void StartAttack(AttackType type)
{
int idx = (int)type;
EnableHitBox(idx);
}
public void EnableHitBox(int index)
{
if (_hitBoxes == null || index >= _hitBoxes.Length) return;
_hitBoxes[index]?.Activate();
}
public void DisableHitBox(int index)
{
if (_hitBoxes == null || index >= _hitBoxes.Length) return;
_hitBoxes[index]?.Deactivate();
}
public void DisableAllHitBoxes()
{
if (_hitBoxes == null) return;
foreach (var hb in _hitBoxes) hb?.Deactivate();
}
}
}

View File

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

View File

@@ -0,0 +1,63 @@
using UnityEngine;
using MoreMountains.Feedbacks;
using BaseGames.Combat;
using BaseGames.Feedback;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人反馈播放器:实现 IFeedbackPlayer将行为语义映射到 MMF_Player 实例。
/// 同时保留 OnHit/OnDeath 语义方法供 EnemyBase 直接调用(转发给接口方法)。
/// </summary>
public class EnemyFeedback : MonoBehaviour, IFeedbackPlayer
{
[Header("命中反馈")]
[SerializeField] private MMF_Player _onHitLight;
[SerializeField] private MMF_Player _onHitMedium;
[SerializeField] private MMF_Player _onHitHeavy;
[Header("受伤 / 死亡反馈")]
[SerializeField] private MMF_Player _onTakeHit;
[SerializeField] private MMF_Player _onDeath;
// ── IFeedbackPlayer ──────────────────────────────────────────────────────
public void PlayHit(HitWeight weight)
{
var player = weight switch
{
HitWeight.Light => _onHitLight,
HitWeight.Medium => _onHitMedium,
HitWeight.Heavy => _onHitHeavy,
_ => _onHitLight,
};
player?.PlayFeedbacks();
}
public void PlayTakeHit() => _onTakeHit?.PlayFeedbacks();
public void PlayDeath() => _onDeath?.PlayFeedbacks();
// 以下方法对敌人无意义,提供空实现保持接口完整
public void PlayParrySuccess() { }
public void PlayHeal() { }
public void PlayLandImpact() { }
public void PlayAttackWhoosh() { }
public void PlayJumpLaunch() { }
public void PlayFootstep() { }
public void TriggerPreset(string presetId) { }
public void PlaySFXById(string sfxId) { }
// ── EnemyBase 语义方法 ────────────────────────────────────────────
/// <summary>受到伤害时调用(由 EnemyBase 触发)。</summary>
public void OnHit(DamageInfo info)
{
PlayTakeHit();
}
/// <summary>死亡时调用(由 EnemyBase 触发)。</summary>
public void OnDeath()
{
PlayDeath();
}
}
}

View File

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

View File

@@ -0,0 +1,142 @@
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人移动组件(架构 07_EnemyModule §3
/// 实现:水平移动、面向目标、击退。
/// ⚠️ 使用 Rigidbody2D.velocityUnity 2022 LTS
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class EnemyMovement : MonoBehaviour
{
[SerializeField] private EnemyStatsSO _config;
[SerializeField] private SpriteRenderer _spriteRenderer;
private Rigidbody2D _rb;
private int _facingDir = 1;
public bool IsGrounded { get; private set; }
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)
{
float speed = _config.WalkSpeed;
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
UpdateFacing(dir);
}
/// <summary>显式指定速度BD 追击任务调用)。</summary>
public void MoveWithSpeed(float dir, float speed)
{
_rb.velocity = new Vector2(dir * speed, _rb.velocity.y);
UpdateFacing(dir);
}
public void FaceTarget(Vector2 targetPos)
{
float dir = targetPos.x < transform.position.x ? -1f : 1f;
UpdateFacing(dir);
}
public void ApplyKnockback(Vector2 dir, float force)
{
_rb.velocity = dir.normalized * force;
}
public void StopHorizontal()
{
_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;
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);
}
}
private void OnDrawGizmos()
{
#if UNITY_EDITOR
// ── 1. 敌人物理轮廓(珊瑚红,区别于玩家绿色)────────────────
Gizmos.color = new Color(1f, 0.45f, 0.35f, 0.65f);
foreach (var col in GetComponents<Collider2D>())
{
if (col.isTrigger) continue;
BaseGames.Combat.HitBox.DrawCollider2DWire(col);
}
// ── 2. 朝向箭头(橙色)──────────────────────────────────────
Vector3 center = transform.position;
DrawArrow2D(center, center + new Vector3(_facingDir * 0.5f, 0f, 0f),
new Color(1f, 0.6f, 0.1f, 0.9f));
#endif
}
private void OnDrawGizmosSelected()
{
#if UNITY_EDITOR
// 运行时:青色箭头显示速度向量(选中时)
if (!Application.isPlaying || _rb == null) return;
Vector2 vel = _rb.velocity;
if (vel.sqrMagnitude < 0.01f) return;
DrawArrow2D(transform.position, transform.position + (Vector3)(vel * 0.12f),
new Color(0.2f, 0.9f, 1f, 0.9f), 0.1f);
#endif
}
// 在 Gizmos 空间绘制带箭头的 2D 有向线段
private static void DrawArrow2D(Vector3 from, Vector3 to, Color color, float headLen = 0.15f)
{
Vector3 dir = to - from;
if (dir.sqrMagnitude < 0.0001f) return;
dir = dir.normalized;
Gizmos.color = color;
Gizmos.DrawLine(from, to);
float cos = 0.8192f, sin = 0.5736f; // cos/sin 35°
float bx = -dir.x, by = -dir.y;
Vector3 wing1 = new Vector3(bx * cos - by * sin, bx * sin + by * cos, 0f) * headLen;
Vector3 wing2 = new Vector3(bx * cos + by * sin, -bx * sin + by * cos, 0f) * headLen;
Gizmos.DrawLine(to, to + wing1);
Gizmos.DrawLine(to, to + wing2);
}
}
}

View File

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

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

@@ -0,0 +1,83 @@
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 更新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;
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)
{
CurrentHP = Mathf.Max(0, CurrentHP - amount);
}
public void TickAttackCooldown(float dt)
{
if (AttackCooldownTimer > 0f)
AttackCooldownTimer -= dt;
}
public void ResetAttackCooldown()
{
AttackCooldownTimer = _config.AttackCooldown;
}
}
}

View File

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

View File

@@ -0,0 +1,37 @@
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人属性配置 SO架构 07_EnemyModule §2
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/Enemies/EnemyStats")]
public class EnemyStatsSO : ScriptableObject
{
[Header("生命")]
public int MaxHP = 50;
[Header("防御")]
public int Defense = 0;
[Header("移动")]
public float WalkSpeed = 2f;
public float RunSpeed = 4f;
[Header("战斗")]
public int AttackDamage = 10;
public float AttackRange = 1.5f;
public float AttackCooldown = 1f;
public float DetectRange = 6f;
[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,11 @@
fileFormatVersion: 2
guid: ed4391dfa14c0304c8932f1ef9f8ce63
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

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);
}
}

Some files were not shown because too many files have changed in this diff Show More