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