用 Godot 4 拆解 MOBA 技能:前摇、判定、后摇与冷却如何做成可靠状态机

2 阅读4分钟

用 Godot 4 拆解 MOBA 技能:前摇、判定、后摇与冷却如何做成可靠状态机

OK,OK,大家好,欢迎大家来到大鹏 AI 教育,我是张大鹏。

做 MOBA 原型时,很多人会把技能写成“按下 Q,立刻冲刺并造成伤害”。演示能跑,但只要加入动画、打断、网络同步或连续输入,逻辑很快就会互相打架。

这篇文章借鉴《英雄联盟》技能的节奏感,但不复刻任何英雄或资源。我们只提炼一个通用工程问题:如何在 Godot 4 中把一次技能拆成前摇、激活、后摇和冷却,并让每个阶段都可观察、可测试、可打断。

原创封面:MOBA 技能状态与时间轴

为什么技能不能只是一个函数

一次完整技能至少包含四段时间:

  • 前摇(wind-up):播放起手动作,通常允许转向,但未必允许移动。
  • 激活(active):位移、投射物或伤害判定真正生效。
  • 后摇(recovery):收招阶段,限制下一次动作,维持手感和可读性。
  • 冷却(cooldown):角色可以继续行动,但该技能暂时不可再次使用。

如果把四段全部塞进一个函数和若干 await,取消技能时很容易留下未清理的碰撞区、特效或速度。状态机的价值不是“代码看起来高级”,而是让任意时刻只有一种明确解释。

先把输入从技能逻辑中分离

在 Input Map 中创建 skill_primary,再把 Q 键、手柄按键等输入绑定到这个动作。脚本只读取“主要技能”这个意图,不关心物理按键。

Godot Input Map:用动作名隔离真实按键

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("skill_primary"):
        skill_machine.request_cast()

输入层只发出请求,能否释放由状态机判断。这样 UI 点击、AI 控制或网络指令也能复用同一个入口。

用枚举表达技能生命周期

class_name DashSkill
extends Node

enum Phase { READY, WIND_UP, ACTIVE, RECOVERY, COOLDOWN }

@export var wind_up := 0.18
@export var active_time := 0.12
@export var recovery := 0.25
@export var cooldown := 4.0
@export var dash_speed := 720.0

var phase: Phase = Phase.READY
var phase_left := 0.0
var cast_direction := Vector2.RIGHT

signal phase_changed(next_phase: Phase)
signal hit_window_changed(enabled: bool)

四个持续时间都使用导出变量,让策划能在 Inspector 中调节。phase_changed 给动画、音效和 UI 使用;技能核心不需要直接操作所有表现节点。

请求释放时锁定关键数据

func request_cast() -> bool:
    if phase != Phase.READY:
        return false

    cast_direction = owner.facing_direction.normalized()
    _enter_phase(Phase.WIND_UP, wind_up)
    return true

方向要在释放时锁定,还是在前摇结束时重新读取,取决于设计。两种都合理,但必须明确。竞技技能最怕“代码偶尔读取当前方向”,导致同样输入出现不同结果。

在物理帧推进状态

位移和命中窗口应跟随 _physics_process(),避免渲染帧率改变技能距离。

func _physics_process(delta: float) -> void:
    if phase == Phase.READY:
        return

    phase_left -= delta

    if phase == Phase.ACTIVE:
        owner.velocity = cast_direction * dash_speed
        owner.move_and_slide()

    if phase_left <= 0.0:
        _advance_phase()
func _advance_phase() -> void:
    match phase:
        Phase.WIND_UP:
            _enter_phase(Phase.ACTIVE, active_time)
            hit_window_changed.emit(true)
        Phase.ACTIVE:
            hit_window_changed.emit(false)
            owner.velocity = Vector2.ZERO
            _enter_phase(Phase.RECOVERY, recovery)
        Phase.RECOVERY:
            _enter_phase(Phase.COOLDOWN, cooldown)
        Phase.COOLDOWN:
            _enter_phase(Phase.READY, 0.0)

func _enter_phase(next: Phase, duration: float) -> void:
    phase = next
    phase_left = duration
    phase_changed.emit(next)

伤害区只在 ACTIVE 阶段开启。即使动画比判定更长,也不应该让整段动画都能造成伤害。

打断不是直接改回 READY

眩晕、死亡或场景切换都会打断技能。取消函数必须清理所有副作用:

func cancel() -> void:
    hit_window_changed.emit(false)
    owner.velocity = Vector2.ZERO
    _enter_phase(Phase.READY, 0.0)

实际项目还应在这里停止循环音效、回收特效、解除霸体标记。一个很好用的规则是:凡是在进入阶段时打开的东西,都要在退出或取消时关闭。

用 Timer 还是 delta

两种方案都能工作:

把 Timer 的 timeout 信号连接到接收函数

  • 阶段很多、需要暂停与精确调试时,用一个状态机统一递减 delta 更直观。
  • 独立的一次性事件适合 Timertimeout 信号。
  • 不要同时让 Timer、动画回调和 _physics_process() 争夺同一状态的控制权。

冷却 UI 可以订阅状态信号,再读取 phase_left / cooldown,而不是反过来由 UI 决定技能是否可用。

验收清单

  • 连续狂按技能键只会成功释放一次。
  • 前摇期间没有提前打开伤害区。
  • 激活时间改变后,位移距离仍等于 dash_speed × active_time 的预期结果。
  • 技能被打断后,碰撞区、速度、音效和特效都被清理。
  • 冷却结束只发生一次,并稳定回到 READY
  • 低帧率与高帧率下,技能距离和命中时机基本一致。

小结

MOBA 技能的手感首先来自清楚的时间结构。把输入请求、阶段推进、物理位移、伤害窗口和表现信号拆开后,动画取消、连招、AI 施法和网络同步才有可靠的落点。

这套结构下一步可以继续扩展“可取消窗口”和“输入缓冲”,让后摇并非绝对锁死,而是能在指定帧衔接下一招。

参考资料

  • 《大鹏 Godot 4 游戏开发入门》第 4 章:_physics_process()、Input Map、信号与 Timer
  • AiGameLOL:官方英雄技能数据仅用于私有研究与结构分析;本文未复用官方美术或技能文案
  • Godot 官方文档:Input、Timer、CharacterBody2D