I am working on a Units library for a personal project that is a physics-heavy C#/Unity3D game, and I am looking to improve it in a few areas:
Is there a way to reduce the code duplication?
Is performance going to be a problem? Is there anything I can do to improve performance?
Can I hide the
value
member variable so it is not visible to the rest of the program, but only within this namespace?Are the Equals() and GetHashCode() methods implemented correctly?
Are there other changes that I should do to make this more idiomatic C#?
[Serializable]
public struct Meter
{
[SerializeField]
internal double value;
public Meter(double value) => this.value = value;
public static Meter operator +(Meter m) => m;
public static Meter operator -(Meter m) => new Meter() { value = -m.value };
public static Meter operator +(Meter m1, Meter m2) => new Meter() { value = m1.value + m2.value };
public static Meter operator -(Meter m1, Meter m2) => new Meter() { value = m1.value - m2.value };
public static Meter operator *(Meter m, double d) => new Meter() { value = m.value * d };
public static Meter operator *(double d, Meter m) => new Meter() { value = m.value * d };
public static Meter operator /(Meter m, double d) => new Meter() { value = m.value / d };
public static double operator /(Meter m1, Meter m2) => m1.value / m2.value;
public static MeterSquared operator *(Meter m1, Meter m2) => new MeterSquared() { value = m1.value * m2.value };
public static MeterPerSecond operator /(Meter m, Second s) => new MeterPerSecond() { value = m.value / s.value };
public readonly static Meter zero = new Meter() { value = 0.0 };
public override bool Equals(object obj) => obj is Meter other ? value.Equals(other.value) : false;
public override int GetHashCode() => value.GetHashCode();
public static bool operator ==(Meter m1, Meter m2) => m1.value == m2.value;
public static bool operator !=(Meter m1, Meter m2) => m1.value != m2.value;
}
[Serializable]
public struct Second
{
[SerializeField]
internal double value;
public Second(double value) => this.value = value;
public static Second operator +(Second s) => s;
public static Second operator -(Second s) => new Second() { value = -s.value };
public static Second operator +(Second s1, Second s2) => new Second() { value = s1.value + s2.value };
public static Second operator -(Second s1, Second s2) => new Second() { value = s1.value - s2.value };
public static Second operator *(Second s, double d) => new Second() { value = s.value * d };
public static Second operator /(Second s, double d) => new Second() { value = s.value / d };
public static double operator /(Second s1, Second s2) => s1.value / s2.value;
public readonly static Second zero = new Second() { value = 0.0 };
public override bool Equals(object obj) => obj is Second other ? value.Equals(other.value) : false;
public override int GetHashCode() => value.GetHashCode();
public static bool operator ==(Second s1, Second s2) => s1.value == s2.value;
public static bool operator !=(Second s1, Second s2) => s1.value != s2.value;
}
/// <summary>
/// m / s, velocity
/// </summary>
[Serializable]
public struct MeterPerSecond
{
[SerializeField]
internal double value;
public MeterPerSecond(double value) => this.value = value;
public static MeterPerSecond operator +(MeterPerSecond m) => m;
public static MeterPerSecond operator -(MeterPerSecond m) => new MeterPerSecond() { value = -m.value };
public static MeterPerSecond operator +(MeterPerSecond m1, MeterPerSecond m2) => new MeterPerSecond() { value = m1.value + m2.value };
public static MeterPerSecond operator -(MeterPerSecond m1, MeterPerSecond m2) => new MeterPerSecond() { value = m1.value - m2.value };
public static MeterPerSecond operator *(MeterPerSecond m, double d) => new MeterPerSecond() { value = m.value * d };
public static MeterPerSecond operator /(MeterPerSecond m, double d) => new MeterPerSecond() { value = m.value / d };
public static Meter operator *(MeterPerSecond m, Second s) => new Meter() { value = m.value * s.value };
public static MeterPerSecondSquared operator /(MeterPerSecond m, Second s) => new MeterPerSecondSquared() { value = m.value * s.value };
public readonly static MeterPerSecond zero = new MeterPerSecond() { value = 0.0 };
public override bool Equals(object obj) => obj is MeterPerSecond other ? value.Equals(other.value) : false;
public override int GetHashCode() => value.GetHashCode();
public static bool operator ==(MeterPerSecond m1, MeterPerSecond m2) => m1.value == m2.value;
public static bool operator !=(MeterPerSecond m1, MeterPerSecond m2) => m1.value != m2.value;
}
Example usage:
public class AstronomicalObject2 : MonoBehaviour
{
public string astronomicalName;
public Meter radius;
[Header("Provide One")]
public Kilogram mass;
public MeterPerSecondSquared surfaceGravity;
public KilogramPerCubicMeter meanDensity;
[Header("Calculated")]
public Meter circumference;
public MeterSquared surfaceArea;
public MeterCubed volume;
public StandardGravitationParameter gm;
[Header("Atmo-Required")]
public Pascal atmosphereSurfacePressure = new Pascal(101325);
public Meter atmosphereScaleHeight = new Meter(8500);
[Header("Atmo-Calculated")]
public Meter atmosphereMaxHeight;
public readonly Pascal atmospherePressureCutoff = new Pascal(1);
private Kilogram old_mass;
private MeterPerSecondSquared old_surfaceGravity;
private KilogramPerCubicMeter old_meanDensity;
void OnEnable()
{
old_mass = mass;
old_surfaceGravity = surfaceGravity;
old_meanDensity = meanDensity;
}
void OnValidate()
{
circumference = 2.0 * Math.PI * radius;
surfaceArea = circumference * 2.0 * radius; // 4.0 * Math.PI * radius * radius;
volume = surfaceArea * radius / 3.0; // 4.0 / 3.0 * Math.PI * radius * radius * radius;
if (mass != old_mass)
{
gm = GravitationConstant.G * mass;
surfaceGravity = gm / (radius * radius);
meanDensity = mass / volume;
}
else if (surfaceGravity != old_surfaceGravity)
{
mass = new Kilogram(surfaceGravity.value * radius.value * radius.value / GravitationConstant.G.value);
gm = GravitationConstant.G * mass;
meanDensity = mass / volume;
}
else if (meanDensity != old_meanDensity)
{
mass = meanDensity * volume;
gm = GravitationConstant.G * mass;
surfaceGravity = gm / (radius * radius);
}
else
{
// Something else changed
// Make sure all the calculated values are accurate, assuming that the mass is the desired value
gm = GravitationConstant.G * mass;
surfaceGravity = gm / (radius * radius);
meanDensity = mass / volume;
}
old_mass = mass;
old_surfaceGravity = surfaceGravity;
old_meanDensity = meanDensity;
// Atmo
if (atmosphereSurfacePressure.value > 0)
{
atmosphereMaxHeight = -atmosphereScaleHeight * Math.Log(atmospherePressureCutoff / atmosphereSurfacePressure);
}
else
{
atmosphereMaxHeight = Meter.zero;
}
}
}
Alternative without the Units classes:
// Excerpt from the OnValidate() method:
circumference_m = 2.0 * Math.PI * radius_m; // m
surfaceArea_m2 = circumference_m * 2.0 * radius_m; // 4.0 * Math.PI * radius * radius = m^2
volume_m3 = surfaceArea_m2 * radius_m / 3.0; // 4.0 / 3.0 * Math.PI * radius * radius * radius = m^3
if (mass_kg != old_mass)
{
gm = PhysicsConstants.G * mass_kg; // (m^3/(kg*s^2)) * kg = m^3/s^2
surfaceGravity_m_s = gm / (radius_m * radius_m); // (m^3/s^2) / m^2 = m/s^2
meanDensity_kg_m3 = mass_kg / volume_m3; // kg / m^3 = kg/m^3
}
else if (surfaceGravity_m_s != old_surfaceGravity)
{
mass_kg = surfaceGravity_m_s * radius_m * radius_m / PhysicsConstants.G; // (m/s^2 * m * m) / (m^3/(kg*s^2)) = kg
gm = PhysicsConstants.G * mass_kg; // (m^3/(kg*s^2)) * kg = m^3/s^2
meanDensity_kg_m3 = mass_kg / volume_m3; // kg / m^3 = kg/m^3
}
else if (meanDensity_kg_m3 != old_meanDensity)
{
mass_kg = meanDensity_kg_m3 * volume_m3; // kg/m^3 * m^3 = kg
gm = PhysicsConstants.G * mass_kg; // (m^3/(kg*s^2)) * kg = m^3/s^2
surfaceGravity_m_s = gm / (radius_m * radius_m); // (m^3/s^2) / m^2 = m/s^2
}
else
{
// Something else changed
// Make sure all the calculated values are accurate, assuming that the mass is the desired value
gm = PhysicsConstants.G * mass_kg; // (m^3/(kg*s^2)) * kg = m^3/s^2
surfaceGravity_m_s = gm / (radius_m * radius_m); // (m^3/s^2) / m^2 = m/s^2
meanDensity_kg_m3 = mass_kg / volume_m3; // kg / m^3 = kg/m^3
}
Thanks for looking at this!
Edit: Added the example usage, plus an example without the Units classes. The purpose of this library is to avoid units errors, like using velocity (m/s) when it should be length (m) or something like that. I am not sure if this is worth the extra effort though... On a work project in the past, I used a nice units library but it was in C++ and made extensive use of templates to make it easy to use and easy to maintain and performant. Not sure that I can achieve the same in C#.
MeterSquared
andMeterPerSecondSquared
are not present. \$\endgroup\$mass = surfaceGravity * radius * radius / PhysicsConstants.G;
, for which the units are:kg = m/s^2 * m * m / (m^3/(kg*s^2))
. So I need definitions for kg, m/s^2, m, and m^3/(kg*s^2). But I also need m^2/s^2, because it first multiplies m/s^2 * m, then m^3/s^2 because it multiplies by another m. The list of unit types is becoming unwieldy, especially with how many lines of code I have to maintain for each struct. If the definitions were shorter, this might be more manageable. \$\endgroup\$