7. 驾驶舱仪表盘
2026/4/14大约 4 分钟
驾驶舱仪表盘
飞行模拟器最有代入感的部分就是驾驶舱。坐在满是仪表和开关的驾驶舱里,看着速度表爬升、高度表变化、罗盘指向目的地——这种感觉是飞行模拟器独特的魅力。本章实现一套完整的模拟驾驶舱仪表盘。
本章你将学到
- 六大基础仪表:速度表、高度表、姿态仪、升降速度表、转弯协调仪、航向仪
- 发动机仪表:转速、油量、油温
- 导航仪表:VOR、ADF、GPS
- 第一人称驾驶舱视角
- 仪表的 3D 物理表盘(看起来像真的)
六大飞行仪表(经典六件套)
每一架飞机都有这六个基础仪表,它们是飞行员最重要的信息来源:
驾驶舱中央仪表布局(标准T形布局):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 空速表 │ │ 姿态仪 │ │ 高度表 │
│ (ASI) │ │ (ADI) │ │ (ALT) │
└──────────┘ └──────────┘ └──────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 转弯协调 │ │ 航向仪 │ │ 升降速度 │
│ (TC) │ │ (HI) │ │ (VSI) │
└──────────┘ └──────────┘ └──────────┘| 仪表 | 全称 | 显示内容 | 重要性 |
|---|---|---|---|
| 空速表 | Air Speed Indicator | 飞机相对空气的速度(节) | 极重要 |
| 姿态仪 | Attitude Director Indicator | 飞机俯仰角和横滚角 | 极重要 |
| 高度表 | Altimeter | 飞行高度(英尺或米) | 极重要 |
| 升降速度表 | Vertical Speed Indicator | 每分钟爬升/下降速度 | 重要 |
| 转弯协调仪 | Turn Coordinator | 转弯速率和侧滑 | 重要 |
| 航向仪 | Heading Indicator | 飞行方向(度) | 重要 |
实现仪表盘系统
仪表盘在 Godot 中有两种实现方式:
- 3D 物理表盘:真实的 3D 模型,指针旋转(更沉浸,性能消耗较高)
- 2D 叠加层:用 CanvasLayer 画 2D 仪表(简单快速)
建议先用 2D 叠加层快速实现,后期再升级为 3D 物理表盘:
GDScript
extends CanvasLayer
## 驾驶舱仪表盘控制器
class_name CockpitInstruments
@onready var aircraft: RealisticFlightController = get_parent()
## 空速表组件
@onready var asi_needle: Control = $SpeedIndicator/Needle
@onready var asi_value: Label = $SpeedIndicator/Value
## 高度表组件
@onready var alt_needle_100: Control = $Altimeter/Needle100ft
@onready var alt_needle_1000: Control = $Altimeter/Needle1000ft
@onready var alt_value: Label = $Altimeter/Value
## 姿态仪组件
@onready var adi_horizon: TextureRect = $AttitudeIndicator/Horizon ## 地平线图片
@onready var adi_pitch_mark: Control = $AttitudeIndicator/PitchMarker
## 航向仪
@onready var hi_compass: Control = $HeadingIndicator/CompassRose
## 升降速度表
@onready var vsi_needle: Control = $VSI/Needle
func _process(_delta: float) -> void:
_update_airspeed()
_update_altitude()
_update_attitude()
_update_heading()
_update_vsi()
func _update_airspeed() -> void:
## 空速表:0-200节,指针旋转270度
var knots := aircraft.airspeed / 0.514
var rotation := knots / 200.0 * 270.0 ## 映射到角度
asi_needle.rotation_degrees = -135.0 + rotation ## 从-135度开始
asi_value.text = "%d kts" % int(knots)
func _update_altitude() -> void:
var feet := aircraft.altitude * 3.28084 ## 米转英尺
## 高度表有两根指针:百位和千位
alt_needle_100.rotation_degrees = fmod(feet, 1000.0) / 1000.0 * 360.0
alt_needle_1000.rotation_degrees = feet / 10000.0 * 360.0
alt_value.text = "%d ft" % int(feet)
func _update_attitude() -> void:
## 姿态仪:地平线图片随俯仰角上下移动,随横滚角旋转
var pitch_deg := rad_to_deg(aircraft.get_pitch_angle()) ## TODO: 实现获取俯仰角
var roll_deg := rad_to_deg(aircraft.get_roll_angle()) ## TODO: 实现获取横滚角
## 地平线移动:每度俯仰对应10像素
adi_horizon.position.y = pitch_deg * 10.0
adi_horizon.rotation_degrees = -roll_deg
func _update_heading() -> void:
## 航向仪:罗盘花逆时针旋转(飞机朝北时不转,朝东时转-90度)
var heading := aircraft.get_heading_degrees() ## TODO: 获取磁航向
hi_compass.rotation_degrees = -heading
func _update_vsi() -> void:
## 升降速度表:±2000英尺/分钟,指针旋转-135到+135度
var vspeed_fpm := aircraft.linear_velocity.y * 196.85 ## 米/秒转英尺/分钟
var rotation := clampf(vspeed_fpm / 2000.0, -1.0, 1.0) * 135.0
vsi_needle.rotation_degrees = rotationC
using Godot;
public partial class CockpitInstruments : CanvasLayer
{
private RealisticFlightController _aircraft;
private Control _asiNeedle;
private Label _asiValue;
private Control _altNeedle100, _altNeedle1000;
private Label _altValue;
private TextureRect _adiHorizon;
private Control _hiCompass;
private Control _vsiNeedle;
public override void _Ready()
{
_aircraft = GetParent<RealisticFlightController>();
_asiNeedle = GetNode<Control>("SpeedIndicator/Needle");
_asiValue = GetNode<Label>("SpeedIndicator/Value");
_altNeedle100 = GetNode<Control>("Altimeter/Needle100ft");
_altNeedle1000 = GetNode<Control>("Altimeter/Needle1000ft");
_altValue = GetNode<Label>("Altimeter/Value");
_adiHorizon = GetNode<TextureRect>("AttitudeIndicator/Horizon");
_hiCompass = GetNode<Control>("HeadingIndicator/CompassRose");
_vsiNeedle = GetNode<Control>("VSI/Needle");
}
public override void _Process(double delta)
{
UpdateAirspeed();
UpdateAltitude();
UpdateAttitude();
UpdateHeading();
UpdateVSI();
}
private void UpdateAirspeed()
{
float knots = _aircraft.Airspeed / 0.514f;
_asiNeedle.RotationDegrees = -135f + knots / 200f * 270f;
_asiValue.Text = $"{(int)knots} kts";
}
private void UpdateAltitude()
{
float feet = _aircraft.Altitude * 3.28084f;
_altNeedle100.RotationDegrees = feet % 1000f / 1000f * 360f;
_altNeedle1000.RotationDegrees = feet / 10000f * 360f;
_altValue.Text = $"{(int)feet} ft";
}
private void UpdateAttitude()
{
// TODO: 从飞机获取俯仰角和横滚角
float pitchDeg = 0f;
float rollDeg = 0f;
_adiHorizon.Position = new Vector2(_adiHorizon.Position.X, pitchDeg * 10f);
_adiHorizon.RotationDegrees = -rollDeg;
}
private void UpdateHeading()
{
// TODO: 从飞机获取磁航向
_hiCompass.RotationDegrees = 0f;
}
private void UpdateVSI()
{
float vspeedFpm = _aircraft.LinearVelocity.Y * 196.85f;
_vsiNeedle.RotationDegrees = Mathf.Clamp(vspeedFpm / 2000f, -1f, 1f) * 135f;
}
}导航仪表
GPS导航显示
GDScript
extends Control
## GPS导航显示器
class_name GPSDisplay
@export var waypoints: Array[Vector3] = [] ## 导航路径点
var current_waypoint_index: int = 0
@onready var aircraft: RealisticFlightController = get_node("/root/Player")
@onready var distance_label: Label = $DistanceLabel
@onready var bearing_label: Label = $BearingLabel
@onready var eta_label: Label = $ETALabel
func _process(_delta: float) -> void:
if current_waypoint_index >= waypoints.size():
return
var target := waypoints[current_waypoint_index]
var to_target := target - aircraft.global_position
## 距离
var distance_km := to_target.length() / 1000.0
distance_label.text = "%.1f km" % distance_km
## 方位角(飞往目标的磁航向)
var bearing := rad_to_deg(atan2(to_target.x, to_target.z))
if bearing < 0:
bearing += 360.0
bearing_label.text = "%03d°" % int(bearing)
## 预计到达时间
if aircraft.airspeed > 1.0:
var eta_seconds := to_target.length() / aircraft.airspeed
var eta_min := int(eta_seconds / 60)
var eta_sec := int(eta_seconds) % 60
eta_label.text = "%d:%02d" % [eta_min, eta_sec]C
using Godot;
using Godot.Collections;
public partial class GPSDisplay : Control
{
[Export] public Array<Vector3> Waypoints = new();
private int _currentWaypointIndex = 0;
private RealisticFlightController _aircraft;
private Label _distanceLabel, _bearingLabel, _etaLabel;
public override void _Ready()
{
_aircraft = GetNode<RealisticFlightController>("/root/Player");
_distanceLabel = GetNode<Label>("DistanceLabel");
_bearingLabel = GetNode<Label>("BearingLabel");
_etaLabel = GetNode<Label>("ETALabel");
}
public override void _Process(double delta)
{
if (_currentWaypointIndex >= Waypoints.Count) return;
var target = Waypoints[_currentWaypointIndex];
var toTarget = target - _aircraft.GlobalPosition;
float distKm = toTarget.Length() / 1000f;
_distanceLabel.Text = $"{distKm:F1} km";
float bearing = Mathf.RadToDeg(Mathf.Atan2(toTarget.X, toTarget.Z));
if (bearing < 0) bearing += 360f;
_bearingLabel.Text = $"{(int)bearing:D3}°";
if (_aircraft.Airspeed > 1f)
{
float etaSec = toTarget.Length() / _aircraft.Airspeed;
_etaLabel.Text = $"{(int)etaSec / 60}:{(int)etaSec % 60:D2}";
}
}
}下一步
驾驶舱完成后,进行 音效与视觉效果的完善。
