diff --git a/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002.png b/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002.png index 8d930ae..0f1a4ed 100644 --- a/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002.png +++ b/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002.png Binary files differ diff --git a/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002_idle.png b/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002_idle.png index cd69636..5c65a43 100644 --- a/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002_idle.png +++ b/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002_idle.png Binary files differ diff --git a/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002_run.png b/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002_run.png index eec38ac..c8dc00b 100644 --- a/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002_run.png +++ b/DungeonShooting_Godot/resource/sprite/role/enemy0002/Enemy0002_run.png Binary files differ diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/AIStateEnum.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/AIStateEnum.cs index be5d336..05dbbc5 100644 --- a/DungeonShooting_Godot/src/game/activity/role/enemy/AIStateEnum.cs +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/AIStateEnum.cs @@ -10,6 +10,10 @@ // /// // AiProbe, /// + /// 找到玩家,准备通知其他敌人 + /// + AiFind, + /// /// 收到其他敌人通知, 前往发现目标的位置 /// AiLeaveFor, diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/AdvancedEnemy.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/AdvancedEnemy.cs index f80fe8d..46b5c5c 100644 --- a/DungeonShooting_Godot/src/game/activity/role/enemy/AdvancedEnemy.cs +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/AdvancedEnemy.cs @@ -65,7 +65,7 @@ public Marker2D NavigationPoint { get; private set; } /// - /// Ai攻击状态, 调用 EnemyAttack() 函数后会刷新 + /// Ai攻击状态, 调用 Attack() 函数后会刷新 /// public AiAttackState AttackState { get; private set; } @@ -354,10 +354,7 @@ return false; } - /// - /// Ai触发的攻击 - /// - public void EnemyAttack() + public override void Attack() { var weapon = WeaponPack.ActiveItem; if (weapon != null) diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/Enemy.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/Enemy.cs index f1f7d3b..a27cedc 100644 --- a/DungeonShooting_Godot/src/game/activity/role/enemy/Enemy.cs +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/Enemy.cs @@ -51,6 +51,11 @@ [Export, ExportFillNode] public Marker2D NavigationPoint { get; private set; } + /// + /// Ai攻击状态, 调用 Attack() 函数后会刷新 + /// + public AiAttackState AttackState { get; private set; } + //锁定目标时间 private float _lockTargetTime = 0; @@ -70,6 +75,8 @@ Hp = 20; StateController.Register(new AiNormalState()); + StateController.Register(new AiTailAfterState()); + StateController.Register(new AiFollowUpState()); StateController.ChangeState(AiStateEnum.AiNormal); } @@ -85,4 +92,140 @@ { World.Enemy_InstanceList.Remove(this); } + + public override void Attack() + { + Debug.Log("触发攻击"); + } + + protected override void OnHit(int damage, bool realHarm) + { + //受到伤害 + var state = StateController.CurrState; + if (state == AiStateEnum.AiNormal || state == AiStateEnum.AiLeaveFor) //|| state == AiStateEnum.AiProbe + { + StateController.ChangeState(AiStateEnum.AiTailAfter); + } + } + + protected override void OnDie() + { + var effPos = Position + new Vector2(0, -Altitude); + //血液特效 + var blood = ObjectManager.GetPoolItem(ResourcePath.prefab_effect_enemy_EnemyBloodEffect_tscn); + blood.Position = effPos - new Vector2(0, 12); + blood.AddToActivityRoot(RoomLayerEnum.NormalLayer); + blood.PlayEffect(); + + //创建敌人碎片 + var count = Utils.Random.RandomRangeInt(3, 6); + for (var i = 0; i < count; i++) + { + var debris = Create(Ids.Id_effect0001); + debris.PutDown(effPos, RoomLayerEnum.NormalLayer); + debris.InheritVelocity(this); + } + + //派发敌人死亡信号 + EventManager.EmitEvent(EventEnum.OnEnemyDie, this); + Destroy(); + } + + /// + /// 检查是否能切换到 AiStateEnum.AiLeaveFor 状态 + /// + public bool CanChangeLeaveFor() + { + if (!World.Enemy_IsFindTarget) + { + return false; + } + + var currState = StateController.CurrState; + if (currState == AiStateEnum.AiNormal)// || currState == AiStateEnum.AiProbe) + { + //判断是否在同一个房间内 + return World.Enemy_FindTargetAffiliationSet.Contains(AffiliationArea); + } + + return false; + } + + /// + /// 返回目标点是否在视野范围内 + /// + public bool IsInViewRange(Vector2 target) + { + var isForward = IsPositionInForward(target); + if (isForward) + { + if (GlobalPosition.DistanceSquaredTo(target) <= ViewRange * ViewRange) //没有超出视野半径 + { + return true; + } + } + + return false; + } + + /// + /// 返回目标点是否在跟随状态下的视野半径内 + /// + 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) + { + ViewRay.Enabled = true; + ViewRay.TargetPosition = ViewRay.ToLocal(target); + ViewRay.ForceRaycastUpdate(); + return ViewRay.IsColliding(); + } + + /// + /// 调用视野检测完毕后, 需要调用 TestViewRayCastOver() 来关闭视野检测射线 + /// + public void TestViewRayCastOver() + { + ViewRay.Enabled = false; + } + + /// + /// 获取锁定目标的时间 + /// + public float GetLockTime() + { + return _lockTargetTime; + } + + /// + /// 强制设置锁定目标时间 + /// + public void SetLockTargetTime(float time) + { + _lockTargetTime = time; + } + + /// + /// 获取攻击范围 + /// + /// 从最小到最大距离的过渡量, 0 - 1, 默认 0.5 + public float GetAttackRange(float weight = 0.5f) + { + return 200; + } } \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiFollowUpState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiFollowUpState.cs index f635a9d..904b327 100644 --- a/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiFollowUpState.cs +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiFollowUpState.cs @@ -109,7 +109,7 @@ if (inAttackRange) //在攻击范围内 { //发起攻击 - Master.EnemyAttack(); + Master.Attack(); //距离够近, 可以切换到环绕模式 if (Master.GlobalPosition.DistanceSquaredTo(playerPos) <= Mathf.Pow(Utils.GetConfigRangeStart(weapon.Attribute.Bullet.DistanceRange), 2) * 0.7f) diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiNormalState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiNormalState.cs index 77cf398..5f469d1 100644 --- a/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiNormalState.cs +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiNormalState.cs @@ -11,7 +11,7 @@ //是否发现玩家 private bool _isFindPlayer; - //下一个运动的角度 + //下一个运动的坐标 private Vector2 _nextPos; //是否移动结束 diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiSurroundState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiSurroundState.cs index a7b2fe4..39cdf6c 100644 --- a/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiSurroundState.cs +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/advancedState/AiSurroundState.cs @@ -158,7 +158,7 @@ else { //发起攻击 - Master.EnemyAttack(); + Master.Attack(); } } } diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/normalState/AiFollowUpState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/normalState/AiFollowUpState.cs new file mode 100644 index 0000000..35c2caa --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/normalState/AiFollowUpState.cs @@ -0,0 +1,108 @@ + +using Godot; + +namespace NnormalState; + +/// +/// 目标在视野内, 跟进目标, 如果距离在子弹有效射程内, 则开火 +/// +public class AiFollowUpState : StateBase +{ + //导航目标点刷新计时器 + private float _navigationUpdateTimer = 0; + private float _navigationInterval = 0.3f; + + public AiFollowUpState() : base(AiStateEnum.AiFollowUp) + { + } + + public override void Enter(AiStateEnum prev, params object[] args) + { + _navigationUpdateTimer = 0; + Master.TargetInView = true; + } + + public override void Process(float delta) + { + var playerPos = Player.Current.GetCenterPosition(); + + //更新玩家位置 + if (_navigationUpdateTimer <= 0) + { + //每隔一段时间秒更改目标位置 + _navigationUpdateTimer = _navigationInterval; + Master.NavigationAgent2D.TargetPosition = playerPos; + } + else + { + _navigationUpdateTimer -= delta; + } + + var masterPosition = Master.GlobalPosition; + + //是否在攻击范围内 + var inAttackRange = masterPosition.DistanceSquaredTo(playerPos) <= Mathf.Pow(Master.GetAttackRange(0.7f), 2); + + //枪口指向玩家 + Master.LookTargetPosition(playerPos); + + if (!Master.NavigationAgent2D.IsNavigationFinished()) + { + if (Master.AttackState != AiAttackState.LockingTime && Master.AttackState != AiAttackState.Attack) + { + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = (nextPos - masterPosition - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + } + else + { + Master.AnimatedSprite.Play(AnimatorNames.Idle); + Master.BasisVelocity = Vector2.Zero; + } + } + else + { + Master.BasisVelocity = Vector2.Zero; + } + + //检测玩家是否在视野内 + if (Master.IsInTailAfterViewRange(playerPos)) + { + Master.TargetInView = !Master.TestViewRayCast(playerPos); + //关闭射线检测 + Master.TestViewRayCastOver(); + } + else + { + Master.TargetInView = false; + } + + //在视野中, 或者锁敌状态下, 或者攻击状态下, 继续保持原本逻辑 + if (Master.TargetInView || Master.AttackState == AiAttackState.LockingTime || Master.AttackState == AiAttackState.Attack) + { + if (inAttackRange) //在攻击范围内 + { + //发起攻击 + Master.Attack(); + + //距离够近, 可以切换到环绕模式 + // if (Master.GlobalPosition.DistanceSquaredTo(playerPos) <= Mathf.Pow(Utils.GetConfigRangeStart(weapon.Attribute.Bullet.DistanceRange), 2) * 0.7f) + // { + // ChangeState(AiStateEnum.AiSurround); + // } + } + } + else //不在视野中 + { + ChangeState(AiStateEnum.AiTailAfter); + } + } + + public override void DebugDraw() + { + var playerPos = Player.Current.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/activity/role/enemy/normalState/AiNormalState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/normalState/AiNormalState.cs index 0cdef1d..9cf340a 100644 --- a/DungeonShooting_Godot/src/game/activity/role/enemy/normalState/AiNormalState.cs +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/normalState/AiNormalState.cs @@ -1,17 +1,177 @@ -namespace NnormalState; +using Godot; + +namespace NnormalState; /// /// AI 正常状态 /// public class AiNormalState : StateBase { + //是否发现玩家 + private bool _isFindPlayer; + + //下一个运动的坐标 + private Vector2 _nextPos; + + //是否移动结束 + private bool _isMoveOver; + + //上一次移动是否撞墙 + private bool _againstWall; + + //撞墙法线角度 + private float _againstWallNormalAngle; + + //移动停顿计时器 + private float _pauseTimer; + private bool _moveFlag; + + //上一帧位置 + private Vector2 _prevPos; + //卡在一个位置的时间 + private float _lockTimer; + public AiNormalState() : base(AiStateEnum.AiNormal) { - + } + + public override void Enter(AiStateEnum prev, params object[] args) + { + _isFindPlayer = false; + _isMoveOver = true; + _againstWall = false; + _againstWallNormalAngle = 0; + _pauseTimer = 0; + _moveFlag = false; } public override void Process(float delta) { - //Master.BasisVelocity = (Player.Current.Position - Master.Position).LimitLength(10); + //其他敌人发现玩家 + if (Master.CanChangeLeaveFor()) + { + ChangeState(AiStateEnum.AiLeaveFor); + return; + } + + if (_isFindPlayer) //已经找到玩家了 + { + //现临时处理, 直接切换状态 + ChangeState(AiStateEnum.AiTailAfter); + } + else //没有找到玩家 + { + //检测玩家 + var player = Player.Current; + //玩家中心点坐标 + var playerPos = player.GetCenterPosition(); + + if (Master.IsInViewRange(playerPos) && !Master.TestViewRayCast(playerPos)) //发现玩家 + { + //发现玩家 + _isFindPlayer = true; + } + else if (_pauseTimer >= 0) + { + Master.AnimatedSprite.Play(AnimatorNames.Idle); + _pauseTimer -= delta; + } + else if (_isMoveOver) //没发现玩家, 且已经移动完成 + { + RunOver(); + _isMoveOver = false; + } + else //移动中 + { + if (_lockTimer >= 1) //卡在一个点超过一秒 + { + RunOver(); + _isMoveOver = false; + _lockTimer = 0; + } + else if (Master.NavigationAgent2D.IsNavigationFinished()) //到达终点 + { + _pauseTimer = Utils.Random.RandomRangeFloat(0.3f, 2f); + _isMoveOver = true; + _moveFlag = false; + Master.BasisVelocity = Vector2.Zero; + } + else if (!_moveFlag) + { + _moveFlag = true; + var pos = Master.GlobalPosition; + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = (nextPos - pos - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + _prevPos = pos; + } + else + { + var pos = Master.GlobalPosition; + var lastSlideCollision = Master.GetLastSlideCollision(); + if (lastSlideCollision != null && lastSlideCollision.GetCollider() is AdvancedRole) //碰到其他角色 + { + _pauseTimer = Utils.Random.RandomRangeFloat(0.1f, 0.5f); + _isMoveOver = true; + _moveFlag = false; + Master.BasisVelocity = Vector2.Zero; + } + else + { + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = (nextPos - pos - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + } + + if (_prevPos.DistanceSquaredTo(pos) <= 0.01f) + { + _lockTimer += delta; + } + else + { + _prevPos = pos; + } + } + } + + //关闭射线检测 + Master.TestViewRayCastOver(); + } + } + + //移动结束 + private void RunOver() + { + float angle; + if (_againstWall) + { + angle = Utils.Random.RandomRangeFloat(_againstWallNormalAngle - Mathf.Pi * 0.5f, + _againstWallNormalAngle + Mathf.Pi * 0.5f); + } + else + { + angle = Utils.Random.RandomRangeFloat(0, Mathf.Pi * 2f); + } + + var len = Utils.Random.RandomRangeInt(30, 200); + _nextPos = new Vector2(len, 0).Rotated(angle) + Master.GlobalPosition; + //获取射线碰到的坐标 + if (Master.TestViewRayCast(_nextPos)) //碰到墙壁 + { + _nextPos = Master.ViewRay.GetCollisionPoint(); + _againstWall = true; + _againstWallNormalAngle = Master.ViewRay.GetCollisionNormal().Angle(); + } + else + { + _againstWall = false; + } + + Master.NavigationAgent2D.TargetPosition = _nextPos; + Master.LookTargetPosition(_nextPos); } } \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/normalState/AiTailAfterState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/normalState/AiTailAfterState.cs new file mode 100644 index 0000000..3d29972 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/normalState/AiTailAfterState.cs @@ -0,0 +1,124 @@ + +using Godot; + +namespace NnormalState; + +/// +/// AI 发现玩家, 跟随玩家 +/// +public class AiTailAfterState : StateBase +{ + /// + /// 目标是否在视野半径内 + /// + private bool _isInViewRange; + + //导航目标点刷新计时器 + private float _navigationUpdateTimer = 0; + private float _navigationInterval = 0.3f; + + //目标从视野消失时已经过去的时间 + private float _viewTimer; + + public AiTailAfterState() : base(AiStateEnum.AiTailAfter) + { + } + + public override void Enter(AiStateEnum prev, params object[] args) + { + _isInViewRange = true; + _navigationUpdateTimer = 0; + _viewTimer = 0; + } + + public override void Process(float delta) + { + //这个状态下不会有攻击事件, 所以没必要每一帧检查是否弹药耗尽 + + var playerPos = Player.Current.GetCenterPosition(); + + //更新玩家位置 + if (_navigationUpdateTimer <= 0) + { + //每隔一段时间秒更改目标位置 + _navigationUpdateTimer = _navigationInterval; + Master.NavigationAgent2D.TargetPosition = playerPos; + } + else + { + _navigationUpdateTimer -= delta; + } + + //枪口指向玩家 + Master.LookTargetPosition(playerPos); + + if (!Master.NavigationAgent2D.IsNavigationFinished()) + { + if (Master.AttackState != AiAttackState.LockingTime && Master.AttackState != AiAttackState.Attack) + { + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + } + else + { + Master.AnimatedSprite.Play(AnimatorNames.Idle); + Master.BasisVelocity = Vector2.Zero; + } + } + else + { + Master.BasisVelocity = Vector2.Zero; + } + //检测玩家是否在视野内, 如果在, 则切换到 AiTargetInView 状态 + if (Master.IsInTailAfterViewRange(playerPos)) + { + if (!Master.TestViewRayCast(playerPos)) //看到玩家 + { + //关闭射线检测 + Master.TestViewRayCastOver(); + //切换成发现目标状态 + ChangeState(AiStateEnum.AiFollowUp); + return; + } + else + { + //关闭射线检测 + Master.TestViewRayCastOver(); + } + } + + //检测玩家是否在穿墙视野范围内, 直接检测距离即可 + _isInViewRange = Master.IsInViewRange(playerPos); + if (_isInViewRange) + { + _viewTimer = 0; + } + else //超出视野 + { + if (_viewTimer > 10) //10秒 + { + ChangeState(AiStateEnum.AiNormal); + } + else + { + _viewTimer += delta; + } + } + } + + public override void DebugDraw() + { + var playerPos = Player.Current.GetCenterPosition(); + if (_isInViewRange) + { + Master.DrawLine(new Vector2(0, -8), Master.ToLocal(playerPos), Colors.Orange); + } + else + { + Master.DrawLine(new Vector2(0, -8), Master.ToLocal(playerPos), Colors.Blue); + } + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/event/EventEnum.cs b/DungeonShooting_Godot/src/game/event/EventEnum.cs index 6a64560..28dd63a 100644 --- a/DungeonShooting_Godot/src/game/event/EventEnum.cs +++ b/DungeonShooting_Godot/src/game/event/EventEnum.cs @@ -7,7 +7,7 @@ public enum EventEnum { /// - /// 敌人死亡, 参数为死亡的敌人的实例, 参数类型为 + /// 敌人死亡, 参数为死亡的敌人的实例, 参数类型为 /// OnEnemyDie, ///