飞行管理计算机 (FMC)

飞行管理系统(FMS)的一项核心能力,是利用飞行数据实时估算飞机当前的质量与重心,而不是只依赖机组预输入的数字。
- 质量估算:平飞时升力必须等于重力。FMS 通过迎角、俯仰配平与推力等实际飞行响应反推当前质量——飞机越重,维持同一航迹所需的迎角或推力就越大。
- 重心估算:重心位置决定飞机所需的配平状态,FMS 从升降舵/平尾的配平位置反推重心;两者结合即可估算全机质量与重心。
- 持续修正:起飞前机组输入起飞重量(燃油 + 业载),飞行中 FMS 用燃油流量与实测飞行特性不断修正估算值。
空客会把实时估算的总重(GW)与重心(CG)显示在 MCDU 性能页上,用来动态更新最优巡航高度、燃油预测与进近速度,飞行员无需精确输入重量;波音的 FMS 也有类似的重量估算机制。
FMC 门控
物理体上必须装有 ≥1 个 FMC(ccpe:fmc),否则以下方法全部返回 nil。不在物理体上或底层物理数据不可用时同样返回 nil。
| 方法 | 返回 | 说明 |
|---|---|---|
getPhysicsCenterOfMassRel() |
table / nil | 重心相对最后放置的 FMC 的方块中心(AIC 等同 FMC)的机体局部系位置 {x, y, z} |
getPhysicsChainCenterOfMassRel() |
table / nil | 整条物理体链的总质心相对最后放置的 FMC 的方块中心(AIC 等同 FMC)的机体局部系位置 {x, y, z} |
getPhysicsMass() |
number / nil | 电脑所在物理体的质量(kg) |
getPhysicsChainMass() |
number / nil | 物理体含全部约束链的总质量(kg) |
getPhysicsGravityForce() |
number / nil | 所在物理体的重力(pN = 质量 × 11) |
getPhysicsChainGravityForce() |
number / nil | 整条物理体链的总重力(pN = 链总质量 × 11) |
getStressRemaining() |
number / nil | 最后放置的 FMC 的附着面方块所在 Create 应力网络的剩余应力(su,过载时为负) |
getStressCapacity() |
number / nil | 该网络的总容量(su) |
重心语义
getPhysicsCenterOfMassRel() 返回重心相对机体上最后放置的 FMC(含约束链;多个 FMC/AIC 时取最后放置的那个)的方块中心的机体局部系(plot 帧)偏移:
FMC 参考点 = 其 BlockPos(角点)加半格(+0.5),即方块单元中心,不是方块角点。两个偏移都经过 Sable 转换 plot − rotationPoint(与 getSensors() 的 pos 同一坐标系)——结果不随物理体移动/旋转变化,适合稳定地识别重心装在机体的哪个位置(比如离 FMC 中心前后/上下多远)。
物理体原点 = 质心
Sable 运行时会把物理体原点(rotationPoint)与质心保持同步,因此上式第一项 ≈ 0,该值 ≈ FMC 方块中心相对物理体原点的偏移取反(即重心相对 FMC 方块中心的方位)。
- 需要世界系时,用
getOrientation()把该向量旋转到世界。
链质心
getPhysicsChainCenterOfMassRel() 返回整条物理体链(含约束连接,如轴承;始终含电脑所在物理体)的总质心相对机体上最后放置的 FMC(含约束链;多个 FMC/AIC 时取最后放置的那个)的方块中心的机体局部系偏移:
其中第一项 = 世界系按质量加权平均链上各物理体质心 Σ(mᵢ·comᵢ)/Σmᵢ,再经电脑所在物理体的 pose 逆变换转回其 plot 帧 − rotationPoint;第二项与 getPhysicsCenterOfMassRel() 同一参考点(FMC 的 BlockPos 角点 + 半格)。结果与 getSensors() 的 pos 同帧,不随物理体移动/旋转变化。
Sable 没有现成的链质心 API(MergedMassTracker 只合并单个物理体自身 + 其 plot 内 contraptions),该值由本 mod 在服务端每 tick 计算。门控与 getPhysicsChainMass() 相同(机体含约束链上 ≥1 个 FMC)。
重力
重力是标量(大小,方向向下):
getPhysicsGravityForce() 用电脑所在物理体自身的质量;getPhysicsChainGravityForce() 用链总质量(见 getPhysicsChainMass())。
附着方块应力网络
getStressRemaining() 与 getStressCapacity() 读取最后放置的 FMC(AIC 等同 FMC)所贴着的方块所在 Create 应力网络:
- 附着方块 — FMC 支撑面方向上的方块(FMC:由 blockstate 的
FACE/FACING决定的支撑面;AIC:FACING背面的方块)。附着方块必须是 Create 动力方块(KineticBlockEntity,如齿轮箱、传动轴、螺旋桨轴承),否则两个方法都返回nil。 getStressCapacity()— 网络总容量(su)。getStressRemaining()— 剩余应力 = 总容量 − 当前总应力(su),网络过载时为负。
两个方法与其余 FMC 方法同门控(机体含约束链上必须有 ≥1 个 FMC、电脑必须在物理体上);读数每 tick 刷新。
示例
local ss = require("ccpe.sensor_system")
if not ss.isOnBody() then
error("电脑不在物理体上")
end
print("质量 (kg): ", ss.getPhysicsMass())
print("链总质量 (kg): ", ss.getPhysicsChainMass())
print("重力 (pN): ", ss.getPhysicsGravityForce())
print("链总重力 (pN): ", ss.getPhysicsChainGravityForce())
-- 重心相对最后放置的 FMC(机体局部系,旋转时不变)
local com = ss.getPhysicsCenterOfMassRel()
if com then
print("重心相对FMC:", string.format("x=%.2f y=%.2f z=%.2f", com.x, com.y, com.z))
end
-- 最后放置的 FMC 的附着面方块所在 Create 应力网络(su)
print("应力容量 (su): ", ss.getStressCapacity())
print("剩余应力 (su): ", ss.getStressRemaining())
螺旋桨转速工具
FMC 还提供螺旋桨转速求解工具(同为 FMC 门控):根据期望推力与当前飞行状态,反解出螺旋桨(Propeller Bearing)应输出的转速。该工具依赖 aeronautics 的螺旋桨物理配置。
initPropeller(N, S)
使用前必须先初始化一次:
-- N = 螺旋桨(Propeller Bearing)数量
-- S = 每个螺旋桨上动力方块的数量(风帆 / 对称风帆 / 羊毛方块)
local ok = ss.initPropeller(N, S)
| 参数 | 说明 |
|---|---|
N |
螺旋桨数量(≥ 1) |
S |
每个螺旋桨上的动力方块数量(≥ 1) |
返回 true 表示成功;机体(含约束链)上没有 FMC(门控不满足)或参数非法时返回 false。
getPropellerRPM(F, P, V, θ?)
-- F = 期望推力;P = 气压(海平面 = 1.0);V = 速度(m/s)
-- θ = 螺旋桨平面与速度方向的夹角(度,可选,默认 0)
local rpm = ss.getPropellerRPM(F, P, V, thetaDeg)
公式(由 aeronautics 推力/气流模型反解):
其中 T(Propeller Bearing Thrust,默认 0.2)与 A(Propeller Bearing Airflow,默认 0.05)来自 aeronautics 配置(aeronautics > server > Physics)。配置在进游戏(服务器启动)时与放置/加载 FMC 时缓存一次(静态缓存,不逐 tick 读取)——游戏中修改配置后,需要重进世界或重新放置一次 FMC 才生效。
返回所需转速 R;未 init、门控不满足(无 FMC)或参数非法(如 P ≤ 0)返回 nil。
θ(气流角,度)——缩放气流项 V × sin(θ) / (S^0.5 × A):螺旋桨穿过自身所推空气所需的额外转速。按机型取用:
- 固定翼飞机平飞:
θ = 90——气流穿过桨盘,气流项全额生效(sin 90° = 1); - 直升机垂直起降:
θ = 0——无气流项。
默认 0。
气压可用
getPressure()(静压孔读数)、速度可用getSpeed()/getAverageSpeed()(皮托管读数)直接代入,组合成推力闭环控制。
高度-气压换算工具
同为 FMC 门控(因此装 AIC 也满足),传感器系统提供两个世界高度 Y 与气压之间的纯换算工具,用的是与游戏物理(以及静压孔读数 getPressure())完全相同的空气模型:
| 方法 | 返回 | 说明 |
|---|---|---|
getPressureFromAltitude(Y) |
number / nil | 世界高度 Y 处的气压(大气压分数,海平面 = 1.0) |
getAltitudeFromPressure(P) |
number / nil | 气压为 P 的世界高度 Y(上面方法的反函数) |
门控与其余 FMC 方法相同(机体含约束链上必须有 ≥1 个 FMC,AIC 等同 FMC;电脑必须在物理体上),否则返回 nil。两者都是纯数学(mainThread = false):只读一份缓存的空气曲线快照,不在电脑线程上碰世界。
计算公式
游戏大气模型为 P(Y) = basePressure × 高度曲线(Y),其中曲线是对维度数据包 dimension_physics 加载的锚点做的分段三次 Hermite 插值:
主世界默认锚点(basePressure = 1.0,海平面 63):
| 高度 Y | 值 | 斜率 |
|---|---|---|
| −38.37 | 1.5(地下钳位) | −0.006 |
| 63 | 1.0 | −0.004 |
| 263 | 0.4493 | −0.001797 |
| 280 | 0.4198 | −0.001679 |
| 320 | 0(建筑高度上限) | −0.02099 |
−38 m ~ 280 m 之间它与简单指数 P ≈ e^(−0.004·(Y − 63)) 数值上几乎一致;280 m 以上曲线向下弯向建筑高度上限(Y=320)处的 0,320 m 以上保持 0——主世界默认上限以上没有空气(无升力/阻力/推力)。曲线不是解析可逆的(分段三次),所以反向用数值二分:
二分单调、精度到双精度,两个方法严格互逆:getAltitudeFromPressure(getPressureFromAltitude(Y)) ≈ Y。高度基准 = 世界 Y,与 getAltitude() 一致。
缓存了什么
为满足 mainThread = false 的线程安全,曲线参数被一次性复制进一份静态 volatile 快照:
basePressure- 锚点
{高度, 值, 斜率}(默认 5 组) - 二分区间
[minY, minY + logicalHeight]
快照在进游戏(服务器启动,用主世界)和放置/加载 FMC 或 AIC(onLoad)时刷新一次,与螺旋桨配置缓存同款策略,不逐 tick 读取;门控(当前机体上有没有 FMC/AIC)仍每 tick 判定。
缓存时效
改了 dimension_physics 数据包(/reload)后,需重新放置/加载一次 FMC 或 AIC(或重进世界)才会刷新快照。缓存是全局的(所有电脑共用一份曲线):多个机体在不同维度时,取最后加载的那个维度的曲线。
风帆气动工具
同为 FMC 门控(因此装 AIC 也满足),传感器系统提供两个纯数学工具,围绕游戏气动模型的两条方程——升力方程与无方向阻力方程——做设计计算(配平、巡航速度估算、失速/最低气压分析),无需实时飞行数据:
升力: L = k3 × P × |V| = 0.475 × P × |V| (仅 Create 普通帆)
无方向阻力: D = k2 × P × |V| = 0.06888202261 × P × |V| (普通帆与对称帆共用)
公式镜像 Sable 的 BlockSubLevelLiftProvider.sable$contributeLiftAndDrag()(每个物理子步、每块帆计算一次)。两个工具都固定 n·v = 0 条件——气流速度与帆面法向垂直,即无法向速度分量(平飞)。该条件下法向阻力恒为 0,输出只剩升力 + 无方向阻力:
| 方法 | 返回 | 说明 |
|---|---|---|
solveSailLift(P, V, L?) |
number / nil | 升力方程(Create SailBlock):三个量传任意两个、缺失的传 nil,返回第三个 |
solveSailDirectionlessDrag(P, V, D?) |
number / nil | 无方向阻力方程(普通帆 / 对称帆通用):同上 |
参数约定:三个量传任意两个,缺失的传 nil
两个方法都是「已知方程中任意两个量、求解剩下一个」的统一求解器。三个参数分别对应方程里的气压 P、速度 V 与输出量(升力 L / 阻力 D),传任意两个,缺失的那个传 nil,返回缺失量:
-- 升力方程 L = 0.475 × P × |V|
ss.solveSailLift(P, V, nil) -- → 升力 L(正向)
ss.solveSailLift(P, nil, L) -- → 速度 V = L/(0.475·P) (平飞时 L = 重力 → 所需空速)
ss.solveSailLift(nil, V, L) -- → 气压 P = L/(0.475·|V|) (维持该升力的最低气压 → 最高可用高度)
-- 无方向阻力方程 D = 0.06888202261 × P × |V|(两种帆相同)
ss.solveSailDirectionlessDrag(P, V, nil) -- → 阻力 D(正向)
ss.solveSailDirectionlessDrag(P, nil, D) -- → 速度 V = D/(0.06888202261·P)
ss.solveSailDirectionlessDrag(nil, V, D) -- → 气压 P = D/(0.06888202261·|V|)
- 只传 1 个量或 3 个量都传 →
nil(欠定/超定)。 P— 气压(大气压分数,海平面 = 1.0,与getPressure()同语义);P ≤ 0→nil。V— 速度大小(m/s,与getSpeed()同单位);负数按绝对值|V|处理;求解 P 时|V| = 0(除零)→nil。L/D— 力标量;负值无解 →nil。- 门控与其余 FMC 工具相同(机体含约束链上必须有 ≥1 个 FMC,AIC 等同 FMC;电脑必须在物理体上),否则返回
nil。纯数学(mainThread = false),零主线程调度。
返回值
返回缺失的那个量,都是每秒等效力标量(力 / 速度 m/s / 气压分数)——与 substepsPerTick 配置无关,与游戏内图纸(冲量×60)同量纲,可与推力读数对比。不再返回每物理子步冲量。
计算公式
Create 普通帆(SailBlock,全默认参数):
无方向阻力(普通帆与对称帆共用,k2 都未覆写):
D = k2 × P × |V| = 0.06888202261 × P × |V|
V = D / (0.06888202261 × P)
P = D / (0.06888202261 × |V|)
Simulated 对称帆(SymmetricSailBlock:k3 = 0、k1 = 1.75)不产生升力,只有无方向阻力——由同一个 solveSailDirectionlessDrag 计算(k2 与普通帆相同)。
k2 = 0.06888202261(Sable 默认值,(−0.75 + √(0.75² + 0.475²)) / 2——恰好压住默认升力发散的最小阻尼)。法向阻力系数 k1(普通帆 0.75 / 对称帆 1.75)在本工具中不出现:它乘的是 (n·v),而该工具条件 n·v = 0。n·v = 0 时升力也取该速度下的最大值(|V − 法向阻力| = |V|);任何迎角/偏航分量都只会让它变小。
缓存了什么
系数(k2、k3)是写在 mod 里的常量,无静态缓存。门控仍每 tick 判定。
示例
local ss = require("ccpe.sensor_system")
-- 正向:P = 0.47、60 m/s 时普通帆的升力 + 无方向阻力(每秒力标量)
local lift = ss.solveSailLift(0.47, 60, nil)
local drag = ss.solveSailDirectionlessDrag(0.47, 60, nil)
print("lift (N): ", lift) -- 0.475 × P × V
print("drag (N): ", drag) -- 0.06888202261 × P × V
-- 反解:机翼需要 13.3 N 升力(≈ 一架小飞机的重力),P = 0.47 时所需空速
local v = ss.solveSailLift(0.47, nil, 13.3)
print("required speed (m/s): ", v) -- 13.3 / (0.475 × 0.47)
多块帆的总升力:线性累加
Sable 对每一块帆独立计算同一条升力公式,逐帆累加进总冲量(ServerSubLevel.prePhysicsTick() 循环每块帆;LiftProviderGroup 分组只影响图纸/记录器的力箭头显示,不改变逐帆力)——没有"帆越多、单帆越弱"的衰减。因此在无旋转(纯平移)、所有帆同朝向、同高度的平飞下,总升力严格 = 帆数 × 单帆升力,工具乘以帆数成立。
偏离项(都来自"每块帆用自己的局部量",而非帆间干扰):
- 角速度:每块帆用自己位置的局部气流
v_local = V + ω×r;旋转时离重心越远的帆局部气流越大、单帆升力越大,总升力 ≠ 帆数 ×(机体速度对应的单帆升力)。 - 帆朝向不一致(上反角、舵面偏转):升力是沿各自法线的矢量,方向不同则总矢量和 < 标量和;且 n·v ≠ 0 使单帆升力低于最大值。
- 气压逐帆取点:P 在每块帆自己的方块中心取值,跨大高度差时略有差异(通常可忽略)。
通用阻力工具
乌云

上图是 Contraption Diagram(图纸)的力箭头截图:螺旋桨的推力明显远大于飞机整体受到的阻力。若只有图纸上画出的这两个力,净力应为正、飞机应持续加速;但实测巡航速度却保持恒定(dv/dt ≈ 0),说明还存在一个图纸未画出的未知力在抵消多余的推力——它就是通用阻力(universal drag):
- 方向:与速度方向相反;
- 大小:正比于质量与速度(F = −m·d·v)。
它由 Rapier 对每个 sublevel 刚体施加的恒定速度阻尼产生,不经过任何力组,所以图纸与飞行记录器 CSV 都看不到它——这正是"图纸净力 ≠ 实际净力"的原因,也是本工具存在的意义。
同为 FMC 门控(因此装 AIC 也满足),传感器系统提供一个纯数学工具:计算通用阻力(速度阻尼)的等效力——Rapier 对每个 sublevel 刚体施加的恒定速度阻尼。它不经过任何力组,图纸与飞行记录器 CSV 都看不到它;本工具让它在设计计算中可用(净力平衡:推力 − 帆阻力 − 通用阻力 ≈ 0)。
公式对应每子步阻尼 v ← v/(1+d·Δt) 的连续近似:
| 方法 | 返回 | 说明 |
|---|---|---|
getUniversalDragForce(m, V) |
number / nil | 通用阻力等效力标量 = m × d × |V| |
参数与约定:
m— 质量(kg,与getPhysicsMass()/getPhysicsChainMass()同单位);m ≤ 0→nil。V— 速度大小(m/s,与getSpeed()同单位);负数按绝对值|V|处理。d— 通用阻力系数,默认 0.09(SableDimensionPhysics.DEFAULT_UNIVERSAL_DRAG),可被维度数据包dimension_physics的"universal_drag"字段覆盖。- 门控与其余 FMC 工具相同(机体含约束链上必须有 ≥1 个 FMC,AIC 等同 FMC;电脑必须在物理体上),否则返回
nil。纯数学(mainThread = false),零主线程调度。
与帆工具不同,返回值是单个"每秒力"(连续近似已按秒归一,不涉及子步 Δt)——它不随气压缩放,只与质量、速度成正比。
缓存了什么
系数 d 在进游戏(服务器启动)与放置/加载 FMC 或 AIC(onLoad)时缓存一次,与大气曲线快照同款策略;读不到时保留 Sable 默认(0.09)。门控仍每 tick 判定。
示例
local ss = require("ccpe.sensor_system")
-- 当前质量与速度下的通用阻力等效力(d 默认 0.09)
local drag = ss.getUniversalDragForce(ss.getPhysicsChainMass(), 60)
print("通用阻力 (N):", drag) -- m × 0.09 × V
m代入getPhysicsMass()/getPhysicsChainMass()、V代入getSpeed()(或|v|)即可闭合全机力平衡:巡航时推力 − 帆阻力 − 通用阻力 ≈ 0。可用记录值复核(如 m ≈ 45.25 kg、v ≈ 62.6 m/s → F ≈ 255 N)。
最高巡航高度求解工具
此工具仅适用于如下图的简单气动模型:推力线过重心,升力中心与重力线在垂直方向上对齐,阻力线基本过重心
同为 FMC 门控(因此装 AIC 也满足),这个纯数学求解器反解巡航方程组:给定飞机质量、帆数与螺旋桨配置,求在给定最大转速下能达到的最高稳态高度与所需空速。
对未知量 (v, P) 求解两个稳态巡航方程(平飞、气流与帆面法向垂直故法向阻力为 0):
(1) 升力 = 重力: k3·P·v·N_w = m·g
→ x = P·v = m·g/(k3·N_w) (与高度无关,直接钉死)
(2) 推力 = 阻力: P·S^1.5·N_p·T·R·(1 − v/(S^0.5·R·A)) = k2·P·v·N_s + m·d·v
| 方法 | 返回 | 说明 |
|---|---|---|
solveMaxCruise(m, wingSails, symmetricSails, propellerCount, sailsPerPropeller, maxRpm) |
table / nil | {velocity=..., altitude=...}——maxRpm 下的稳态巡航状态 |
代入 v = x/P 后方程 (2) 化为关于 P 的一元二次——无需矩阵(方程组对 (v, P) 双线性,消元后恰为一元二次):
a·P² + b·P + c = 0
a = S^1.5·N_p·T·R
b = −(S·N_p·T·x/A + k2·x·N_s) (S^1.5/S^0.5 = S、R 消去)
c = −m·d·x
P* = (−b + √(b²−4ac))/(2a),v* = x/P*,高度 = 气压曲线反解(P*)(二分)
N_w=wingSails——升力帆数(只有普通帆产生升力)。N_s=wingSails + symmetricSails + propellerCount × sailsPerPropeller——总动力方块数(风帆 + 对称风帆 + 螺旋桨动力方块),全部计入无方向阻力k2·P·v。- 推力模型带气流削减系数
(1 − v/(S^0.5·R·A))——即getPropellerRPM模型在固定翼平飞(θ = 90)下的形式:桨面有最大有效速度S^0.5·R·A,超过它推力转负。 - 系数来源(与其余工具相同):
k3 = 0.475、k2 = 0.06888202261、g = 11、d = 0.09(通用阻力,数据包可覆盖)、T = 0.2/A = 0.05(aeronautics 配置)、维度大气曲线——均在进游戏与放置/加载 FMC/AIC 时缓存。 - 可行性:解出的
P*超出维度大气值域(贴地也升力不足 / 高于大气顶)、门控不满足(无 FMC)或参数非法(m ≤ 0、wingSails < 1、propellerCount < 1、sailsPerPropeller < 1、maxRpm ≤ 0)时返回nil。maxRpm = 256只是转速上限,实际还受应力网络容量(getStressRemaining())约束。
示例
local ss = require("ccpe.sensor_system")
-- 给定装配在最大转速下的最高稳态巡航
-- 质量 45.25
-- 升力帆数 32
-- 对称帆数 20
-- 螺旋桨数 1
-- 每个螺旋桨上的动力方块数 30
-- 最大转速 256
local cruise = ss.solveMaxCruise(45.25, 32, 20, 1, 30, 256)
print("cruise speed (m/s):", cruise.velocity)
print("max altitude (Y): ", cruise.altitude)
m用getPhysicsChainMass()、帆数/桨数用你装配的真实方块数、转速上限用 256;结果与飞行记录器数据对照验证。