4. 机场与地形系统
2026/4/14大约 4 分钟
机场与地形系统
飞行模拟器的世界是飞机的舞台。一个好的飞行模拟器需要有真实感的地形——山脉、河流、城市——以及功能完整的机场——跑道、停机坪、塔台、机库。本章讲解如何构建这些环境。
本章你将学到
- 地形生成:高度图方法快速建立大范围地形
- 机场系统:跑道、滑行道、停机坪的程序化生成
- 程序化城市:在地形上自动放置建筑群
- 导航辅助:跑道灯光系统、进近灯
- 跑道数据结构设计
地形系统
飞行模拟器的地形需要面积大(100km²以上)但又不能占用太多内存。高度图地形是最佳方案:
每个像素代表一块地形区域,像素的灰度值(0-255)映射到地形高度(比如0-4000米)。
地形分块加载
因为地形太大,只加载玩家附近的区域:
GDScript
extends Node3D
## 地形分块管理器 - 动态加载/卸载地形块
class_name TerrainChunkManager
@export var chunk_size: float = 5000.0 ## 每块地形的边长(米)
@export var view_distance: int = 2 ## 加载半径(格数)
@export var chunk_scene: PackedScene ## 地形块场景
var loaded_chunks: Dictionary = {} ## key: Vector2i(chunk_x, chunk_z) value: Node3D
var player_aircraft: Node3D
func _process(_delta: float) -> void:
if player_aircraft == null:
return
_update_loaded_chunks()
func _update_loaded_chunks() -> void:
## 计算玩家当前所在的地形格
var player_pos := player_aircraft.global_position
var current_chunk := Vector2i(
int(player_pos.x / chunk_size),
int(player_pos.z / chunk_size)
)
## 计算需要加载的所有格子
var needed_chunks: Array[Vector2i] = []
for dx in range(-view_distance, view_distance + 1):
for dz in range(-view_distance, view_distance + 1):
needed_chunks.append(current_chunk + Vector2i(dx, dz))
## 加载新格子
for chunk_coord in needed_chunks:
if chunk_coord not in loaded_chunks:
_load_chunk(chunk_coord)
## 卸载不再需要的格子
var to_remove: Array = []
for chunk_coord in loaded_chunks:
if chunk_coord not in needed_chunks:
to_remove.append(chunk_coord)
for chunk_coord in to_remove:
_unload_chunk(chunk_coord)
func _load_chunk(coord: Vector2i) -> void:
var chunk := chunk_scene.instantiate() as Node3D
chunk.global_position = Vector3(coord.x * chunk_size, 0, coord.y * chunk_size)
add_child(chunk)
loaded_chunks[coord] = chunk
func _unload_chunk(coord: Vector2i) -> void:
var chunk := loaded_chunks[coord]
chunk.queue_free()
loaded_chunks.erase(coord)C
using Godot;
using System.Collections.Generic;
public partial class TerrainChunkManager : Node3D
{
[Export] public float ChunkSize = 5000f;
[Export] public int ViewDistance = 2;
[Export] public PackedScene ChunkScene;
private Dictionary<Vector2I, Node3D> _loadedChunks = new();
public Node3D PlayerAircraft { get; set; }
public override void _Process(double delta)
{
if (PlayerAircraft == null) return;
UpdateLoadedChunks();
}
private void UpdateLoadedChunks()
{
var pos = PlayerAircraft.GlobalPosition;
var current = new Vector2I((int)(pos.X / ChunkSize), (int)(pos.Z / ChunkSize));
var needed = new HashSet<Vector2I>();
for (int dx = -ViewDistance; dx <= ViewDistance; dx++)
for (int dz = -ViewDistance; dz <= ViewDistance; dz++)
needed.Add(current + new Vector2I(dx, dz));
foreach (var coord in needed)
if (!_loadedChunks.ContainsKey(coord))
LoadChunk(coord);
var toRemove = new List<Vector2I>();
foreach (var coord in _loadedChunks.Keys)
if (!needed.Contains(coord)) toRemove.Add(coord);
foreach (var coord in toRemove) UnloadChunk(coord);
}
private void LoadChunk(Vector2I coord)
{
var chunk = ChunkScene.Instantiate<Node3D>();
chunk.GlobalPosition = new Vector3(coord.X * ChunkSize, 0, coord.Y * ChunkSize);
AddChild(chunk);
_loadedChunks[coord] = chunk;
}
private void UnloadChunk(Vector2I coord)
{
_loadedChunks[coord].QueueFree();
_loadedChunks.Remove(coord);
}
}机场设计
跑道数据结构
一个机场可以有多条跑道,每条跑道有两个方向(编号):
GDScript
## 跑道数据
class_name RunwayData
var runway_id: String ## 跑道编号(如 "01/19")
var center_position: Vector3 ## 跑道中心点
var heading: float ## 跑道方向(磁航向,度)
var length: float ## 跑道长度(米)
var width: float ## 跑道宽度(米)
var surface: String ## 跑道面材质("asphalt"、"concrete"、"grass")
## 返回跑道某一端的三维坐标
func get_threshold(is_low_end: bool) -> Vector3:
var dir := Vector3(sin(deg_to_rad(heading)), 0, cos(deg_to_rad(heading)))
var half_len := length / 2.0
return center_position + dir * (half_len if is_low_end else -half_len)C
using Godot;
public class RunwayData
{
public string RunwayId;
public Vector3 CenterPosition;
public float Heading;
public float Length;
public float Width;
public string Surface;
public Vector3 GetThreshold(bool isLowEnd)
{
var dir = new Vector3(Mathf.Sin(Mathf.DegToRad(Heading)), 0, Mathf.Cos(Mathf.DegToRad(Heading)));
float halfLen = Length / 2f;
return CenterPosition + dir * (isLowEnd ? halfLen : -halfLen);
}
}跑道灯光系统
跑道灯是夜间飞行和低能见度降落的关键导引:
| 灯型 | 颜色 | 位置 | 作用 |
|---|---|---|---|
| 跑道边灯 | 白色 | 跑道两侧 | 标示跑道边界 |
| 接地区灯 | 白色 | 跑道入口1/3段 | 标示最佳接地区 |
| 跑道入口灯 | 绿色 | 跑道起始端 | 标示跑道起点 |
| 跑道末端灯 | 红色 | 跑道末端 | 警示不可超越 |
| 进近灯系统 | 白色序列 | 跑道外延伸段 | 引导飞机对正跑道 |
| PAPI | 红/白 | 跑道侧方 | 指示下滑角是否正确 |
PAPI灯实现(精密进近坡度指示器)
PAPI是降落时最重要的目视参考。它有4个灯,根据飞机是否在正确下滑道上显示不同颜色组合:
飞机太高:4白0红 ●●●●
略高: 3白1红 ●●●○
正确: 2白2红 ●●○○ ← 目标
略低: 1白3红 ●○○○
飞机太低:0白4红 ○○○○下一步
机场和地形搭建完成后,进入 天气系统。
