Newer
Older
DungeonShooting / DungeonShooting_Godot / src / game / activity / role / enemy / Enemy.cs
@小李xl 小李xl on 20 Oct 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 System;
  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. public RayCast2D ViewRay { get; private set; }
  51.  
  52. /// <summary>
  53. /// 导航代理
  54. /// </summary>
  55. public NavigationAgent2D NavigationAgent2D { get; private set; }
  56.  
  57. /// <summary>
  58. /// 导航代理中点
  59. /// </summary>
  60. public Marker2D NavigationPoint { get; private set; }
  61.  
  62. /// <summary>
  63. /// Ai攻击状态, 调用 EnemyAttack() 函数后会刷新
  64. /// </summary>
  65. public AiAttackState AttackState { get; private set; }
  66. //锁定目标时间
  67. private float _lockTargetTime = 0;
  68.  
  69. public override void OnInit()
  70. {
  71. base.OnInit();
  72. IsAi = true;
  73. StateController = AddComponent<StateController<Enemy, AiStateEnum>>();
  74.  
  75. AttackLayer = PhysicsLayer.Wall | PhysicsLayer.Prop | PhysicsLayer.Player;
  76. EnemyLayer = PhysicsLayer.Player;
  77. Camp = CampEnum.Camp2;
  78.  
  79. RoleState.MoveSpeed = 20;
  80.  
  81. MaxHp = 20;
  82. Hp = 20;
  83.  
  84. //视野射线
  85. ViewRay = GetNode<RayCast2D>("ViewRay");
  86. NavigationPoint = GetNode<Marker2D>("NavigationPoint");
  87. NavigationAgent2D = NavigationPoint.GetNode<NavigationAgent2D>("NavigationAgent2D");
  88.  
  89. //PathSign = new PathSign(this, PathSignLength, GameApplication.Instance.Node3D.Player);
  90.  
  91. //注册Ai状态机
  92. StateController.Register(new AiNormalState());
  93. StateController.Register(new AiProbeState());
  94. StateController.Register(new AiTailAfterState());
  95. StateController.Register(new AiFollowUpState());
  96. StateController.Register(new AiLeaveForState());
  97. StateController.Register(new AiSurroundState());
  98. StateController.Register(new AiFindAmmoState());
  99. //默认状态
  100. StateController.ChangeStateInstant(AiStateEnum.AiNormal);
  101. }
  102.  
  103. public override void EnterTree()
  104. {
  105. base.EnterTree();
  106. if (!World.Enemy_InstanceList.Contains(this))
  107. {
  108. World.Enemy_InstanceList.Add(this);
  109. }
  110. }
  111.  
  112. public override void ExitTree()
  113. {
  114. base.ExitTree();
  115. World.Enemy_InstanceList.Remove(this);
  116. }
  117.  
  118. protected override void OnDie()
  119. {
  120. //扔掉所有武器
  121. var weapons = WeaponPack.GetAndClearItem();
  122. for (var i = 0; i < weapons.Length; i++)
  123. {
  124. weapons[i].ThrowWeapon(this);
  125. }
  126.  
  127. var effPos = Position + new Vector2(0, -Altitude);
  128. //血液特效
  129. var blood = ResourceManager.LoadAndInstantiate<AutoDestroyParticles>(ResourcePath.prefab_effect_enemy_EnemyBloodEffect_tscn);
  130. blood.Position = effPos - new Vector2(0, 12);
  131. blood.AddToActivityRoot(RoomLayerEnum.NormalLayer);
  132. //创建敌人碎片
  133. var count = Utils.Random.RandomRangeInt(3, 6);
  134. for (var i = 0; i < count; i++)
  135. {
  136. var debris = Create(Ids.Id_effect0001);
  137. debris.PutDown(effPos, RoomLayerEnum.NormalLayer);
  138. debris.InheritVelocity(this);
  139. }
  140. //派发敌人死亡信号
  141. EventManager.EmitEvent(EventEnum.OnEnemyDie, this);
  142. Destroy();
  143. }
  144.  
  145. protected override void Process(float delta)
  146. {
  147. base.Process(delta);
  148.  
  149. //目标在视野内的时间
  150. var currState = StateController.CurrState;
  151. if (currState == AiStateEnum.AiSurround || currState == AiStateEnum.AiFollowUp)
  152. {
  153. var weapon = WeaponPack.ActiveItem;
  154. if (weapon != null)
  155. {
  156. if (weapon.GetBeLoadedStateState() >= 2 && !weapon.IsAttackIntervalTime()) //必须在可以开火时记录时间
  157. {
  158. _lockTargetTime += delta;
  159. }
  160. else
  161. {
  162. _lockTargetTime = 0;
  163. }
  164. if (AttackState == AiAttackState.LockingTime) //锁定玩家状态
  165. {
  166. var aiLockRemainderTime = weapon.GetAiLockRemainderTime();
  167. MountLookTarget = aiLockRemainderTime >= weapon.Attribute.AiAttackAttr.LockAngleTime;
  168. //更新瞄准辅助线
  169. if (weapon.Attribute.AiAttackAttr.ShowSubline)
  170. {
  171. if (SubLine == null)
  172. {
  173. InitSubLine();
  174. }
  175. else
  176. {
  177. SubLine.Enable = true;
  178. }
  179.  
  180. //播放警告删掉动画
  181. if (!SubLine.IsPlayWarnAnimation && aiLockRemainderTime <= 0.5f)
  182. {
  183. SubLine.PlayWarnAnimation(0.5f);
  184. }
  185. }
  186. }
  187. else
  188. {
  189. //关闭辅助线
  190. if (SubLine != null)
  191. {
  192. SubLine.Enable = false;
  193. }
  194. if (AttackState == AiAttackState.Attack || AttackState == AiAttackState.AttackInterval)
  195. {
  196. if (weapon.Attribute.AiAttackAttr.AttackLockAngle) //开火时锁定枪口角度
  197. {
  198. //连发状态锁定角度
  199. MountLookTarget = !(weapon.GetContinuousCount() > 0 || weapon.GetAttackTimer() > 0);
  200. }
  201. else
  202. {
  203. MountLookTarget = true;
  204. }
  205. }
  206. else
  207. {
  208. MountLookTarget = true;
  209. }
  210. }
  211. }
  212. else
  213. {
  214. MountLookTarget = true;
  215. _lockTargetTime = 0;
  216. }
  217. }
  218. else
  219. {
  220. MountLookTarget = true;
  221. _lockTargetTime = 0;
  222. }
  223.  
  224. //拾起武器操作
  225. EnemyPickUpWeapon();
  226. }
  227.  
  228. protected override void OnHit(int damage, bool realHarm)
  229. {
  230. //受到伤害
  231. var state = StateController.CurrState;
  232. if (state == AiStateEnum.AiNormal || state == AiStateEnum.AiProbe || state == AiStateEnum.AiLeaveFor)
  233. {
  234. StateController.ChangeState(AiStateEnum.AiTailAfter);
  235. }
  236. }
  237.  
  238. /// <summary>
  239. /// 返回地上的武器是否有可以拾取的, 也包含没有被其他敌人标记的武器
  240. /// </summary>
  241. public bool CheckUsableWeaponInUnclaimed()
  242. {
  243. foreach (var unclaimedWeapon in World.Weapon_UnclaimedWeapons)
  244. {
  245. //判断是否能拾起武器, 条件: 相同的房间
  246. if (unclaimedWeapon.AffiliationArea == AffiliationArea)
  247. {
  248. if (!unclaimedWeapon.IsTotalAmmoEmpty())
  249. {
  250. if (!unclaimedWeapon.HasSign(SignNames.AiFindWeaponSign))
  251. {
  252. return true;
  253. }
  254. else
  255. {
  256. //判断是否可以移除该标记
  257. var enemy = unclaimedWeapon.GetSign<Enemy>(SignNames.AiFindWeaponSign);
  258. if (enemy == null || enemy.IsDestroyed) //标记当前武器的敌人已经被销毁
  259. {
  260. unclaimedWeapon.RemoveSign(SignNames.AiFindWeaponSign);
  261. return true;
  262. }
  263. else if (!enemy.IsAllWeaponTotalAmmoEmpty()) //标记当前武器的敌人已经有新的武器了
  264. {
  265. unclaimedWeapon.RemoveSign(SignNames.AiFindWeaponSign);
  266. return true;
  267. }
  268. }
  269. }
  270. }
  271. }
  272.  
  273. return false;
  274. }
  275. /// <summary>
  276. /// 寻找可用的武器
  277. /// </summary>
  278. public Weapon FindTargetWeapon()
  279. {
  280. Weapon target = null;
  281. var position = Position;
  282. foreach (var weapon in World.Weapon_UnclaimedWeapons)
  283. {
  284. //判断是否能拾起武器, 条件: 相同的房间, 或者当前房间目前没有战斗, 或者不在战斗房间
  285. if (weapon.AffiliationArea == AffiliationArea)
  286. {
  287. //还有弹药
  288. if (!weapon.IsTotalAmmoEmpty())
  289. {
  290. //查询是否有其他敌人标记要拾起该武器
  291. if (weapon.HasSign(SignNames.AiFindWeaponSign))
  292. {
  293. var enemy = weapon.GetSign<Enemy>(SignNames.AiFindWeaponSign);
  294. if (enemy == this) //就是自己标记的
  295. {
  296.  
  297. }
  298. else if (enemy == null || enemy.IsDestroyed) //标记当前武器的敌人已经被销毁
  299. {
  300. weapon.RemoveSign(SignNames.AiFindWeaponSign);
  301. }
  302. else if (!enemy.IsAllWeaponTotalAmmoEmpty()) //标记当前武器的敌人已经有新的武器了
  303. {
  304. weapon.RemoveSign(SignNames.AiFindWeaponSign);
  305. }
  306. else //放弃这把武器
  307. {
  308. continue;
  309. }
  310. }
  311.  
  312. if (target == null) //第一把武器
  313. {
  314. target = weapon;
  315. }
  316. else if (target.Position.DistanceSquaredTo(position) >
  317. weapon.Position.DistanceSquaredTo(position)) //距离更近
  318. {
  319. target = weapon;
  320. }
  321. }
  322. }
  323. }
  324.  
  325. return target;
  326. }
  327.  
  328. /// <summary>
  329. /// 检查是否能切换到 AiStateEnum.AiLeaveFor 状态
  330. /// </summary>
  331. /// <returns></returns>
  332. public bool CanChangeLeaveFor()
  333. {
  334. if (!World.Enemy_IsFindTarget)
  335. {
  336. return false;
  337. }
  338.  
  339. var currState = StateController.CurrState;
  340. if (currState == AiStateEnum.AiNormal || currState == AiStateEnum.AiProbe)
  341. {
  342. //判断是否在同一个房间内
  343. return World.Enemy_FindTargetAffiliationSet.Contains(AffiliationArea);
  344. }
  345. return false;
  346. }
  347. /// <summary>
  348. /// Ai触发的攻击
  349. /// </summary>
  350. public void EnemyAttack()
  351. {
  352. var weapon = WeaponPack.ActiveItem;
  353. if (weapon != null)
  354. {
  355. AttackState = weapon.AiTriggerAttackState();
  356. }
  357. else //没有武器
  358. {
  359. AttackState = AiAttackState.NoWeapon;
  360. }
  361. }
  362.  
  363. /// <summary>
  364. /// 获取武器攻击范围 (最大距离值与最小距离的中间值)
  365. /// </summary>
  366. /// <param name="weight">从最小到最大距离的过渡量, 0 - 1, 默认 0.5</param>
  367. public float GetWeaponRange(float weight = 0.5f)
  368. {
  369. if (WeaponPack.ActiveItem != null)
  370. {
  371. var attribute = WeaponPack.ActiveItem.Attribute;
  372. return Mathf.Lerp(Utils.GetConfigRangeStart(attribute.BulletDistanceRange), Utils.GetConfigRangeEnd(attribute.BulletDistanceRange), weight);
  373. }
  374.  
  375. return 0;
  376. }
  377.  
  378. /// <summary>
  379. /// 返回目标点是否在视野范围内
  380. /// </summary>
  381. public bool IsInViewRange(Vector2 target)
  382. {
  383. var isForward = IsPositionInForward(target);
  384. if (isForward)
  385. {
  386. if (GlobalPosition.DistanceSquaredTo(target) <= ViewRange * ViewRange) //没有超出视野半径
  387. {
  388. return true;
  389. }
  390. }
  391.  
  392. return false;
  393. }
  394.  
  395. /// <summary>
  396. /// 返回目标点是否在跟随状态下的视野半径内
  397. /// </summary>
  398. public bool IsInTailAfterViewRange(Vector2 target)
  399. {
  400. var isForward = IsPositionInForward(target);
  401. if (isForward)
  402. {
  403. if (GlobalPosition.DistanceSquaredTo(target) <= TailAfterViewRange * TailAfterViewRange) //没有超出视野半径
  404. {
  405. return true;
  406. }
  407. }
  408.  
  409. return false;
  410. }
  411.  
  412. /// <summary>
  413. /// 调用视野检测, 如果被墙壁和其它物体遮挡, 则返回被挡住视野的物体对象, 视野无阻则返回 null
  414. /// </summary>
  415. public bool TestViewRayCast(Vector2 target)
  416. {
  417. ViewRay.Enabled = true;
  418. ViewRay.TargetPosition = ViewRay.ToLocal(target);
  419. ViewRay.ForceRaycastUpdate();
  420. return ViewRay.IsColliding();
  421. }
  422.  
  423. /// <summary>
  424. /// 调用视野检测完毕后, 需要调用 TestViewRayCastOver() 来关闭视野检测射线
  425. /// </summary>
  426. public void TestViewRayCastOver()
  427. {
  428. ViewRay.Enabled = false;
  429. }
  430.  
  431. /// <summary>
  432. /// AI 拾起武器操作
  433. /// </summary>
  434. private void EnemyPickUpWeapon()
  435. {
  436. //这几个状态不需要主动拾起武器操作
  437. var state = StateController.CurrState;
  438. if (state == AiStateEnum.AiNormal)
  439. {
  440. return;
  441. }
  442. //拾起地上的武器
  443. if (InteractiveItem is Weapon weapon)
  444. {
  445. if (WeaponPack.ActiveItem == null) //手上没有武器, 无论如何也要拾起
  446. {
  447. TriggerInteractive();
  448. return;
  449. }
  450.  
  451. //没弹药了
  452. if (weapon.IsTotalAmmoEmpty())
  453. {
  454. return;
  455. }
  456. var index = WeaponPack.FindIndex((we, i) => we.ItemConfig.Id == weapon.ItemConfig.Id);
  457. if (index != -1) //与武器背包中武器类型相同, 补充子弹
  458. {
  459. if (!WeaponPack.GetItem(index).IsAmmoFull())
  460. {
  461. TriggerInteractive();
  462. }
  463.  
  464. return;
  465. }
  466.  
  467. // var index2 = Holster.FindWeapon((we, i) =>
  468. // we.Attribute.WeightType == weapon.Attribute.WeightType && we.IsTotalAmmoEmpty());
  469. var index2 = WeaponPack.FindIndex((we, i) => we.IsTotalAmmoEmpty());
  470. if (index2 != -1) //扔掉没子弹的武器
  471. {
  472. ThrowWeapon(index2);
  473. TriggerInteractive();
  474. return;
  475. }
  476. // if (Holster.HasVacancy()) //有空位, 拾起武器
  477. // {
  478. // TriggerInteractive();
  479. // return;
  480. // }
  481. }
  482. }
  483.  
  484. /// <summary>
  485. /// 获取锁定目标的时间
  486. /// </summary>
  487. public float GetLockTime()
  488. {
  489. return _lockTargetTime;
  490. }
  491.  
  492. /// <summary>
  493. /// 强制设置锁定目标时间
  494. /// </summary>
  495. public void SetLockTargetTime(float time)
  496. {
  497. _lockTargetTime = time;
  498. }
  499.  
  500. }