terminalCode Example

Sandbox: BaseBulletWeapon — hitscan shooting with BulletConfiguration, recoil, and IronSightsWeapon

calendar_today May 4, 2026 schedule ~1 min read person patrickjr verified 50

BaseBulletWeapon — Hitscan Shooting with Recoil and Camera Noise

BaseBulletWeapon extends BaseWeapon and implements hitscan bullet firing with configurable aim cone, recoil, and camera noise. IronSightsWeapon extends it further with ADS (aim down sights) support.

BulletConfiguration

CSHARP
public struct BulletConfiguration
{
    public float Damage { get; set; }
    public float BulletRadius { get; set; }       // Sphere trace radius
    public Vector2 AimConeBase { get; set; }       // (horizontal, vertical) degrees when hip-firing
    public Vector2 AimConeSpread { get; set; }     // Additional spread per shot (resets over time)
    public Vector2 RecoilPitch { get; set; }       // Random pitch recoil range
    public Vector2 RecoilYaw { get; set; }         // Random yaw recoil range
    public float CameraRecoilStrength { get; set; }
    public float CameraRecoilFrequency { get; set; }
    public float Range { get; set; }               // Max trace distance
}

ShootBullet

CSHARP
protected void ShootBullet( float fireRate, in BulletConfiguration config )
{
    if ( HasOwner && ( !HasAmmo() || IsReloading() ) )
    {
        TryAutoReload();
        return;
    }

    if ( TimeUntilNextShotAllowed > 0 ) return;

    AddShootDelay( fireRate );
    ConsumeAmmo();

    // Build aim direction with cone spread
    var dir = AimRay.Forward.WithAimCone( config.AimConeBase.x );

    // Sphere trace from eye
    var tr = Scene.Trace.Ray( AimRay.Position, AimRay.Position + dir * config.Range )
        .IgnoreGameObjectHierarchy( AimIgnoreRoot )
        .WithoutTags( "playercontroller" )
        .Radius( config.BulletRadius )
        .UseHitboxes()
        .Run();

    ShootEffects( tr.EndPosition, tr.Hit, tr.Normal, tr.GameObject, tr.Surface );
    TraceAttack( TraceAttackInfo.From( tr, config.Damage ) );
    TimeSinceShoot = 0;

    // Recoil — only when held by a player
    if ( !HasOwner )
    {
        // Standalone: apply physical force to the weapon's rigidbody
        if ( ShootForce > 0f && GetComponent<Rigidbody>( true ) is var rb )
        {
            var muzzle = WeaponModel?.MuzzleTransform?.WorldTransform ?? WorldTransform;
            rb.ApplyForce( muzzle.Rotation.Up * ShootForce );
        }
        return;
    }

    // Player recoil: nudge eye angles
    Owner.Controller.EyeAngles += new Angles(
        Random.Shared.Float( config.RecoilPitch.x, config.RecoilPitch.y ),
        Random.Shared.Float( config.RecoilYaw.x, config.RecoilYaw.y ),
        0
    );

    // Camera noise (first-person only)
    if ( !Owner.Controller.ThirdPerson && Owner.IsLocalPlayer )
    {
        _ = new Sandbox.CameraNoise.Recoil( config.CameraRecoilStrength, config.CameraRecoilFrequency );
    }
}

IronSightsWeapon — ADS

CSHARP
public abstract class IronSightsWeapon : BaseBulletWeapon
{
    [Property] public float IronSightsFireScale { get; set; } = 0.2f; // Reduces aim cone when ADS

    private bool _isAiming;
    public bool IsAiming => _isAiming;

    public override bool CanSecondaryAttack() => false; // Secondary = ADS, not attack

    public override void OnControl( Player player )
    {
        base.OnControl( player );

        var wantsAim = Input.Down( "attack2" );
        if ( wantsAim == _isAiming ) return;

        _isAiming = wantsAim;
        ViewModel?.RunEvent<ViewModel>( x =>
        {
            x.Renderer?.Set( "ironsights", _isAiming ? 1 : 0 );
            x.Renderer?.Set( "ironsights_fire_scale", _isAiming ? IronSightsFireScale : 1f );
        } );
    }

    protected BulletConfiguration GetBullet()
    {
        if ( !_isAiming ) return Bullet;

        var config = Bullet;
        config.AimConeBase *= IronSightsFireScale;
        config.AimConeSpread *= IronSightsFireScale;
        return config;
    }
}

Key points

Was this helpful?