Files
sbox-public/engine/Sandbox.Engine/Scene/Components/Game/PlayerController/PlayerController.Pressing.cs
Garry Newman ca96c2a945 Make Inventory, BaseWeapon, AmmoResource, Screenshake first party (#5245)
https://sbox.game/dev/doc/gameplay/inventory-weapons/

Added

InventoryComponent - a slot based inventory, hotbar or shared bucket slots, with a starting loadout
BaseInventoryItem - base class for inventory items, with hooks to veto switching, holstering and dropping
BaseWeapon - a GMod style base weapon - fire modes, magazines and reserve ammo, reloading, dry fire, ballistics, spread, crosshair and HUD
BaseWeaponModel - view/world model base with muzzle flash, brass ejection, tracers and animation hooks
Ammo types are resources (.ammo) - weapons share reserve pools by type, clamped to a max reserve
BaseWeapon.ShootBullets / ShootBullet - bullet tracing with damage, impulse and hitbox tags
Weapon hold types editor
Animgraph enum parameters can be set by option name - Renderer.Set( "holdtype", "pistol" )
NpcUsage on BaseWeapon - engagement range and burst behaviour so NPCs can use any weapon
ICameraModifier - cameras compose their view through an ordered modifier chain, read the result from CameraComponent.View
CameraEffectSystem - screen shake, directional punches and tilts, with world epicenter falloff - camera.AddShake / AddPunch / AddTilt
EnvShake component - shakes the view, rumbles controllers, kicks physics
TracerEffect - an animated tracer line, ITargetedEffect for effects that fly at a target
SceneAnchor - a world position that can follow an object or bone
SkinnedModelRenderer.GetClosestBone( worldPosition )
Vector3.WithAimCone can take a custom Random for reproducible spread
Improve/Fix

PlayerController searches parents for IPressable when pressing
PlayerController camera runs through the modifier chain - IEvents.PostCameraSetup is obsolete
SoundPlayer preview uses shortcuts - Space toggles playback, Ctrl+Space restarts
Fixed Radius( 0 ) traces missing everything
Fixed NRE in AssetPreviewWidget
Removed

Removed CameraComponent.ISceneCameraSetup (obsolete since October, nothing uses)
2026-07-04 12:46:59 +01:00

268 lines
6.5 KiB
C#

namespace Sandbox;
public sealed partial class PlayerController : Component
{
/// <summary>
/// The object we're currently looking at
/// </summary>
public Component Hovered { get; set; }
/// <summary>
/// The object we're currently using by holding down USE
/// </summary>
public Component Pressed { get; set; }
/// <summary>
/// The tooltip of the currently hovered Pressable object
/// </summary>
public List<IPressable.Tooltip> Tooltips { get; } = new List<IPressable.Tooltip>();
/// <summary>
/// Called in Update when Using is enabled
/// </summary>
public void UpdateLookAt()
{
Tooltips.Clear();
if ( !EnablePressing )
return;
if ( Pressed.IsValid() )
{
UpdatePressed();
return;
}
UpdateHovered();
}
/// <summary>
/// Called every frame to update our pressed object
/// </summary>
void UpdatePressed()
{
if ( string.IsNullOrWhiteSpace( UseButton ) )
return;
//DebugOverlay.Text( Pressed.WorldPosition, $"Using: {Pressed}" );
// keep pressing while use is down
bool keepPressing = Input.Down( UseButton );
// unless the IPressable says it wants to stop
if ( keepPressing && Pressed is IPressable p )
{
var @event = new IPressable.Event { Ray = EyeTransform.ForwardRay, Source = this };
if ( p.GetTooltip( @event ) is { } tt )
{
tt.Pressable = p;
Tooltips.Add( tt );
}
keepPressing = p.Pressing( @event );
}
if ( GetDistanceFromGameObject( Pressed.GameObject, EyePosition ) > ReachLength )
{
keepPressing = false;
}
if ( !keepPressing )
{
StopPressing();
}
}
float GetDistanceFromGameObject( GameObject obj, Vector3 point )
{
float distance = Vector3.DistanceBetween( obj.WorldPosition, EyePosition );
foreach ( var c in Pressed.GetComponentsInChildren<Collider>() )
{
var closestPoint = c.FindClosestPoint( EyePosition );
var dist = Vector3.DistanceBetween( closestPoint, EyePosition );
if ( dist < distance )
{
distance = dist;
}
}
return distance;
}
/// <summary>
/// Called every frame to update our hovered status, unless it's being pressed
/// </summary>
void UpdateHovered()
{
SwitchHovered( TryGetLookedAt() );
//DebugOverlay.Text( Hovered.WorldPosition, $"Looking: {Hovered}" );
// If it's pressable then send an update to the hovered component every frame
// This is to allow things like cursors to update their position
if ( Hovered is IPressable p )
{
var @event = new IPressable.Event { Ray = EyeTransform.ForwardRay, Source = this };
p.Look( @event );
}
// We are pressing "use", so press our hovered component
if ( Input.Pressed( UseButton ) )
{
StartPressing( Hovered );
}
}
/// <summary>
/// Stop pressing. Pressed will become null.
/// </summary>
public void StopPressing()
{
if ( !Pressed.IsValid() )
return;
IEvents.PostToGameObject( GameObject, x => x.StopPressing( Pressed ) );
if ( Pressed is IPressable pressable )
{
pressable.Release( new IPressable.Event { Ray = EyeTransform.ForwardRay, Source = this } );
}
Pressed = default;
}
/// <summary>
/// Start pressing a target component. This is called automatically when Use is pressed.
/// </summary>
public void StartPressing( Component obj )
{
StopPressing();
if ( !obj.IsValid() )
{
IEvents.PostToGameObject( GameObject, x => x.FailPressing() );
return;
}
var pressable = obj.GetComponent<IPressable>();
if ( pressable is not null )
{
if ( !pressable.CanPress( new IPressable.Event { Ray = EyeTransform.ForwardRay, Source = this } ) )
{
IEvents.PostToGameObject( GameObject, x => x.FailPressing() );
return;
}
pressable.Press( new IPressable.Event { Ray = EyeTransform.ForwardRay, Source = this } );
}
Pressed = obj;
IEvents.PostToGameObject( GameObject, x => x.StartPressing( obj ) );
}
/// <summary>
/// Called every frame with the component we're looking at - even if it's null
/// </summary>
void SwitchHovered( Component obj )
{
var ev = new IPressable.Event { Ray = EyeTransform.ForwardRay, Source = this };
// Didn't change
if ( Hovered == obj )
{
if ( Hovered is IPressable stillHovering )
{
stillHovering.Look( ev );
}
return;
}
// Stop hovering old one
if ( Hovered is IPressable stoppedHovering )
{
stoppedHovering.Blur( ev );
Hovered = default;
}
Hovered = obj;
// Start hovering new one
if ( Hovered is IPressable startedHovering )
{
startedHovering.Hover( ev );
startedHovering.Look( ev );
}
}
/// <summary>
/// Get the best component we're looking at. We don't just return any old component, by default
/// we only return components that implement IPressable. Components can implement GetUsableComponent
/// to search and provide better alternatives.
/// </summary>
Component TryGetLookedAt()
{
// Search with a ray first, and if that doesn't hit anything
// use a bigger sphere until it's 4inches wide. This means that
// when looking through holes we'll be able to use stuff, but also
// when trying to use smaller things it won't be so fiddly. This is
// what we did in Rust and it worked great.
for ( float f = 0.0f; f <= 4.0f; f += 2.0f )
{
var hits = Scene.Trace
.Ray( EyePosition, EyePosition + EyeAngles.Forward * (ReachLength - f) )
.IgnoreGameObjectHierarchy( GameObject )
.Radius( f )
.HitTriggers()
.RunAll();
foreach ( var hit in hits )
{
// Get the GameObject of the collider, not the physics body
var hitObject = hit.Collider?.GameObject ?? hit.GameObject;
if ( !hitObject.IsValid() )
continue;
// Allow our other components to provide something
Component foundComponent = default;
IEvents.PostToGameObject( GameObject, x => foundComponent = x.GetUsableComponent( hitObject ) ?? foundComponent );
if ( foundComponent.IsValid() )
return foundComponent;
// Check for IPressable components
foreach ( var c in hitObject.GetComponentsInParent<IPressable>( includeSelf: true ) )
{
var @event = new IPressable.Event { Ray = EyeTransform.ForwardRay, Source = this };
var canPress = c.CanPress( @event );
if ( c.GetTooltip( @event ) is { } tt )
{
tt.Enabled = tt.Enabled && canPress;
tt.Pressable = c;
Tooltips.Add( tt );
}
if ( !canPress )
continue;
return c as Component;
}
// If we hit a non-trigger and found nothing pressable, we should stop
// This prevents looking through solid objects
// WorldPhysics is not a standard Collider
if ( hit.Collider is null || !hit.Collider.IsTrigger )
break;
}
}
return default;
}
}