强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Godot 3 GDScript 教程 / Godot 3 GDScript 教程(十八):性能优化与调试

性能优化与调试

无论游戏设计多精彩,如果帧率不稳定或加载缓慢,玩家都会流失。本章系统讲解 Godot 3 的性能分析工具、优化策略和调试方法。


性能监视器

关键指标

指标说明目标值
Time: FPS帧率≥ 60
Time: Process每帧处理时间(ms)≤ 16.67
Render: Draw Calls绘制调用次数≤ 100
Render: Object绘制对象数尽量少
Memory: Static静态内存使用因项目而异
Physics: Active活跃刚体数尽量少
# 实时 FPS + 详细性能信息
func _process(delta):
    var perf = Performance
    var info = "FPS: %d\n" % perf.get_monitor(Performance.TIME_FPS)
    info += "Draw Calls: %d\n" % perf.get_monitor(Performance.RENDER_DRAW_CALLS_IN_FRAME)
    info += "Memory: %.2f MB\n" % (perf.get_monitor(Performance.MEMORY_STATIC) / 1048576.0)
    $DebugLabel.text = info

💡 提示:FPS 低于 30 通常意味着有明显的性能问题。稳定 60 FPS 是大多数游戏的目标。


帧率分析

60 FPS → 每帧 16.67 ms    30 FPS → 每帧 33.33 ms

帧时间组成:
├── 物理处理 (Physics Process)
├── 脚本处理 (Process)
├── 渲染 (Rendering)
└── 等待 (Idle)

卡顿检测

var frame_times: Array = []
var warning_threshold: float = 20.0  # ms

func _process(delta):
    var frame_ms = delta * 1000.0
    frame_times.append(frame_ms)
    if frame_times.size() > 120:
        frame_times.pop_front()
    if frame_ms > warning_threshold:
        push_warning("卡顿警告: %.1f ms" % frame_ms)

DrawCall 优化

DrawCall 是 CPU 向 GPU 发送的绘制命令,过多会导致 CPU 瓶颈。

方式DrawCall 数说明
100 个独立 Sprite100每个一个 DrawCall
100 个同图集 Sprite~1自动批处理
100 个不同材质 Mesh100无法批处理
100 个同材质 Mesh~1自动批处理

减少 DrawCall 的策略

# 策略一:使用纹理图集(Texture Atlas)
# 将多个小图合并到一张大图中,同图集的 Sprite 自动合批

# 策略二:使用 MultiMeshInstance(3D 大量同模型物体)
func create_multimesh(objects: Array):
    var multimesh = MultiMesh.new()
    multimesh.instance_count = objects.size()
    multimesh.mesh = preload("res://meshes/tree.obj")
    multimesh.transform_format = MultiMesh.TRANSFORM_3D
    for i in range(objects.size()):
        var t = Transform.IDENTITY
        t.origin = objects[i].position
        multimesh.set_instance_transform(i, t)

    var instance = MultiMeshInstance.new()
    instance.multimesh = multimesh
    add_child(instance)

⚠️ 注意:过多的独立灯光会打断 2D 批处理。控制 Light2D 数量。


LOD 系统

LOD(Level of Detail)根据距离使用不同精度的模型。

extends Spatial

export var lod_distances: Array = [20.0, 50.0, 100.0]
export var lod_meshes: Array = []
var camera: Camera = null

func _ready():
    camera = get_viewport().get_camera()

func _process(delta):
    if camera == null:
        return
    var dist = global_translation.distance_to(camera.global_translation)
    var level = lod_meshes.size() - 1
    for i in range(lod_distances.size()):
        if dist < lod_distances[i]:
            level = i
            break
    $MeshInstance.mesh = lod_meshes[level]

可见性剔除

方式说明适用场景
视锥剔除相机视锥外自动不渲染引擎自动
遮挡剔除被遮挡物体不渲染室内场景
距离剔除超过距离隐藏开放世界
VisibilityNotifier检测是否在视锥内自动管理
# 距离剔除管理
extends Node

export var cull_distance: float = 100.0
var camera: Camera = null

func _ready():
    camera = get_viewport().get_camera()

func _process(delta):
    if camera == null:
        return
    for obj in get_children():
        if obj is Spatial:
            obj.visible = obj.global_translation.distance_to(camera.global_translation) < cull_distance

GDScript 性能技巧

缓存节点引用

# ❌ 每帧查找节点
func _process(delta):
    get_node("UI/HealthBar").value = health

# ✅ 缓存引用
onready var health_bar = $UI/HealthBar
func _process(delta):
    health_bar.value = health

使用类型标注

# ❌ 无类型(慢)
var speed = 100.0
func get_damage(): return 50

# ✅ 有类型(快 2-5 倍)
var speed: float = 100.0
func get_damage() -> int: return 50

距离计算优化

# ❌ distance_to 每帧计算
if pos.distance_to(target) < range:

# ✅ distance_squared_to 避免开方
if pos.distance_squared_to(target) < range * range:

降低检查频率

# ❌ 每帧检查所有敌人
func _process(delta):
    for enemy in enemies:
        check_enemy(enemy)

# ✅ 用 Timer 降低频率
func _ready():
    var timer = Timer.new()
    timer.wait_time = 0.2  # 每 0.2 秒检查一次
    timer.connect("timeout", self, "_check_enemies")
    add_child(timer)
    timer.start()
操作
节点查找get_node() 每帧onready var 缓存
距离计算distance_to()distance_squared_to()
字符串拼接+ 拼接% 格式化
数学函数pow(x, 2)x * x

内存管理

# 释放节点
node.queue_free()  # 延迟到帧末尾释放(推荐)
# 不要使用 free(),除非确定没有其他引用

# 常见内存问题
# - 节点未释放:确保所有临时节点调用 queue_free()
# - 循环引用:使用 weakref()
# - 信号未断开:在 _exit_tree() 中断开
# - 资源缓存:使用 preload() 避免重复加载

对象池

extends Node

var pool: Dictionary = {}

func get_instance(scene_path: String) -> Node:
    if scene_path in pool and pool[scene_path].size() > 0:
        var inst = pool[scene_path].pop_back()
        inst.visible = true
        return inst
    return load(scene_path).instance()

func recycle(scene_path: String, instance: Node):
    instance.visible = false
    instance.get_parent().remove_child(instance)
    if not scene_path in pool:
        pool[scene_path] = []
    pool[scene_path].append(instance)

调试器使用

打印调试

print("Debug: ", value)
printerr("Error: ", error_msg)
prints("Player", name, "HP", hp)  # 空格分印

设置断点

func _process(delta):
    breakpoint  # 强制断点

日志系统

# logger.gd(Autoload)
extends Node

enum LogLevel { DEBUG, INFO, WARNING, ERROR }
var current_level: int = LogLevel.DEBUG

func info(category: String, message: String):
    _log(LogLevel.INFO, category, message)

func error(category: String, message: String):
    _log(LogLevel.ERROR, category, message)

func _log(level: int, category: String, message: String):
    if level < current_level:
        return
    var time = OS.get_datetime()
    var ts = "%02d:%02d:%02d" % [time["hour"], time["minute"], time["second"]]
    var lvl = ["DEBUG", "INFO", "WARN", "ERROR"][level]
    var msg = "[%s] [%s] [%s] %s" % [ts, lvl, category, message]

    match level:
        LogLevel.WARNING: push_warning(msg)
        LogLevel.ERROR: push_error(msg)
        _: print(msg)

# 使用:Logger.info("Game", "关卡加载完成")

性能瓶颈定位

系统化流程

1. 运行游戏,观察 FPS
2. 打开 Profiler,查看哪个类别耗时最多
3. 定位到具体函数
4. 用代码计时确认
5. 优化并验证

代码计时

func benchmark(label: String, callable: FuncRef, iterations: int = 1000):
    var start = OS.get_ticks_usec()
    for i in range(iterations):
        callable.call_func()
    var elapsed = OS.get_ticks_usec() - start
    print("%s: %d 次, 平均 %.2f µs" % [label, iterations, elapsed / float(iterations)])

常见瓶颈及优化

瓶颈类型症状优化方案
CPU 脚本Process 时间高优化算法、减少每帧计算
CPU 物理Physics 时间高减少碰撞形状
CPU 渲染DrawCall 多合批、减少材质切换
GPU 着色器片段着色器复杂简化着色器、降低分辨率
内存持续增长检查泄漏、对象池

游戏开发场景

动态分辨率缩放

extends Node

var base_resolution: Vector2 = Vector2(1920, 1080)
var current_scale: float = 1.0
var target_fps: int = 60

func _process(delta):
    var fps = Performance.get_monitor(Performance.TIME_FPS)
    if fps < target_fps - 5:
        current_scale = max(0.5, current_scale - 0.1 * delta)
    elif fps > target_fps + 10:
        current_scale = min(1.0, current_scale + 0.05 * delta)
    get_viewport().size = base_resolution * current_scale

扩展阅读

💡 总结:使用 Profiler 定位瓶颈是第一步,不要盲目优化。最常见的优化点是:减少 DrawCall(批处理)、降低每帧计算量(缓存、降低频率)、内存管理(对象池、及时释放)。