From 20c25682221579ab067270ba086042a86159af95 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Fri, 3 Jul 2026 14:58:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20EnemyBrainContext=E2=80=94=E2=80=94?= =?UTF-8?q?EnemyBase=E5=AD=90=E7=B3=BB=E7=BB=9F=E9=80=82=E9=85=8D=E4=B8=BA?= =?UTF-8?q?BrainGraph=E8=83=BD=E5=8A=9B=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/_Game/Scripts/Enemies/AIBrain.meta | 8 ++ .../Enemies/AIBrain/EnemyBrainContext.cs | 105 ++++++++++++++++++ .../Enemies/AIBrain/EnemyBrainContext.cs.meta | 11 ++ .../Scripts/Enemies/BaseGames.Enemies.asmdef | 1 + 4 files changed, 125 insertions(+) create mode 100644 Assets/_Game/Scripts/Enemies/AIBrain.meta create mode 100644 Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs create mode 100644 Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs.meta diff --git a/Assets/_Game/Scripts/Enemies/AIBrain.meta b/Assets/_Game/Scripts/Enemies/AIBrain.meta new file mode 100644 index 00000000..e8d13898 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/AIBrain.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4dbaeb7890d1b7140b255b738fdc3ce0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs b/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs new file mode 100644 index 00000000..d6020fa6 --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs @@ -0,0 +1,105 @@ +using UnityEngine; +using BaseGames.AI; + +namespace BaseGames.Enemies +{ + /// + /// 把 EnemyBase 的子系统包装成 BrainGraph 的能力接口(每敌人一实例)。 + /// 状态逻辑只依赖这些接口,不直接引用具体子系统。 + /// + public sealed class EnemyBrainContext : IAiContext, ISensor, IMover, ICombatant, IActorVitals + { + readonly EnemyBase _enemy; + readonly Blackboard _blackboard = new Blackboard(); + Vector2 _lastKnown; + float _lostTimer; + + public EnemyBrainContext(EnemyBase enemy) { _enemy = enemy; } + + // ---- IAiContext ---- + public ISensor Sensor => this; + public IMover Mover => this; + public ICombatant Combat => this; + public IActorVitals Vitals => this; + public Blackboard Blackboard => _blackboard; + + /// 每帧由 EnemyAiBrain 在 Tick 之前调用,维护最后已知位置与丢失计时。 + public void Refresh(float dt) + { + if (_enemy.IsPlayerVisible() && _enemy.PlayerTransform != null) + { + _lastKnown = _enemy.PlayerTransform.position; + _lostTimer = 0f; + } + else + { + _lostTimer += dt; + } + } + + /// 对象池复用时清空临时态。 + public void ResetScratch() + { + _blackboard.Clear(); + _lastKnown = _enemy.transform.position; + _lostTimer = 0f; + } + + // ---- ISensor ---- + public bool SeesPlayer() => _enemy.IsPlayerVisible(); + public bool InRange(float range) => _enemy.IsPlayerInRange(range); + public bool LostFor(float seconds) => !_enemy.IsPlayerVisible() && _lostTimer >= seconds; + public Vector2 LastKnown => _lastKnown; + + // ---- IMover ---- + public void MoveTo(Vector2 target) => _enemy.MoveTo(target); + public void FacePlayer() => _enemy.FacePlayer(); + public void Stop() => _enemy.StopMovement(); + public void WalkRandom() + { + var nav = _enemy.Nav; + if (nav != null && !nav.IsMoving) nav.WalkToRandom(); + } + public void LookAround() => _enemy.BeginLookAround(); + + // ---- ICombatant ---- + public bool UseAbility(string abilityId) + { + var a = _enemy.Abilities?.Get(abilityId); + return a != null && a.Execute(); + } + public bool IsAbilityRunning(string abilityId = null) + { + var reg = _enemy.Abilities; + if (reg == null) return false; + if (abilityId != null) + { + var a = reg.Get(abilityId); + return a != null && a.IsRunning; + } + var all = reg.All; + for (int i = 0; i < all.Count; i++) + if (all[i].IsRunning) return true; + return false; + } + public bool NoAbilityRunning => !IsAbilityRunning(); + public bool CanUseAbility(string abilityId) + { + var a = _enemy.Abilities?.Get(abilityId); + return a != null && a.CanUse; + } + + // ---- IActorVitals ---- + public bool IsAlive => _enemy.IsAlive; + public bool IsControllable => _enemy.CurrentState == EnemyStateType.Controlled; + public float HpPercent + { + get + { + var stats = _enemy.Stats; + return stats != null && stats.MaxHP > 0 ? (float)stats.CurrentHP / stats.MaxHP : 0f; + } + } + public bool HpBelow(float ratio) => HpPercent < ratio; + } +} diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs.meta b/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs.meta new file mode 100644 index 00000000..9934fd4b --- /dev/null +++ b/Assets/_Game/Scripts/Enemies/AIBrain/EnemyBrainContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9459e1a1d93189547ab3354d181bf4ff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef b/Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef index 38eff23e..a6bc0a21 100644 --- a/Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef +++ b/Assets/_Game/Scripts/Enemies/BaseGames.Enemies.asmdef @@ -3,6 +3,7 @@ "rootNamespace": "BaseGames.Enemies", "references": [ "BaseGames.Core", + "BaseGames.AI", "BaseGames.Core.Events", "BaseGames.Combat", "BaseGames.Feedback",