terminalCode Example
Sandbox: BaseCarryable — unified AimRay, WeaponModel resolution, and seat-aware damage
BaseCarryable — Unified Weapon Base with AimRay and Seat Support
BaseCarryable is the root class for all holdable items. It handles viewmodel/worldmodel management, a unified AimRay, muzzle point resolution, and damage attribution that works whether the weapon is held by a player, controlled from a vehicle seat, or placed standalone in the world.CSHARP
public partial class BaseCarryable : Component, IKillIcon
{
[Property, Feature( "Inventory" )] public string DisplayName { get; set; }
[Property, Feature( "Inventory" )] public Texture DisplayIcon { get; set; }
[Property, Feature( "Inventory" )] public GameObject ItemPrefab { get; set; } // Dropped pickup prefab
[Property, Feature( "Inventory" )] public int Value { get; set; } = 0; // Auto-switch priority
public GameObject ViewModel { get; protected set; }
public GameObject WorldModel { get; protected set; }
// Optional explicit muzzle — falls back to WeaponModel.MuzzleTransform
[Property] public GameObject MuzzleGameObject { get; set; }
// The owner player, or null if standalone/seated
public Player Owner => GetComponentInParent<Player>( true );
public bool HasOwner => Owner.IsValid();
}Unified AimRay
CSHARP
public Ray AimRay
{
get
{
if ( HasOwner )
{
var owner = Owner;
// Third-person: use scene camera direction
if ( owner.Controller.ThirdPerson && Scene.Camera.IsValid() )
return Scene.Camera.Transform.World.ForwardRay;
return owner.EyeTransform.ForwardRay;
}
// Seated: use camera direction if IsTargetedAim, else muzzle
var seated = ClientInput.Current;
if ( seated.IsValid() && IsTargetedAim && Scene.Camera.IsValid() )
return Scene.Camera.Transform.World.ForwardRay;
var muzzle = MuzzleTransform.WorldTransform;
return new Ray( muzzle.Position, muzzle.Rotation.Forward );
}
}
// Override in weapons that support player-directed aim from seats (e.g. RPG, Physgun)
public virtual bool IsTargetedAim => false;WeaponModel resolution
CSHARP
public WeaponModel WeaponModel
{
get
{
var go = ViewModel;
// If first-person is excluded (e.g. third-person mode), skip viewmodel
if ( Scene.Camera.RenderExcludeTags.Contains( "firstperson" ) ) go = default;
if ( !go.IsValid() ) go = WorldModel;
if ( !go.IsValid() ) go = GameObject;
var wm = go.GetComponentInChildren<WeaponModel>();
if ( wm.IsValid() ) return wm;
// Standalone weapons may have a WorldModel without the stored reference
return GameObject.GetComponentInChildren<WeaponModel>();
}
}Damage attribution
CSHARP
// Returns the correct attacker for damage: player > seated player > weapon itself
protected GameObject EffectiveAttacker
{
get
{
if ( HasOwner ) return Owner.GameObject;
var seatedPlayer = ClientInput.Current;
if ( seatedPlayer.IsValid() ) return seatedPlayer.GameObject;
var killSource = GetComponentInParent<IKillSource>( true );
if ( killSource is Component c ) return c.GameObject;
return GameObject;
}
}
// TraceAttack is an RPC to the host — sends hit info for server-side damage application
[Rpc.Host]
public void TraceAttack( TraceAttackInfo attack )
{
if ( !attack.Target.IsValid() ) return;
var damagable = attack.Target.GetComponentInChildren<IDamageable>();
if ( damagable is not null )
{
var info = new DamageInfo { Attacker = EffectiveAttacker, ... };
damagable.OnDamage( info );
}
// Apply physics impulse
if ( attack.Target.GetComponentInChildren<Rigidbody>() is var rb && rb.IsValid() )
rb.ApplyImpulseAt( attack.Position, Vector3.Direction( attack.Origin, attack.Position ) * rb.Mass * 100 );
}Key points
- AimIgnoreRoot returns Owner.GameObject when held — prevents the weapon from hitting its own player
- IsInUse() returns false by default — override to prevent auto-switching (e.g. while physgun is grabbing)
- ItemPrefab is the pickup prefab spawned when the weapon is dropped; DroppedWeapon component triggers a fresh prefab clone instead
- ShouldAvoid (from BaseWeapon) returns !HasAmmo() — inventory uses this to skip empty weapons in auto-switch
Was this helpful?