技术拾遗

开发过程中遇到的小问题及解决方法记录


#001 Windows 平台攻击动画卡住问题

日期:2026-01-29

现象

  • 在 Windows 平台上,玩家角色攻击时第一段攻击动画会直接卡住
  • 过一会儿才恢复,无法完成动作也无法接上预输入的第二段连招
  • 同样的代码在 macOS 上运行正常

问题代码

# attack_component.gd
func handle_attack_input(input_direction: Vector2):
    if attack_cooldown > 0:
        attack_cooldown -= get_process_delta_time()  # ❌ 问题在这里
    
    if is_attacking:
        attack_timer += get_process_delta_time()     # ❌ 问题在这里
        # ...

而调用方是在 _physics_process() 中:

# player.gd
func _physics_process(_delta):
    attack_component.handle_attack_input(direction)

根因分析

函数 用途
get_process_delta_time() 返回 _process() 的帧时间(与显示帧率同步,可变)
get_physics_process_delta_time() 返回 _physics_process() 的帧时间(固定物理帧率,通常 60fps)

_physics_process() 中调用了依赖 get_process_delta_time() 的函数,导致:

  1. Windows 上更容易出问题:Windows 对窗口焦点、VSync、帧率调度的处理与 macOS 不同。当窗口失焦或帧率波动时,get_process_delta_time() 可能返回 0 或极小值
  2. 攻击计时器不增长attack_timer += 0 导致永远达不到 attack_duration,动画就"卡住"了
  3. 预输入窗口无法触发:计时器不增长意味着永远进不了预输入检测的时间窗口

解决方案

让函数显式接收 delta 参数,而不是依赖全局获取:

# attack_component.gd
func handle_attack_input(input_direction: Vector2, delta: float = -1.0):
    # 如果没有传入 delta,使用物理帧时间作为后备
    if delta < 0:
        delta = get_physics_process_delta_time()
    
    if attack_cooldown > 0:
        attack_cooldown -= delta  # ✅ 使用传入的 delta
    
    if is_attacking:
        attack_timer += delta     # ✅ 使用传入的 delta
        # ...
# player.gd
func _physics_process(delta):
    attack_component.handle_attack_input(direction, delta)  # ✅ 传入 delta

经验总结

  1. 永远不要在 _physics_process() 中使用 get_process_delta_time(),反之亦然
  2. 显式传递 delta 参数比依赖全局获取更安全、更可复用
  3. 跨平台测试很重要,Windows 和 macOS 在帧率调度上存在差异
  4. 当遇到"某平台正常、另一平台异常"的问题时,优先检查时间相关的代码

涉及文件

  • gdt/scripts/player/components/attack_component.gd
  • gdt/scripts/player/player.gd

#002 Y-Sort 容器中的层级控制

日期:2026-01-29

场景

  • 需要在角色周围添加气场/光环效果
  • 效果应该在角色后面(被角色遮挡),但在地图上面(不被地图遮挡)

失败尝试

方案 问题
气场作为角色子节点,z_index = -1 被地图挡住(地图 z_index 可能也是 0 或更高)
气场作为角色子节点,z_index = 50 跑到角色前面了
使用 show_behind_parent = true 在 Y-Sort 容器中表现不符合预期

根因分析

Y-Sort 容器 (y_sort_enabled = true) 中:

  • 只有直接子节点参与排序
  • 子节点的子节点(孙节点)不会单独参与排序,它们随父节点一起绘制
  • 排序依据是 global_position.yy 值越小越先绘制(视觉上在"后面")

所以,当气场是 Player 的子节点时,它无法独立参与 Y-Sort 排序。

解决方案

把气场作为 YSort_Container直接子节点(与 Player 同级),通过调整 y 坐标让它在 Player "后面"绘制:

# aura_effect.gd
@export var y_offset: float = -1.0  # 负值 = 在 Y-Sort 中先绘制 = 视觉上在后面

func _process(delta):
    if follow_target:
        # 气场的 y 坐标 = 玩家 y 坐标 + 偏移
        global_position = follow_target.global_position + Vector2(0, y_offset)
# player.gd
func _setup_aura_effect():
    # 添加到 YSort_Container(Player 的父节点)而不是 Player 本身
    var ysort_container = get_parent()
    ysort_container.add_child(aura_effect)
    aura_effect.set_follow_target(self)

排序结果

Y-Sort 排序 (按 y 坐标):
  [0] TileMapLayer: y=0.0      ← 地图,先绘制
  [1] AuraEffect: y=599.0      ← 气场,后绘制(在地图上面)
  [2] Player: y=600.0          ← 玩家,最后绘制(在气场上面)

经验总结

  1. Y-Sort 只对直接子节点生效,想让某个效果独立参与排序必须放到同一层级
  2. 通过微调 y 坐标控制绘制顺序是比 z_index 更可靠的方法(在 Y-Sort 中)
  3. 调试时输出各节点的 global_position.y 可以直观看到排序顺序

涉及文件

  • gdt/scripts/player/components/aura_effect.gd
  • gdt/scripts/player/player.gd

持续更新中...