#units #gamedev #unit

fts_units

fts_units是一个库,它通过使用单位使编译时类型安全的数学运算成为可能。

2次发布

0.1.1 2019年6月12日
0.1.0 2019年6月10日

#257 in 渲染


用于 fts_gamemath

Unlicense OR MIT

72KB
1K SLoC

fts_units

Crate API

fts_units是一个Rust库,它通过使用单位使编译时类型安全的数学运算成为可能。

它提供了一系列引人注目的功能。

国际单位制

SI单位提供强大的支持。可以创建自定义系统,但目前不是开发重点。

基本数学

use fts_units::si_system::quantities::f32::*;

let d = Meters::new(10.0);  // units are meters
let t = Seconds::new(2.0);  // units are seconds
let v = d / t;              // units are m·s⁻¹ (MetersPerSecond)

// compile error! meters plus time doesn't make sense
let _ = d + t; // compile errors! meters + time doesn't make sense

// compile error! can't mix different ratios (Unit and Kilo)
let _ = d + Kilometers::new(2.3);

组合运算

基本数学运算可以任意组合。有效的类型不是预定义的。如果你的计算结果有14次方的米,它仍然可以正常工作。

use fts_units::si_system::quantities::*;

fn calc_ballistic_range(speed: MetersPerSecond<f32>, gravity: MetersPerSecond2<f32>, initial_height: Meters<f32>)
-> Meters<f32>
{
    let d2r = 0.01745329252;
    let angle : f32 = 45.0 * d2r;
    let cos = Dimensionless::<f32>::new(angle.cos());
    let sin = Dimensionless::<f32>::new(angle.sin());

    let range = (speed*cos/gravity) * (speed*sin + (speed*speed*sin*sin + Dimensionless::<f32>::new(2.0)*gravity*initial_height).sqrt());
    range
}

类型控制

fts_units对存储类型提供了完全控制。

let s = Seconds::<f32>::new(22.3);
let ns = Nanoseconds::<i64>::new(237_586_538);

如果你主要使用f32值,则便利模块会包装所有常见类型。

use fts_units::si_system::quantities::f32::*;

转换

可以转换相同维度的量。

let d = Kilometers::new(15.3);
let t = Hours::new(2.7);
let kph = d / t; // KilometersPerHour

let mps : MetersPerSecond = kph.convert_into();
let mps = MetersPerSecond::convert_from(kph);

尝试将不同维度的量转换会产生编译时错误。

let d = Meters::<f32>::new(5.5);
let _ : Seconds<f32> = d.convert_into(); // compile error!
let _ : Meters<f64> = d.convert_into(); // also compile error!

转换

可以按照正常转换规则将数量转换为。这个特性使用了num-traits

let m = Meters::<f32>::new(7.73);
let i : Meters<i32> = m.cast_into();
assert_eq!(i.amount(), 7);

永远不会隐式执行转换或转换。这确保了在处理不同尺度时完全控制。例如,在纳秒和年之间转换。

显示

SI系统支持人类可读的显示输出。

println!("{}", MetersPerSecond2::<f32>::new(9.8));
// 9.8 m·s⁻²

println!("{}", KilometersPerHour::<f32>::new(65.5));
// 65.5 km·h⁻¹

任意比率

si_system数量可以具有完全任意的比率。

type R = RatioT<P37,P10>;
let q : QuantityT<f32, SIUnitsT<SIRatiosT<R, Zero, Zero>, SIExponentsT<P1, Z0, Z0>>> = 1.1.into();

没有宏或构建脚本

此crate是纯Rust代码。没有宏或构建脚本来自动生成任何内容。

这是为了使源代码易于阅读、理解和扩展而做出的明确选择。

自定义金额

struct QuantityT<T,U>适用于任何T,其中T:Amount

在以下内置类型中实现了 Amountu8u16u32u64u128i8i16i32i64i128f32,和 f64。

Amount 也可以为任何自定义类型实现。例如 Vector3<f32>QuantityT<Vector3<f32>, _> 将正确支持或不支持您想要的运算符。如果 Vector3<f32> 实现 std::ops::Add<Vector3f<f32>> 但不实现 std::ops::Mul<Vector3<f32>>,则 QuantityT<Vector3<f32>, _> 的行为也将相同。

实现

fts_units 完全是编译时实现,没有运行时成本。单位存储为零大小类型,编译后会消失。

如果您使用的是提供的SI系统,那么这些实际上并不重要。您永远不需要输入这些类型。但是,如果您犯了一个错误并生成了编译错误,了解底层类型将帮助您理解错误的来源。

了解实现的最佳方式是快速浏览几个重要的结构体。对于大多数结构体,都有一个匹配的特质。我选择使用 T 后缀来表示结构体。例如,Quantity(特质)和 QuantityT(结构体)。还有 Ratio(特质)和 RatiotT(结构体)。T 表示结构体必须提供一个类型。

QuantityT 是基本的结构体。米、秒和米/秒都是具有不同 U 类型的 QuantityT 结构体。

pub struct QuantityT<T:Amount, U> {
    amount : T,
    _u: PhantomData<U>
}

RatioT 是一个将分子和分母以类型形式存储的结构体。千米的比率为 1000 / 1。纳米的比率为 1 / 1_000_000_000。

pub struct RatioT<NUM, DEN>
    where
        NUM: Integer + NonZero,
        DEN: Integer + NonZero,
{
    _num: PhantomData<NUM>,
    _den: PhantomData<DEN>,
}

这里会变得有些复杂。具有 SI 单位的量有一个比率和指数列表。

pub struct SIUnitsT<RATIOS,EXPONENTS>
    where
        RATIOS: SIRatios,
        EXPONENTS: SIExponents
{
    _r: PhantomData<RATIOS>,
    _e: PhantomData<EXPONENTS>,
}

#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct SIRatiosT<L,M,T>
    where
        L: Ratio,
        M: Ratio,
        T: Ratio
{
    _l: PhantomData<L>,
    _m: PhantomData<M>,
    _t: PhantomData<T>
}

#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct SIExponentsT<L,M,T>
    where
        L: Integer,
        M: Integer,
        T: Integer,
{
    _l: PhantomData<L>,
    _m: PhantomData<M>,
    _t: PhantomData<T>
}

以下是完全展开的一些示例类型。

// Ratios and exponents are stored in Length/Mass/Time order
type Kilometers = QuantityT<f32,
    SIUnitsT<
        SIRatiosT<Kilo, Zero, Zero>,
        SIExponentsT<P1, Z0, Z0>>>;

type CentimetersPerSecondSquared = QuantityT<f64,
    SIUnitsT<
        SIRatiosT<Centi, Zero, Unit>,
        SIExponentsT<P1, Z0, N2>>>;

只要单位类型支持该操作,就支持 Quantity 的加、乘、除和平方根等操作。

当与si_system一起工作时,这意味着我们正在使用 QuantityT<T,SIUnitsT<R,E>>。所有操作都需要匹配的 T 类型。加法和减法在 SIUnitsT<R,E> 中实现,乘法和除法在 R 类型不冲突时实现。如果 T 支持开方且所有 E 值都是偶数,则实现开方。

您可以通过使用 CastAmount 特征来更改 T。您可以通过使用 ConvertUnits 来更改 U

注意事项

国际单位制

国际单位制目前仅支持长度、质量和时间维度。这几乎是大多数游戏所需要的。

电流、温度、物质的量和光强度将稍后添加。这是一项微不足道的工作,但需要一定数量的复制/粘贴。这些维度将在基本 API 稳定后添加。

错误信息不佳

fts_units 利用出色的 typenum crate 进行编译时数学。不幸的是,这导致 糟糕的 错误信息。

const generics 到来时,这将大幅改进。

此代码

let _ = Meters::new(5.0) + Seconds::new(2.0);

产生此错误

error[E0308]: mismatched types
  --> examples\sandbox.rs:82:32
   |
82 |     let _ = Meters::new(5.0) + Seconds::new(2.0);
   |                                ^^^^^^^^^^^^^^^^^ expected struct `fts_units::ratio::RatioT`, found struct `fts_units::ratio::RatioZero`
   |
   = note: expected type `fts_units::quantity::QuantityT<_, fts_units::si_system::SIUnitsT<fts_units::si_system::SIRatiosT<fts_units::ratio::RatioT<typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>, typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>>, _, fts_units::ratio::RatioZero>, fts_units::si_system::SIExponentsT<typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>, _, typenum::int::Z0>>>`
              found type `fts_units::quantity::QuantityT<_, fts_units::si_system::SIUnitsT<fts_units::si_system::SIRatiosT<fts_units::ratio::RatioZero, _, fts_units::ratio::RatioT<typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>, typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>>>, fts_units::si_system::SIExponentsT<typenum::int::Z0, _, typenum::int::PInt<typenum::uint::UInt<typenum::uint::UTerm, typenum::bit::B1>>>>>`

这是一个视觉噩梦。但它可以理解!

当排成一行或放入差异工具中时,差异很容易发现。'找到的类型' 在第一个 SIUnitsT 插槽中有一个 RatioZero 类型,而期望的是非零类型。如果您记得插槽是长度/质量/时间,这应该是有意义的。米值有一个非零长度比率。秒值有一个零长度比率。要添加两个 SIUnitsT 量,它们必须具有完全相同的比率和指数。

数量级

支持飞托到拍。不幸的是,Atto/Zepto/Yocto 和 Exa/Zetta/Yotta 不受支持。它们需要 128 位比率,而 fts_units 目前由于 typenum 而受到 64 位的限制。

当 const generics 到来时,这应该会改变。

导出单位

国际单位制的一个优点是导出单位。每个人都知道 Force = Mass * Acceleration。力是一个如此常见的量,它有一个名字,牛顿。牛顿被存储在 kg⋅m⋅s⁻² 中。这也允许像千牛顿的力或太瓦的功率这样的单位。

遗憾的是,fts_units 不支持导出单位。当 特殊化 到来时,将更容易很好地支持。

常见问题解答

fts是什么意思?

这是我的首字母。

你为什么做这个?

因为我一直想要它存在。

为什么使用fts_units?

为什么有人应该使用 fts_units 而不是 uomdimensioned

这是一个好问题。您可能会更喜欢其中的一个 crate!我认为 fts_units 有更好的 API。我喜欢有明确的控制权进行转换和转换。我喜欢它不使用宏,因此代码易于阅读和理解。

这正是我想要的。

许可证:Unlicense 或 MIT

依赖关系

~215–305KB