Files
sbox-public/engine/Sandbox.Engine/Scene/Components/Particles/ParticleFloat.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

317 lines
7.7 KiB
C#

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Sandbox;
public interface IDynamicFloatContext
{
/// <summary>
/// Should return the lifetime delta we're going to use to evaluate
/// </summary>
float LifetimeDelta { get; }
/// <summary>
/// Should return the seed we're using for randomness
/// </summary>
int RandomSeed { get; }
}
/// <summary>
/// Represents a floating-point value that can change over time with support for various evaluation modes.
/// </summary>
[Expose]
public struct ParticleFloat : IJsonConvert
{
public ValueType Type { readonly get; set; }
public EvaluationType Evaluation { readonly get; set; }
public Curve CurveA { readonly get; set; }
public Curve CurveB { readonly get; set; }
[JsonInclude]
public Vector4 Constants;
[JsonIgnore]
public float ConstantValue
{
readonly get => Constants.x;
set => Constants.x = value;
}
[JsonIgnore]
public float ConstantA
{
readonly get => Constants.x;
set => Constants.x = value;
}
[JsonIgnore]
public float ConstantB
{
readonly get => Constants.y;
set => Constants.y = value;
}
[JsonIgnore]
public CurveRange CurveRange
{
readonly get => new CurveRange( CurveA, CurveB );
set
{
CurveA = value.A;
CurveB = value.B;
}
}
public static implicit operator ParticleFloat( float v )
{
return new ParticleFloat { Type = ValueType.Constant, ConstantValue = v };
}
public ParticleFloat()
{
}
public ParticleFloat( float a, float b )
{
Type = ValueType.Range;
Evaluation = EvaluationType.Seed;
ConstantA = a;
ConstantB = b;
}
public enum ValueType
{
/// <summary>
/// A value that doesn't change over time.
/// </summary>
Constant,
/// <summary>
/// The value is interpolated between two fixed floats.
/// </summary>
Range,
/// <summary>
/// A curve that defines how the value changes over time or based on an evaluation factor.
/// </summary>
Curve,
/// <summary>
/// Two curves where the value is interpolated between them.
/// </summary>
CurveRange
}
public enum EvaluationType
{
/// <summary>
/// Evaluates the value based on the lifetime using its normalized age.
/// </summary>
Life,
/// <summary>
/// Evaluates the value based on the current frame, introducing randomness for dynamic effects.
/// </summary>
Frame,
/// <summary>
/// Evaluates the value based on a random seed. This means that in most situations, it's random per context.
/// Like if this is on a particle, the value will be random per particle.
/// </summary>
Seed,
[Obsolete( "This is moved to seed. This struct won't be particle specific in the future" )]
Particle = Seed
}
/// <summary>
/// Evaluates the value based on the given delta and random seed, optimized for performance.
/// </summary>
[MethodImpl( MethodImplOptions.AggressiveInlining )]
public readonly float Evaluate( in float delta, in float randomFixed )
{
float d = Evaluation switch
{
EvaluationType.Life => delta,
EvaluationType.Frame => Random.Shared.Float( 0, 1 ),
EvaluationType.Seed => randomFixed,
_ => delta,
};
return Type switch
{
ValueType.Constant => ConstantValue,
ValueType.Range => MathX.Lerp( ConstantA, ConstantB, d ),
ValueType.Curve => CurveA.Evaluate( d ),
ValueType.CurveRange => CurveRange.Evaluate( d, randomFixed ),
_ => ConstantValue,
};
}
/// <summary>
/// Evaluates the value using a dynamic context and seed, optimized for clarity and functionality.
/// </summary>
[MethodImpl( MethodImplOptions.AggressiveInlining )]
public readonly float Evaluate( IDynamicFloatContext context, int seed, [CallerLineNumber] int line = 0 )
{
int randomFloatIndex = unchecked(context.RandomSeed ^ (line * 73856093) ^ seed);
return Evaluate( context.LifetimeDelta, Game.Random.FloatDeterministic( randomFloatIndex ) );
}
/// <summary>
/// Checks if the value is nearly zero.
/// </summary>
[MethodImpl( MethodImplOptions.AggressiveInlining )]
public readonly bool IsNearlyZero()
{
if ( Type == ParticleFloat.ValueType.Constant && ConstantValue.AlmostEqual( 0.0f ) )
return true;
return false;
}
/// <summary>
/// Reads a ParticleFloat instance from JSON, refactored for modularity.
/// </summary>
public static object JsonRead( ref Utf8JsonReader reader, Type typeToConvert )
{
ParticleFloat value = new ParticleFloat();
if ( reader.TokenType == JsonTokenType.Number )
{
value = (float)reader.GetDouble();
return value;
}
if ( reader.TokenType != JsonTokenType.StartObject )
{
Log.Info( $"Unknown Token: {reader.TokenType}" );
return value;
}
while ( reader.Read() && reader.TokenType != JsonTokenType.EndObject )
{
if ( reader.TokenType == JsonTokenType.PropertyName )
{
var name = reader.GetString();
reader.Read();
switch ( name )
{
case "Type":
value.Type = JsonSerializer.Deserialize<ValueType>( ref reader, Json.options );
break;
case "Evaluation":
value.Evaluation = JsonSerializer.Deserialize<EvaluationType>( ref reader, Json.options );
break;
case "CurveA":
value.CurveA = JsonSerializer.Deserialize<Curve>( ref reader, Json.options );
break;
case "CurveB":
value.CurveB = JsonSerializer.Deserialize<Curve>( ref reader, Json.options );
break;
case "Constants":
value.Constants = JsonSerializer.Deserialize<Vector4>( ref reader, Json.options );
break;
default:
reader.Skip();
break;
}
}
}
return value;
}
/// <summary>
/// Writes a ParticleFloat instance to JSON, refactored for modularity.
/// </summary>
public static void JsonWrite( object value, Utf8JsonWriter writer )
{
var target = (ParticleFloat)value;
if ( target.Type == ValueType.Constant )
{
writer.WriteNumberValue( target.ConstantValue );
return;
}
writer.WriteStartObject();
writer.WritePropertyName( "Type" );
JsonSerializer.Serialize( writer, target.Type, Json.options );
writer.WritePropertyName( "Evaluation" );
JsonSerializer.Serialize( writer, target.Evaluation, Json.options );
// We only need to write this if it's not a constant
if ( target.Type == ValueType.Curve || target.Type == ValueType.CurveRange )
{
writer.WritePropertyName( "CurveA" );
JsonSerializer.Serialize( writer, target.CurveA, Json.options );
writer.WritePropertyName( "CurveB" );
JsonSerializer.Serialize( writer, target.CurveB, Json.options );
}
if ( target.Type == ValueType.Constant || target.Type == ValueType.Range )
{
writer.WritePropertyName( "Constants" );
JsonSerializer.Serialize( writer, target.Constants, Json.options );
}
writer.WriteEndObject();
}
/// <summary>
/// This is only here to remain "compatible" with RangedFloat
/// </summary>
[Obsolete, EditorBrowsable( EditorBrowsableState.Never )]
public float GetValue()
{
return Evaluate( Random.Shared.Float(), 3 );
}
}
[Expose]
public struct ParticleVector3
{
[JsonInclude]
public ParticleFloat X;
[JsonInclude]
public ParticleFloat Y;
[JsonInclude]
public ParticleFloat Z;
public static implicit operator ParticleVector3( Vector3 v )
{
return new ParticleVector3 { X = v.x, Y = v.y, Z = v.z };
}
public readonly Vector3 Evaluate( float delta, float a, float b, float c )
{
var x = X.Evaluate( delta, a );
var y = Y.Evaluate( delta, b );
var z = Z.Evaluate( delta, c );
return new Vector3( x, y, z );
}
[MethodImpl( MethodImplOptions.AggressiveInlining )]
public readonly Vector3 Evaluate( Particle p, int seed, [CallerLineNumber] int line = 0 )
{
return Evaluate( p.LifeDelta, p.Rand( seed, line ), p.Rand( seed + 1, line ), p.Rand( seed + 2, line ) );
}
public readonly bool IsNearlyZero()
{
return X.IsNearlyZero() && Y.IsNearlyZero() && Z.IsNearlyZero();
}
}