Newer
Older
DungeonShooting / DungeonShooting_Godot / src / game / role / enemy / Enemy.cs
  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.Collections.Generic;
  15. using Godot;
  16.  
  17. /// <summary>
  18. /// 基础敌人
  19. /// </summary>
  20. [RegisterActivity(ActivityIdPrefix.Enemy + "0001", ResourcePath.prefab_role_Enemy_tscn)]
  21. public partial class Enemy : Role
  22. {
  23. /// <summary>
  24. /// 公共属性, 是否找到目标, 如果找到目标, 则与目标同房间的所有敌人都会知道目标的位置
  25. /// </summary>
  26. public static bool IsFindTarget { get; private set; }
  27.  
  28. /// <summary>
  29. /// 公共属性, 在哪个区域找到的目标, 所有该区域下的敌人都会知道目标的位置
  30. /// </summary>
  31. public static HashSet<AffiliationArea> FindTargetAffiliationSet { get; } = new HashSet<AffiliationArea>();
  32. /// <summary>
  33. /// 公共属性, 找到的目标的位置, 如果目标在视野内, 则一直更新
  34. /// </summary>
  35. public static Vector2 FindTargetPosition { get; private set; }
  36.  
  37. /// <summary>
  38. /// 记录所有存活的敌人
  39. /// </summary>
  40. private static readonly List<Enemy> _enemieList = new List<Enemy>();
  41.  
  42. /// <summary>
  43. /// 敌人身上的状态机控制器
  44. /// </summary>
  45. public StateController<Enemy, AiStateEnum> StateController { get; private set; }
  46.  
  47. /// <summary>
  48. /// 视野半径, 单位像素, 发现玩家后改视野范围可以穿墙
  49. /// </summary>
  50. public float ViewRange { get; set; } = 250;
  51.  
  52. /// <summary>
  53. /// 发现玩家后的视野半径
  54. /// </summary>
  55. public float TailAfterViewRange { get; set; } = 400;
  56.  
  57. /// <summary>
  58. /// 背后的视野半径, 单位像素
  59. /// </summary>
  60. public float BackViewRange { get; set; } = 50;
  61.  
  62. /// <summary>
  63. /// 视野检测射线, 朝玩家打射线, 检测是否碰到墙
  64. /// </summary>
  65. public RayCast2D ViewRay { get; private set; }
  66.  
  67. /// <summary>
  68. /// 导航代理
  69. /// </summary>
  70. public NavigationAgent2D NavigationAgent2D { get; private set; }
  71.  
  72. /// <summary>
  73. /// 导航代理中点
  74. /// </summary>
  75. public Marker2D NavigationPoint { get; private set; }
  76.  
  77. //开火间隙时间
  78. private float _enemyAttackTimer = 0;
  79. //目标在视野内的时间
  80. private float _targetInViewTime = 0;
  81.  
  82. public override void OnInit()
  83. {
  84. base.OnInit();
  85. IsAi = true;
  86. StateController = AddComponent<StateController<Enemy, AiStateEnum>>();
  87.  
  88. AttackLayer = PhysicsLayer.Wall | PhysicsLayer.Props | PhysicsLayer.Player;
  89. Camp = CampEnum.Camp2;
  90.  
  91. MoveSpeed = 20;
  92.  
  93. Holster.SlotList[2].Enable = true;
  94. Holster.SlotList[3].Enable = true;
  95. MaxHp = 20;
  96. Hp = 20;
  97.  
  98. //视野射线
  99. ViewRay = GetNode<RayCast2D>("ViewRay");
  100. NavigationPoint = GetNode<Marker2D>("NavigationPoint");
  101. NavigationAgent2D = NavigationPoint.GetNode<NavigationAgent2D>("NavigationAgent2D");
  102.  
  103. //PathSign = new PathSign(this, PathSignLength, GameApplication.Instance.Node3D.Player);
  104.  
  105. //注册Ai状态机
  106. StateController.Register(new AiNormalState());
  107. StateController.Register(new AiProbeState());
  108. StateController.Register(new AiTailAfterState());
  109. StateController.Register(new AiFollowUpState());
  110. StateController.Register(new AiLeaveForState());
  111. StateController.Register(new AiSurroundState());
  112. StateController.Register(new AiFindAmmoState());
  113. //默认状态
  114. StateController.ChangeState(AiStateEnum.AiNormal);
  115. }
  116.  
  117. public override void _EnterTree()
  118. {
  119. if (!_enemieList.Contains(this))
  120. {
  121. _enemieList.Add(this);
  122. }
  123. }
  124.  
  125. public override void _ExitTree()
  126. {
  127. base._ExitTree();
  128. _enemieList.Remove(this);
  129. }
  130.  
  131. protected override void OnDie()
  132. {
  133. //扔掉所有武器
  134. var weapons = Holster.GetAndClearWeapon();
  135. for (var i = 0; i < weapons.Length; i++)
  136. {
  137. weapons[i].ThrowWeapon(this);
  138. }
  139. //派发敌人死亡信号
  140. EventManager.EmitEvent(EventEnum.OnEnemyDie, this);
  141. Destroy();
  142. }
  143.  
  144. protected override void Process(float delta)
  145. {
  146. base.Process(delta);
  147. _enemyAttackTimer -= delta;
  148.  
  149. //目标在视野内的时间
  150. var currState = StateController.CurrState;
  151. if (currState == AiStateEnum.AiSurround || currState == AiStateEnum.AiFollowUp)
  152. {
  153. _targetInViewTime += delta;
  154. }
  155. else
  156. {
  157. _targetInViewTime = 0;
  158. }
  159.  
  160. EnemyPickUpWeapon();
  161. }
  162.  
  163. protected override void OnHit(int damage)
  164. {
  165. //受到伤害
  166. var state = StateController.CurrState;
  167. if (state == AiStateEnum.AiNormal || state == AiStateEnum.AiProbe || state == AiStateEnum.AiLeaveFor)
  168. {
  169. StateController.ChangeStateLate(AiStateEnum.AiTailAfter);
  170. }
  171. }
  172.  
  173. /// <summary>
  174. /// 返回地上的武器是否有可以拾取的, 也包含没有被其他敌人标记的武器
  175. /// </summary>
  176. public bool CheckUsableWeaponInUnclaimed()
  177. {
  178. foreach (var unclaimedWeapon in Weapon.UnclaimedWeapons)
  179. {
  180. //判断是否能拾起武器, 条件: 相同的房间
  181. if (unclaimedWeapon.Affiliation == Affiliation)
  182. {
  183. if (!unclaimedWeapon.IsTotalAmmoEmpty())
  184. {
  185. if (!unclaimedWeapon.HasSign(SignNames.AiFindWeaponSign))
  186. {
  187. return true;
  188. }
  189. else
  190. {
  191. //判断是否可以移除该标记
  192. var enemy = unclaimedWeapon.GetSign<Enemy>(SignNames.AiFindWeaponSign);
  193. if (enemy == null || enemy.IsDestroyed) //标记当前武器的敌人已经被销毁
  194. {
  195. unclaimedWeapon.RemoveSign(SignNames.AiFindWeaponSign);
  196. return true;
  197. }
  198. else if (!enemy.IsAllWeaponTotalAmmoEmpty()) //标记当前武器的敌人已经有新的武器了
  199. {
  200. unclaimedWeapon.RemoveSign(SignNames.AiFindWeaponSign);
  201. return true;
  202. }
  203. }
  204. }
  205. }
  206. }
  207.  
  208. return false;
  209. }
  210. /// <summary>
  211. /// 寻找可用的武器
  212. /// </summary>
  213. public Weapon FindTargetWeapon()
  214. {
  215. Weapon target = null;
  216. var position = Position;
  217. foreach (var weapon in Weapon.UnclaimedWeapons)
  218. {
  219. //判断是否能拾起武器, 条件: 相同的房间, 或者当前房间目前没有战斗, 或者不在战斗房间
  220. if (weapon.Affiliation == Affiliation)
  221. {
  222. //还有弹药
  223. if (!weapon.IsTotalAmmoEmpty())
  224. {
  225. //查询是否有其他敌人标记要拾起该武器
  226. if (weapon.HasSign(SignNames.AiFindWeaponSign))
  227. {
  228. var enemy = weapon.GetSign<Enemy>(SignNames.AiFindWeaponSign);
  229. if (enemy == this) //就是自己标记的
  230. {
  231.  
  232. }
  233. else if (enemy == null || enemy.IsDestroyed) //标记当前武器的敌人已经被销毁
  234. {
  235. weapon.RemoveSign(SignNames.AiFindWeaponSign);
  236. }
  237. else if (!enemy.IsAllWeaponTotalAmmoEmpty()) //标记当前武器的敌人已经有新的武器了
  238. {
  239. weapon.RemoveSign(SignNames.AiFindWeaponSign);
  240. }
  241. else //放弃这把武器
  242. {
  243. continue;
  244. }
  245. }
  246.  
  247. if (target == null) //第一把武器
  248. {
  249. target = weapon;
  250. }
  251. else if (target.Position.DistanceSquaredTo(position) >
  252. weapon.Position.DistanceSquaredTo(position)) //距离更近
  253. {
  254. target = weapon;
  255. }
  256. }
  257. }
  258. }
  259.  
  260. return target;
  261. }
  262.  
  263. /// <summary>
  264. /// 检查是否能切换到 AiStateEnum.AiLeaveFor 状态
  265. /// </summary>
  266. /// <returns></returns>
  267. public bool CanChangeLeaveFor()
  268. {
  269. if (!IsFindTarget)
  270. {
  271. return false;
  272. }
  273.  
  274. var currState = StateController.CurrState;
  275. if (currState == AiStateEnum.AiNormal || currState == AiStateEnum.AiProbe)
  276. {
  277. //判断是否在同一个房间内
  278. return FindTargetAffiliationSet.Contains(Affiliation);
  279. }
  280. return false;
  281. }
  282. /// <summary>
  283. /// 更新敌人视野
  284. /// </summary>
  285. public static void UpdateEnemiesView()
  286. {
  287. IsFindTarget = false;
  288. FindTargetAffiliationSet.Clear();
  289. for (var i = 0; i < _enemieList.Count; i++)
  290. {
  291. var enemy = _enemieList[i];
  292. var state = enemy.StateController.CurrState;
  293. if (state == AiStateEnum.AiFollowUp || state == AiStateEnum.AiSurround) //目标在视野内
  294. {
  295. if (!IsFindTarget)
  296. {
  297. IsFindTarget = true;
  298. FindTargetPosition = Player.Current.GetCenterPosition();
  299. FindTargetAffiliationSet.Add(Player.Current.Affiliation);
  300. }
  301. FindTargetAffiliationSet.Add(enemy.Affiliation);
  302. }
  303. }
  304. }
  305.  
  306. /// <summary>
  307. /// Ai触发的攻击
  308. /// </summary>
  309. public void EnemyAttack(float delta)
  310. {
  311. var weapon = Holster.ActiveWeapon;
  312. if (weapon != null)
  313. {
  314. if (weapon.IsTotalAmmoEmpty()) //当前武器弹药打空
  315. {
  316. //切换到有子弹的武器
  317. var index = Holster.FindWeapon((we, i) => !we.IsTotalAmmoEmpty());
  318. if (index != -1)
  319. {
  320. Holster.ExchangeByIndex(index);
  321. }
  322. else //所有子弹打光
  323. {
  324. }
  325. }
  326. else if (weapon.Reloading) //换弹中
  327. {
  328.  
  329. }
  330. else if (weapon.IsAmmoEmpty()) //弹夹已经打空
  331. {
  332. Reload();
  333. }
  334. else if (_targetInViewTime >= weapon.Attribute.AiTargetLockingTime) //正常射击
  335. {
  336. if (weapon.GetDelayedAttackTime() > 0)
  337. {
  338. Attack();
  339. }
  340. else
  341. {
  342. if (weapon.Attribute.ContinuousShoot) //连发
  343. {
  344. Attack();
  345. }
  346. else //单发
  347. {
  348. if (_enemyAttackTimer <= 0)
  349. {
  350. _enemyAttackTimer = 60f / weapon.Attribute.StartFiringSpeed;
  351. Attack();
  352. }
  353. }
  354. }
  355. }
  356. }
  357. }
  358.  
  359. /// <summary>
  360. /// 获取武器攻击范围 (最大距离值与最小距离的中间值)
  361. /// </summary>
  362. /// <param name="weight">从最小到最大距离的过渡量, 0 - 1, 默认 0.5</param>
  363. public float GetWeaponRange(float weight = 0.5f)
  364. {
  365. if (Holster.ActiveWeapon != null)
  366. {
  367. var attribute = Holster.ActiveWeapon.Attribute;
  368. return Mathf.Lerp(attribute.MinDistance, attribute.MaxDistance, weight);
  369. }
  370.  
  371. return 0;
  372. }
  373.  
  374. /// <summary>
  375. /// 返回目标点是否在视野范围内
  376. /// </summary>
  377. public bool IsInViewRange(Vector2 target)
  378. {
  379. var isForward = IsPositionInForward(target);
  380. if (isForward)
  381. {
  382. if (GlobalPosition.DistanceSquaredTo(target) <= ViewRange * ViewRange) //没有超出视野半径
  383. {
  384. return true;
  385. }
  386. }
  387.  
  388. return false;
  389. }
  390.  
  391. /// <summary>
  392. /// 返回目标点是否在跟随状态下的视野半径内
  393. /// </summary>
  394. public bool IsInTailAfterViewRange(Vector2 target)
  395. {
  396. var isForward = IsPositionInForward(target);
  397. if (isForward)
  398. {
  399. if (GlobalPosition.DistanceSquaredTo(target) <= TailAfterViewRange * TailAfterViewRange) //没有超出视野半径
  400. {
  401. return true;
  402. }
  403. }
  404.  
  405. return false;
  406. }
  407.  
  408. /// <summary>
  409. /// 调用视野检测, 如果被墙壁和其它物体遮挡, 则返回被挡住视野的物体对象, 视野无阻则返回 null
  410. /// </summary>
  411. public bool TestViewRayCast(Vector2 target)
  412. {
  413. ViewRay.Enabled = true;
  414. ViewRay.TargetPosition = ViewRay.ToLocal(target);
  415. ViewRay.ForceRaycastUpdate();
  416. return ViewRay.IsColliding();
  417. }
  418.  
  419. /// <summary>
  420. /// 调用视野检测完毕后, 需要调用 TestViewRayCastOver() 来关闭视野检测射线
  421. /// </summary>
  422. public void TestViewRayCastOver()
  423. {
  424. ViewRay.Enabled = false;
  425. }
  426.  
  427. /// <summary>
  428. /// AI 拾起武器操作
  429. /// </summary>
  430. private void EnemyPickUpWeapon()
  431. {
  432. //这几个状态不需要主动拾起武器操作
  433. var state = StateController.CurrState;
  434. if (state == AiStateEnum.AiNormal)
  435. {
  436. return;
  437. }
  438. //拾起地上的武器
  439. if (InteractiveItem is Weapon weapon)
  440. {
  441. if (Holster.ActiveWeapon == null) //手上没有武器, 无论如何也要拾起
  442. {
  443. TriggerInteractive();
  444. return;
  445. }
  446.  
  447. //没弹药了
  448. if (weapon.IsTotalAmmoEmpty())
  449. {
  450. return;
  451. }
  452. var index = Holster.FindWeapon((we, i) => we.ItemId == weapon.ItemId);
  453. if (index != -1) //与武器袋中武器类型相同, 补充子弹
  454. {
  455. if (!Holster.GetWeapon(index).IsAmmoFull())
  456. {
  457. TriggerInteractive();
  458. }
  459.  
  460. return;
  461. }
  462.  
  463. // var index2 = Holster.FindWeapon((we, i) =>
  464. // we.Attribute.WeightType == weapon.Attribute.WeightType && we.IsTotalAmmoEmpty());
  465. var index2 = Holster.FindWeapon((we, i) => we.IsTotalAmmoEmpty());
  466. if (index2 != -1) //扔掉没子弹的武器
  467. {
  468. ThrowWeapon(index2);
  469. TriggerInteractive();
  470. return;
  471. }
  472. // if (Holster.HasVacancy()) //有空位, 拾起武器
  473. // {
  474. // TriggerInteractive();
  475. // return;
  476. // }
  477. }
  478. }
  479.  
  480. }