A visual effect component that renders a pulsing, wavy line between a tree and its owning player. It configures a LineRenderer, computes per-point positions with waves, flow and width pulses, and updates a color/alpha gradient each frame.
using System;
using System.Collections.Generic;
using Sandbox;
public sealed class PerkTreehuggerVfx : Component
{
private const int NUM_POINTS = 8;
private const float MIN_HEIGHT = 5f;
private const float TREE_HEIGHT_FACTOR = 0.25f;
private const float PLAYER_HEIGHT_FACTOR = 0.55f;
private const float WAVE_SPEED = 12f;
private const float WAVE_SPEED_2 = 19f;
private const float WAVE_FREQ = 9f;
private const float WAVE_FREQ_2 = 16f;
private const float WAVE_SCALE = 0.03f;
private const float FORWARD_SCALE = 0.045f;
private const float FLOW_SPEED = 1.85f;
private const float FLOW_SURGE_SCALE = 0.07f;
private const float FLOW_WAVE_SCALE = 0.016f;
private const float WIDTH_PULSE_SPEED = 9.5f;
private const float WIDTH_MIN = 1.1f;
private const float WIDTH_MAX = 4.6f;
[Property] public LineRenderer LineRenderer { get; set; }
[Sync] public Player OwnerPlayer { get; set; }
[Sync] public Tree SourceTree { get; set; }
private float _phase;
private bool _rendererConfigured;
protected override void OnAwake()
{
base.OnAwake();
_phase = Game.Random.Float( 0f, MathF.PI * 2f );
TryEnsureLineRenderer();
}
protected override void OnUpdate()
{
base.OnUpdate();
if ( !TryEnsureLineRenderer() )
return;
if ( !OwnerPlayer.IsValid() )
{
GameObject.Destroy();
return;
}
if ( !SourceTree.IsValid() || SourceTree.IsDying || OwnerPlayer.IsDead )
{
LineRenderer.Enabled = false;
return;
}
var start = SourceTree.WorldPosition + new Vector3( 0f, 0f, Math.Max( SourceTree.Height * TREE_HEIGHT_FACTOR, MIN_HEIGHT ) );
var end = OwnerPlayer.WorldPosition + new Vector3( 0f, 0f, Math.Max( OwnerPlayer.Height * PLAYER_HEIGHT_FACTOR, MIN_HEIGHT ) );
var delta = end - start;
var length = delta.Length;
if ( length <= 0f )
{
LineRenderer.Enabled = false;
return;
}
LineRenderer.Enabled = true;
EnsureLinePoints();
var dir = delta / length;
var perp = new Vector3( -dir.y, dir.x, 0f );
var time = Time.Now;
var travel = 0.04f + ((time * FLOW_SPEED + _phase * 0.11f) % 1f) * 0.92f;
LineRenderer.Width = Utils.Map( MathF.Sin( time * WIDTH_PULSE_SPEED + _phase * 0.8f ), -1f, 1f, WIDTH_MIN, WIDTH_MAX, EasingType.SineInOut );
for ( int i = 0; i < NUM_POINTS; i++ )
{
var t = i / (float)(NUM_POINTS - 1);
var basePos = Vector3.Lerp( start, end, t );
var envelope = MathF.Sin( t * MathF.PI );
var offsetScale = length * WAVE_SCALE * envelope * Utils.Map( t, 0f, 1f, 0.35f, 1.55f, EasingType.QuadOut );
var forwardScale = length * FORWARD_SCALE * envelope * Utils.Map( t, 0f, 1f, 0.35f, 1.75f, EasingType.QuadOut );
var wave = MathF.Sin( t * WAVE_FREQ - time * WAVE_SPEED + _phase );
wave += MathF.Sin( t * WAVE_FREQ_2 - time * WAVE_SPEED_2 + _phase * 1.23f ) * 0.35f;
var forwardWave = MathF.Sin( t * WAVE_FREQ_2 - time * (WAVE_SPEED + 3.5f) + _phase * 0.8f );
var packet = MathF.Max( 1f - MathF.Abs( t - travel ) / 0.22f, 0f );
packet *= packet;
var flowWave = MathF.Sin( (t - travel) * MathF.PI * 2.8f - time * 4.2f + _phase * 0.45f );
var pos = basePos
+ perp * wave * offsetScale
+ dir * (forwardWave * forwardScale
+ flowWave * length * FLOW_WAVE_SCALE * envelope
+ packet * length * FLOW_SURGE_SCALE * Utils.Map( t, 0f, 1f, 0.15f, 1f, EasingType.QuadOut ));
LineRenderer.VectorPoints[i] = pos.WithZ( Math.Max( pos.z, MIN_HEIGHT ) );
}
UpdateGradient( time, travel );
}
private bool TryEnsureLineRenderer()
{
if ( LineRenderer == null || !LineRenderer.IsValid() )
LineRenderer = GameObject.GetComponent<LineRenderer>( includeDisabled: true )
?? GameObject.GetComponentInChildren<LineRenderer>( includeDisabled: true, includeSelf: true );
if ( LineRenderer == null || !LineRenderer.IsValid() )
return false;
if ( !_rendererConfigured )
{
LineRenderer.UseVectorPoints = true;
LineRenderer.Additive = true;
LineRenderer.Opaque = false;
LineRenderer.CastShadows = false;
LineRenderer.Lighting = false;
LineRenderer.DepthFeather = 8f;
LineRenderer.FogStrength = 0f;
LineRenderer.Face = SceneLineObject.FaceMode.Camera;
LineRenderer.StartCap = SceneLineObject.CapStyle.Rounded;
LineRenderer.EndCap = SceneLineObject.CapStyle.Rounded;
LineRenderer.Enabled = false;
_rendererConfigured = true;
}
EnsureLinePoints();
return true;
}
private void EnsureLinePoints()
{
if ( LineRenderer.VectorPoints != null && LineRenderer.VectorPoints.Count == NUM_POINTS )
return;
LineRenderer.VectorPoints = new List<Vector3>( NUM_POINTS );
for ( int i = 0; i < NUM_POINTS; i++ )
{
LineRenderer.VectorPoints.Add( Vector3.Zero );
}
}
private void UpdateGradient( float time, float travel )
{
var pulse = Utils.Map( MathF.Sin( time * 8.5f + _phase ), -1f, 1f, 1.15f, 1.7f );
var leadPulse = Utils.Map( MathF.Sin( time * 14f + _phase * 1.2f ), -1f, 1f, 1.2f, 2f ) * pulse;
var packetStart = Math.Max( travel - 0.18f, 0.04f );
var packetLeadIn = Math.Max( packetStart - 0.09f, 0.03f );
var packetEnd = Math.Min( travel + 0.09f, 0.98f );
var gradient = new Gradient();
gradient.AddAlpha( 0f, 0f );
gradient.AddAlpha( 0.03f, 0.42f * pulse );
gradient.AddAlpha( packetLeadIn, 0.62f * pulse );
gradient.AddAlpha( packetStart, 0.95f * pulse );
gradient.AddAlpha( travel, 1.95f * leadPulse );
gradient.AddAlpha( packetEnd, 1.15f * leadPulse );
gradient.AddAlpha( 1f, 0f );
gradient.AddColor( 0f, new Color( 0.08f, 0.22f, 0.03f, 0f ) );
gradient.AddColor( 0.08f, new Color( 0.35f, 1.2f, 0.12f, 0.5f * pulse ) );
gradient.AddColor( packetLeadIn, new Color( 0.6f, 2f, 0.2f, 0.8f * pulse ) );
gradient.AddColor( packetStart, new Color( 0.95f, 2.1f, 0.35f, 1f * pulse ) );
gradient.AddColor( travel, new Color( 1.8f, 2.8f, 0.65f, 1.5f * leadPulse ) );
gradient.AddColor( packetEnd, new Color( 1.2f, 2.5f, 0.48f, 1.15f * leadPulse ) );
gradient.AddColor( 1f, new Color( 1.2f, 3.6f, 0.48f, 0f ) );
LineRenderer.Color = gradient;
}
}