using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Sandbox;
public interface IDynamicFloatContext
{
///
/// Should return the lifetime delta we're going to use to evaluate
///
float LifetimeDelta { get; }
///
/// Should return the seed we're using for randomness
///
int RandomSeed { get; }
}
///
/// Represents a floating-point value that can change over time with support for various evaluation modes.
///
[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
{
///
/// A value that doesn't change over time.
///
Constant,
///
/// The value is interpolated between two fixed floats.
///
Range,
///
/// A curve that defines how the value changes over time or based on an evaluation factor.
///
Curve,
///
/// Two curves where the value is interpolated between them.
///
CurveRange
}
public enum EvaluationType
{
///
/// Evaluates the value based on the lifetime using its normalized age.
///
Life,
///
/// Evaluates the value based on the current frame, introducing randomness for dynamic effects.
///
Frame,
///
/// 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.
///
Seed,
[Obsolete( "This is moved to seed. This struct won't be particle specific in the future" )]
Particle = Seed
}
///
/// Evaluates the value based on the given delta and random seed, optimized for performance.
///
[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,
};
}
///
/// Evaluates the value using a dynamic context and seed, optimized for clarity and functionality.
///
[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 ) );
}
///
/// Checks if the value is nearly zero.
///
[MethodImpl( MethodImplOptions.AggressiveInlining )]
public readonly bool IsNearlyZero()
{
if ( Type == ParticleFloat.ValueType.Constant && ConstantValue.AlmostEqual( 0.0f ) )
return true;
return false;
}
///
/// Reads a ParticleFloat instance from JSON, refactored for modularity.
///
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( ref reader, Json.options );
break;
case "Evaluation":
value.Evaluation = JsonSerializer.Deserialize( ref reader, Json.options );
break;
case "CurveA":
value.CurveA = JsonSerializer.Deserialize( ref reader, Json.options );
break;
case "CurveB":
value.CurveB = JsonSerializer.Deserialize( ref reader, Json.options );
break;
case "Constants":
value.Constants = JsonSerializer.Deserialize( ref reader, Json.options );
break;
default:
reader.Skip();
break;
}
}
}
return value;
}
///
/// Writes a ParticleFloat instance to JSON, refactored for modularity.
///
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();
}
///
/// This is only here to remain "compatible" with RangedFloat
///
[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();
}
}