diff --git a/DungeonShooting_Godot/src/game/event/EventEnum.cs b/DungeonShooting_Godot/src/game/event/EventEnum.cs index ec25a87..7e59f96 100644 --- a/DungeonShooting_Godot/src/game/event/EventEnum.cs +++ b/DungeonShooting_Godot/src/game/event/EventEnum.cs @@ -4,5 +4,8 @@ /// public enum EventEnum { - Test, + /// + /// 敌人发现玩家 + /// + OnEnemyFindPlayer, } diff --git a/DungeonShooting_Godot/src/game/role/Role.cs b/DungeonShooting_Godot/src/game/role/Role.cs index dc7063b..e3a2568 100644 --- a/DungeonShooting_Godot/src/game/role/Role.cs +++ b/DungeonShooting_Godot/src/game/role/Role.cs @@ -302,6 +302,14 @@ } /// + /// 获取当前角色的中心点坐标 + /// + public Vector2 GetCenterPosition() + { + return MountPoint.GlobalPosition; + } + + /// /// 使角色看向指定的坐标, /// 注意, 调用该函数会清空 LookTarget, 因为拥有 LookTarget 时也会每帧更新玩家视野位置 /// diff --git a/DungeonShooting_Godot/src/game/role/StateController.cs b/DungeonShooting_Godot/src/game/role/StateController.cs index d3c40b4..f349561 100644 --- a/DungeonShooting_Godot/src/game/role/StateController.cs +++ b/DungeonShooting_Godot/src/game/role/StateController.cs @@ -8,6 +8,11 @@ public class StateController : Component where T : ActivityObject where S : Enum { /// + /// 获取当前状态 + /// + public S CurrState => CurrStateBase != null ? CurrStateBase.State : default; + + /// /// 当前活跃的状态对象实例 /// public StateBase CurrStateBase { get; private set; } diff --git a/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs b/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs index 3c3e222..3d9a920 100644 --- a/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs +++ b/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs @@ -30,7 +30,7 @@ /// /// 敌人身上的状态机控制器 /// - public StateController StateController { get; } + public StateController StateController { get; } /// /// 视野半径, 单位像素, 发现玩家后改视野范围可以穿墙 @@ -63,10 +63,11 @@ public Position2D NavigationPoint { get; } private float _enemyAttackTimer = 0; + private EventBinder _eventBinder; public Enemy() : base(ResourcePath.prefab_role_Enemy_tscn) { - StateController = new StateController(); + StateController = new StateController(); AddComponent(StateController); AttackLayer = PhysicsLayer.Wall | PhysicsLayer.Props | PhysicsLayer.Player; @@ -88,14 +89,15 @@ StateController.Register(new AiNormalState()); StateController.Register(new AiProbeState()); StateController.Register(new AiTailAfterState()); - StateController.Register(new AIAttackState()); + StateController.Register(new AiTargetInViewState()); + StateController.Register(new AiLeaveForState()); } public override void _Ready() { base._Ready(); //默认状态 - StateController.ChangeState(AIStateEnum.AINormal); + StateController.ChangeState(AiStateEnum.AiNormal); NavigationAgent2D.SetTargetLocation(GameApplication.Instance.Room.Player.GlobalPosition); } @@ -106,11 +108,14 @@ { _enemies.Add(this); } + _eventBinder = EventManager.AddEventListener(EventEnum.OnEnemyFindPlayer, OnEnemyFindPlayer); } public override void _ExitTree() { _enemies.Remove(this); + _eventBinder.RemoveEventListener(); + _eventBinder = null; } public override void _PhysicsProcess(float delta) @@ -174,6 +179,23 @@ } /// + /// 返回目标点是否在跟随状态下的视野半径内 + /// + public bool IsInTailAfterViewRange(Vector2 target) + { + var isForward = IsPositionInForward(target); + if (isForward) + { + if (GlobalPosition.DistanceSquaredTo(target) <= TailAfterViewRange * TailAfterViewRange) //没有超出视野半径 + { + return true; + } + } + + return false; + } + + /// /// 调用视野检测, 如果被墙壁和其它物体遮挡, 则返回被挡住视野的物体对象, 视野无阻则返回 null /// public bool TestViewRayCast(Vector2 target) @@ -191,4 +213,15 @@ { ViewRay.Enabled = false; } + + //其他敌人发现玩家 + private void OnEnemyFindPlayer(object obj) + { + var state = StateController.CurrState; + if (state != AiStateEnum.AiLeaveFor && state != AiStateEnum.AiTailAfter && state != AiStateEnum.AiTargetInView) + { + //前往指定地点 + StateController.ChangeStateLate(AiStateEnum.AiLeaveFor, GameApplication.Instance.Room.Player.GetCenterPosition()); + } + } } diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AIAttackState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AIAttackState.cs deleted file mode 100644 index ba2241e..0000000 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AIAttackState.cs +++ /dev/null @@ -1,82 +0,0 @@ - -using Godot; - -/// -/// 目标在视野范围内, 发起攻击 -/// -public class AIAttackState : StateBase -{ - - /// - /// 是否在视野内 - /// - public bool IsInView; - - //导航目标点刷新计时器 - private float _navigationUpdateTimer = 0; - private float _navigationInterval = 0.3f; - - public AIAttackState() : base(AIStateEnum.AIAttack) - { - } - - public override void Enter(AIStateEnum prev, params object[] args) - { - _navigationUpdateTimer = 0; - IsInView = true; - } - - public override void PhysicsProcess(float delta) - { - var masterPos = Master.GlobalPosition; - var playerPos = GameApplication.Instance.Room.Player.GlobalPosition; - - //更新玩家位置 - if (_navigationUpdateTimer <= 0) - { - //每隔一段时间秒更改目标位置 - _navigationUpdateTimer = _navigationInterval; - if (Master.NavigationAgent2D.GetTargetLocation() != playerPos) - { - Master.NavigationAgent2D.SetTargetLocation(playerPos); - } - } - else - { - _navigationUpdateTimer -= delta; - } - - //计算移动 - var nextPos = Master.NavigationAgent2D.GetNextLocation(); - Master.LookTargetPosition(playerPos); - Master.AnimatedSprite.Animation = AnimatorNames.Run; - Master.Velocity = (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * Master.MoveSpeed; - Master.CalcMove(delta); - - //检测玩家是否在视野内 - if (masterPos.DistanceSquaredTo(playerPos) <= Master.TailAfterViewRange * Master.TailAfterViewRange) - { - IsInView = !Master.TestViewRayCast(playerPos); - Master.TestViewRayCastOver(); - } - else - { - IsInView = false; - } - - if (IsInView) - { - Master.EnemyAttack(); - } - else - { - ChangeStateLate(AIStateEnum.AITailAfter); - } - } - - public override void DebugDraw() - { - var playerPos = GameApplication.Instance.Room.Player.GlobalPosition; - Master.DrawLine(new Vector2(0, -8), Master.ToLocal(playerPos), Colors.Red); - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AIStateEnum.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AIStateEnum.cs index 6e94ffa..d7f44b6 100644 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AIStateEnum.cs +++ b/DungeonShooting_Godot/src/game/role/enemy/state/AIStateEnum.cs @@ -1,20 +1,24 @@ -public enum AIStateEnum +public enum AiStateEnum { /// /// ai状态, 正常, 未发现目标 /// - AINormal, + AiNormal, /// /// 发现目标, 但不知道在哪 /// - AIProbe, + AiProbe, + /// + /// 收到其他敌人通知, 前往发现目标的位置 + /// + AiLeaveFor, /// /// 发现目标, 并且知道位置 /// - AITailAfter, + AiTailAfter, /// - /// 目标在视野内, 发起攻击 + /// 目标在视野内 /// - AIAttack, + AiTargetInView, } \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiLeaveForState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiLeaveForState.cs new file mode 100644 index 0000000..6cb48a9 --- /dev/null +++ b/DungeonShooting_Godot/src/game/role/enemy/state/AiLeaveForState.cs @@ -0,0 +1,70 @@ + +using Godot; + +/// +/// 前往目标位置 +/// +public class AiLeaveForState : StateBase +{ + private Vector2 _targetPosition; + + public AiLeaveForState() : base(AiStateEnum.AiLeaveFor) + { + } + + public override void Enter(AiStateEnum prev, params object[] args) + { + if (args.Length > 0 && args[0] is Vector2 pos) + { + UpdateTargetPosition(pos); + } + else + { + ChangeState(AiStateEnum.AiNormal); + } + } + + public override void PhysicsProcess(float delta) + { + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextLocation(); + Master.LookTargetPosition(_targetPosition); + Master.AnimatedSprite.Animation = AnimatorNames.Run; + Master.Velocity = (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * + Master.MoveSpeed; + Master.CalcMove(delta); + + var playerPos = GameApplication.Instance.Room.Player.GetCenterPosition(); + //检测玩家是否在视野内, 如果在, 则切换到 AiTargetInView 状态 + if (Master.IsInTailAfterViewRange(playerPos)) + { + if (!Master.TestViewRayCast(playerPos)) //看到玩家 + { + //关闭射线检测 + Master.TestViewRayCastOver(); + //切换成发现目标状态 + ChangeStateLate(AiStateEnum.AiTargetInView); + //派发发现玩家事件 + EventManager.EmitEvent(EventEnum.OnEnemyFindPlayer); + return; + } + else + { + //关闭射线检测 + Master.TestViewRayCastOver(); + } + } + + //移动到目标掉了, 还没发现目标 + if (Master.NavigationAgent2D.IsNavigationFinished()) + { + ChangeStateLate(AiStateEnum.AiNormal); + } + } + + public void UpdateTargetPosition(Vector2 pos) + { + _targetPosition = pos; + Master.NavigationAgent2D.SetTargetLocation(pos); + } +} diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiNormalState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiNormalState.cs index 426f362..9e51c18 100644 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiNormalState.cs +++ b/DungeonShooting_Godot/src/game/role/enemy/state/AiNormalState.cs @@ -4,7 +4,7 @@ /// /// AI 正常状态 /// -public class AiNormalState : StateBase +public class AiNormalState : StateBase { //是否发现玩家 private bool _isFindPlayer; @@ -24,11 +24,11 @@ //移动停顿计时器 private float _pauseTimer; - public AiNormalState() : base(AIStateEnum.AINormal) + public AiNormalState() : base(AiStateEnum.AiNormal) { } - public override void Enter(AIStateEnum prev, params object[] args) + public override void Enter(AiStateEnum prev, params object[] args) { _isFindPlayer = false; _isMoveOver = true; @@ -43,14 +43,14 @@ if (_isFindPlayer) //已经找到玩家了 { //现临时处理, 直接切换状态 - ChangeStateLate(AIStateEnum.AITailAfter); + ChangeStateLate(AiStateEnum.AiTailAfter); } else //没有找到玩家 { //检测玩家 var player = GameApplication.Instance.Room.Player; //玩家中心点坐标 - var playerPos = player.MountPoint.GlobalPosition; + var playerPos = player.GetCenterPosition(); if (Master.IsInViewRange(playerPos) && !Master.TestViewRayCast(playerPos)) //发现玩家 { @@ -81,7 +81,7 @@ _isMoveOver = true; } } - + //关闭射线检测 Master.TestViewRayCastOver(); } } diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiProbeState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiProbeState.cs index 4b1d6ca..9e610d1 100644 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiProbeState.cs +++ b/DungeonShooting_Godot/src/game/role/enemy/state/AiProbeState.cs @@ -2,9 +2,9 @@ /// /// Ai 不确定玩家位置 /// -public class AiProbeState : StateBase +public class AiProbeState : StateBase { - public AiProbeState() : base(AIStateEnum.AIProbe) + public AiProbeState() : base(AiStateEnum.AiProbe) { } } \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiTailAfterState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiTailAfterState.cs index c2331a8..1bd0e91 100644 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiTailAfterState.cs +++ b/DungeonShooting_Godot/src/game/role/enemy/state/AiTailAfterState.cs @@ -2,14 +2,14 @@ using Godot; /// -/// AI 发现玩家 +/// AI 发现玩家, 跟随玩家 /// -public class AiTailAfterState : StateBase +public class AiTailAfterState : StateBase { /// /// 目标是否在视野半径内 /// - public bool IsInViewRange; + private bool _isInViewRange; //导航目标点刷新计时器 private float _navigationUpdateTimer = 0; @@ -17,22 +17,21 @@ //目标从视野消失时已经过去的时间 private float _viewTimer; - - public AiTailAfterState() : base(AIStateEnum.AITailAfter) + + public AiTailAfterState() : base(AiStateEnum.AiTailAfter) { } - public override void Enter(AIStateEnum prev, params object[] args) + public override void Enter(AiStateEnum prev, params object[] args) { - IsInViewRange = true; + _isInViewRange = true; _navigationUpdateTimer = 0; _viewTimer = 0; } - + public override void PhysicsProcess(float delta) { - var masterPos = Master.GlobalPosition; - var playerPos = GameApplication.Instance.Room.Player.GlobalPosition; + var playerPos = GameApplication.Instance.Room.Player.GetCenterPosition(); //更新玩家位置 if (_navigationUpdateTimer <= 0) @@ -56,26 +55,29 @@ Master.Velocity = (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * Master.MoveSpeed; Master.CalcMove(delta); - //是否看到玩家' - //检测玩家是否在视野内 - if (masterPos.DistanceSquaredTo(playerPos) <= Master.TailAfterViewRange * Master.TailAfterViewRange) + //检测玩家是否在视野内, 如果在, 则切换到 AiTargetInView 状态 + if (Master.IsInTailAfterViewRange(playerPos)) { - if (!Master.TestViewRayCast(playerPos)) + if (!Master.TestViewRayCast(playerPos)) //看到玩家 { + //关闭射线检测 Master.TestViewRayCastOver(); - //切换成攻击状态 - ChangeStateLate(AIStateEnum.AIAttack); + //切换成发现目标状态 + ChangeStateLate(AiStateEnum.AiTargetInView); + //派发发现玩家事件 + EventManager.EmitEvent(EventEnum.OnEnemyFindPlayer); return; } else { + //关闭射线检测 Master.TestViewRayCastOver(); } } - //检测玩家是否在视野内, 此时视野可穿墙, 直接检测距离即可 - IsInViewRange = masterPos.DistanceSquaredTo(playerPos) <= Master.ViewRange * Master.ViewRange; - if (IsInViewRange) + //检测玩家是否在穿墙视野范围内, 直接检测距离即可 + _isInViewRange = Master.IsInViewRange(playerPos); + if (_isInViewRange) { _viewTimer = 0; } @@ -83,7 +85,7 @@ { if (_viewTimer > 10) //10秒 { - ChangeStateLate(AIStateEnum.AINormal); + ChangeStateLate(AiStateEnum.AiNormal); } else { @@ -94,8 +96,8 @@ public override void DebugDraw() { - var playerPos = GameApplication.Instance.Room.Player.GlobalPosition; - if (IsInViewRange) + var playerPos = GameApplication.Instance.Room.Player.GetCenterPosition(); + if (_isInViewRange) { Master.DrawLine(new Vector2(0, -8), Master.ToLocal(playerPos), Colors.Orange); } diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiTargetInViewState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiTargetInViewState.cs new file mode 100644 index 0000000..1856869 --- /dev/null +++ b/DungeonShooting_Godot/src/game/role/enemy/state/AiTargetInViewState.cs @@ -0,0 +1,82 @@ + +using Godot; + +/// +/// 目标在视野范围内, 发起攻击 +/// +public class AiTargetInViewState : StateBase +{ + + /// + /// 是否在视野内 + /// + public bool IsInView; + + //导航目标点刷新计时器 + private float _navigationUpdateTimer = 0; + private float _navigationInterval = 0.3f; + + public AiTargetInViewState() : base(AiStateEnum.AiTargetInView) + { + } + + public override void Enter(AiStateEnum prev, params object[] args) + { + _navigationUpdateTimer = 0; + IsInView = true; + } + + public override void PhysicsProcess(float delta) + { + var playerPos = GameApplication.Instance.Room.Player.GetCenterPosition(); + + //更新玩家位置 + if (_navigationUpdateTimer <= 0) + { + //每隔一段时间秒更改目标位置 + _navigationUpdateTimer = _navigationInterval; + if (Master.NavigationAgent2D.GetTargetLocation() != playerPos) + { + Master.NavigationAgent2D.SetTargetLocation(playerPos); + } + } + else + { + _navigationUpdateTimer -= delta; + } + + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextLocation(); + Master.LookTargetPosition(playerPos); + Master.AnimatedSprite.Animation = AnimatorNames.Run; + Master.Velocity = (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * Master.MoveSpeed; + Master.CalcMove(delta); + + //检测玩家是否在视野内 + if (Master.IsInTailAfterViewRange(playerPos)) + { + IsInView = !Master.TestViewRayCast(playerPos); + //关闭射线检测 + Master.TestViewRayCastOver(); + } + else + { + IsInView = false; + } + + if (IsInView) + { + Master.EnemyAttack(); + } + else + { + ChangeStateLate(AiStateEnum.AiTailAfter); + } + } + + public override void DebugDraw() + { + var playerPos = GameApplication.Instance.Room.Player.GetCenterPosition(); + Master.DrawLine(new Vector2(0, -8), Master.ToLocal(playerPos), Colors.Red); + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/room/RoomManager.cs b/DungeonShooting_Godot/src/game/room/RoomManager.cs index 66265a7..9643006 100644 --- a/DungeonShooting_Godot/src/game/room/RoomManager.cs +++ b/DungeonShooting_Godot/src/game/room/RoomManager.cs @@ -112,7 +112,6 @@ public override void _Process(float delta) { - Enemy.UpdateEnemiesView(); if (GameApplication.Instance.Debug) { Update();