Skip to content

Commit 623586e

Browse files
committed
Add aimpunch camera recoil system (issue #153)
- Implements Source engine-style aimpunch with automatic recovery - Camera kicks when shooting, smoothly recovers when stopped - Per-weapon configuration via UseAimPunch and AimPunchRecoverySpeed - Resets on weapon switch to prevent carry-over - Backward compatible (defaults to legacy instant recoil)
1 parent 59629aa commit 623586e

6 files changed

Lines changed: 111 additions & 3 deletions

File tree

code/swb_base/Weapon.Shoot.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,18 @@ public virtual void Shoot( ShootInfo shootInfo, bool isPrimary )
101101
// Barrel smoke
102102
barrelHeat += 1;
103103

104-
// Recoil
105-
Owner.ApplyEyeAnglesOffset( GetRecoilAngles( shootInfo ) );
104+
// Recoil / AimPunch
105+
var recoilAngles = GetRecoilAngles( shootInfo );
106+
if ( shootInfo.UseAimPunch )
107+
{
108+
// Apply aimpunch (camera recoil with recovery)
109+
Owner.ApplyAimPunch( recoilAngles, shootInfo.AimPunchRecoverySpeed );
110+
}
111+
else
112+
{
113+
// Apply instant recoil (legacy behavior)
114+
Owner.ApplyEyeAnglesOffset( recoilAngles );
115+
}
106116

107117
// Screenshake
108118
if ( shootInfo.ScreenShake is not null )

code/swb_base/structures/ShootInfo.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ public class ShootInfo : Component
6767
/// <summary>Weapon recoil</summary>
6868
[Property, Group( "Bullets" )] public float Recoil { get; set; } = 0.1f;
6969

70+
/// <summary>Enable aimpunch (camera recoil with recovery)</summary>
71+
[Property, Group( "Bullets" )] public bool UseAimPunch { get; set; } = false;
72+
73+
/// <summary>Aimpunch recovery speed (higher = faster recovery)</summary>
74+
[Property, Group( "Bullets" )] public float AimPunchRecoverySpeed { get; set; } = 5f;
75+
7076
/// <summary>Rate Per Minute, firing speed (higher is faster)</summary>
7177
[Property, Group( "Bullets" )] public int RPM { get; set; } = 200;
7278

code/swb_player/Inventory.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public void SetActive( GameObject gameObject )
5656
oldActive.OnCarryStop();
5757
}
5858

59+
// Reset aimpunch when switching weapons
60+
player?.ResetAimPunch();
61+
5962
if ( gameObject.Components.TryGet<IInventoryItem>( out var newActive, FindMode.EverythingInSelf ) )
6063
{
6164
newActive.OnCarryStart();
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
namespace SWB.Player;
2+
3+
public partial class PlayerBase
4+
{
5+
Angles aimPunchAngle;
6+
float aimPunchRecoverySpeed = 5f;
7+
RealTimeSince timeSinceLastPunch;
8+
9+
/// <summary>
10+
/// Apply aimpunch recoil to the camera
11+
/// </summary>
12+
/// <param name="punchAmount">The angular offset to apply</param>
13+
/// <param name="recoverySpeed">How fast the camera recovers (higher = faster)</param>
14+
public virtual void ApplyAimPunch( Angles punchAmount, float recoverySpeed = 5f )
15+
{
16+
// Accumulate the punch angle
17+
aimPunchAngle += punchAmount;
18+
aimPunchRecoverySpeed = recoverySpeed;
19+
timeSinceLastPunch = 0;
20+
21+
// Apply the kick to the camera immediately
22+
ApplyEyeAnglesOffset( punchAmount );
23+
}
24+
25+
/// <summary>
26+
/// Reset aimpunch angle (called on weapon switch, death, etc.)
27+
/// </summary>
28+
public virtual void ResetAimPunch()
29+
{
30+
aimPunchAngle = Angles.Zero;
31+
}
32+
33+
/// <summary>
34+
/// Handle aimpunch recovery over time
35+
/// </summary>
36+
public virtual void HandleAimPunch()
37+
{
38+
if ( !IsAlive )
39+
{
40+
aimPunchAngle = Angles.Zero;
41+
return;
42+
}
43+
44+
// Get the active weapon to check if player is shooting
45+
var activeWeapon = Inventory?.Active?.GetComponent<SWB.Base.Weapon>();
46+
bool isShooting = activeWeapon?.IsShooting() ?? false;
47+
48+
// Only recover when NOT shooting (with small delay after last shot)
49+
if ( isShooting || timeSinceLastPunch < 0.1f )
50+
return;
51+
52+
// Recover the punch angle back toward zero
53+
if ( aimPunchAngle.pitch != 0 || aimPunchAngle.yaw != 0 || aimPunchAngle.roll != 0 )
54+
{
55+
var decayAmount = aimPunchRecoverySpeed * Time.Delta;
56+
57+
// Store old angle to calculate what changed
58+
var oldPitch = aimPunchAngle.pitch;
59+
var oldYaw = aimPunchAngle.yaw;
60+
var oldRoll = aimPunchAngle.roll;
61+
62+
// Decay toward zero
63+
aimPunchAngle = new Angles(
64+
aimPunchAngle.pitch.Approach( 0, decayAmount ),
65+
aimPunchAngle.yaw.Approach( 0, decayAmount ),
66+
aimPunchAngle.roll.Approach( 0, decayAmount )
67+
);
68+
69+
// Calculate the change (this will be negative, pulling camera back)
70+
var deltaAngle = new Angles(
71+
aimPunchAngle.pitch - oldPitch,
72+
aimPunchAngle.yaw - oldYaw,
73+
aimPunchAngle.roll - oldRoll
74+
);
75+
76+
// Apply the recovery movement
77+
ApplyEyeAnglesOffset( deltaAngle );
78+
}
79+
}
80+
}

code/swb_player/PlayerBase.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ protected override void OnUpdate()
224224
ViewModelCamera.Enabled = IsFirstPerson && IsAlive;
225225
HandleFlinch();
226226
HandleScreenShake();
227+
HandleAimPunch();
227228
}
228229

229230
if ( IsAlive )
@@ -259,7 +260,7 @@ public void TriggerAnimation( Animations animation )
259260

260261
public void ApplyEyeAnglesOffset( Angles offset )
261262
{
262-
CameraMovement?.EyeAnglesOffset += offset;
263+
CameraMovement.EyeAnglesOffset += offset;
263264
}
264265

265266
public void ParentToBone( GameObject weaponObject, string boneName )

code/swb_shared/IPlayerBase.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ public interface IPlayerBase : IValid, Sandbox.Component.IDamageable
115115
/// <param name="offset">The suggested angular offset to apply</param>
116116
public void ApplyEyeAnglesOffset( Angles offset );
117117

118+
/// <summary>
119+
/// Apply aimpunch (camera recoil with recovery) to the player's view.
120+
/// Unlike ApplyEyeAnglesOffset, this will smoothly recover over time.
121+
/// </summary>
122+
/// <param name="punchAmount">The angular offset to apply</param>
123+
/// <param name="recoverySpeed">How fast the camera recovers (higher = faster)</param>
124+
public void ApplyAimPunch( Angles punchAmount, float recoverySpeed = 5f );
125+
118126
/// <summary>
119127
/// Called when the weapon object should be attached/parented to the player's body
120128
/// </summary>

0 commit comments

Comments
 (0)