TeleportVolume.cs

A component for a trigger volume that teleports GameObjects that enter it to a specified Destination. It makes colliders into triggers, resolves the touched body (rigidbody root by default), optionally preserves relative offset and velocity, invokes an OnTriggered callback, and sets the object's transform to the destination.

Networking
using Sandbox;
using System;

namespace LegacyEntityPack;

/// <summary>
/// A trigger volume that teleports things that touch it. Modern scene-system port of the
/// legacy <c>trigger_teleport</c> entity.
///
/// Put this on a GameObject that has a Collider - it is turned into a trigger automatically.
/// The legacy targetname lookup is replaced by a direct <see cref="Destination"/> reference.
/// </summary>
[Title( "Teleport Volume" )]
[Category( "Triggers" )]
[Icon( "auto_fix_normal" )]
public class TeleportVolume : Component, Component.ITriggerListener
{
	/// <summary>Where touching objects get teleported to.</summary>
	[Property] public GameObject Destination { get; set; }

	/// <summary>
	/// Teleport with an offset based on where the object was inside the trigger (think world
	/// portals). Place the destination accordingly.
	/// </summary>
	[Property] public bool TeleportRelative { get; set; }

	/// <summary>If set, the teleported object keeps its velocity instead of being reset to zero.</summary>
	[Property] public bool KeepVelocity { get; set; }

	/// <summary>Fired with the teleported object, just before it is moved.</summary>
	[Property] public Action<GameObject> OnTriggered { get; set; }

	protected override void OnAwake()
	{
		foreach ( var collider in Components.GetAll<Collider>( FindMode.EverythingInSelfAndDescendants ) )
			collider.IsTrigger = true;
	}

	public void OnTriggerEnter( Collider other )
	{
		if ( !Destination.IsValid() || !other.IsValid() )
			return;

		var body = ResolveBody( other );
		if ( body is null )
			return;

		if ( Networking.IsActive && body.Network.IsProxy )
			return;

		var offset = TeleportRelative ? body.WorldPosition - WorldPosition : Vector3.Zero;

		if ( !KeepVelocity )
			ClearVelocity( body );

		// Fire before moving, so hooked logic can react (e.g. disable the destination's
		// own teleport trigger) before this object lands in it.
		OnTriggered?.Invoke( body );

		TeleportBody( body, Destination.WorldTransform.WithScale( body.WorldScale ), offset );
	}

	/// <summary>Resolve which object to teleport from the touching collider. Defaults to the rigidbody root.</summary>
	protected virtual GameObject ResolveBody( Collider other )
		=> other.Rigidbody.IsValid() ? other.Rigidbody.GameObject : other.GameObject;

	/// <summary>Zero out the body's velocity. Override for custom controllers.</summary>
	protected virtual void ClearVelocity( GameObject body )
	{
		var rb = body.Components.Get<Rigidbody>();
		if ( rb.IsValid() )
			rb.Velocity = Vector3.Zero;

		var controller = body.Components.Get<CharacterController>();
		if ( controller.IsValid() )
			controller.Velocity = Vector3.Zero;
	}

	/// <summary>Move the body to the destination. Override for custom controllers that need extra state updates.</summary>
	protected virtual void TeleportBody( GameObject body, Transform destination, Vector3 offset )
	{
		body.WorldTransform = destination;
		body.WorldPosition += offset;
	}
}