godot-ability-system by thedivergentai/gd-agentic-skills
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-ability-system构建灵活、可扩展技能系统的专家指导。
is_casting 或 animation_playing。打断动画会破坏状态机。强制要求:在实现相应模式之前,请先阅读对应的脚本。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
包含冷却注册表、can_use 检查和视觉冷却进度的技能编排。与角色逻辑解耦,可用于玩家、敌人或炮塔。
包含元数据、状态和效果数组的可脚本化技能资源基类。提供可继承的虚拟 execute() 方法(用于 ProjectileAbility、BuffAbility)。
资源驱动的增益系统设置。扩展 Resource 并创建高度模块化、可拖放的技能数据。
# ability_base.gd - 所有技能的基类
class_name Ability
extends Resource
@export var ability_id: String
@export var display_name: String
@export var icon: Texture2D
@export var description: String
@export_group("Costs")
@export var mana_cost: int = 0
@export var stamina_cost: int = 0
@export var health_cost: int = 0 # 生命分流类技能
@export_group("Timing")
@export var cooldown: float = 5.0
@export var cast_time: float = 0.0 # 0 = 瞬发
@export var channel_time: float = 0.0 # 引导类技能
@export_group("Unlocking")
@export var unlock_level: int = 1
@export var prerequisites: Array[String] = [] # 其他技能 ID
## 重写这些方法
func can_cast(caster: Node) -> bool:
return true # 额外检查(范围、目标等)
func execute(caster: Node, target: Node = null) -> void:
pass # 技能效果
func on_cast_start(caster: Node) -> void:
pass # 动画、效果
func on_cast_complete(caster: Node) -> void:
execute(caster)
func on_cancel(caster: Node) -> void:
pass # 返还资源
# fireball.gd
class_name FireballAbility
extends Ability
@export var damage: int = 50
@export var projectile_scene: PackedScene
@export var range: float = 500.0
func can_cast(caster: Node) -> bool:
var target = caster.get_target()
if not target:
return false
var distance := caster.global_position.distance_to(target.global_position)
return distance <= range
func execute(caster: Node, target: Node = null) -> void:
var projectile := projectile_scene.instantiate()
caster.get_parent().add_child(projectile)
projectile.global_position = caster.global_position
projectile.target = target
projectile.damage = damage
# ability_manager.gd
class_name AbilityManager
extends Node
signal ability_cast(ability_id: String)
signal ability_ready(ability_id: String)
signal cooldown_started(ability_id: String, duration: float)
var abilities: Dictionary = {} # ability_id → Ability
var cooldowns: Dictionary = {} # ability_id → float (剩余时间)
var is_casting: bool = false
var global_cooldown: float = 0.0 # GCD 计时器
@export var gcd_duration: float = 1.0 # 全局冷却时间
func register_ability(ability: Ability) -> void:
abilities[ability.ability_id] = ability
cooldowns[ability.ability_id] = 0.0
func can_use_ability(ability_id: String, caster: Node) -> bool:
var ability := abilities.get(ability_id) as Ability
if not ability:
return false
# 检查 GCD
if global_cooldown > 0.0:
return false
# 检查特定冷却
if cooldowns.get(ability_id, 0.0) > 0.0:
return false
# 检查是否正在施法
if is_casting and ability.cast_time > 0.0:
return false
# 检查资源
if not has_resources(caster, ability):
return false
# 技能特定检查
return ability.can_cast(caster)
func use_ability(ability_id: String, caster: Node, target: Node = null) -> bool:
if not can_use_ability(ability_id, caster):
return false
var ability := abilities[ability_id]
# 消耗资源
consume_resources(caster, ability)
# 开始施法
if ability.cast_time > 0.0:
start_cast(ability, caster, target)
else:
# 瞬发施法
ability.execute(caster, target)
trigger_cooldown(ability_id, ability.cooldown)
ability_cast.emit(ability_id)
return true
func start_cast(ability: Ability, caster: Node, target: Node) -> void:
is_casting = true
ability.on_cast_start(caster)
# 为施法完成创建计时器
var timer := get_tree().create_timer(ability.cast_time)
await timer.timeout
if is_casting: # 未被中断
ability.on_cast_complete(caster)
trigger_cooldown(ability.ability_id, ability.cooldown)
is_casting = false
func interrupt_cast() -> void:
if is_casting:
is_casting = false
# 如果需要,触发 ability.on_cancel()
func trigger_cooldown(ability_id: String, duration: float) -> void:
cooldowns[ability_id] = duration
global_cooldown = gcd_duration
cooldown_started.emit(ability_id, duration)
func _physics_process(delta: float) -> void:
# 更新冷却时间
for ability_id in cooldowns.keys():
if cooldowns[ability_id] > 0.0:
cooldowns[ability_id] -= delta
if cooldowns[ability_id] <= 0.0:
ability_ready.emit(ability_id)
# 更新 GCD
if global_cooldown > 0.0:
global_cooldown -= delta
func has_resources(caster: Node, ability: Ability) -> bool:
return (caster.mana >= ability.mana_cost and
caster.stamina >= ability.stamina_cost and
caster.health > ability.health_cost)
func consume_resources(caster: Node, ability: Ability) -> void:
caster.mana -= ability.mana_cost
caster.stamina -= ability.stamina_cost
caster.health -= ability.health_cost
# combo_tracker.gd
extends Node
var combo_chain: Array[String] = []
var combo_window: float = 2.0 # 继续连击的秒数
var last_ability_time: float = 0.0
func register_ability_use(ability_id: String) -> void:
var current_time := Time.get_ticks_msec() * 0.001
# 如果时间间隔过长则重置
if current_time - last_ability_time > combo_window:
combo_chain.clear()
combo_chain.append(ability_id)
last_ability_time = current_time
# 检查连击完成情况
check_combos()
func check_combos() -> void:
# 示例:"slash" → "slash" → "spin" = "whirlwind"
if combo_chain.size() >= 3:
var last_three := combo_chain.slice(-3)
if last_three == ["slash", "slash", "spin"]:
trigger_combo_ability("whirlwind")
combo_chain.clear()
func trigger_combo_ability(combo_id: String) -> void:
# 执行强大的连击技能
pass
# charge_ability.gd - 拥有多次充能的技能(如《英雄联盟》中的闪现)
class_name ChargeAbility
extends Ability
@export var max_charges: int = 2
@export var charge_recharge_time: float = 20.0
var current_charges: int = max_charges
var recharge_timer: float = 0.0
func can_cast(caster: Node) -> bool:
return current_charges > 0
func execute(caster: Node, target: Node = null) -> void:
current_charges -= 1
# 如果未达到最大充能数,开始充能
if current_charges < max_charges and recharge_timer == 0.0:
recharge_timer = charge_recharge_time
func tick(delta: float) -> void:
if recharge_timer > 0.0:
recharge_timer -= delta
if recharge_timer <= 0.0:
current_charges += 1
if current_charges < max_charges:
recharge_timer = charge_recharge_time # 继续充能
else:
recharge_timer = 0.0
# skill_node.gd
class_name SkillNode
extends Resource
@export var skill_id: String
@export var display_name: String
@export var description: String
@export var icon: Texture2D
@export_group("Requirements")
@export var prerequisites: Array[String] = [] # 其他 skill_ids
@export var character_level_required: int = 1
@export var points_required: int = 1
@export var mutually_exclusive_with: Array[String] = [] # 不能同时拥有
@export_group("Progression")
@export var max_rank: int = 1
@export var current_rank: int = 0
@export_group("Effects")
@export var unlocks_ability: String = "" # 要授予的技能 ID
@export var stat_bonuses: Dictionary = {} # "strength": 5, "crit_chance": 0.05
func can_unlock(player_skills: Dictionary, player_level: int, available_points: int) -> bool:
# 已满级
if current_rank >= max_rank:
return false
# 点数不足
if available_points < points_required:
return false
# 等级要求
if player_level < character_level_required:
return false
# 前置条件
for prereq_id in prerequisites:
if not player_skills.has(prereq_id) or player_skills[prereq_id].current_rank == 0:
return false
# 互斥性
for exclusive_id in mutually_exclusive_with:
if player_skills.has(exclusive_id) and player_skills[exclusive_id].current_rank > 0:
return false
return true
func unlock() -> void:
current_rank += 1
# skill_tree.gd
class_name SkillTree
extends Node
signal skill_unlocked(skill_id: String, rank: int)
signal points_changed(new_total: int)
var skills: Dictionary = {} # skill_id → SkillNode
var skill_points: int = 0
func add_skill(skill: SkillNode) -> void:
skills[skill.skill_id] = skill
func can_unlock_skill(skill_id: String, player_level: int) -> bool:
var skill := skills.get(skill_id) as SkillNode
if not skill:
return false
return skill.can_unlock(skills, player_level, skill_points)
func unlock_skill(skill_id: String, player_level: int) -> bool:
if not can_unlock_skill(skill_id, player_level):
return false
var skill := skills[skill_id]
skill.unlock()
skill_points -= skill.points_required
# 应用效果
apply_skill_effects(skill)
skill_unlocked.emit(skill_id, skill.current_rank)
points_changed.emit(skill_points)
return true
func apply_skill_effects(skill: SkillNode) -> void:
# 如果指定了,则授予技能
if skill.unlocks_ability != "":
var ability_manager := get_node("/root/AbilityManager")
# 注册新技能
# 应用状态加成
var player := get_tree().get_first_node_in_group("player")
for stat_name in skill.stat_bonuses.keys():
var bonus = skill.stat_bonuses[stat_name]
player.set(stat_name, player.get(stat_name) + bonus)
func add_skill_points(amount: int) -> void:
skill_points += amount
points_changed.emit(skill_points)
func reset_tree(refund_points: bool = true) -> void:
var total_spent := 0
for skill in skills.values():
total_spent += skill.current_rank * skill.points_required
skill.current_rank = 0
if refund_points:
skill_points += total_spent
points_changed.emit(skill_points)
# 已在上面的 AbilityManager 中展示
# 每个技能有独立的冷却时间
# 所有类型为 "summon" 的技能共享冷却
var summon_cooldown: float = 0.0
func use_summon_ability(ability: Ability) -> void:
ability.execute()
summon_cooldown = 3.0 # 所有召唤技能进入 3 秒冷却
多次使用,随时间充能。
# save_system.gd
func save_ability_cooldowns() -> Dictionary:
var data := {}
var current_time := Time.get_unix_time_from_system()
for ability_id in ability_manager.cooldowns.keys():
var remaining := ability_manager.cooldowns[ability_id]
if remaining > 0.0:
data[ability_id] = current_time + remaining # 绝对时间
return data
func load_ability_cooldowns(data: Dictionary) -> void:
var current_time := Time.get_unix_time_from_system()
for ability_id in data.keys():
var end_time: float = data[ability_id]
var remaining := max(0.0, end_time - current_time)
ability_manager.cooldowns[ability_id] = remaining
# 防止在攻击动画期间滥用技能
func _on_animation_player_animation_started(anim_name: String) -> void:
if anim_name.begins_with("attack_"):
ability_manager.is_casting = true
func _on_animation_player_animation_finished(anim_name: String) -> void:
if anim_name.begins_with("attack_"):
ability_manager.is_casting = false
设计你的技能场景,使其不硬编码依赖于玩家上下文(例如,获取父节点来减少生命值)。相反,父上下文应该注入自身或连接信号。
不要强制执行严格的类检查。依赖鸭子类型:if collision.get_collider().has_method("hit"): collision.get_collider().hit()
对于范围效果技能,将实体分配到组中。通过调用 get_tree().call_group("enemies", "apply_damage", 50) 来高效处理伤害,而不是手动循环。
每周安装数
76
代码仓库
GitHub 星标数
59
首次出现
2026 年 2 月 10 日
安全审计
安装于
gemini-cli74
opencode74
codex73
kimi-cli71
github-copilot71
amp71
Expert guidance for building flexible, extensible ability systems.
is_casting or animation_playing before allowing new casts. Interrupting animations breaks state machines.MANDATORY : Read the appropriate script before implementing the corresponding pattern.
Ability orchestration with cooldown registry, can_use checks, and visual cooldown progress. Decoupled from character logic for use on players, enemies, or turrets.
Scriptable ability resource base class with metadata, stats, and effects array. Virtual execute() method for inheritance (ProjectileAbility, BuffAbility).
Resource-Driven Buff System setup. Extends Resource and creates highly modular, drag-and-drop ability data.
# ability_base.gd - Base class for all abilities
class_name Ability
extends Resource
@export var ability_id: String
@export var display_name: String
@export var icon: Texture2D
@export var description: String
@export_group("Costs")
@export var mana_cost: int = 0
@export var stamina_cost: int = 0
@export var health_cost: int = 0 # Life tap abilities
@export_group("Timing")
@export var cooldown: float = 5.0
@export var cast_time: float = 0.0 # 0 = instant
@export var channel_time: float = 0.0 # Channeled abilities
@export_group("Unlocking")
@export var unlock_level: int = 1
@export var prerequisites: Array[String] = [] # Other ability IDs
## Override these
func can_cast(caster: Node) -> bool:
return true # Additional checks (range, target, etc.)
func execute(caster: Node, target: Node = null) -> void:
pass # Ability effect
func on_cast_start(caster: Node) -> void:
pass # Animation, effects
func on_cast_complete(caster: Node) -> void:
execute(caster)
func on_cancel(caster: Node) -> void:
pass # Refund resources
# fireball.gd
class_name FireballAbility
extends Ability
@export var damage: int = 50
@export var projectile_scene: PackedScene
@export var range: float = 500.0
func can_cast(caster: Node) -> bool:
var target = caster.get_target()
if not target:
return false
var distance := caster.global_position.distance_to(target.global_position)
return distance <= range
func execute(caster: Node, target: Node = null) -> void:
var projectile := projectile_scene.instantiate()
caster.get_parent().add_child(projectile)
projectile.global_position = caster.global_position
projectile.target = target
projectile.damage = damage
# ability_manager.gd
class_name AbilityManager
extends Node
signal ability_cast(ability_id: String)
signal ability_ready(ability_id: String)
signal cooldown_started(ability_id: String, duration: float)
var abilities: Dictionary = {} # ability_id → Ability
var cooldowns: Dictionary = {} # ability_id → float (time remaining)
var is_casting: bool = false
var global_cooldown: float = 0.0 # GCD timer
@export var gcd_duration: float = 1.0 # Global cooldown
func register_ability(ability: Ability) -> void:
abilities[ability.ability_id] = ability
cooldowns[ability.ability_id] = 0.0
func can_use_ability(ability_id: String, caster: Node) -> bool:
var ability := abilities.get(ability_id) as Ability
if not ability:
return false
# Check GCD
if global_cooldown > 0.0:
return false
# Check specific cooldown
if cooldowns.get(ability_id, 0.0) > 0.0:
return false
# Check if already casting
if is_casting and ability.cast_time > 0.0:
return false
# Check resources
if not has_resources(caster, ability):
return false
# Ability-specific checks
return ability.can_cast(caster)
func use_ability(ability_id: String, caster: Node, target: Node = null) -> bool:
if not can_use_ability(ability_id, caster):
return false
var ability := abilities[ability_id]
# Consume resources
consume_resources(caster, ability)
# Start cast
if ability.cast_time > 0.0:
start_cast(ability, caster, target)
else:
# Instant cast
ability.execute(caster, target)
trigger_cooldown(ability_id, ability.cooldown)
ability_cast.emit(ability_id)
return true
func start_cast(ability: Ability, caster: Node, target: Node) -> void:
is_casting = true
ability.on_cast_start(caster)
# Create timer for cast completion
var timer := get_tree().create_timer(ability.cast_time)
await timer.timeout
if is_casting: # Not interrupted
ability.on_cast_complete(caster)
trigger_cooldown(ability.ability_id, ability.cooldown)
is_casting = false
func interrupt_cast() -> void:
if is_casting:
is_casting = false
# Trigger ability.on_cancel() if needed
func trigger_cooldown(ability_id: String, duration: float) -> void:
cooldowns[ability_id] = duration
global_cooldown = gcd_duration
cooldown_started.emit(ability_id, duration)
func _physics_process(delta: float) -> void:
# Tick cooldowns
for ability_id in cooldowns.keys():
if cooldowns[ability_id] > 0.0:
cooldowns[ability_id] -= delta
if cooldowns[ability_id] <= 0.0:
ability_ready.emit(ability_id)
# Tick GCD
if global_cooldown > 0.0:
global_cooldown -= delta
func has_resources(caster: Node, ability: Ability) -> bool:
return (caster.mana >= ability.mana_cost and
caster.stamina >= ability.stamina_cost and
caster.health > ability.health_cost)
func consume_resources(caster: Node, ability: Ability) -> void:
caster.mana -= ability.mana_cost
caster.stamina -= ability.stamina_cost
caster.health -= ability.health_cost
# combo_tracker.gd
extends Node
var combo_chain: Array[String] = []
var combo_window: float = 2.0 # Seconds to continue combo
var last_ability_time: float = 0.0
func register_ability_use(ability_id: String) -> void:
var current_time := Time.get_ticks_msec() * 0.001
# Reset if too much time passed
if current_time - last_ability_time > combo_window:
combo_chain.clear()
combo_chain.append(ability_id)
last_ability_time = current_time
# Check for combo completion
check_combos()
func check_combos() -> void:
# Example: "slash" → "slash" → "spin" = "whirlwind"
if combo_chain.size() >= 3:
var last_three := combo_chain.slice(-3)
if last_three == ["slash", "slash", "spin"]:
trigger_combo_ability("whirlwind")
combo_chain.clear()
func trigger_combo_ability(combo_id: String) -> void:
# Execute powerful combo ability
pass
# charge_ability.gd - Abilities with multiple charges (like League of Legends Flash)
class_name ChargeAbility
extends Ability
@export var max_charges: int = 2
@export var charge_recharge_time: float = 20.0
var current_charges: int = max_charges
var recharge_timer: float = 0.0
func can_cast(caster: Node) -> bool:
return current_charges > 0
func execute(caster: Node, target: Node = null) -> void:
current_charges -= 1
# Start recharging if not at max
if current_charges < max_charges and recharge_timer == 0.0:
recharge_timer = charge_recharge_time
func tick(delta: float) -> void:
if recharge_timer > 0.0:
recharge_timer -= delta
if recharge_timer <= 0.0:
current_charges += 1
if current_charges < max_charges:
recharge_timer = charge_recharge_time # Continue recharging
else:
recharge_timer = 0.0
# skill_node.gd
class_name SkillNode
extends Resource
@export var skill_id: String
@export var display_name: String
@export var description: String
@export var icon: Texture2D
@export_group("Requirements")
@export var prerequisites: Array[String] = [] # Other skill_ids
@export var character_level_required: int = 1
@export var points_required: int = 1
@export var mutually_exclusive_with: Array[String] = [] # Can't have both
@export_group("Progression")
@export var max_rank: int = 1
@export var current_rank: int = 0
@export_group("Effects")
@export var unlocks_ability: String = "" # Ability ID to grant
@export var stat_bonuses: Dictionary = {} # "strength": 5, "crit_chance": 0.05
func can_unlock(player_skills: Dictionary, player_level: int, available_points: int) -> bool:
# Already maxed
if current_rank >= max_rank:
return false
# Not enough points
if available_points < points_required:
return false
# Level requirement
if player_level < character_level_required:
return false
# Prerequisites
for prereq_id in prerequisites:
if not player_skills.has(prereq_id) or player_skills[prereq_id].current_rank == 0:
return false
# Mutual exclusivity
for exclusive_id in mutually_exclusive_with:
if player_skills.has(exclusive_id) and player_skills[exclusive_id].current_rank > 0:
return false
return true
func unlock() -> void:
current_rank += 1
# skill_tree.gd
class_name SkillTree
extends Node
signal skill_unlocked(skill_id: String, rank: int)
signal points_changed(new_total: int)
var skills: Dictionary = {} # skill_id → SkillNode
var skill_points: int = 0
func add_skill(skill: SkillNode) -> void:
skills[skill.skill_id] = skill
func can_unlock_skill(skill_id: String, player_level: int) -> bool:
var skill := skills.get(skill_id) as SkillNode
if not skill:
return false
return skill.can_unlock(skills, player_level, skill_points)
func unlock_skill(skill_id: String, player_level: int) -> bool:
if not can_unlock_skill(skill_id, player_level):
return false
var skill := skills[skill_id]
skill.unlock()
skill_points -= skill.points_required
# Apply effects
apply_skill_effects(skill)
skill_unlocked.emit(skill_id, skill.current_rank)
points_changed.emit(skill_points)
return true
func apply_skill_effects(skill: SkillNode) -> void:
# Grant ability if specified
if skill.unlocks_ability != "":
var ability_manager := get_node("/root/AbilityManager")
# Register new ability
# Apply stat bonuses
var player := get_tree().get_first_node_in_group("player")
for stat_name in skill.stat_bonuses.keys():
var bonus = skill.stat_bonuses[stat_name]
player.set(stat_name, player.get(stat_name) + bonus)
func add_skill_points(amount: int) -> void:
skill_points += amount
points_changed.emit(skill_points)
func reset_tree(refund_points: bool = true) -> void:
var total_spent := 0
for skill in skills.values():
total_spent += skill.current_rank * skill.points_required
skill.current_rank = 0
if refund_points:
skill_points += total_spent
points_changed.emit(skill_points)
# Already shown in AbilityManager above
# Each ability has independent cooldown
# All abilities of type "summon" share cooldown
var summon_cooldown: float = 0.0
func use_summon_ability(ability: Ability) -> void:
ability.execute()
summon_cooldown = 3.0 # All summons on 3s cooldown
Multiple uses, recharges over time.
# save_system.gd
func save_ability_cooldowns() -> Dictionary:
var data := {}
var current_time := Time.get_unix_time_from_system()
for ability_id in ability_manager.cooldowns.keys():
var remaining := ability_manager.cooldowns[ability_id]
if remaining > 0.0:
data[ability_id] = current_time + remaining # Absolute time
return data
func load_ability_cooldowns(data: Dictionary) -> void:
var current_time := Time.get_unix_time_from_system()
for ability_id in data.keys():
var end_time: float = data[ability_id]
var remaining := max(0.0, end_time - current_time)
ability_manager.cooldowns[ability_id] = remaining
# Prevent ability spam during attack animations
func _on_animation_player_animation_started(anim_name: String) -> void:
if anim_name.begins_with("attack_"):
ability_manager.is_casting = true
func _on_animation_player_animation_finished(anim_name: String) -> void:
if anim_name.begins_with("attack_"):
ability_manager.is_casting = false
Design your ability scenes so they have no hardcoded dependencies on the player context (e.g., getting parent nodes to reduce health). Instead, the parent context should inject itself or wire the signals.
Do not enforce strict class checks. Rely on duck-typing: if collision.get_collider().has_method("hit"): collision.get_collider().hit()
For Area-of-Effect abilities, assign entities to groups. Process damage efficiently by calling get_tree().call_group("enemies", "apply_damage", 50) instead of looping manually.
Weekly Installs
76
Repository
GitHub Stars
59
First Seen
Feb 10, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli74
opencode74
codex73
kimi-cli71
github-copilot71
amp71
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
120,000 周安装