NPCSteeringSystem.Context.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. using System.Linq;
  2. using System.Numerics;
  3. using Content.Server.Examine;
  4. using Content.Server.NPC.Components;
  5. using Content.Server.NPC.Pathfinding;
  6. using Content.Shared.Climbing;
  7. using Content.Shared.Interaction;
  8. using Content.Shared.Movement.Components;
  9. using Content.Shared.NPC;
  10. using Content.Shared.Physics;
  11. using Robust.Shared.Map;
  12. using Robust.Shared.Physics;
  13. using Robust.Shared.Physics.Components;
  14. using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
  15. namespace Content.Server.NPC.Systems;
  16. public sealed partial class NPCSteeringSystem
  17. {
  18. private void ApplySeek(Span<float> interest, Vector2 direction, float weight)
  19. {
  20. if (weight == 0f || direction == Vector2.Zero)
  21. return;
  22. var directionAngle = (float)direction.ToAngle().Theta;
  23. for (var i = 0; i < InterestDirections; i++)
  24. {
  25. var angle = i * InterestRadians;
  26. var dot = MathF.Cos(directionAngle - angle);
  27. dot = (dot + 1f) * 0.5f;
  28. interest[i] = Math.Clamp(interest[i] + dot * weight, 0f, 1f);
  29. }
  30. }
  31. #region Seek
  32. /// <summary>
  33. /// Takes into account agent-specific context that may allow it to bypass a node which is not FreeSpace.
  34. /// </summary>
  35. private bool IsFreeSpace(
  36. EntityUid uid,
  37. NPCSteeringComponent steering,
  38. PathPoly node)
  39. {
  40. if (node.Data.IsFreeSpace)
  41. {
  42. return true;
  43. }
  44. // Handle the case where the node is a climb, we can climb, and we are climbing.
  45. else if ((node.Data.Flags & PathfindingBreadcrumbFlag.Climb) != 0x0 &&
  46. (steering.Flags & PathFlags.Climbing) != 0x0 &&
  47. TryComp<ClimbingComponent>(uid, out var climbing) &&
  48. climbing.IsClimbing)
  49. {
  50. return true;
  51. }
  52. // TODO: Ideally for "FreeSpace" we check all entities on the tile and build flags dynamically (pathfinder refactor in future).
  53. var ents = _entSetPool.Get();
  54. _lookup.GetLocalEntitiesIntersecting(node.GraphUid, node.Box.Enlarged(-0.04f), ents, flags: LookupFlags.Static);
  55. var result = true;
  56. if (ents.Count > 0)
  57. {
  58. var fixtures = _fixturesQuery.GetComponent(uid);
  59. var physics = _physicsQuery.GetComponent(uid);
  60. foreach (var intersecting in ents)
  61. {
  62. if (!_physics.IsCurrentlyHardCollidable((uid, fixtures, physics), intersecting))
  63. {
  64. continue;
  65. }
  66. result = false;
  67. break;
  68. }
  69. }
  70. _entSetPool.Return(ents);
  71. return result;
  72. }
  73. /// <summary>
  74. /// Attempts to head to the target destination, either via the next pathfinding node or the final target.
  75. /// </summary>
  76. private bool TrySeek(
  77. EntityUid uid,
  78. InputMoverComponent mover,
  79. NPCSteeringComponent steering,
  80. PhysicsComponent body,
  81. TransformComponent xform,
  82. Angle offsetRot,
  83. float moveSpeed,
  84. Span<float> interest,
  85. float frameTime,
  86. ref bool forceSteer)
  87. {
  88. var ourCoordinates = xform.Coordinates;
  89. var destinationCoordinates = steering.Coordinates;
  90. var inLos = true;
  91. // Check if we're in LOS if that's required.
  92. // TODO: Need something uhh better not sure on the interaction between these.
  93. if (!steering.ForceMove && steering.ArriveOnLineOfSight)
  94. {
  95. // TODO: use vision range
  96. inLos = _interaction.InRangeUnobstructed(uid, steering.Coordinates, 10f);
  97. if (inLos)
  98. {
  99. steering.LineOfSightTimer += frameTime;
  100. if (steering.LineOfSightTimer >= steering.LineOfSightTimeRequired)
  101. {
  102. steering.Status = SteeringStatus.InRange;
  103. ResetStuck(steering, ourCoordinates);
  104. return true;
  105. }
  106. }
  107. else
  108. {
  109. steering.LineOfSightTimer = 0f;
  110. }
  111. }
  112. else
  113. {
  114. steering.LineOfSightTimer = 0f;
  115. steering.ForceMove = false;
  116. }
  117. // We've arrived, nothing else matters.
  118. if (xform.Coordinates.TryDistance(EntityManager, destinationCoordinates, out var targetDistance) &&
  119. inLos &&
  120. targetDistance <= steering.Range)
  121. {
  122. steering.Status = SteeringStatus.InRange;
  123. ResetStuck(steering, ourCoordinates);
  124. return true;
  125. }
  126. // Grab the target position, either the next path node or our end goal..
  127. var targetCoordinates = GetTargetCoordinates(steering);
  128. if (!targetCoordinates.IsValid(EntityManager))
  129. {
  130. steering.Status = SteeringStatus.NoPath;
  131. return false;
  132. }
  133. var needsPath = false;
  134. // If the next node is invalid then get new ones
  135. if (!targetCoordinates.IsValid(EntityManager))
  136. {
  137. if (steering.CurrentPath.TryPeek(out var poly) &&
  138. (poly.Data.Flags & PathfindingBreadcrumbFlag.Invalid) != 0x0)
  139. {
  140. steering.CurrentPath.Dequeue();
  141. // Try to get the next node temporarily.
  142. targetCoordinates = GetTargetCoordinates(steering);
  143. needsPath = true;
  144. ResetStuck(steering, ourCoordinates);
  145. }
  146. }
  147. // Check if mapids match.
  148. var targetMap = _transform.ToMapCoordinates(targetCoordinates);
  149. var ourMap = _transform.ToMapCoordinates(ourCoordinates);
  150. if (targetMap.MapId != ourMap.MapId)
  151. {
  152. steering.Status = SteeringStatus.NoPath;
  153. return false;
  154. }
  155. var direction = targetMap.Position - ourMap.Position;
  156. // Need to be pretty close if it's just a node to make sure LOS for door bashes or the likes.
  157. bool arrived;
  158. if (targetCoordinates.Equals(steering.Coordinates))
  159. {
  160. // What's our tolerance for arrival.
  161. // If it's a pathfinding node it might be different to the destination.
  162. arrived = direction.Length() <= steering.Range;
  163. }
  164. // If next node is a free tile then get within its bounds.
  165. // This is to avoid popping it too early
  166. else if (steering.CurrentPath.TryPeek(out var node) && IsFreeSpace(uid, steering, node))
  167. {
  168. arrived = node.Box.Contains(ourCoordinates.Position);
  169. }
  170. // Try getting into blocked range I guess?
  171. // TODO: Consider melee range or the likes.
  172. else
  173. {
  174. arrived = direction.Length() <= SharedInteractionSystem.InteractionRange - 0.05f;
  175. }
  176. // Are we in range
  177. if (arrived)
  178. {
  179. // Node needs some kind of special handling like access or smashing.
  180. if (steering.CurrentPath.TryPeek(out var node) && !IsFreeSpace(uid, steering, node))
  181. {
  182. // Ignore stuck while handling obstacles.
  183. ResetStuck(steering, ourCoordinates);
  184. SteeringObstacleStatus status;
  185. // Breaking behaviours and the likes.
  186. lock (_obstacles)
  187. {
  188. // We're still coming to a stop so wait for the do_after.
  189. if (body.LinearVelocity.LengthSquared() > 0.01f)
  190. {
  191. return true;
  192. }
  193. status = TryHandleFlags(uid, steering, node);
  194. }
  195. // TODO: Need to handle re-pathing in case the target moves around.
  196. switch (status)
  197. {
  198. case SteeringObstacleStatus.Completed:
  199. steering.DoAfterId = null;
  200. break;
  201. case SteeringObstacleStatus.Failed:
  202. steering.DoAfterId = null;
  203. // TODO: Blacklist the poly for next query
  204. steering.Status = SteeringStatus.NoPath;
  205. return false;
  206. case SteeringObstacleStatus.Continuing:
  207. CheckPath(uid, steering, xform, needsPath, targetDistance);
  208. return true;
  209. default:
  210. throw new ArgumentOutOfRangeException();
  211. }
  212. }
  213. // Distance should already be handled above.
  214. // It was just a node, not the target, so grab the next destination (either the target or next node).
  215. if (steering.CurrentPath.Count > 0)
  216. {
  217. forceSteer = true;
  218. steering.CurrentPath.Dequeue();
  219. // Alright just adjust slightly and grab the next node so we don't stop moving for a tick.
  220. // TODO: If it's the last node just grab the target instead.
  221. targetCoordinates = GetTargetCoordinates(steering);
  222. if (!targetCoordinates.IsValid(EntityManager))
  223. {
  224. SetDirection(uid, mover, steering, Vector2.Zero);
  225. steering.Status = SteeringStatus.NoPath;
  226. return false;
  227. }
  228. targetMap = _transform.ToMapCoordinates(targetCoordinates);
  229. // Can't make it again.
  230. if (ourMap.MapId != targetMap.MapId)
  231. {
  232. SetDirection(uid, mover, steering, Vector2.Zero);
  233. steering.Status = SteeringStatus.NoPath;
  234. return false;
  235. }
  236. // Gonna resume now business as usual
  237. direction = targetMap.Position - ourMap.Position;
  238. ResetStuck(steering, ourCoordinates);
  239. }
  240. else
  241. {
  242. needsPath = true;
  243. }
  244. }
  245. // Stuck detection
  246. // Check if we have moved further than the movespeed * stuck time.
  247. else if (AntiStuck &&
  248. ourCoordinates.TryDistance(EntityManager, steering.LastStuckCoordinates, out var stuckDistance) &&
  249. stuckDistance < NPCSteeringComponent.StuckDistance)
  250. {
  251. var stuckTime = _timing.CurTime - steering.LastStuckTime;
  252. // Either 1 second or how long it takes to move the stuck distance + buffer if we're REALLY slow.
  253. var maxStuckTime = Math.Max(1, NPCSteeringComponent.StuckDistance / moveSpeed * 1.2f);
  254. if (stuckTime.TotalSeconds > maxStuckTime)
  255. {
  256. // TODO: Blacklist nodes (pathfinder factor wehn)
  257. // TODO: This should be a warning but
  258. // A) NPCs get stuck on non-anchored static bodies still (e.g. closets)
  259. // B) NPCs still try to move in locked containers (e.g. cow, hamster)
  260. // and I don't want to spam grafana even harder than it gets spammed rn.
  261. Log.Debug($"NPC {ToPrettyString(uid)} found stuck at {ourCoordinates}");
  262. needsPath = true;
  263. if (stuckTime.TotalSeconds > maxStuckTime * 3)
  264. {
  265. steering.Status = SteeringStatus.NoPath;
  266. return false;
  267. }
  268. }
  269. }
  270. else
  271. {
  272. ResetStuck(steering, ourCoordinates);
  273. }
  274. // If not in LOS and no path then get a new one fam.
  275. if ((!inLos && steering.ArriveOnLineOfSight && steering.CurrentPath.Count == 0) ||
  276. (!steering.ArriveOnLineOfSight && steering.CurrentPath.Count == 0))
  277. {
  278. needsPath = true;
  279. }
  280. // TODO: Probably need partial planning support i.e. patch from the last node to where the target moved to.
  281. CheckPath(uid, steering, xform, needsPath, targetDistance);
  282. // If we don't have a path yet then do nothing; this is to avoid stutter-stepping if it turns out there's no path
  283. // available but we assume there was.
  284. if (steering is { Pathfind: true, CurrentPath.Count: 0 })
  285. return true;
  286. if (moveSpeed == 0f || direction == Vector2.Zero)
  287. {
  288. steering.Status = SteeringStatus.NoPath;
  289. return false;
  290. }
  291. var input = direction.Normalized();
  292. var tickMovement = moveSpeed * frameTime;
  293. // We have the input in world terms but need to convert it back to what movercontroller is doing.
  294. input = offsetRot.RotateVec(input);
  295. var norm = input.Normalized();
  296. var weight = MapValue(direction.Length(), tickMovement * 0.5f, tickMovement * 0.75f);
  297. ApplySeek(interest, norm, weight);
  298. // Prefer our current direction
  299. if (weight > 0f && body.LinearVelocity.LengthSquared() > 0f)
  300. {
  301. const float sameDirectionWeight = 0.1f;
  302. norm = body.LinearVelocity.Normalized();
  303. ApplySeek(interest, norm, sameDirectionWeight);
  304. }
  305. return true;
  306. }
  307. private void ResetStuck(NPCSteeringComponent component, EntityCoordinates ourCoordinates)
  308. {
  309. component.LastStuckCoordinates = ourCoordinates;
  310. component.LastStuckTime = _timing.CurTime;
  311. }
  312. private void CheckPath(EntityUid uid, NPCSteeringComponent steering, TransformComponent xform, bool needsPath, float targetDistance)
  313. {
  314. if (!_pathfinding)
  315. {
  316. steering.CurrentPath.Clear();
  317. steering.PathfindToken?.Cancel();
  318. steering.PathfindToken = null;
  319. return;
  320. }
  321. if (!needsPath && steering.CurrentPath.Count > 0)
  322. {
  323. needsPath = steering.CurrentPath.Count > 0 && (steering.CurrentPath.Peek().Data.Flags & PathfindingBreadcrumbFlag.Invalid) != 0x0;
  324. // If the target has sufficiently moved.
  325. var lastNode = GetCoordinates(steering.CurrentPath.Last());
  326. if (lastNode.TryDistance(EntityManager, steering.Coordinates, out var lastDistance) &&
  327. lastDistance > steering.RepathRange)
  328. {
  329. needsPath = true;
  330. }
  331. }
  332. // Request the new path.
  333. if (needsPath)
  334. {
  335. RequestPath(uid, steering, xform, targetDistance);
  336. }
  337. }
  338. /// <summary>
  339. /// We may be pathfinding and moving at the same time in which case early nodes may be out of date.
  340. /// </summary>
  341. public void PrunePath(EntityUid uid, MapCoordinates mapCoordinates, Vector2 direction, List<PathPoly> nodes)
  342. {
  343. if (nodes.Count <= 1)
  344. return;
  345. // Work out if we're inside any nodes, then use the next one as the starting point.
  346. var index = 0;
  347. var found = false;
  348. for (var i = 0; i < nodes.Count; i++)
  349. {
  350. var node = nodes[i];
  351. var matrix = _transform.GetWorldMatrix(node.GraphUid);
  352. // Always want to prune the poly itself so we point to the next poly and don't backtrack.
  353. if (matrix.TransformBox(node.Box).Contains(mapCoordinates.Position))
  354. {
  355. index = i + 1;
  356. found = true;
  357. break;
  358. }
  359. }
  360. if (found)
  361. {
  362. nodes.RemoveRange(0, index);
  363. _pathfindingSystem.Simplify(nodes);
  364. return;
  365. }
  366. // Otherwise, take the node after the nearest node.
  367. // TODO: Really need layer support
  368. CollisionGroup mask = 0;
  369. if (TryComp<PhysicsComponent>(uid, out var physics))
  370. {
  371. mask = (CollisionGroup)physics.CollisionMask;
  372. }
  373. for (var i = 0; i < nodes.Count; i++)
  374. {
  375. var node = nodes[i];
  376. if (!node.Data.IsFreeSpace)
  377. break;
  378. var nodeMap = _transform.ToMapCoordinates(node.Coordinates);
  379. // If any nodes are 'behind us' relative to the target we'll prune them.
  380. // This isn't perfect but should fix most cases of stutter stepping.
  381. if (nodeMap.MapId == mapCoordinates.MapId &&
  382. Vector2.Dot(direction, nodeMap.Position - mapCoordinates.Position) < 0f)
  383. {
  384. nodes.RemoveAt(i);
  385. continue;
  386. }
  387. break;
  388. }
  389. _pathfindingSystem.Simplify(nodes);
  390. }
  391. /// <summary>
  392. /// Get the coordinates we should be heading towards.
  393. /// </summary>
  394. private EntityCoordinates GetTargetCoordinates(NPCSteeringComponent steering)
  395. {
  396. // Depending on what's going on we may return the target or a pathfind node.
  397. // Even if we're at the last node may not be able to head to target in case we get stuck on a corner or the likes.
  398. if (_pathfinding && steering.CurrentPath.Count >= 1 && steering.CurrentPath.TryPeek(out var nextTarget))
  399. {
  400. return GetCoordinates(nextTarget);
  401. }
  402. return steering.Coordinates;
  403. }
  404. /// <summary>
  405. /// Gets the fraction this value is between min and max
  406. /// </summary>
  407. /// <returns></returns>
  408. private float MapValue(float value, float minValue, float maxValue)
  409. {
  410. if (maxValue > minValue)
  411. {
  412. var mapped = (value - minValue) / (maxValue - minValue);
  413. return Math.Clamp(mapped, 0f, 1f);
  414. }
  415. return value >= minValue ? 1f : 0f;
  416. }
  417. #endregion
  418. #region Static Avoidance
  419. /// <summary>
  420. /// Tries to avoid static blockers such as walls.
  421. /// </summary>
  422. private void CollisionAvoidance(
  423. EntityUid uid,
  424. Angle offsetRot,
  425. Vector2 worldPos,
  426. float agentRadius,
  427. int layer,
  428. int mask,
  429. TransformComponent xform,
  430. Span<float> danger)
  431. {
  432. var objectRadius = 0.25f;
  433. var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius);
  434. var ents = _entSetPool.Get();
  435. _lookup.GetEntitiesInRange(uid, detectionRadius, ents, LookupFlags.Dynamic | LookupFlags.Static);
  436. foreach (var ent in ents)
  437. {
  438. // TODO: If we can access the door or smth.
  439. if (!_physicsQuery.TryGetComponent(ent, out var otherBody) ||
  440. !otherBody.Hard ||
  441. !otherBody.CanCollide ||
  442. otherBody.BodyType == BodyType.KinematicController ||
  443. (mask & otherBody.CollisionLayer) == 0x0 &&
  444. (layer & otherBody.CollisionMask) == 0x0)
  445. {
  446. continue;
  447. }
  448. var xformB = _xformQuery.GetComponent(ent);
  449. if (!_physics.TryGetNearest(uid, ent,
  450. out var pointA, out var pointB, out var distance,
  451. xform, xformB))
  452. {
  453. continue;
  454. }
  455. if (distance > detectionRadius)
  456. continue;
  457. var weight = 1f;
  458. var obstacleDirection = pointB - pointA;
  459. // Inside each other so just use worldPos
  460. if (distance == 0f)
  461. {
  462. obstacleDirection = _transform.GetWorldPosition(xformB) - worldPos;
  463. }
  464. else
  465. {
  466. weight = (detectionRadius - distance) / detectionRadius;
  467. }
  468. if (obstacleDirection == Vector2.Zero)
  469. continue;
  470. obstacleDirection = offsetRot.RotateVec(obstacleDirection);
  471. var norm = obstacleDirection.Normalized();
  472. for (var i = 0; i < InterestDirections; i++)
  473. {
  474. var dot = Vector2.Dot(norm, Directions[i]);
  475. danger[i] = MathF.Max(dot * weight, danger[i]);
  476. }
  477. }
  478. _entSetPool.Return(ents);
  479. }
  480. #endregion
  481. #region Dynamic Avoidance
  482. /// <summary>
  483. /// Tries to avoid mobs of the same faction.
  484. /// </summary>
  485. private void Separation(
  486. EntityUid uid,
  487. Angle offsetRot,
  488. Vector2 worldPos,
  489. float agentRadius,
  490. int layer,
  491. int mask,
  492. PhysicsComponent body,
  493. TransformComponent xform,
  494. Span<float> danger)
  495. {
  496. var objectRadius = 0.25f;
  497. var detectionRadius = MathF.Max(0.35f, agentRadius + objectRadius);
  498. var ourVelocity = body.LinearVelocity;
  499. _factionQuery.TryGetComponent(uid, out var ourFaction);
  500. var ents = _entSetPool.Get();
  501. _lookup.GetEntitiesInRange(uid, detectionRadius, ents, LookupFlags.Dynamic);
  502. foreach (var ent in ents)
  503. {
  504. // TODO: If we can access the door or smth.
  505. if (!_physicsQuery.TryGetComponent(ent, out var otherBody) ||
  506. !otherBody.Hard ||
  507. !otherBody.CanCollide ||
  508. (mask & otherBody.CollisionLayer) == 0x0 &&
  509. (layer & otherBody.CollisionMask) == 0x0 ||
  510. !_factionQuery.TryGetComponent(ent, out var otherFaction) ||
  511. !_npcFaction.IsEntityFriendly((uid, ourFaction), (ent, otherFaction)) ||
  512. // Use <= 0 so we ignore stationary friends in case.
  513. Vector2.Dot(otherBody.LinearVelocity, ourVelocity) <= 0f)
  514. {
  515. continue;
  516. }
  517. var xformB = _xformQuery.GetComponent(ent);
  518. if (!_physics.TryGetNearest(uid, ent, out var pointA, out var pointB, out var distance, xform, xformB))
  519. {
  520. continue;
  521. }
  522. if (distance > detectionRadius)
  523. continue;
  524. var weight = 1f;
  525. var obstacleDirection = pointB - pointA;
  526. // Inside each other so just use worldPos
  527. if (distance == 0f)
  528. {
  529. obstacleDirection = _transform.GetWorldPosition(xformB) - worldPos;
  530. // Welp
  531. if (obstacleDirection == Vector2.Zero)
  532. {
  533. obstacleDirection = _random.NextAngle().ToVec();
  534. }
  535. }
  536. else
  537. {
  538. weight = distance / detectionRadius;
  539. }
  540. obstacleDirection = offsetRot.RotateVec(obstacleDirection);
  541. var norm = obstacleDirection.Normalized();
  542. weight *= 0.25f;
  543. for (var i = 0; i < InterestDirections; i++)
  544. {
  545. var dot = Vector2.Dot(norm, Directions[i]);
  546. danger[i] = MathF.Max(dot * weight, danger[i]);
  547. }
  548. }
  549. _entSetPool.Return(ents);
  550. }
  551. #endregion
  552. // TODO: Alignment
  553. // TODO: Cohesion
  554. private void Blend(NPCSteeringComponent steering, float frameTime, Span<float> interest, Span<float> danger)
  555. {
  556. /*
  557. * Future sloth notes:
  558. * Pathfinder cleanup:
  559. - Cleanup whatever the fuck is happening in pathfinder
  560. - Use Flee for melee behavior / actions and get the seek direction from that rather than bulldozing
  561. - Must always have a path
  562. - Path should return the full version + the snipped version
  563. - Pathfinder needs to do diagonals
  564. - Next node is either <current node + 1> or <nearest node + 1> (on the full path)
  565. - If greater than <1.5m distance> repath
  566. */
  567. // IDK why I didn't do this sooner but blending is a lot better than lastdir for fixing stuttering.
  568. const float BlendWeight = 10f;
  569. var blendValue = Math.Min(1f, frameTime * BlendWeight);
  570. for (var i = 0; i < InterestDirections; i++)
  571. {
  572. var currentInterest = interest[i];
  573. var lastInterest = steering.Interest[i];
  574. var interestDiff = (currentInterest - lastInterest) * blendValue;
  575. steering.Interest[i] = lastInterest + interestDiff;
  576. var currentDanger = danger[i];
  577. var lastDanger = steering.Danger[i];
  578. var dangerDiff = (currentDanger - lastDanger) * blendValue;
  579. steering.Danger[i] = lastDanger + dangerDiff;
  580. }
  581. }
  582. }