VaultGate.cs
using Sandbox;
using System;
// A gate that opens once, on cue, by moving and/or rotating itself from
// where it was placed to an "open" pose over a few seconds.
//
// Put this on the gate GameObject (the one carrying its model and
// collider) and assign it to MissionTimer.VaultGate. The timer calls Open()
// when the robbery finishes.
//
// The open pose is RELATIVE to how the gate sits in the editor, so you
// only describe the movement, not a world position:
// - a portcullis sliding up: OpenOffset = 0,0,110 OpenRotation = 0,0,0
// - a gate swinging on a hinge: OpenOffset = 0,0,0 OpenRotation = 0,90,0
// (a swing pivots on the object's ORIGIN, so for a hinge the origin
// has to be on the hinge edge - parent the model under an empty
// GameObject placed on the hinge and put this on that parent)
// OpenOffset is in the gate's own local axes, so "up" stays up however the
// gate is rotated in the level.
//
// Every machine runs Open() itself - MissionTimer runs independently on
// each client and they all reach the same moment - so the gate is never
// networked. It's pure presentation plus a collider moving out of the way.
//
// COLLIDER NOTE. For the collider to move smoothly with the gate, give the
// gate a Rigidbody with MotionEnabled off (that makes it "keyframed": moved
// by code, pushes players, not knocked around by physics). A plain static
// collider will still end up in the right place, but the physics engine
// treats a moving static as a teleport every frame.
public sealed class VaultGate : Component
{
// How far the gate moves when open, in its own local axes.
[Property]
public Vector3 OpenOffset { get; set; } = Vector3.Up * 110f;
// How much the gate turns when open (pitch, yaw, roll).
[Property]
public Angles OpenRotation { get; set; } = Angles.Zero;
[Property]
public float OpenDuration { get; set; } = 3f;
public bool IsOpen { get; private set; }
Vector3 _closedPosition;
Rotation _closedRotation;
// 0 = closed, 1 = fully open.
float _progress;
protected override void OnStart()
{
// Remembered once, from where the level designer put it. Local
// rather than world so the gate still works if its parent (the
// bank) gets moved around in the editor.
_closedPosition = LocalPosition;
_closedRotation = LocalRotation;
}
public void Open()
{
if ( IsOpen )
return;
IsOpen = true;
Log.Info( $"VaultGate '{GameObject.Name}' opening." );
}
protected override void OnUpdate()
{
if ( !IsOpen || _progress >= 1f )
return;
_progress = MathF.Min( _progress + Time.Delta / MathF.Max( OpenDuration, 0.01f ), 1f );
// Eased so it starts heavy and settles, rather than moving at a
// constant, robotic speed. Standard smoothstep.
var t = _progress * _progress * (3f - 2f * _progress);
// The offset is in the gate's own frame, so rotate it by the
// closed rotation before adding it to the closed position.
LocalPosition = _closedPosition + _closedRotation * (OpenOffset * t);
LocalRotation = _closedRotation * Rotation.From( OpenRotation * t );
}
}