chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

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

View File

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,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,41 @@
#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移向玩家。
/// OnStart 缓存玩家 TransformOnUpdate 每帧更新导航目标。
/// </summary>
public class BD_MoveToPlayer : Action
{
private EnemyBase _enemy;
private Transform _playerTransform;
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;
_enemy.MoveTo(_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,35 @@
#if GRAPH_DESIGNER
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action敌人巡逻行为Phase 1 简单实现)。
/// 挂在 BT 节点上,持续令敌人向当前朝向移动。
/// </summary>
public class BD_Patrol : Action
{
private EnemyBase _enemy;
public override void OnStart()
{
_enemy = GetComponent<EnemyBase>();
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
// Phase 1固定向右巡逻Phase 2 改为往返检测
_enemy.MoveInDirection(1f);
return TaskStatus.Running;
}
public override void OnEnd()
{
_enemy?.StopMovement();
}
}
}
#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,17 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Enemies.AI",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Enemies.AI",
"references": [
"BaseGames.Enemies",
"Opsive.BehaviorDesigner.Runtime"
],
"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,3 @@
// Placeholder to prevent asmdef-no-scripts warning.
namespace BaseGames.Enemies.AI { }

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e18876511cb4b9e4ab43cbce1215c72c
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",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Enemies",
"references": [
"BaseGames.Core",
"BaseGames.Core.Events",
"BaseGames.Combat",
"BaseGames.Feedback",
"MoreMountains.Tools",
"Kybernetik.Animancer"
],
"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,8 @@
fileFormatVersion: 2
guid: fe0b3fb5ffe55d84a86db3085bd868f1
folderAsset: yes
DefaultImporter:
externalObjects: {}
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,17 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Enemies.Boss.Patterns",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Enemies.Boss.Patterns",
"references": [
"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,3 @@
// Placeholder to prevent asmdef-no-scripts warning.
namespace BaseGames.Enemies.Boss.Patterns { }

View File

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

View File

@@ -0,0 +1,24 @@
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人动画配置 SO架构 07_EnemyModule §5
/// 所有字段为 AnimationClip由 Animancer 直接播放。
/// </summary>
[CreateAssetMenu(menuName = "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 Dead;
}
}

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,164 @@
using UnityEngine;
using Animancer;
using BaseGames.Combat;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人基类(架构 07_EnemyModule §1
/// 实现 IDamageable为 Behavior Designer 任务提供统一虚方法接口。
/// Phase 1 实现完整骨架BD 接口、受击、死亡流程。
/// ⚠️ _nav 字段类型为 IPathAgent在 BaseGames.Enemies.Navigation 中实现具体类)。
/// </summary>
public class EnemyBase : MonoBehaviour, IDamageable
{
[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.TransformEventChannelSO _onEnemyDied;
// ── 导航代理IPathAgent由 EnemyNavAgent 实现)───────────────────
// Phase 1通过接口引用避免对 Navigation 程序集的直接依赖。
// 由子类 / Inspector 注入,或者运行时 GetComponent<IPathAgent>() 获取。
protected IPathAgent _nav;
// ── 状态 ──────────────────────────────────────────────────────────
private EnemyStateType _currentState;
public EnemyStateType CurrentState => _currentState;
// ── IDamageable ───────────────────────────────────────────────────
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;
}
// Phase 2根据霸体结果选 Stagger / Hurt
ForceState(EnemyStateType.Hurt);
}
// ── 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.DistanceToPlayer <= range;
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 void ForceState(EnemyStateType newState)
{
_currentState = newState;
// Phase 2根据状态播放对应动画 / 触发硬直计时
}
// ── Unity 生命周期 ────────────────────────────────────────────────
protected virtual void Awake()
{
_nav = GetComponent<IPathAgent>() ?? new NullPathAgent();
if (_stats != null && _statsSO != null)
_stats.Initialize(_statsSO);
// Phase 1简单查找玩家Phase 2 改为事件频道订阅
var playerGO = GameObject.FindWithTag("Player");
if (playerGO != null) _playerTransform = playerGO.transform;
}
protected virtual void Update()
{
_stats?.TickAttackCooldown(Time.deltaTime);
if (_playerTransform != null && _stats != null)
_stats.DistanceToPlayer = Vector2.Distance(transform.position, _playerTransform.position);
}
protected virtual void Start()
{
// 播放 Idle 动画(若 Animancer 和配置都就绪)
if (_animancer != null && _animConfig != null && _animConfig.Idle != null)
_animancer.Play(_animConfig.Idle);
}
// ── 内部 ──────────────────────────────────────────────────────────
private Transform _playerTransform;
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(transform);
}
}
// ── 枚举(架构 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,39 @@
using UnityEngine;
using BaseGames.Combat;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人战斗组件Phase 1 桩,架构 07_EnemyModule §4
/// Phase 2 实现HitBox 按 AttackType 索引管理、伤害来源 SO 注入。
/// </summary>
public class EnemyCombat : MonoBehaviour
{
[SerializeField] private HitBox[] _hitBoxes; // Inspector 按 AttackType 索引绑定
public void StartAttack(AttackType type)
{
// Phase 1 桩Phase 2 播放攻击动画,由 AnimationEvent 触发 HitBox On/Off
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,62 @@
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 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,67 @@
using UnityEngine;
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;
private Rigidbody2D _rb;
private int _facingDir = 1;
public bool IsGrounded { get; private set; }
private void Awake()
{
_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);
}
/// <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);
}
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);
}
}
}

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,45 @@
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人运行时数值组件(架构 07_EnemyModule §2
/// 由 EnemyBase.Awake() 通过 Initialize(EnemyStatsSO) 注入配置。
/// </summary>
public class EnemyStats : MonoBehaviour
{
private EnemyStatsSO _config;
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; }
public void Initialize(EnemyStatsSO so)
{
_config = so;
MaxHP = so.MaxHP;
CurrentHP = so.MaxHP;
Defense = so.Defense;
}
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 != null ? _config.AttackCooldown : 1f;
}
}
}

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,31 @@
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人属性配置 SO架构 07_EnemyModule §2
/// </summary>
[CreateAssetMenu(menuName = "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;
}
}

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,42 @@
using System;
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// 导航代理抽象接口(架构 07_EnemyModule §5
/// EnemyBase 和 BD Task 只依赖此接口,不依赖具体导航库。
/// 实现EnemyNavAgentPathBerserker2d测试用 NullPathAgent。
/// </summary>
public interface IPathAgent
{
/// <summary>请求移动到世界坐标 target。</summary>
void RequestMoveTo(Vector2 target);
/// <summary>立即停止导航(清除路径)。</summary>
void StopNavigation();
/// <summary>是否已到达目标(距离 ≤ stoppingDistance。</summary>
bool IsAtDestination();
/// <summary>运行时覆盖移动速度。</summary>
void SetSpeed(float speed);
/// <summary>当前帧是否在移动(速度 > 0.01 且有有效路径)。</summary>
bool IsMoving { get; }
/// <summary>路径寻路失败事件(目标不可达时触发)。</summary>
event Action OnNavPathFailed;
}
// ── 无导航 / 测试用空实现 ─────────────────────────────────────────────
public sealed class NullPathAgent : IPathAgent
{
public void RequestMoveTo(Vector2 _) { }
public void StopNavigation() { }
public bool IsAtDestination() => true;
public void SetSpeed(float _) { }
public bool IsMoving => false;
public event Action OnNavPathFailed { add { } remove { } }
}
}

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Enemies.Navigation",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Enemies.Navigation",
"references": [
"BaseGames.Enemies",
"PathBerserker2d"
],
"autoReferenced": true,
"overrideReferences": false,
"includePlatforms": []
}

View File

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

View File

@@ -0,0 +1,53 @@
using UnityEngine;
using PathBerserker2d;
using BaseGames.Enemies;
namespace BaseGames.Enemies.Navigation
{
/// <summary>
/// PathBerserker2d 导航代理包装器(架构 07_EnemyModule §5
/// 实现 IPathAgent 接口,使 EnemyBase 和 BD Task 无需直接依赖 PB2d 类型。
/// PB2d APIUpdatePath(Vector2)、Stop()、TransformBasedMovement.movementSpeed、IsFollowingAPath。
/// </summary>
[RequireComponent(typeof(NavAgent))]
[RequireComponent(typeof(TransformBasedMovement))]
public class EnemyNavAgent : MonoBehaviour, IPathAgent
{
private NavAgent _navAgent;
private TransformBasedMovement _movement;
/// <summary>正在沿路径移动时为 true。</summary>
public bool IsMoving => _navAgent != null && _navAgent.IsFollowingAPath;
public event System.Action OnNavPathFailed;
private void Awake()
{
_navAgent = GetComponent<NavAgent>();
_movement = GetComponent<TransformBasedMovement>();
}
public void RequestMoveTo(Vector2 target)
{
_navAgent?.UpdatePath(target);
}
public void StopNavigation()
{
_navAgent?.Stop();
}
public bool IsAtDestination()
{
if (_navAgent == null) return true;
// 已停止 OR 在目标线段上且不再跟随路径
return _navAgent.IsIdle;
}
public void SetSpeed(float speed)
{
if (_movement != null) _movement.movementSpeed = speed;
}
}
}

View File

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

View File

@@ -0,0 +1,3 @@
// Placeholder to prevent asmdef-no-scripts warning.
namespace BaseGames.Enemies.Navigation { }

View File

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

View File

@@ -0,0 +1,3 @@
// Placeholder to prevent asmdef-no-scripts warning.
namespace BaseGames.Enemies { }

View File

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