简介:StableApproach 是面向 X-Plane 11 飞行模拟爱好者和飞行训练者的飞行数据监控与稳定进近分析配置包,主要用于记录并评估进近着陆阶段的速度、高度、下降率、航道对准、姿态与推力等关键参数,帮助用户定位操作偏差、改进飞行技巧。压缩包共 19 个文件,约 19KB,以 15 个 sacfg 机型配置文件为主,另含 2 个 json 参数/版本文件、1 个 md 说明文档和 1 个 gitignore;sacfg 可用于不同机型的监控阈值定制,json 与说明文档便于了解配置结构和导入方式。已有约 700 人学习,适合希望系统复盘模拟飞行表现、提升进近稳定性的初中级模拟飞行玩家。通过调整各机型配置中的警告阈值与监控范围,读者可围绕着陆稳定性建立可复用的分析流程,并结合生成的数据报告查看着陆阶段的偏差,从而在虚拟飞行中形成更规范的操作习惯。
1. 在最后 200 英尺才发现偏了,复盘只能靠回忆
模拟飞行里最容易骗自己的环节就是进近。手飞一段 ILS,目视看跑道在风挡里挺正,接地那一刻才发现速度比 VREF 高了十几节,下降率表指着 900 英尺每分,航向道其实早偏了一个点。麻烦在于 X-Plane 11 只给你当下的仪表读数,落地之后这些数字全没了,想复盘只能凭记忆。StableApproach 补的正是这一段:它在 X-Plane 11 进程内持续做飞行数据监控,按真实航司的进近达标判据,在 1000 英尺和 500 英尺各判一次,落地后输出一份带接地速度、接地下降率、坡度、偏差的报告。适合正在练手飞进近、又不想每次都请真人坐旁边盯着的开发者。
2. 技术选型与工程骨架:Lua 脚本怎么在 X-Plane 11 进程内做实时监控
先把定位说清楚:它不是一个落地后去解析日志的外部分析工具,而是挂在模拟器内部、每帧读 dataref 的实时监控层。这个差别决定了整个技术栈。外部程序走 UDP 拿数据,天然多一层跨进程延迟,接地瞬间那几帧最容易丢;而放在进程内,接地前后每一帧都能抓到。
所以选型基本被锁死在 FlyWithLua 上:Lua 5.1 的运行环境由插件自带,脚本放进Scripts目录就被加载,dataref()一行就能订阅任意仿真变量,绘制、日志、文件读写全是标准库。代价也要认:Lua 是单线程,每帧的预算可能只有几百微秒,任何字符串拼接、文件 IO 都不能放进无条件执行的路径里。这一点在后面讲避坑时会反复出现。
2.1 目录结构与文件职责
一个监控插件最怕写成两千行的单文件,改一条阈值要翻半天。常见做法是按「数据 → 窗口 → 判据 → 输出」四层拆开,主脚本只负责把它们串起来:
| 文件 | 职责 | 改动频率 |
|---|---|---|
StableApproach.lua | 入口,注册回调,串联各模块 | 低 |
sa_datarefs.lua | 集中声明所有 dataref 与单位换算 | 低 |
sa_sampler.lua | 定频采样、写入环形缓冲 | 低 |
sa_state.lua | IDLE/APPROACH/FLARE/ROLLOUT 状态机 | 中 |
sa_criteria.lua | 判据表,纯数据 | 高 |
sa_config.lua | VREF、判决高度、跑道表 | 高 |
Output/StableApproach/landings.csv | 每次落地的记录 | 运行时生成 |
sa_config.lua和sa_criteria.lua之所以单独拆出来,是因为它们才是实际使用中反复调的东西。换机型只改 VREF,换标准只改阈值,业务代码一行不用碰。FlyWithLua 支持dofile按相对路径加载同级文件,模块返回一个 table 就够用。
2.2 数据源:dataref 声明与采样节拍
X-Plane 11 的 dataref 命名分得极细,同一个物理量往往有好几个版本,选错了读数就是错的。这里的原则是:高度用无线电高度而不是气压高度,速度用座舱指示空速而不是真空速,偏差用「点」为单位而不是原始偏航距。
-- sa_datarefs.lua:所有外部数据在这里一次性声明,不散落到业务代码里 -- 第三个参数 "readonly" 表示只读,写操作会被 FlyWithLua 直接拒绝 dataref("AGL_M", "sim/flightmodel/position/y_agl", "readonly") -- 无线电高度,单位米 dataref("VS_FPM", "sim/flightmodel/position/vh_ind_fpm", "readonly") -- 垂直速度,英尺/分 dataref("IAS_KT", "sim/cockpit2/gauges/indicators/airspeed_kts_pilot", "readonly") -- 指示空速,节 dataref("ROLL_DEG", "sim/flightmodel/position/phi", "readonly") -- 坡度,度 dataref("LOC_DOTS", "sim/cockpit2/radios/indicators/nav1_hdef_dots_pilot", "readonly") -- 航向道偏差,点 dataref("GS_DOTS", "sim/cockpit2/radios/indicators/nav1_vdef_dots_pilot", "readonly") -- 下滑道偏差,点 dataref("FLAP_RATIO", "sim/cockpit2/controls/flap_ratio", "readonly") -- 襟翼手柄位置 0~1 dataref("GEAR_DOWN", "sim/cockpit2/controls/gear_handle_down", "readonly") -- 起落架手柄 1=放下 dataref("ON_GROUND", "sim/flightmodel/failures/onground_any", "readonly") -- 任意机轮接地 dataref("RUN_TIME_S", "sim/time/total_running_time_sec", "readonly") -- 模拟器运行秒数,用于节拍这里踩过的最典型一坑是y_agl的单位是米,而所有人的判据表都是英尺写的,1000 英尺的判决点如果忘了乘 3.28084,脚本会在 305 英尺才触发判决,看起来像是「完全不工作」。
采样节拍不能跟着帧率走。帧率 60 和帧率 25 下,同样的「连续 10 帧超限」代表的时长完全不同,判据就失去了意义。所以用模拟器时间自己做节流:
-- sa_sampler.lua:自己控节拍,20Hz,与帧率解耦 local SAMPLE_HZ = 20 local last_sample = 0 function sa_sample_tick() -- RUN_TIME_S 是模拟时间,不是墙钟,暂停时不会继续累加 if RUN_TIME_S - last_sample < 1.0 / SAMPLE_HZ then return end last_sample = RUN_TIME_S sa_push({ t = RUN_TIME_S, agl_ft = AGL_M * 3.28084, -- 米换英尺,判据表内统一用英尺 vs = VS_FPM, ias = IAS_KT, roll = ROLL_DEG, loc = LOC_DOTS, gs = GS_DOTS, flap = FLAP_RATIO, gear = GEAR_DOWN, }) sa_state_update() -- 采样后立刻喂状态机,保证两者看到同一帧数据 end do_every_frame("sa_sample_tick()") -- 每帧调用,但内部按时间戳节流SAMPLE_HZ是最值得调的参数。10Hz 足够判断 1000 英尺和 500 英尺的判决点,但接地瞬间的峰值可能被抹平;20Hz 是一个折中,一秒钟 20 个样本,环形缓冲占的内存可以忽略。do_every_frame在这个用法下并没有想象中危险,因为函数体第一行就返回了,真正干活的只有每秒 20 次。真正危险的是把string.format和io.write直接写进这个函数。
2.3 状态机:为什么不能用一堆 if 堆出来
早期版本我用if agl < 1000 then check() end直接判,结果落地滑跑阶段高度在 0 附近抖动,判定被反复触发,日志刷了几百行,HUD 上的结论在「达标」和「不达标」之间来回跳。这类玄学问题的根子在于:所有判断都基于瞬时值,而状态本身没有记忆。
四个状态就够用:待机、进近、拉平、滑跑。关键是每个状态只在一处跃迁,跃迁时做一次动作,绝不重复。
-- sa_state.lua:四态机,跨线才动作,天然抗抖动 local ST = { IDLE = 0, APPROACH = 1, FLARE = 2, ROLLOUT = 3 } local state = ST.IDLE function sa_state_update() local s = sa_latest() -- 取刚才那一帧 if state == ST.IDLE then -- 低于 3000 英尺 + 起落架放下 + 空速低于 200 节,认为进入进近 if s.agl_ft < 3000 and s.gear == 1 and s.ias < 200 then state = ST.APPROACH sa_reset_window() end elseif state == ST.APPROACH then if s.agl_ft < 30 then state = ST.FLARE elseif s.agl_ft > 3500 then state = ST.IDLE -- 复飞或爬升离场,退回待机 end elseif state == ST.FLARE then if ON_GROUND == 1 then state = ST.ROLLOUT sa_on_touchdown() -- 结算上一帧数据,不能用当前帧 end elseif state == ST.ROLLOUT then if s.ias < 30 then sa_write_report() -- 滑跑到低速才落盘,避免半截数据 state = ST.IDLE end end endST.FLARE和ST.ROLLOUT分开是有原因的。接地瞬间ON_GROUND从 0 变 1 的那一帧,垂直速度往往已经被地面法向力抹掉,真正的接地下降率在上一帧。所以状态机在跃迁时调用sa_on_touchdown(),函数内部取的是缓冲里倒数第二个样本。这个细节直接决定了报告里的「接地下降率」是不是废数据。
3. 核心判定链路:从 20Hz 采样到一份着陆报告的完整推导
整条链路的顺序是固定的:采样进环形缓冲 → 需要时取窗口中位数 → 在判决高度取快照 → 逐条跑判据 → 记录失败项 → 落地后结算成一行 CSV。每一环都有它存在的理由,砍掉任何一环都会在某个具体场景下翻车。
3.1 滑动窗口:单帧数据不能直接拿来做判断
X-Plane 11 的vh_ind_fpm在乱流和低帧率下会有明显的尖峰,某一帧跳到 -1600 再把下一帧拉回 -700,如果按瞬时值判,这架飞机会在 500 英尺被判不达标,而飞行员什么也没做错。更隐蔽的是航向道偏差:飞机压着航向道飞,偏差值会在 0 附近以 ±0.15 点的幅度抖,某帧恰好 1.02 点,就误判了。
解法是取窗口内的中位数而不是均值。均值会被尖峰拉偏,中位数直接把离群点丢掉。窗口长度 1 秒、20 个样本,对进近判据来说足够。
-- sa_window.lua:1 秒滑动窗口,20 个样本,取中位数抗单帧毛刺 local N = 20 local buf, idx, cnt = {}, 1, 0 function sa_reset_window() buf, idx, cnt = {}, 1, 0 end function sa_push(s) buf[idx] = s idx = idx % N + 1 if cnt < N then cnt = cnt + 1 end end -- 按字段取中位数,调用方只关心"这个量现在是多少" function sa_window(key) if cnt == 0 then return 0 end local tmp = {} for i = 1, cnt do tmp[i] = buf[i][key] end table.sort(tmp) return tmp[math.floor((cnt + 1) / 2)] endsa_reset_window()必须在进入APPROACH时调用一次,否则上一段航程的残留样本会污染新窗口,出现刚进近就判不达标的怪现象。table.sort在这只有 20 个元素,开销可以接受;如果想把采样提到 50Hz,就该换成定长插入排序或者直接维护一个局部最大值,别在每帧做全排序。
3.2 判据表:把 SOP 写成数据而不是代码
真实航司的进近标准大同小异:在 1000 英尺(仪表条件)或 500 英尺(目视条件)必须同时满足若干条件,任一条不满足就是不达标,要么复飞要么修正后重新判定。把这些条件写进if-else是最容易腐化的做法,因为每加一条就要动一次函数体,测试成本成倍上升。
改成表之后,判据就是数据,新增一条只是加一行:
| 判据 | 字段 | 判决高度 | 阈值 | 说明 |
|---|---|---|---|---|
| 起落架 | gear | 1000 ft | = 1 | 手柄必须在放下位 |
| 襟翼 | flap | 1000 ft | ≥ 0.9 | 襟翼接近着陆位 |
| 速度 | ias | 1000 ft | ≤ VREF+10 | 相对值,随机型变 |
| 下降率 | vs | 500 ft | ≥ -1000 fpm | 绝对值上限 1000 |
| 坡度 | roll | 500 ft | ≤ 15° | 目视段更严 |
| 航向道 | loc | 500 ft | ≤ 1.0 点 | 双座取偏差大的一侧 |
| 下滑道 | gs | 500 ft | ≤ 1.0 点 | 同上 |
-- sa_criteria.lua:判据即数据,加一条判据只加一行 -- cmp 说明越界方向:lt 小于即错,gt 大于即错,absgt 取绝对值后大于即错 -- ref = "vref" 表示阈值是相对 VREF 的偏移量,运行时再算 return { { id="GEAR", field="gear", cmp="lt", limit=1, at=1000, label="起落架未放下" }, { id="FLAP", field="flap", cmp="lt", limit=0.9, at=1000, label="襟翼未到着陆位" }, { id="SPEED", field="ias", cmp="gt", limit=10, at=1000, ref="vref", label="速度高于 VREF+10" }, { id="SINK", field="vs", cmp="lt", limit=-1000, at=500, label="下降率超过 1000fpm" }, { id="BANK", field="roll", cmp="absgt", limit=15, at=500, label="坡度超过 15 度" }, { id="LOC", field="loc", cmp="absgt", limit=1.0, at=500, label="航向道偏差超过 1 点" }, { id="GS", field="gs", cmp="absgt", limit=1.0, at=500, label="下滑道偏差超过 1 点" }, }判据表只描述「什么是错」,判定函数只负责按cmp分支,两者职责不混:
-- 在指定高度做一次判决,返回是否达标 + 失败项列表 function sa_evaluate(agl_target, snap, vref) local fails = {} for _, c in ipairs(CRITERIA) do if c.at == agl_target and snap[c.field] ~= nil then local lim = c.limit if c.ref == "vref" then lim = vref + c.limit -- 相对阈值:VREF + 偏移量 end local v = snap[c.field] local bad = (c.cmp == "lt" and v < lim) or (c.cmp == "gt" and v > lim) or (c.cmp == "absgt" and math.abs(v) > lim) if bad then fails[#fails + 1] = c.label end end end return #fails == 0, fails endsnap是从窗口里取中位数拼出来的快照表,不是单帧原始值;vref从配置里来,随机型和着陆重量变化。返回失败项列表而不是布尔值,是为了让 HUD 能显示「速度高于 VREF+10、下滑道偏差 1.3 点」这样的具体原因——只告诉飞行员「不达标」等于没说。
还有一点容易忽略:at是精确匹配,所以必须在AGL_M穿越 1000 英尺和 500 英尺的那一帧触发采样快照,而不是等到「小于 1000 之后随便一帧」。做法是在状态机里记一个last_agl,检测last_agl > 1000 and now <= 1000这样的下穿事件:
-- 下降穿越检测,保证判决点只触发一次 local function crossed(cur, prev, line) return prev ~= nil and prev > line and cur <= line end function sa_check_crossing(s) if crossed(s.agl_ft, last_agl, 1000) then last_verdict = { ok, fails = sa_evaluate(1000, sa_snapshot(), SA.vref) } elseif crossed(s.agl_ft, last_agl, 500) then last_verdict = { ok, fails = sa_evaluate(500, sa_snapshot(), SA.vref) } end last_agl = s.agl_ft end3.3 告警与落地报告:HUD 一行字和 CSV 一行记录
输出分两种,走的是完全不同的路径。实时告警用 FlyWithLua 的画字接口直接画在屏幕上,不写日志——写日志会被Log.txt刷屏,而且日志窗口在进近时挡视线。落地报告走文件追加,一次落地只写一行。
-- 实时 HUD:只在非待机状态绘制,坐标是屏幕像素 function sa_draw() if state == ST.IDLE then return end local txt = string.format( "AGL %4.0fft VS %5.0f IAS %3.0f ROLL %4.1f LOC %+.2f GS %+.2f %s", sa_window("agl_ft"), sa_window("vs"), sa_window("ias"), sa_window("roll"), sa_window("loc"), sa_window("gs"), (last_verdict and (last_verdict.ok and "STABLE" or "GO AROUND")) or "-----") draw_string(txt, 20, 20) end do_every_frame("sa_draw()")draw_string的坐标以像素为单位,跟着窗口分辨率走,所以在 1080p 上调好的位置到 4K 会偏到屏幕外。想省事就按SCREEN_WIDTH算比例,别写死20, 20。另外这个函数同样在每帧调用,string.format每次都跑,实测在低端机上会占掉一两个百分点的帧率。常见做法是只在数值变化时重拼字符串,缓存上一次的结果。
落地报告更简单,但字段设计要想清楚,因为它决定了你以后能不能做纵向对比:
-- 结算函数里预先算好 td 表,这里只负责落盘 function sa_write_report() local td = sa_last_landing local f = io.open(SCRIPT_DIR .. "/Output/StableApproach/landings.csv", "a") if not f then return end f:write(string.format("%s,%s,%.0f,%.0f,%.1f,%.2f,%.2f,%s\n", os.date("%Y-%m-%d %H:%M:%S"), td.rwy, -- 跑道标识,来自配置里的跑道表 td.td_vs, -- 接地下降率,取接地前一帧 td.td_ias, -- 接地指示空速 td.td_bank, -- 接地坡度 td.loc_dev, -- 接地时航向道偏差 td.vs_50ft, -- 50 英尺时下降率,判断拉平质量的抓手 td.verdict)) -- STABLE / GO_AROUND endvs_50ft这个字段是我后来才加的,它比接地下降率更能说明拉平手法:接地率低但 50 英尺下降率很大,说明是在跑道上「等着」而不是主动拉平,长期这么飞对机体不友好。CSV 用追加模式、落地后一次性写入,避免飞行过程中反复开关文件句柄。
4. 跑起来:FlyWithLua 安装、参数配置与一段可复现的验证航段
4.1 安装与首次加载的三个确认点
环境准备不复杂,但有三处必须逐一确认,跳过任何一个都会得到「脚本装好了但没反应」。
# 1) 确认 FlyWithLua 已经装好并且被加载过 ls "X-Plane 11/Resources/plugins/FlyWithLua/Scripts" # 2) 把项目整体拷进 Scripts 下的独立子目录,别把文件平铺进去 cp -r StableApproach "X-Plane 11/Resources/plugins/FlyWithLua/Scripts/" # 3) 启动后看 Log.txt,FlyWithLua 的加载信息和 Lua 报错都写在这里 grep -i flywithlua "X-Plane 11/Log.txt" | tail -n 40第一点:Scripts目录必须存在。FlyWithLua 的安装包解压后如果只有Modules没有Scripts,说明版本装错了,脚本永远不会被加载。
第二点:子目录里要有主脚本文件,FlyWithLua 会扫描Scripts下的.lua文件。放进子目录是为了让dofile的相对路径成立,同时方便整包删除。文件名不要带空格和中文,某些系统上会解析失败。
第三点:Log.txt里如果出现FlyWithLua: Error ...,那就是 Lua 语法或运行时错误,括号和行号都写得很清楚。最常见的报错是attempt to index a nil value,一般意味着某个dofile返回了nil,检查模块文件最后是不是忘了return。
启动进游戏后,在 FlyWithLua 菜单里能看到脚本名字,说明加载成功。如果菜单里没有,别急着改代码,先确认菜单本身有没有打开——这是一个非常丢人的排查经历。
4.2 配置怎么调:VREF、判决高度与跑道表
配置集中在一个 Lua 表里,改完不用重启模拟器,用 FlyWithLua 的 reload 功能重载脚本即可。但注意 reload 之后部分全局变量会残留,状态机会停在上一次的状态,所以重载后先手动把飞机放到停机位,等状态机回到 IDLE。
-- sa_config.lua:机型相关的量全放这里,判据代码不动 SA = { vref = 140, -- 着陆参考速度,节,随机型和着陆重量走 check_imc_ft = 1000, -- 仪表条件下的判决高度 check_vmc_ft = 500, -- 目视条件下的判决高度 sink_limit_fpm = 1000, -- 下降率上限,正数表示"不超过" bank_limit_deg = 15, -- 500 英尺坡度上限 dot_limit = 1.0, -- 航向道/下滑道偏差上限,单位点 touchdown_vs_lim = 300, -- 报告里标"重接地"的提示线,英尺每分 sample_hz = 20, -- 采样频率 runways = { -- 跑道表用于算接地点距离,键名自定,坐标用十进制度 ["DEMO/36"] = { thr_lat = 30.00, thr_lon = 120.00, elev_ft = 30, length_m = 3200 }, }, }vref是最需要认真的一个值。写小了,判据永远显示速度偏高,你会以为脚本坏了;写大了,超速根本判不出来。正确做法是按当前着陆重量查机型手册,一般比失速速度高 1.3 倍。check_imc_ft和check_vmc_ft的区别在于天气:能见度差、云底低的时候用 1000 英尺这一档,目视条件好可以用 500 英尺,两者不是同时生效的,用哪个取决于你在模拟什么运行环境。
runways这张表是可选功能。不填也能跑,只是报告里没有「接地点距离跑道入口多少米」这一项。要填就得填准,坐标差 0.01 度就是一千米量级的误差,算出来的距离毫无意义。
4.3 首飞验证:一段 15 分钟就能跑完的复现航段
装完不要直接上长航线,先做一个可控的验证,把每一环都逼出来。
第一步,选一条有 ILS 的跑道,天气设成静风、能见度 10 公里以上,重量配到中等。第二步,从 8 海里外建立盲降,自动驾驶跟到 1500 英尺,然后断开手飞。第三步,故意在 1200 英尺时把速度加到 VREF+20,观察 1000 英尺的判决是否报「速度高于 VREF+10」。第四步,在 700 英尺时故意压一个 20 度的坡度,看 500 英尺的判决是否报坡度超标。第五步,正常落地,去看 CSV 里是不是多了一行。
这套动作能把状态机跃迁、两次判决点、失败项渲染、文件写入四条路径全部走一遍。如果第四步没报,八成是crossed的下穿检测写成了>=导致在边界帧反复触发又被覆盖;如果第五步没写文件,八成是SCRIPT_DIR拼接的路径目录不存在,io.open返回了nil而代码里只写了if not f then return end,静默失败了。
验证通过之后再去跑真实航线。我一般还会开着tail -f Log.txt跑第一趟,看有没有零散的运行时错误被吞掉。
5. 避坑与排查:五次让我在接地前翻车的具体问题
5.1 现象:HUD 一直显示待机,进近全程不触发
原因:AGL_M的单位是米,而进入进近的判断写的是agl_ft < 3000,实际拿到的是agl_m的值,305 米换算前相当于 1000 英尺,飞机在 900 米高度就已经满足条件,起飞爬升阶段就被误触发,然后又因为gear和速度条件不满足退回 IDLE,反复横跳,进近时反而卡在一个中间态。
解决:在sa_datarefs.lua里就把单位换算做掉,对外只暴露agl_ft,业务代码里不允许出现* 3.28084。这条规矩看起来啰嗦,但能一次性消灭一整类错误。
5.2 现象:报告里的接地下降率是 0 或者个位数
原因:ON_GROUND变 1 的那一帧,vh_ind_fpm已经被地面法向力抹平,读出来接近 0。判定逻辑在跃迁时读的是当前帧而不是接地前的最后一帧。
解决:环形缓冲天然就是后悔药,sa_on_touchdown()里取倒数第二个样本作为接地值,同时用倒数第三个样本校验,两者差异超过 200 fpm 就标记该条记录可疑。X-Plane 的回放功能可以精确复现接地瞬间,用它对比一次就能确认取的是哪一帧。
5.3 现象:进近阶段帧率从 60 掉到 22
原因:sa_draw()在每帧做一次string.format,加上sa_sample_tick()无条件执行,两项叠加。更糟的是有人在采样函数里顺手写了io.write做调试输出,磁盘 IO 直接卡在渲染线程上。
解决:采样函数第一行先做时间戳比较,不满足就return;绘制函数缓存上一次拼好的字符串,数值变化超过阈值才重拼;所有日志只在状态跃迁时写一行,绝不逐帧写。
5.4 现象:航向道和下滑道偏差恒为 0,判据永远通过
原因:nav1_hdef_dots_pilot需要 NAV1 实际调谐到 ILS 频率才有值,忘了调频率、或者飞机默认调的是 VOR,读出来就是 0。另一种情况是在右座飞,读的却是pilot侧,值也不对。
解决:判定前先看偏差值是否有效,真机上这类 dataref 在无信号时会返回 0 而不是 NaN,所以不能靠判空,得结合 NAV1 频率和「是否处于进近状态」一起看。稳妥做法是同时订阅pilot和copilot两套,取绝对值大的那个。
5.5 现象:改了阈值但行为没变
原因:FlyWithLua 的 reload 只重载脚本,sa_criteria.lua用dofile加载后返回的 table 被局部变量持有,重载时机不对就会读到旧表。另一种情况是文件末尾漏了return,dofile返回nil,ipairs直接报错但错误被静默吞掉。
解决:所有模块文件末尾必须有return,加载时做一次非空断言;改完配置后不要靠 reload,直接重进一次飞行,用最笨的方法换确定性。
6. 二次开发:往判据表里加一行,再用回放验证
想加自己的标准,别去动sa_evaluate。比如你飞的是小飞机,想在 500 英尺额外要求「油门不在慢车位」,只要在sa_criteria.lua里加一行:{ id="PWR", field="thr", cmp="lt", limit=0.05, at=500, label="油门收光" },然后在采样结构体里补一个thr字段指向油门杆位置 dataref。整个改动不超过五行,回归测试范围也就在这一条判据上。
导出数据做纵向对比是另一个高频需求。现在的 CSV 一场飞行只留一行,想分析整个进近过程,可以加一个「进近轨迹」开关,在APPROACH状态里每 0.5 秒追加一条t,agl,vs,ias,roll,loc,gs到独立文件。字段别贪多,七个就够,多了以后做图表反而要筛。
验证方法上,X-Plane 自带的回放是最省事的手段:找一段已知不达标的落地录下来,回放时把脚本挂上,看判决点、失败项、接地数据是否与手动飞时一致。回放时间戳和RUN_TIME_S不对齐也没关系,因为脚本用的是相对时间差。我第一次加判据的时候没做这一步,直接上真人飞,结果一个单位写错,飞了三个晚上都在怀疑手感,最后发现是英尺和米的问题。从那以后我每加一条判据,都强制用同一段回放跑三遍:一遍正常、一遍故意越界、一遍边界值,跑完才允许自己上飞机。希望帮到你。
本文还有配套的精品资源,点击获取