用 Godot 4 拆解 MOBA 技能:前摇、判定、后摇与冷却如何做成可靠状态机
OK,OK,大家好,欢迎大家来到大鹏 AI 教育,我是张大鹏。
做 MOBA 原型时,很多人会把技能写成“按下 Q,立刻冲刺并造成伤害”。演示能跑,但只要加入动画、打断、网络同步或连续输入,逻辑很快就会互相打架。
这篇文章借鉴《英雄联盟》技能的节奏感,但不复刻任何英雄或资源。我们只提炼一个通用工程问题:如何在 Godot 4 中把一次技能拆成前摇、激活、后摇和冷却,并让每个阶段都可观察、可测试、可打断。
为什么技能不能只是一个函数
一次完整技能至少包含四段时间:
- 前摇(wind-up):播放起手动作,通常允许转向,但未必允许移动。
- 激活(active):位移、投射物或伤害判定真正生效。
- 后摇(recovery):收招阶段,限制下一次动作,维持手感和可读性。
- 冷却(cooldown):角色可以继续行动,但该技能暂时不可再次使用。
如果把四段全部塞进一个函数和若干 await,取消技能时很容易留下未清理的碰撞区、特效或速度。状态机的价值不是“代码看起来高级”,而是让任意时刻只有一种明确解释。
先把输入从技能逻辑中分离
在 Input Map 中创建 skill_primary,再把 Q 键、手柄按键等输入绑定到这个动作。脚本只读取“主要技能”这个意图,不关心物理按键。
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
两种方案都能工作:
- 阶段很多、需要暂停与精确调试时,用一个状态机统一递减
delta更直观。 - 独立的一次性事件适合
Timer和timeout信号。 - 不要同时让 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