PlayerGrab.cs

A player component that allows grabbing, holding and rotating physics objects. It raycasts from the player camera to find objects with a specific tag, attaches their Rigidbody for SmoothMove-based positioning, and handles mouse-driven rotation while disabling look controls during rotation.

NetworkingFile Access
using System;
using Sandbox;

public class PlayerGrab : Component
{
	/// <summary>
	/// Input Action name for grabbing an object
	/// </summary>
    [Property] public string GrabActionName { get; set; } = "Attack1";
	/// <summary>
	/// Input Action name for rotating a held object
	/// </summary>
	[Property] public string RotationActionName { get; set; } = "reload";
	/// <summary>
	/// Interact tag to search gameobjects for
	/// </summary>
    [Property] public string TagName { get; set; } = "grab";
	/// <summary>
	/// Length of the ray for raycast
	/// </summary>
    [Property] public float RayLength { get; set; } = 125.0f;
	/// <summary>
	/// Base rotation speed factor
	/// </summary>
    [Property, MinMax(0f, 1.0f)] public float RotationSpeedFactor { get; set; } = 0.5f;
	/// <summary>
	/// Minimum mass factor for rotating a held object
	/// </summary>
    [Property] public float MinRotationMassFactor { get; set; } = 0.1f;
    public event Action<GameObject> OnObjectGrab;
    public event Action<GameObject> OnObjectRelease;
    public event Action<GameObject> OnObjectRotate;
    public bool IsHoldingObject;
    private GameObject _grabbedObject;
    private Rigidbody _grabbedBody;
    private CameraComponent _camera;
    private PlayerController _playerController;
    private SceneTraceResult _trace;
    private Rotation _targetRotation;
    private bool _isRotating;
    private float _grabDistance;

    protected override void OnStart()
    {
        base.OnStart();

        _playerController = GetComponentInChildren<PlayerController>();
        if ( _playerController == null )
        {
			Log.Error( "Failed to get PlayerController" );
			return;
        }

        _camera = _playerController.UseCameraControls ? Scene.Camera : GetComponentInChildren<CameraComponent>();
        if ( _camera == null )
        {
            Log.Error( "Failed to get CameraComponent" );
            return;
        }

    }

	protected override void OnUpdate()
    {
        if ( StartedGrab() )
        {
            _trace = CastRay();

            if ( _trace.Hit && _trace.GameObject.Tags.Has( TagName ) )
            {
                GrabObject( _trace.GameObject );
            }
        }

        if ( ReleasedGrab() )
        {
            ReleaseObject();
        }

        _isRotating = Input.Down( RotationActionName );

        if ( IsHoldingObject && _isRotating )
        {
	        _playerController.UseLookControls = false;

			float mass = _grabbedBody.Mass.Clamp( 1f, 50f );
			float massFactor = MathF.Max( 1.0f / MathF.Sqrt( mass ), MinRotationMassFactor );
            float yaw = Input.MouseDelta.x * RotationSpeedFactor * massFactor;
            float pitch = Input.MouseDelta.y * RotationSpeedFactor * massFactor;
            Rotation camRotation = _camera.WorldRotation;

            _targetRotation = Rotation.FromAxis( camRotation.Up, yaw ) *
                              Rotation.FromAxis( camRotation.Right, pitch ) *
                              _targetRotation;

			OnObjectRotate?.Invoke( _grabbedObject );

        }

        if ( IsHoldingObject )
        {
            UpdateGrabbedObjectPosition();
        }

        if ( !_isRotating && !_playerController.UseLookControls )
        {
	        _playerController.UseLookControls = true;
        }
    }

    private bool StartedGrab() => Input.Pressed( GrabActionName ) && !IsHoldingObject;
    private bool ReleasedGrab() => Input.Released( GrabActionName ) && IsHoldingObject;

    private void GrabObject( GameObject gameObject )
    {
        if ( gameObject == null )
        {
            Log.Warning("[PlayerGrab] No object to grab");
            return;
        }

        var body = gameObject.Components.Get<Rigidbody>();
        if ( body == null )
        {
            Log.Warning($"[PlayerGrab] No Rigidbody on object");
            return;
        }

        _grabbedObject = gameObject;
        _grabbedBody = body;
        _grabDistance = Vector3.DistanceBetween(
            _camera.WorldPosition,
            _grabbedObject.WorldPosition
        );

        _targetRotation = _grabbedObject.WorldRotation;
        IsHoldingObject = true;

        OnObjectGrab?.Invoke( _grabbedObject );
    }

    private void ReleaseObject()
    {
        OnObjectRelease?.Invoke( _grabbedObject );

        IsHoldingObject = false;
        _grabbedObject = null;
        _grabbedBody = null;
        _trace = new SceneTraceResult();
    }

    private void UpdateGrabbedObjectPosition()
    {
        if ( _grabbedBody == null ) return;

        Vector3 targetPos =
            _camera.WorldPosition +
            _camera.WorldRotation.Forward * _grabDistance;

        float massFactor = _grabbedBody.Mass.Clamp( 1f, 50f );
        float arriveTime = 0.05f + (massFactor * 0.01f);
        Transform targetTransform = new Transform( targetPos, _targetRotation );

        _grabbedBody.SmoothMove(
            targetTransform,
            arriveTime,
            Time.Delta
        );
    }

    private SceneTraceResult CastRay()
    {
        Vector3 direction =
            (_camera.ScreenToWorld( Screen.Size * 0.5f ) - _camera.WorldPosition).Normal;

        Vector3 start = GameObject.WorldPosition + new Vector3( 0.0f, 0.0f, 64.0f );
        Vector3 end = start + direction * RayLength;

        return Scene.Trace.Ray( start, end )
            .IgnoreGameObjectHierarchy( GameObject )
            .UseHitboxes()
            .Run();
    }
}