Newer
Older
DungeonShooting / DungeonShooting_Godot / src / game / activity / role / enemy / Enemy.cs
@小李xl 小李xl on 22 Nov 2023 16 KB 完善ai状态机
  1. #region 基础敌人设计思路
  2. /*
  3. 敌人有三种状态:
  4. 状态1: 未发现玩家, 视野不可穿墙, 该状态下敌人移动比较规律, 移动速度较慢, 一旦玩家进入视野或者听到玩家枪声, 立刻切换至状态3, 该房间的敌人不能再回到状态1
  5. 状态2: 发现有玩家, 但不知道在哪, 视野不可穿墙, 该情况下敌人移动速度明显加快, 移动不规律, 一旦玩家进入视野或者听到玩家枪声, 立刻切换至状态3
  6. 状态3: 明确知道玩家的位置, 视野允许穿墙, 移动速度与状态2一致, 进入该状态时, 敌人之间会相互告知玩家所在位置, 并朝着玩家位置开火,
  7. 如果有墙格挡, 则有一定概率继续开火, 一旦玩家立刻敌人视野超哥一段时间, 敌人自动切换为状态2
  8.  
  9. 敌人状态1只存在于少数房间内, 比如特殊房间, 大部分情况下敌人应该是状态2, 或者玩家进入房间时就被敌人发现
  10. */
  11. #endregion
  12.  
  13.  
  14. using EnemyState;
  15. using Godot;
  16.  
  17. /// <summary>
  18. /// 高级敌人,可以携带武器
  19. /// </summary>
  20. [Tool]
  21. public partial class Enemy : Role
  22. {
  23. /// <summary>
  24. /// 目标是否在视野内
  25. /// </summary>
  26. public bool TargetInView { get; set; } = true;
  27. /// <summary>
  28. /// 敌人身上的状态机控制器
  29. /// </summary>
  30. public StateController<Enemy, AIStateEnum> StateController { get; private set; }
  31.  
  32. /// <summary>
  33. /// 视野半径, 单位像素, 发现玩家后改视野范围可以穿墙
  34. /// </summary>
  35. public float ViewRange { get; set; } = 250;
  36.  
  37. /// <summary>
  38. /// 发现玩家后跟随玩家的视野半径
  39. /// </summary>
  40. public float TailAfterViewRange { get; set; } = 400;
  41.  
  42. /// <summary>
  43. /// 背后的视野半径, 单位像素
  44. /// </summary>
  45. public float BackViewRange { get; set; } = 50;
  46.  
  47. /// <summary>
  48. /// 是否可以拾起武器
  49. /// </summary>
  50. [Export]
  51. public bool CanPickUpWeapon { get; set; } = true;
  52. /// <summary>
  53. /// 视野检测射线, 朝玩家打射线, 检测是否碰到墙
  54. /// </summary>
  55. [Export, ExportFillNode]
  56. public RayCast2D ViewRay { get; set; }
  57.  
  58. /// <summary>
  59. /// 导航代理
  60. /// </summary>
  61. [Export, ExportFillNode]
  62. public NavigationAgent2D NavigationAgent2D { get; set; }
  63.  
  64. /// <summary>
  65. /// 导航代理中点
  66. /// </summary>
  67. [Export, ExportFillNode]
  68. public Marker2D NavigationPoint { get; set; }
  69.  
  70. /// <summary>
  71. /// 不通过武发射子弹的开火点
  72. /// </summary>
  73. [Export, ExportFillNode]
  74. public Marker2D FirePoint { get; set; }
  75. /// <summary>
  76. /// 当前敌人所看向的对象, 也就是枪口指向的对象
  77. /// </summary>
  78. public ActivityObject LookTarget { get; set; }
  79.  
  80. /// <summary>
  81. /// 攻击锁定目标时间
  82. /// </summary>
  83. public float LockingTime { get; set; } = 1f;
  84. /// <summary>
  85. /// 锁定目标已经走过的时间
  86. /// </summary>
  87. public float LockTargetTime { get; set; } = 0;
  88.  
  89. public override void OnInit()
  90. {
  91. base.OnInit();
  92. IsAi = true;
  93.  
  94. if (!CanPickUpWeapon)
  95. {
  96. WeaponPack.SetCapacity(0);
  97. }
  98. StateController = AddComponent<StateController<Enemy, AIStateEnum>>();
  99.  
  100. AttackLayer = PhysicsLayer.Wall | PhysicsLayer.Player;
  101. EnemyLayer = PhysicsLayer.Player;
  102. Camp = CampEnum.Camp2;
  103.  
  104. RoleState.MoveSpeed = 20;
  105.  
  106. MaxHp = 20;
  107. Hp = 20;
  108.  
  109. //注册Ai状态机
  110. StateController.Register(new AiNormalState());
  111. StateController.Register(new AiTailAfterState());
  112. StateController.Register(new AiFollowUpState());
  113. StateController.Register(new AiLeaveForState());
  114. StateController.Register(new AiSurroundState());
  115. StateController.Register(new AiFindAmmoState());
  116. StateController.Register(new AiAttackState());
  117. StateController.Register(new AiAstonishedState());
  118. StateController.Register(new AiNotifyState());
  119. //默认状态
  120. StateController.ChangeStateInstant(AIStateEnum.AiNormal);
  121. }
  122.  
  123. public override void EnterTree()
  124. {
  125. if (!World.Enemy_InstanceList.Contains(this))
  126. {
  127. World.Enemy_InstanceList.Add(this);
  128. }
  129. }
  130.  
  131. public override void ExitTree()
  132. {
  133. World.Enemy_InstanceList.Remove(this);
  134. }
  135.  
  136. protected override void OnDie()
  137. {
  138. //扔掉所有武器
  139. ThrowAllWeapon();
  140.  
  141. var effPos = Position + new Vector2(0, -Altitude);
  142. //血液特效
  143. var blood = ObjectManager.GetPoolItem<AutoDestroyParticles>(ResourcePath.prefab_effect_enemy_EnemyBlood0001_tscn);
  144. blood.Position = effPos - new Vector2(0, 12);
  145. blood.AddToActivityRoot(RoomLayerEnum.NormalLayer);
  146. blood.PlayEffect();
  147.  
  148. var realVelocity = GetRealVelocity();
  149. //创建敌人碎片
  150. var count = Utils.Random.RandomRangeInt(3, 6);
  151. for (var i = 0; i < count; i++)
  152. {
  153. var debris = Create(Ids.Id_enemy_dead0001);
  154. debris.PutDown(effPos, RoomLayerEnum.NormalLayer);
  155. debris.MoveController.AddForce(Velocity + realVelocity);
  156. }
  157. //派发敌人死亡信号
  158. EventManager.EmitEvent(EventEnum.OnEnemyDie, this);
  159. Destroy();
  160. }
  161.  
  162. protected override void Process(float delta)
  163. {
  164. base.Process(delta);
  165. if (IsDie)
  166. {
  167. return;
  168. }
  169. //看向目标
  170. if (LookTarget != null && MountLookTarget)
  171. {
  172. var pos = LookTarget.Position;
  173. LookPosition = pos;
  174. //脸的朝向
  175. var gPos = Position;
  176. if (pos.X > gPos.X && Face == FaceDirection.Left)
  177. {
  178. Face = FaceDirection.Right;
  179. }
  180. else if (pos.X < gPos.X && Face == FaceDirection.Right)
  181. {
  182. Face = FaceDirection.Left;
  183. }
  184. //枪口跟随目标
  185. MountPoint.SetLookAt(pos);
  186. }
  187.  
  188. //拾起武器操作
  189. EnemyPickUpWeapon();
  190. }
  191.  
  192. public override bool IsAllWeaponTotalAmmoEmpty()
  193. {
  194. if (!CanPickUpWeapon)
  195. {
  196. return false;
  197. }
  198. return base.IsAllWeaponTotalAmmoEmpty();
  199. }
  200.  
  201. protected override void OnHit(ActivityObject target, int damage, float angle, bool realHarm)
  202. {
  203. //受到伤害
  204. var state = StateController.CurrState;
  205. if (state == AIStateEnum.AiNormal)
  206. {
  207. LookTarget = target;
  208. //判断是否进入通知状态
  209. if (World.Enemy_InstanceList.FindIndex(enemy =>
  210. enemy != this && !enemy.IsDie && enemy.AffiliationArea == AffiliationArea &&
  211. enemy.StateController.CurrState == AIStateEnum.AiNormal) != -1)
  212. {
  213. //进入惊讶状态, 然后再进入通知状态
  214. StateController.ChangeState(AIStateEnum.AiAstonished, AIStateEnum.AiNotify);
  215. }
  216. else
  217. {
  218. //进入惊讶状态, 然后再进入跟随状态
  219. StateController.ChangeState(AIStateEnum.AiAstonished, AIStateEnum.AiTailAfter);
  220. }
  221. }
  222. else if (state == AIStateEnum.AiLeaveFor)
  223. {
  224. LookTarget = target;
  225. StateController.ChangeState(AIStateEnum.AiAstonished, AIStateEnum.AiTailAfter);
  226. }
  227. }
  228.  
  229. /// <summary>
  230. /// 返回地上的武器是否有可以拾取的, 也包含没有被其他敌人标记的武器
  231. /// </summary>
  232. public bool CheckUsableWeaponInUnclaimed()
  233. {
  234. foreach (var unclaimedWeapon in World.Weapon_UnclaimedWeapons)
  235. {
  236. //判断是否能拾起武器, 条件: 相同的房间
  237. if (unclaimedWeapon.AffiliationArea == AffiliationArea)
  238. {
  239. if (!unclaimedWeapon.IsTotalAmmoEmpty())
  240. {
  241. if (!unclaimedWeapon.HasSign(SignNames.AiFindWeaponSign))
  242. {
  243. return true;
  244. }
  245. else
  246. {
  247. //判断是否可以移除该标记
  248. var enemy = unclaimedWeapon.GetSign<Enemy>(SignNames.AiFindWeaponSign);
  249. if (enemy == null || enemy.IsDestroyed) //标记当前武器的敌人已经被销毁
  250. {
  251. unclaimedWeapon.RemoveSign(SignNames.AiFindWeaponSign);
  252. return true;
  253. }
  254. else if (!enemy.IsAllWeaponTotalAmmoEmpty()) //标记当前武器的敌人已经有新的武器了
  255. {
  256. unclaimedWeapon.RemoveSign(SignNames.AiFindWeaponSign);
  257. return true;
  258. }
  259. }
  260. }
  261. }
  262. }
  263.  
  264. return false;
  265. }
  266. /// <summary>
  267. /// 寻找可用的武器
  268. /// </summary>
  269. public Weapon FindTargetWeapon()
  270. {
  271. Weapon target = null;
  272. var position = Position;
  273. foreach (var weapon in World.Weapon_UnclaimedWeapons)
  274. {
  275. //判断是否能拾起武器, 条件: 相同的房间, 或者当前房间目前没有战斗, 或者不在战斗房间
  276. if (weapon.AffiliationArea == AffiliationArea)
  277. {
  278. //还有弹药
  279. if (!weapon.IsTotalAmmoEmpty())
  280. {
  281. //查询是否有其他敌人标记要拾起该武器
  282. if (weapon.HasSign(SignNames.AiFindWeaponSign))
  283. {
  284. var enemy = weapon.GetSign<Enemy>(SignNames.AiFindWeaponSign);
  285. if (enemy == this) //就是自己标记的
  286. {
  287.  
  288. }
  289. else if (enemy == null || enemy.IsDestroyed) //标记当前武器的敌人已经被销毁
  290. {
  291. weapon.RemoveSign(SignNames.AiFindWeaponSign);
  292. }
  293. else if (!enemy.IsAllWeaponTotalAmmoEmpty()) //标记当前武器的敌人已经有新的武器了
  294. {
  295. weapon.RemoveSign(SignNames.AiFindWeaponSign);
  296. }
  297. else //放弃这把武器
  298. {
  299. continue;
  300. }
  301. }
  302.  
  303. if (target == null) //第一把武器
  304. {
  305. target = weapon;
  306. }
  307. else if (target.Position.DistanceSquaredTo(position) >
  308. weapon.Position.DistanceSquaredTo(position)) //距离更近
  309. {
  310. target = weapon;
  311. }
  312. }
  313. }
  314. }
  315.  
  316. return target;
  317. }
  318.  
  319. /// <summary>
  320. /// 获取武器攻击范围 (最大距离值与最小距离的中间值)
  321. /// </summary>
  322. /// <param name="weight">从最小到最大距离的过渡量, 0 - 1, 默认 0.5</param>
  323. public float GetWeaponRange(float weight = 0.5f)
  324. {
  325. if (WeaponPack.ActiveItem != null)
  326. {
  327. var attribute = WeaponPack.ActiveItem.Attribute;
  328. return Mathf.Lerp(Utils.GetConfigRangeStart(attribute.Bullet.DistanceRange), Utils.GetConfigRangeEnd(attribute.Bullet.DistanceRange), weight);
  329. }
  330.  
  331. return 0;
  332. }
  333.  
  334. /// <summary>
  335. /// 返回目标点是否在视野范围内
  336. /// </summary>
  337. public bool IsInViewRange(Vector2 target)
  338. {
  339. var isForward = IsPositionInForward(target);
  340. if (isForward)
  341. {
  342. if (GlobalPosition.DistanceSquaredTo(target) <= ViewRange * ViewRange) //没有超出视野半径
  343. {
  344. return true;
  345. }
  346. }
  347.  
  348. return false;
  349. }
  350.  
  351. /// <summary>
  352. /// 返回目标点是否在跟随状态下的视野半径内
  353. /// </summary>
  354. public bool IsInTailAfterViewRange(Vector2 target)
  355. {
  356. var isForward = IsPositionInForward(target);
  357. if (isForward)
  358. {
  359. if (GlobalPosition.DistanceSquaredTo(target) <= TailAfterViewRange * TailAfterViewRange) //没有超出视野半径
  360. {
  361. return true;
  362. }
  363. }
  364.  
  365. return false;
  366. }
  367.  
  368. /// <summary>
  369. /// 调用视野检测, 如果被墙壁和其它物体遮挡, 则返回true
  370. /// </summary>
  371. public bool TestViewRayCast(Vector2 target)
  372. {
  373. ViewRay.Enabled = true;
  374. ViewRay.TargetPosition = ViewRay.ToLocal(target);
  375. ViewRay.ForceRaycastUpdate();
  376. return ViewRay.IsColliding();
  377. }
  378.  
  379. /// <summary>
  380. /// 调用视野检测完毕后, 需要调用 TestViewRayCastOver() 来关闭视野检测射线
  381. /// </summary>
  382. public void TestViewRayCastOver()
  383. {
  384. ViewRay.Enabled = false;
  385. }
  386.  
  387. /// <summary>
  388. /// AI 拾起武器操作
  389. /// </summary>
  390. private void EnemyPickUpWeapon()
  391. {
  392. //这几个状态不需要主动拾起武器操作
  393. var state = StateController.CurrState;
  394. if (state == AIStateEnum.AiNormal || state == AIStateEnum.AiNotify || state == AIStateEnum.AiAstonished || state == AIStateEnum.AiAttack)
  395. {
  396. return;
  397. }
  398. //拾起地上的武器
  399. if (InteractiveItem is Weapon weapon)
  400. {
  401. if (WeaponPack.ActiveItem == null) //手上没有武器, 无论如何也要拾起
  402. {
  403. TriggerInteractive();
  404. return;
  405. }
  406.  
  407. //没弹药了
  408. if (weapon.IsTotalAmmoEmpty())
  409. {
  410. return;
  411. }
  412. var index = WeaponPack.FindIndex((we, i) => we.ActivityBase.Id == weapon.ActivityBase.Id);
  413. if (index != -1) //与武器背包中武器类型相同, 补充子弹
  414. {
  415. if (!WeaponPack.GetItem(index).IsAmmoFull())
  416. {
  417. TriggerInteractive();
  418. }
  419.  
  420. return;
  421. }
  422.  
  423. // var index2 = Holster.FindWeapon((we, i) =>
  424. // we.Attribute.WeightType == weapon.Attribute.WeightType && we.IsTotalAmmoEmpty());
  425. var index2 = WeaponPack.FindIndex((we, i) => we.IsTotalAmmoEmpty());
  426. if (index2 != -1) //扔掉没子弹的武器
  427. {
  428. ThrowWeapon(index2);
  429. TriggerInteractive();
  430. return;
  431. }
  432. // if (Holster.HasVacancy()) //有空位, 拾起武器
  433. // {
  434. // TriggerInteractive();
  435. // return;
  436. // }
  437. }
  438. }
  439. /// <summary>
  440. /// 获取锁定目标的剩余时间
  441. /// </summary>
  442. public float GetLockRemainderTime()
  443. {
  444. var weapon = WeaponPack.ActiveItem;
  445. if (weapon == null)
  446. {
  447. return LockingTime - LockTargetTime;
  448. }
  449. return weapon.Attribute.AiAttackAttr.LockingTime - LockTargetTime;
  450. }
  451.  
  452. public override void LookTargetPosition(Vector2 pos)
  453. {
  454. LookTarget = null;
  455. base.LookTargetPosition(pos);
  456. }
  457. /// <summary>
  458. /// 执行移动操作
  459. /// </summary>
  460. public void DoMove()
  461. {
  462. AnimatedSprite.Play(AnimatorNames.Run);
  463. //计算移动
  464. var nextPos = NavigationAgent2D.GetNextPathPosition();
  465. BasisVelocity = (nextPos - Position - NavigationPoint.Position).Normalized() * RoleState.MoveSpeed;
  466. }
  467.  
  468. /// <summary>
  469. /// 执行站立操作
  470. /// </summary>
  471. public void DoIdle()
  472. {
  473. AnimatedSprite.Play(AnimatorNames.Idle);
  474. BasisVelocity = Vector2.Zero;
  475. }
  476. /// <summary>
  477. /// 更新房间中标记的目标位置
  478. /// </summary>
  479. public void UpdateMarkTargetPosition()
  480. {
  481. if (LookTarget != null)
  482. {
  483. AffiliationArea.RoomInfo.MarkTargetPosition[LookTarget.Id] = LookTarget.Position;
  484. }
  485. }
  486.  
  487. /// <summary>
  488. /// 从标记出生时调用, 预加载波不会调用
  489. /// </summary>
  490. public virtual void OnBornFromMark()
  491. {
  492. //罚站 0.7 秒
  493. StateController.Enable = false;
  494. this.CallDelay(0.7f, () => StateController.Enable = true);
  495. }
  496. }