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 个独立 Sprite | 100 | 每个一个 DrawCall |
| 100 个同图集 Sprite | ~1 | 自动批处理 |
| 100 个不同材质 Mesh | 100 | 无法批处理 |
| 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(批处理)、降低每帧计算量(缓存、降低频率)、内存管理(对象池、及时释放)。