Repository navigation
HUD Overlay Development
本頁是 HUD Overlay 的實作入口,適合要新增樣式、修改既有樣式、接入音訊/媒體或調整 60Hz 渲染的人員。使用者操作請先看 HUD Overlay 總覽;共用資料與卡片契約則分別看 HUD Shared Core、HUD Telemetry Cards。
完整原始規格仍保留在 Repository 的 hud_overlay/HUD_DEVELOPMENT_GUIDE.md。本 Wiki 頁面將它整理成實際開發時的決策順序,並以已發布的 v1.8(43d0abb)實作為準;Repository 指引中的舊敘述仍須對照固定版本程式。
v1.8 的產品 backend、MCP 與 Agent CLI 使用 Rust;Python 不再是產品 backend runtime。舊版 PR #443 尚未合併的說明已不適用。
flowchart LR
UDP[Forza Data Out UDP 60Hz] --> Backend[Rust backend sidecar]
Backend --> TWS[/ws/telemetry]
Backend --> OWS[/ws/overlay]
TWS --> Coord[shared/coordinator.js]
Coord --> Frame[hud:frame]
OWS --> Host[hud_overlay/index.html]
Frame --> Host
Control[Overlay control] --> BC[BroadcastChannel]
BC --> Host
Host --> IFrame[HUD style iframe]
IFrame --> Core[shared/hud-core.js]
Core --> Style[style hooks]
Host --> Cards[TelemetryCardsManager]
責任邊界如下:
| 區域 | 責任 | 新增樣式時是否通常要修改 |
|---|---|---|
backend-rust |
取得 UDP、系統音訊/媒體並提供 API/WebSocket | 否;除非真的新增資料來源 |
frontend Overlay control |
顯示設定、保存設定、發送控制訊息 | 否;一般 style 由動態 scanner 發現 |
hud_overlay/index.html |
launcher host、style URL、iframe、共用卡片與事件轉送 | 否;避免重新建立靜態 HUD 清單 |
hud_overlay/shared |
canonical telemetry、訊息協定、樣式 registry 與卡片 | 只有共用契約需要改變時 |
hud_overlay/<style>/index.html |
單一 HUD 的 DOM/Canvas 與註冊 hook | 是;大多數樣式工作只在這裡 |
在 source tree 建立:
hud_overlay/
custom_dash/
index.html
author.json
assets/ # 有圖片或字型時才需要
頁面至少要引入 shared/hud-base.css 與 shared/hud-core.js,建立一個明確的主容器,再註冊並啟用 style:
HUDCore.registerStyle('custom_dash', {
containerId: 'customDashContainer',
scaleMultiplier: 1.0,
onInit: function (payload) {
// 讀取 payload 的單位與初始狀態
},
onElementsChange: function (elements) {
var container = document.getElementById('customDashContainer');
if (container) {
container.style.display = elements.showGauge === false ? 'none' : 'block';
}
},
onFrame: function (data, payload) {
// 只更新 latest snapshot 或 dirty flag;實際繪圖放到 render loop
},
onAnimate: function () {
// 啟動時執行此樣式的 sweep
}
});
HUDCore.init('custom_dash');registerStyle() 的 id 和 init() 必須相同。原則上也應與目錄名稱相同;目前 fm4ui 是已知例外:探索/URL ID 是 fm4ui,頁面內 registry ID 是 fm4_re,修改時不要把兩個名稱混用。
Backend 的 /api/hud/styles 會掃描內建 /hud 與使用者 /hud_user 目錄。只要有有效的 index.html,一般樣式就會出現在 selector,不需要在 launcher 或 OverlayView.tsx 新增 <option>。建議同時提供:
{
"author": "Your Name",
"description": "Short description of the style and its special features."
}同名的 user package 會覆寫內建 package。shared、assets、telemetry、common、fonts、css、js 是保留名稱,不應拿來當 style ID。
| Hook/欄位 | 必要性 | 實作責任 |
|---|---|---|
containerId |
建議必填 | 主儀表容器,Core 會對它套用標準縮放與顯隱 |
scaleMultiplier |
建議必填 | 以原始畫布和構圖校準的 style 基準倍率 |
scaleBaseline |
選填 | 樣式需要獨立基準縮放時使用 |
globalScaleFactor |
選填 | Core 預設 0.75;只有有獨立 viewport 契約的樣式才覆寫 |
onInit(payload) |
建議提供 | 初始化單位、資產或 style 狀態 |
onElementsChange(elements) |
建議提供 | 映射控制頁的顯示開關 |
onFrame(data, payload) |
核心 | 接收 canonical 顯示 frame;smoothing 可跟隨高刷新率 |
onAudio(data) |
選填 | 接收 32 段頻譜與左右 VU |
onMedia(data) |
選填 | 接收標題、演出者和播放狀態 |
onAnimate() |
建議提供 | 啟動/刷新時的 sweep |
onScale(scale) |
選填 | 只有內部 Canvas/字型也要同步調整時才需要 |
onFrame 的 data 保留原始欄位並提供唯讀 sourceTelemetry;外層顯示欄位可能插值,原始證據仍須由 sourceTelemetry 取得。coordinator 補上可直接使用的欄位,例如 speed、speed_kmh、speed_mph、rpm、gear、throttle、brake、power、torque、boost、TireTemp、susp_fl、slip_fl、heading_deg、sessionMaxima 和 lockup。payload 會帶 isMetric、redlineRpm、sessionMaxima、lockup、lcState 等共用資訊。沿用共用單位與檔位語意;若來源補零/截負不符合嚴格資料需求,依樣式契約讀取原始證據。ST8100 的換檔燈百分比是明確例外,不可與共用估算紅線混用。
一般 style 只需要共用設定:scale、unit/effectiveUnits、elements、glowIntensity、customColor、useDefaultColors、enableSmoothing。設定透過 /api/overlay/config 初始化,之後 backend 的 hud:config 傳入 launcher,再轉成 iframe 的 { type: "config", data: config };控制頁 BroadcastChannel 也使用 config。Core 在收到 config 時計算最終 scale(包含 globalScaleFactor),hud:scale 只重新套用,沒有 hud:set-scale handler。
只有在畫面真的消費設定時,才把它寫入文件或 UI。已確認的特殊例子:
| 功能 | 設定 | 目前行為 |
|---|---|---|
| S650 主題 | s650Theme |
normal、heritage67、foxbody、track
|
| S650 中央 widget | s650CenterWidget |
disable、drive、tire_temp、performance、music
|
| S650 垂直偏移 | s650HmiOffsetY |
由 HMI outer container 消費 |
| VFD VU 位置 | vfdVuOffset |
VFD 會讀取並套用 |
| VFD audio 位置 | vfdAudioOffset |
設定欄位存在,但目前 VFD renderer 尚未消費 |
| Classic JDM |
classicJdmTachStyle、classicJdmAux1/2 等 |
TRD/Defi、三聯錶與主題;油溫/油壓 fallback 限制見樣式頁 |
| LFA 展開 |
lfaManualExpand、lfaAutoExpand
|
手動或依已確認比賽/有效媒體展開 |
| R34 MFD |
r34MfdMode、r34ShowCluster、r34Lighting
|
五頁、四錶顯隱與日夜 |
| ST8100 | 專屬 StackSt8100Settings
|
盤面/量程、LCD 欄位、三組警示 |
| Drift 解析度 preset | 無 | 已移除;固定框架由 Drift HUD 繪製,外層 scale/viewport 由框架管理 |
Core 會同步:--hud-glow-intensity、--hud-custom-color、window._currentCustomColor、window._currentUseDefaultColors。Canvas 和 DOM 必須使用同一份 palette;建議至少分成 primary、accent、dim、hot、glow 五種語意色。自訂色適合覆寫 primary/accent/glow,紅線和警示色應維持獨立。
共用卡片另有 --tc-font-scale、--tc-corners-scale、--tc-gradar-scale、G-Force/四角卡的 offset 變數;Compass 位置由 manager 管理,詳見 HUD Telemetry Cards。
不要在每個 iframe 重新建立 WebSocket。launcher 已接收 /ws/overlay 的 hud:audio 和 hud:media,再轉交給 HUDCore:
var latestTelemetry = null;
var frameId = null;
var stopped = false;
var audioState = { spectrum: new Array(32).fill(0), vu_left: 0, vu_right: 0 };
var mediaState = { title: '', artist: '', status: 'none', has_media: false };
HUDCore.registerStyle('radio_dash', {
containerId: 'radioDashContainer',
onAudio: function (data) {
if (!data || data.success !== true || !Array.isArray(data.spectrum)) return;
if (data.spectrum.length !== 32) return;
audioState.spectrum = data.spectrum.map(function (value) {
return Math.max(0, Math.min(1, Number(value) || 0));
});
audioState.vu_left = Math.max(0, Math.min(1, Number(data.vu_left) || 0));
audioState.vu_right = Math.max(0, Math.min(1, Number(data.vu_right) || 0));
},
onMedia: function (data) {
mediaState = data && data.success === true
? data
: { title: '', artist: '', status: 'none', has_media: false };
},
onFrame: function (data) {
latestTelemetry = data;
}
});
function renderLoop() {
if (stopped) return;
drawHud(latestTelemetry, audioState, mediaState); // 由樣式實作
frameId = requestAnimationFrame(renderLoop);
}
function stopStyle() {
stopped = true;
if (frameId !== null) cancelAnimationFrame(frameId);
window.removeEventListener('message', onLifecycleMessage);
window.removeEventListener('pagehide', stopStyle);
}
function onLifecycleMessage(event) {
if (event.data && event.data.type === 'hud:destroy') stopStyle();
}
window.addEventListener('message', onLifecycleMessage);
window.addEventListener('pagehide', stopStyle);
HUDCore.init('radio_dash');
frameId = requestAnimationFrame(renderLoop);音訊與媒體以各自的 backend 快取/廣播節奏更新,不保證與遊戲封包或畫面刷新同步。HUD 保存最新狀態,在受控 render loop 中繪製;音訊中斷時讓 spectrum 衰減或歸零,媒體不可用時回到空狀態。上例只示範 hook/生命週期,完整音訊樣式還須實作 freshness 與衰減。
Core 的 hud:destroy 會清空 body 及該 window 的 cards,但沒有通用 onDestroy hook,不會自動取消樣式自己的 RAF、timer 或 listener。S650/LFA 的唯讀媒體 health 流程是有界的既有例外;不要複製成每 frame 的 API 輪詢。
- 圖片/sprite 在頁面載入時預載,
onFrame不建立Image、DOM 或大陣列。 -
onload和onerror都必須計入完成數,避免一張遺失圖片讓 sweep 永遠卡在未就緒。 - 高解析度畫面可採靜態/動態 Canvas 分層;Advanced 是兩層 Canvas 的參考。
-
onFrame只更新快照、平滑狀態或 dirty flag,完整繪圖交給requestAnimationFrame。 - 不使用
JSON.parse(JSON.stringify(data));不要在 60Hz 路徑深拷貝 telemetry。 - DOM 數字、badge、可存取文字留在 DOM;連續幾何、遮罩、漸層和 glow 放在 Canvas。
- motion effect 要受
elements.showMotionEffect控制,關閉時清除 transform/filter 與平滑狀態。 - 不要在樣式內寫死
position:absolute; bottom/right來定位主 HUD;讓.hud-root-wrapper和 Core 統一處理 viewport。
本地至少完成以下檢查:
-
index.html透過合法 HTTP HUD 路徑載入後可初始化,HUDCore.registerStyle()與HUDCore.init()ID 相同。 - style 能由
/api/hud/styles發現,selector 顯示正確的author.json描述。 - 沒有 telemetry、缺欄位、重複/倒序 timestamp 與 UDP 停止但 RAF 持續時,不顯示假即時值;資料恢復後能繼續繪製。
- 單位切換、不同最大 RPM、前進/倒車檔位、0 速度、缺省胎溫都可渲染。
- master、gauge、motion、cards 等 element 開關真的影響畫面。
- scale、glow、custom color 變更不會讓 Canvas 或 DOM 顏色不同步。
- 若使用音訊/媒體,測試
success:false、沒有播放裝置、paused 和長文字。 - 圖片遺失時仍能完成初始化,並有可辨識的 fallback。
- 多次 Launch/Reload/Destroy 不會累積 listener、timer、animation loop 或第二條 WebSocket。
- Release package 放在 EXE 同級的
hud_overlay/<name>/後可被掃描;內建 package 不受影響。
純文件維護以 git diff --check 與固定版本來源路徑檢查驗證,不需聲稱執行程式測試。實作/UI 變更則依專案分流執行前端測試與必要的 browser/native 驗收;合成 browser 截圖不等同 Windows click-through 或真實遊戲驗收。
若只新增單一樣式,優先只改 hud_overlay/<style>/ 與 author.json。若要改 HUDCore、coordinator、訊息名稱或卡片設定,必須同步檢查所有 18 個內建 package(含 3 個 WIP)、launcher、Overlay control、測試與本 Wiki 的契約頁。變更前先讀 HUD Shared Core 和 HUD Telemetry Cards,變更後再更新 HUD 樣式參考 的特殊設定與限制。
This page is the implementation guide for adding or maintaining HUD styles. A style is discovered dynamically when its directory contains a valid index.html; it registers with HUDCore, consumes canonical hud:frame data, and should render through a paced requestAnimationFrame loop. Do not create a second telemetry WebSocket. Use sourceTelemetry for strict freshness, alarms and history; display aliases may be interpolated or defaulted. Config reaches the iframe as config, and styles own cleanup of their timers/listeners. Audio/media hooks, custom packages, asset preloading, performance rules, and the validation checklist are described above.
このページは HUD スタイルの追加・保守向けの開発ガイドです。有効な index.html を含むディレクトリは動的に検出され、HUDCore に登録して canonical な hud:frame データを利用します。各 iframe で telemetry WebSocket を再作成せず、鮮度/警示/履歴の証拠には sourceTelemetry を使用してください。iframe の設定名は config で、timer/listener の解放は各スタイルの責任です。音声/メディア hook、カスタム package、アセット preload、性能規則、検証チェックリストを上記にまとめています。
FH6-HorizonTuner Wiki · 公開版 v1.8 / 43d0abb 基準(2026-10-16 核對)· EV/CVT 驗收限制請見 調校工作台 · Repository · Release v1.8