1
0

NPCSteeringSystem.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. using System.Numerics;
  2. using System.Threading;
  3. using System.Threading.Tasks;
  4. using Content.Server.Administration.Managers;
  5. using Content.Server.DoAfter;
  6. using Content.Server.NPC.Components;
  7. using Content.Server.NPC.Events;
  8. using Content.Server.NPC.Pathfinding;
  9. using Content.Shared.CCVar;
  10. using Content.Shared.Climbing.Systems;
  11. using Content.Shared.CombatMode;
  12. using Content.Shared.Interaction;
  13. using Content.Shared.Movement.Components;
  14. using Content.Shared.Movement.Events;
  15. using Content.Shared.Movement.Systems;
  16. using Content.Shared.NPC;
  17. using Content.Shared.NPC.Components;
  18. using Content.Shared.NPC.Systems;
  19. using Content.Shared.NPC.Events;
  20. using Content.Shared.Physics;
  21. using Content.Shared.Weapons.Melee;
  22. using Robust.Shared.Configuration;
  23. using Robust.Shared.Map;
  24. using Robust.Shared.Physics;
  25. using Robust.Shared.Physics.Components;
  26. using Robust.Shared.Physics.Systems;
  27. using Robust.Shared.Player;
  28. using Robust.Shared.Random;
  29. using Robust.Shared.Timing;
  30. using Robust.Shared.Utility;
  31. using Content.Shared.Prying.Systems;
  32. using Microsoft.Extensions.ObjectPool;
  33. namespace Content.Server.NPC.Systems;
  34. public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
  35. {
  36. /*
  37. * We use context steering to determine which way to move.
  38. * This involves creating an array of possible directions and assigning a value for the desireability of each direction.
  39. *
  40. * There's multiple ways to implement this, e.g. you can average all directions, or you can choose the highest direction
  41. * , or you can remove the danger map entirely and only having an interest map (AKA game endeavour).
  42. * See http://www.gameaipro.com/GameAIPro2/GameAIPro2_Chapter18_Context_Steering_Behavior-Driven_Steering_at_the_Macro_Scale.pdf
  43. * (though in their case it was for an F1 game so used context steering across the width of the road).
  44. */
  45. [Dependency] private readonly IAdminManager _admin = default!;
  46. [Dependency] private readonly IConfigurationManager _configManager = default!;
  47. [Dependency] private readonly IGameTiming _timing = default!;
  48. [Dependency] private readonly IRobustRandom _random = default!;
  49. [Dependency] private readonly ClimbSystem _climb = default!;
  50. [Dependency] private readonly DoAfterSystem _doAfter = default!;
  51. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  52. [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
  53. [Dependency] private readonly PathfindingSystem _pathfindingSystem = default!;
  54. [Dependency] private readonly PryingSystem _pryingSystem = default!;
  55. [Dependency] private readonly SharedMapSystem _mapSystem = default!;
  56. [Dependency] private readonly SharedInteractionSystem _interaction = default!;
  57. [Dependency] private readonly SharedMeleeWeaponSystem _melee = default!;
  58. [Dependency] private readonly SharedMoverController _mover = default!;
  59. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  60. [Dependency] private readonly SharedTransformSystem _transform = default!;
  61. [Dependency] private readonly SharedCombatModeSystem _combat = default!;
  62. private EntityQuery<FixturesComponent> _fixturesQuery;
  63. private EntityQuery<MovementSpeedModifierComponent> _modifierQuery;
  64. private EntityQuery<NpcFactionMemberComponent> _factionQuery;
  65. private EntityQuery<PhysicsComponent> _physicsQuery;
  66. private EntityQuery<TransformComponent> _xformQuery;
  67. private ObjectPool<HashSet<EntityUid>> _entSetPool =
  68. new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>());
  69. /// <summary>
  70. /// Enabled antistuck detection so if an NPC is in the same spot for a while it will re-path.
  71. /// </summary>
  72. public bool AntiStuck = true;
  73. private bool _enabled;
  74. private bool _pathfinding = true;
  75. public static readonly Vector2[] Directions = new Vector2[InterestDirections];
  76. private readonly HashSet<ICommonSession> _subscribedSessions = new();
  77. private object _obstacles = new();
  78. public override void Initialize()
  79. {
  80. base.Initialize();
  81. Log.Level = LogLevel.Info;
  82. _fixturesQuery = GetEntityQuery<FixturesComponent>();
  83. _modifierQuery = GetEntityQuery<MovementSpeedModifierComponent>();
  84. _factionQuery = GetEntityQuery<NpcFactionMemberComponent>();
  85. _physicsQuery = GetEntityQuery<PhysicsComponent>();
  86. _xformQuery = GetEntityQuery<TransformComponent>();
  87. for (var i = 0; i < InterestDirections; i++)
  88. {
  89. Directions[i] = new Angle(InterestRadians * i).ToVec();
  90. }
  91. UpdatesBefore.Add(typeof(SharedPhysicsSystem));
  92. Subs.CVar(_configManager, CCVars.NPCEnabled, SetNPCEnabled, true);
  93. Subs.CVar(_configManager, CCVars.NPCPathfinding, SetNPCPathfinding, true);
  94. SubscribeLocalEvent<NPCSteeringComponent, ComponentShutdown>(OnSteeringShutdown);
  95. SubscribeNetworkEvent<RequestNPCSteeringDebugEvent>(OnDebugRequest);
  96. }
  97. private void SetNPCEnabled(bool obj)
  98. {
  99. if (!obj)
  100. {
  101. foreach (var (comp, mover) in EntityQuery<NPCSteeringComponent, InputMoverComponent>())
  102. {
  103. mover.CurTickSprintMovement = Vector2.Zero;
  104. comp.PathfindToken?.Cancel();
  105. comp.PathfindToken = null;
  106. }
  107. }
  108. _enabled = obj;
  109. }
  110. private void SetNPCPathfinding(bool value)
  111. {
  112. _pathfinding = value;
  113. if (!_pathfinding)
  114. {
  115. foreach (var comp in EntityQuery<NPCSteeringComponent>(true))
  116. {
  117. comp.PathfindToken?.Cancel();
  118. comp.PathfindToken = null;
  119. }
  120. }
  121. }
  122. private void OnDebugRequest(RequestNPCSteeringDebugEvent msg, EntitySessionEventArgs args)
  123. {
  124. if (!_admin.IsAdmin(args.SenderSession))
  125. return;
  126. if (msg.Enabled)
  127. _subscribedSessions.Add(args.SenderSession);
  128. else
  129. _subscribedSessions.Remove(args.SenderSession);
  130. }
  131. private void OnSteeringShutdown(EntityUid uid, NPCSteeringComponent component, ComponentShutdown args)
  132. {
  133. // Cancel any active pathfinding jobs as they're irrelevant.
  134. component.PathfindToken?.Cancel();
  135. component.PathfindToken = null;
  136. }
  137. /// <summary>
  138. /// Adds the AI to the steering system to move towards a specific target
  139. /// </summary>
  140. public NPCSteeringComponent Register(EntityUid uid, EntityCoordinates coordinates, NPCSteeringComponent? component = null)
  141. {
  142. if (Resolve(uid, ref component, false))
  143. {
  144. if (component.Coordinates.Equals(coordinates))
  145. return component;
  146. component.PathfindToken?.Cancel();
  147. component.PathfindToken = null;
  148. component.CurrentPath.Clear();
  149. }
  150. else
  151. {
  152. component = AddComp<NPCSteeringComponent>(uid);
  153. component.Flags = _pathfindingSystem.GetFlags(uid);
  154. }
  155. ResetStuck(component, Transform(uid).Coordinates);
  156. component.Coordinates = coordinates;
  157. return component;
  158. }
  159. /// <summary>
  160. /// Attempts to register the entity. Does nothing if the coordinates already registered.
  161. /// </summary>
  162. public bool TryRegister(EntityUid uid, EntityCoordinates coordinates, NPCSteeringComponent? component = null)
  163. {
  164. if (Resolve(uid, ref component, false) && component.Coordinates.Equals(coordinates))
  165. {
  166. return false;
  167. }
  168. Register(uid, coordinates, component);
  169. return true;
  170. }
  171. /// <summary>
  172. /// Stops the steering behavior for the AI and cleans up.
  173. /// </summary>
  174. public void Unregister(EntityUid uid, NPCSteeringComponent? component = null)
  175. {
  176. if (!Resolve(uid, ref component, false))
  177. return;
  178. if (EntityManager.TryGetComponent(uid, out InputMoverComponent? controller))
  179. {
  180. controller.CurTickSprintMovement = Vector2.Zero;
  181. var ev = new SpriteMoveEvent(false);
  182. RaiseLocalEvent(uid, ref ev);
  183. }
  184. component.PathfindToken?.Cancel();
  185. component.PathfindToken = null;
  186. RemComp<NPCSteeringComponent>(uid);
  187. }
  188. public override void Update(float frameTime)
  189. {
  190. base.Update(frameTime);
  191. if (!_enabled)
  192. return;
  193. // Not every mob has the modifier component so do it as a separate query.
  194. var npcs = new (EntityUid, NPCSteeringComponent, InputMoverComponent, TransformComponent)[Count<ActiveNPCComponent>()];
  195. var query = EntityQueryEnumerator<ActiveNPCComponent, NPCSteeringComponent, InputMoverComponent, TransformComponent>();
  196. var index = 0;
  197. while (query.MoveNext(out var uid, out _, out var steering, out var mover, out var xform))
  198. {
  199. npcs[index] = (uid, steering, mover, xform);
  200. index++;
  201. }
  202. // Dependency issues across threads.
  203. var options = new ParallelOptions
  204. {
  205. MaxDegreeOfParallelism = 1,
  206. };
  207. var curTime = _timing.CurTime;
  208. Parallel.For(0, index, options, i =>
  209. {
  210. var (uid, steering, mover, xform) = npcs[i];
  211. Steer(uid, steering, mover, xform, frameTime, curTime);
  212. });
  213. if (_subscribedSessions.Count > 0)
  214. {
  215. var data = new List<NPCSteeringDebugData>(index);
  216. for (var i = 0; i < index; i++)
  217. {
  218. var (uid, steering, mover, _) = npcs[i];
  219. data.Add(new NPCSteeringDebugData(
  220. GetNetEntity(uid),
  221. mover.CurTickSprintMovement,
  222. steering.Interest,
  223. steering.Danger,
  224. steering.DangerPoints));
  225. }
  226. var filter = Filter.Empty();
  227. filter.AddPlayers(_subscribedSessions);
  228. RaiseNetworkEvent(new NPCSteeringDebugEvent(data), filter);
  229. }
  230. }
  231. private void SetDirection(EntityUid uid, InputMoverComponent component, NPCSteeringComponent steering, Vector2 value, bool clear = true)
  232. {
  233. if (clear && value.Equals(Vector2.Zero))
  234. {
  235. steering.CurrentPath.Clear();
  236. Array.Clear(steering.Interest);
  237. Array.Clear(steering.Danger);
  238. }
  239. component.CurTickSprintMovement = value;
  240. component.LastInputTick = _timing.CurTick;
  241. component.LastInputSubTick = ushort.MaxValue;
  242. var ev = new SpriteMoveEvent(true);
  243. RaiseLocalEvent(uid, ref ev);
  244. }
  245. /// <summary>
  246. /// Go through each steerer and combine their vectors
  247. /// </summary>
  248. private void Steer(
  249. EntityUid uid,
  250. NPCSteeringComponent steering,
  251. InputMoverComponent mover,
  252. TransformComponent xform,
  253. float frameTime,
  254. TimeSpan curTime)
  255. {
  256. if (Deleted(steering.Coordinates.EntityId))
  257. {
  258. SetDirection(uid, mover, steering, Vector2.Zero);
  259. steering.Status = SteeringStatus.NoPath;
  260. return;
  261. }
  262. // No path set from pathfinding or the likes.
  263. if (steering.Status == SteeringStatus.NoPath)
  264. {
  265. SetDirection(uid, mover, steering, Vector2.Zero);
  266. return;
  267. }
  268. // Can't move at all, just noop input.
  269. if (!mover.CanMove)
  270. {
  271. SetDirection(uid, mover, steering, Vector2.Zero);
  272. steering.Status = SteeringStatus.NoPath;
  273. return;
  274. }
  275. var agentRadius = steering.Radius;
  276. var worldPos = _transform.GetWorldPosition(xform);
  277. var (layer, mask) = _physics.GetHardCollision(uid);
  278. // Use rotation relative to parent to rotate our context vectors by.
  279. var offsetRot = -_mover.GetParentGridAngle(mover);
  280. _modifierQuery.TryGetComponent(uid, out var modifier);
  281. var moveSpeed = GetSprintSpeed(uid, modifier);
  282. var body = _physicsQuery.GetComponent(uid);
  283. var dangerPoints = steering.DangerPoints;
  284. dangerPoints.Clear();
  285. Span<float> interest = stackalloc float[InterestDirections];
  286. Span<float> danger = stackalloc float[InterestDirections];
  287. // TODO: This should be fly
  288. steering.CanSeek = true;
  289. var ev = new NPCSteeringEvent(steering, xform, worldPos, offsetRot);
  290. RaiseLocalEvent(uid, ref ev);
  291. // If seek has arrived at the target node for example then immediately re-steer.
  292. var forceSteer = true;
  293. if (steering.CanSeek && !TrySeek(uid, mover, steering, body, xform, offsetRot, moveSpeed, interest, frameTime, ref forceSteer))
  294. {
  295. SetDirection(uid, mover, steering, Vector2.Zero);
  296. return;
  297. }
  298. DebugTools.Assert(!float.IsNaN(interest[0]));
  299. // Don't steer too frequently to avoid twitchiness.
  300. // This should also implicitly solve tie situations.
  301. // I think doing this after all the ops above is best?
  302. // Originally I had it way above but sometimes mobs would overshoot their tile targets.
  303. if (!forceSteer)
  304. {
  305. SetDirection(uid, mover, steering, steering.LastSteerDirection, false);
  306. return;
  307. }
  308. // Avoid static objects like walls
  309. CollisionAvoidance(uid, offsetRot, worldPos, agentRadius, layer, mask, xform, danger);
  310. DebugTools.Assert(!float.IsNaN(danger[0]));
  311. Separation(uid, offsetRot, worldPos, agentRadius, layer, mask, body, xform, danger);
  312. // Blend last and current tick
  313. Blend(steering, frameTime, interest, danger);
  314. // Remove the danger map from the interest map.
  315. var desiredDirection = -1;
  316. var desiredValue = 0f;
  317. for (var i = 0; i < InterestDirections; i++)
  318. {
  319. var adjustedValue = Math.Clamp(steering.Interest[i] - steering.Danger[i], 0f, 1f);
  320. if (adjustedValue > desiredValue)
  321. {
  322. desiredDirection = i;
  323. desiredValue = adjustedValue;
  324. }
  325. }
  326. var resultDirection = Vector2.Zero;
  327. if (desiredDirection != -1)
  328. {
  329. resultDirection = new Angle(desiredDirection * InterestRadians).ToVec();
  330. }
  331. steering.LastSteerDirection = resultDirection;
  332. DebugTools.Assert(!float.IsNaN(resultDirection.X));
  333. SetDirection(uid, mover, steering, resultDirection, false);
  334. }
  335. private EntityCoordinates GetCoordinates(PathPoly poly)
  336. {
  337. if (!poly.IsValid())
  338. return EntityCoordinates.Invalid;
  339. return new EntityCoordinates(poly.GraphUid, poly.Box.Center);
  340. }
  341. /// <summary>
  342. /// Get a new job from the pathfindingsystem
  343. /// </summary>
  344. private async void RequestPath(EntityUid uid, NPCSteeringComponent steering, TransformComponent xform, float targetDistance)
  345. {
  346. // If we already have a pathfinding request then don't grab another.
  347. // If we're in range then just beeline them; this can avoid stutter stepping and is an easy way to look nicer.
  348. if (steering.Pathfind || targetDistance < steering.RepathRange)
  349. return;
  350. // Short-circuit with no path.
  351. var targetPoly = _pathfindingSystem.GetPoly(steering.Coordinates);
  352. // If this still causes issues future sloth adjust the collision mask.
  353. // Thanks past sloth I already realised.
  354. if (targetPoly != null &&
  355. steering.Coordinates.Position.Equals(Vector2.Zero) &&
  356. TryComp<PhysicsComponent>(uid, out var physics) &&
  357. _interaction.InRangeUnobstructed(uid, steering.Coordinates.EntityId, range: 30f, (CollisionGroup)physics.CollisionMask))
  358. {
  359. steering.CurrentPath.Clear();
  360. steering.CurrentPath.Enqueue(targetPoly);
  361. return;
  362. }
  363. steering.PathfindToken = new CancellationTokenSource();
  364. var flags = _pathfindingSystem.GetFlags(uid);
  365. var result = await _pathfindingSystem.GetPathSafe(
  366. uid,
  367. xform.Coordinates,
  368. steering.Coordinates,
  369. steering.Range,
  370. steering.PathfindToken.Token,
  371. flags);
  372. steering.PathfindToken = null;
  373. if (result.Result == PathResult.NoPath)
  374. {
  375. steering.CurrentPath.Clear();
  376. steering.FailedPathCount++;
  377. if (steering.FailedPathCount >= NPCSteeringComponent.FailedPathLimit)
  378. {
  379. steering.Status = SteeringStatus.NoPath;
  380. }
  381. return;
  382. }
  383. var targetPos = steering.Coordinates.ToMap(EntityManager, _transform);
  384. var ourPos = _transform.GetMapCoordinates(uid, xform: xform);
  385. PrunePath(uid, ourPos, targetPos.Position - ourPos.Position, result.Path);
  386. steering.CurrentPath = new Queue<PathPoly>(result.Path);
  387. }
  388. // TODO: Move these to movercontroller
  389. private float GetSprintSpeed(EntityUid uid, MovementSpeedModifierComponent? modifier = null)
  390. {
  391. if (!Resolve(uid, ref modifier, false))
  392. {
  393. return MovementSpeedModifierComponent.DefaultBaseSprintSpeed;
  394. }
  395. return modifier.CurrentSprintSpeed;
  396. }
  397. }