Skip to content

HUD Overlay Development

罐頭 edited this page Oct 16, 2026 · 3 revisions

HUD Overlay 開發指引

本頁是 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 尚未合併的說明已不適用。

1. 先理解資料流

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]
Loading

責任邊界如下:

區域 責任 新增樣式時是否通常要修改
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 是;大多數樣式工作只在這裡

2. 建立最小可運作樣式

2.1 目錄與檔案

在 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,修改時不要把兩個名稱混用。

2.2 動態探索與 author.json

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。

3. HUDCore hook 契約

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 的換檔燈百分比是明確例外,不可與共用估算紅線混用。

4. 設定與額外功能

一般 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 由框架管理

色彩與 CSS 變數

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。

5. 音訊、媒體與渲染迴圈

不要在每個 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 輪詢。

6. 資產、Canvas 與效能

  • 圖片/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。

7. 測試與驗收清單

本地至少完成以下檢查:

  1. index.html 透過合法 HTTP HUD 路徑載入後可初始化,HUDCore.registerStyle() 與 HUDCore.init() ID 相同。
  2. style 能由 /api/hud/styles 發現,selector 顯示正確的 author.json 描述。
  3. 沒有 telemetry、缺欄位、重複/倒序 timestamp 與 UDP 停止但 RAF 持續時,不顯示假即時值;資料恢復後能繼續繪製。
  4. 單位切換、不同最大 RPM、前進/倒車檔位、0 速度、缺省胎溫都可渲染。
  5. master、gauge、motion、cards 等 element 開關真的影響畫面。
  6. scale、glow、custom color 變更不會讓 Canvas 或 DOM 顏色不同步。
  7. 若使用音訊/媒體,測試 success:false、沒有播放裝置、paused 和長文字。
  8. 圖片遺失時仍能完成初始化,並有可辨識的 fallback。
  9. 多次 Launch/Reload/Destroy 不會累積 listener、timer、animation loop 或第二條 WebSocket。
  10. Release package 放在 EXE 同級的 hud_overlay/<name>/ 後可被掃描;內建 package 不受影響。

8. 變更後的維護規則

純文件維護以 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 樣式參考 的特殊設定與限制。

English

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、性能規則、検証チェックリストを上記にまとめています。

Clone this wiki locally