1
0

SharedCameraRecoilSystem.cs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. using System.Numerics;
  2. using Content.Shared.Movement.Components;
  3. using Content.Shared.Movement.Systems;
  4. using JetBrains.Annotations;
  5. using Robust.Shared.Network;
  6. using Robust.Shared.Maths;
  7. using Robust.Shared.Serialization;
  8. namespace Content.Shared.Camera;
  9. [UsedImplicitly]
  10. public abstract class SharedCameraRecoilSystem : EntitySystem
  11. {
  12. /// <summary>
  13. /// Maximum rate of magnitude restore towards 0 kick.
  14. /// </summary>
  15. private const float RestoreRateMax = 30f;
  16. /// <summary>
  17. /// Minimum rate of magnitude restore towards 0 kick.
  18. /// </summary>
  19. private const float RestoreRateMin = 0.1f;
  20. /// <summary>
  21. /// Time in seconds since the last kick that lerps RestoreRateMin and RestoreRateMax
  22. /// </summary>
  23. private const float RestoreRateRamp = 4f;
  24. /// <summary>
  25. /// The maximum magnitude of the kick applied to the camera at any point.
  26. /// </summary>
  27. protected const float KickMagnitudeMax = 1f;
  28. [Dependency] private readonly SharedContentEyeSystem _eye = default!;
  29. [Dependency] private readonly INetManager _net = default!;
  30. public override void Initialize()
  31. {
  32. SubscribeLocalEvent<CameraRecoilComponent, GetEyeOffsetEvent>(OnCameraRecoilGetEyeOffset);
  33. }
  34. private void OnCameraRecoilGetEyeOffset(Entity<CameraRecoilComponent> ent, ref GetEyeOffsetEvent args)
  35. {
  36. args.Offset += ent.Comp.BaseOffset + ent.Comp.CurrentKick;
  37. }
  38. /// <summary>
  39. /// Applies explosion/recoil/etc kickback to the view of the entity.
  40. /// </summary>
  41. /// <remarks>
  42. /// If the entity is missing <see cref="CameraRecoilComponent" /> and/or <see cref="EyeComponent" />,
  43. /// this call will have no effect. It is safe to call this function on any entity.
  44. /// </remarks>
  45. public abstract void KickCamera(EntityUid euid, Vector2 kickback, CameraRecoilComponent? component = null);
  46. private void UpdateEyes(float frameTime)
  47. {
  48. var query = AllEntityQuery<CameraRecoilComponent, EyeComponent>();
  49. while (query.MoveNext(out var uid, out var recoil, out var eye))
  50. {
  51. // Check if CurrentKick is invalid (NaN or Infinite) and reset if necessary.
  52. // This prevents downstream errors, especially with Math.Sign.
  53. if (!float.IsFinite(recoil.CurrentKick.X) || !float.IsFinite(recoil.CurrentKick.Y))
  54. {
  55. // Log error and reset
  56. Log.Error($"Camera recoil CurrentKick is invalid for entity {ToPrettyString(uid)}. Resetting kick. Value: {recoil.CurrentKick}");
  57. recoil.CurrentKick = Vector2.Zero;
  58. // Allow logic to continue to potentially update eye if LastKick wasn't zero
  59. }
  60. var magnitude = recoil.CurrentKick.Length();
  61. if (magnitude <= 0.005f)
  62. {
  63. // If it's already zero, skip updates. Otherwise, set it to zero.
  64. if (recoil.CurrentKick == Vector2.Zero)
  65. continue;
  66. recoil.CurrentKick = Vector2.Zero;
  67. }
  68. else // Continually restore camera to 0.
  69. {
  70. var normalized = recoil.CurrentKick.Normalized();
  71. recoil.LastKickTime += frameTime;
  72. var restoreRate = MathHelper.Lerp(RestoreRateMin, RestoreRateMax, Math.Min(1, recoil.LastKickTime / RestoreRateRamp));
  73. var restore = normalized * restoreRate * frameTime;
  74. // Sanity check: ensure restore vector is finite.
  75. if (!float.IsFinite(restore.X) || !float.IsFinite(restore.Y))
  76. {
  77. Log.Error($"Camera recoil restore vector is invalid for entity {ToPrettyString(uid)}. Resetting kick. Restore: {restore}, Normalized: {normalized}, Rate: {restoreRate}, FrameTime: {frameTime}");
  78. recoil.CurrentKick = Vector2.Zero;
  79. }
  80. else
  81. {
  82. var newKick = recoil.CurrentKick - restore;
  83. var (x, y) = newKick;
  84. // Check if the sign flipped (meaning we overshot zero)
  85. if (Math.Sign(x) != Math.Sign(recoil.CurrentKick.X))
  86. x = 0;
  87. if (Math.Sign(y) != Math.Sign(recoil.CurrentKick.Y))
  88. y = 0;
  89. recoil.CurrentKick = new Vector2(x, y);
  90. }
  91. }
  92. if (recoil.CurrentKick == recoil.LastKick)
  93. continue;
  94. recoil.LastKick = recoil.CurrentKick;
  95. _eye.UpdateEyeOffset((uid, eye));
  96. }
  97. }
  98. public override void Update(float frameTime)
  99. {
  100. if (_net.IsServer)
  101. UpdateEyes(frameTime);
  102. }
  103. public override void FrameUpdate(float frameTime)
  104. {
  105. UpdateEyes(frameTime);
  106. }
  107. }
  108. [Serializable]
  109. [NetSerializable]
  110. public sealed class CameraKickEvent : EntityEventArgs
  111. {
  112. public readonly NetEntity NetEntity;
  113. public readonly Vector2 Recoil;
  114. public CameraKickEvent(NetEntity netEntity, Vector2 recoil)
  115. {
  116. Recoil = recoil;
  117. NetEntity = netEntity;
  118. }
  119. }