
Godot Genre Fighting
- 135 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-genre-fighting for development tasks
About
godot-genre-fighting: A skill for development. This provides functionality for development workflows.
- godot-genre-fighting
Godot Genre Fighting by the numbers
- 135 all-time installs (skills.sh)
- +10 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,658 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-genre-fightingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 135 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-genre-fighting for development tasks
Files
Genre: Fighting Game
Expert blueprint for 2D/3D fighters emphasizing frame-perfect combat and competitive balance.
NEVER Do (Expert Anti-Patterns)
Frame-Data & Logic
- NEVER use variable framerates; strictly lock logic to a Deterministic Fixed Loop (using
_physics_processwith a frame-counter) and call `reset_physics_interpolation()` on teleport. - NEVER use standard Physics for hit detection; strictly use `PhysicsDirectSpaceState.intersect_shape()` to query hitboxes instantly without Area2D signal lag.
- NEVER skip Damage Scaling; strictly apply 10% reduction per hit in a combo to prevent infinite matches.
- NEVER make all moves safe on block; strictly ensure high-reward moves have Recovery Windows where the attacker is punishable.
- NEVER rely on
Area2D.get_overlapping_areas(); strictly use `intersect_shape()` for immediate, frame-perfect resolution. - NEVER forget Hitbox Proximity (Proximity Guard); strictly trigger guard states when a hitbox enters a nearby zone, even if it hasn't landed.
Character & Animation
- NEVER use simple parenting (
scale.x = -1) for character flip; strictly adjust the dedicated Visuals node while managing hitbox offsets programmatically. - NEVER use string-based animation triggers; strictly use
AnimationMixerwithADVANCE_MANUALfor frame-synced playback. - NEVER use
yieldorawaitfor frame-critical logic; strictly use Integer Frame Counting within state machines to manage recovery/startup windows perfectly. - NEVER store frame data in raw scripts; strictly use `Resource` files (.tres) with delegated logic for damage scaling, cancels, and combo-state tracking.
- NEVER use deep node hierarchies for character parts; strictly keep skeletons shallow to reduce transformation overhead.
Input & Networking
- NEVER skip Input Buffering; strictly implement a 5-10 frame buffer to ensure lenient, responsive execution for the player.
- NEVER leave
Input.use_accumulated_inputenabled; strictly disable it to preserve sub-frame timing for precise combo links. - NEVER use client-side hit detection for netplay; strictly use rollback netcode or server validation to prevent desyncs.
- NEVER use standard TCP for multiplayer; strictly use UDP/ENet to avoid head-of-line blocking during latency spikes.
- NEVER rely on the SceneTree for fighter transforms in netplay; strictly manage positions in a serializable data buffer.
---
🛠 Expert Components (scripts/)
Original Expert Patterns
- fighting_input_buffer.gd - Frame-locked input engine (60fps) with motion command fuzzy matching (QCF/DP).
- hitbox_component.gd - Professional hitbox/hurtbox utility with layered collision zones (High/Low/Throw).
Modular Components
- deterministic_physics_loop.gd - Custom loop pattern for frame-perfect game state progression.
- direct_hitbox_query.gd - PhysicsServer shape-casting for immediate collision resolution.
- hit_stop_controller.gd - Dynamic time-scale manipulation for "impact" feel.
- manual_animation_advancer.gd - Frame-synced animation control via manual delta processing.
- rollback_state_serializer.gd - Serialization logic for managing discrete game state snapshots.
- bitwise_state_flags.gd - High-performance bitwise flags for fighter state tracking.
- input_accumulation_control.gd - Toggle for disabling Godot's input accumulation for sub-frame timing.
- raw_byte_network_sync.gd - UDP-based state synchronization for netplay efficiency.
- string_name_optimization.gd - Pattern for using pointer-level
StringNamecomparisons in AI states. - round_timer_logic.gd - Logic for frame-synced match timers and timeout triggers.
---
Core Loop
Neutral Game → Confirm Hit → Execute Combo → Advantage State → Repeat
Skill Chain
godot-project-foundations, godot-characterbody-2d, godot-input-handling, animation, godot-combat-system, godot-state-machine-advanced, multiplayer-lobby
---
Frame-Based Combat System
Fighting games operate on frame data - discrete time units (typically 60fps).
Frame Data Fundamentals
class_name Attack
extends Resource
@export var name: String
@export var startup_frames: int # Frames before hitbox becomes active
@export var active_frames: int # Frames hitbox is active
@export var recovery_frames: int # Frames after hitbox deactivates
@export var on_hit_advantage: int # Frame advantage when attack hits
@export var on_block_advantage: int # Frame advantage when blocked
@export var damage: int
@export var hitstun: int # Frames opponent is stunned
@export var blockstun: int # Frames opponent is in blockstun
func get_total_frames() -> int:
return startup_frames + active_frames + recovery_frames
func is_safe_on_block() -> bool:
return on_block_advantage >= 0Frame-Accurate Processing
extends Node
var frame_count: int = 0
const FRAME_DURATION := 1.0 / 60.0
var accumulator: float = 0.0
func _process(delta: float) -> void:
accumulator += delta
while accumulator >= FRAME_DURATION:
process_game_frame()
frame_count += 1
accumulator -= FRAME_DURATION
func process_game_frame() -> void:
# All game logic runs here at fixed 60fps
for fighter in fighters:
fighter.process_frame()---
Input System
Input Buffering
Store inputs and execute when valid:
class_name InputBuffer
extends Node
const BUFFER_FRAMES := 8 # Industry standard: 5-10 frames
var buffer: Array[InputEvent] = []
func add_input(input: InputEvent) -> void:
buffer.append(input)
if buffer.size() > BUFFER_FRAMES:
buffer.pop_front()
func consume_input(action: StringName) -> bool:
for i in range(buffer.size() - 1, -1, -1):
if buffer[i].is_action(action):
buffer.remove_at(i)
return true
return falseMotion Input Detection (Quarter Circle, DP, etc.)
class_name MotionDetector
extends Node
const QCF := ["down", "down_forward", "forward"] # Quarter Circle Forward
const DP := ["forward", "down", "down_forward"] # Dragon Punch
const MOTION_WINDOW := 15 # Frames to complete motion
var direction_history: Array[String] = []
func add_direction(dir: String) -> void:
if direction_history.is_empty() or direction_history[-1] != dir:
direction_history.append(dir)
# Keep last N directions
if direction_history.size() > 20:
direction_history.pop_front()
func check_motion(motion: Array[String]) -> bool:
if direction_history.size() < motion.size():
return false
# Check if motion appears in recent history
var recent := direction_history.slice(-MOTION_WINDOW)
return _contains_sequence(recent, motion)
func _contains_sequence(haystack: Array, needle: Array) -> bool:
var idx := 0
for dir in haystack:
if dir == needle[idx]:
idx += 1
if idx >= needle.size():
return true
return false---
Hitbox/Hurtbox System
class_name HitboxComponent
extends Area2D
enum BoxType { HITBOX, HURTBOX, THROW, PROJECTILE }
@export var box_type: BoxType
@export var attack_data: Attack
@export var owner_fighter: Fighter
signal hit_confirmed(target: Fighter, attack: Attack)
func _ready() -> void:
monitoring = (box_type == BoxType.HITBOX or box_type == BoxType.THROW)
monitorable = (box_type == BoxType.HURTBOX)
connect("area_entered", _on_area_entered)
func _on_area_entered(area: Area2D) -> void:
if area is HitboxComponent:
var other := area as HitboxComponent
if other.box_type == BoxType.HURTBOX and other.owner_fighter != owner_fighter:
hit_confirmed.emit(other.owner_fighter, attack_data)---
Combo System
Hit Confirmation and Combo Counter
class_name ComboTracker
extends Node
var combo_count: int = 0
var combo_damage: int = 0
var in_combo: bool = false
var damage_scaling: float = 1.0
const SCALING_PER_HIT := 0.9 # 10% reduction per hit
func start_combo() -> void:
in_combo = true
combo_count = 0
combo_damage = 0
damage_scaling = 1.0
func add_hit(base_damage: int) -> int:
combo_count += 1
var scaled_damage := int(base_damage * damage_scaling)
combo_damage += scaled_damage
damage_scaling *= SCALING_PER_HIT
return scaled_damage
func drop_combo() -> void:
in_combo = false
combo_count = 0
damage_scaling = 1.0Cancel System
enum CancelType { NONE, NORMAL, SPECIAL, SUPER }
func can_cancel_into(from_attack: Attack, to_attack: Attack) -> bool:
# Normal → Special → Super hierarchy
match to_attack.cancel_type:
CancelType.NORMAL:
return from_attack.cancel_type == CancelType.NONE
CancelType.SPECIAL:
return from_attack.cancel_type in [CancelType.NONE, CancelType.NORMAL]
CancelType.SUPER:
return true # Supers can cancel anything
return false---
Character States
enum FighterState {
IDLE, WALKING, CROUCHING, JUMPING,
ATTACKING, BLOCKING, HITSTUN, BLOCKSTUN,
KNOCKDOWN, WAKEUP, THROW, THROWN
}
class_name FighterStateMachine
extends Node
var current_state: FighterState = FighterState.IDLE
var state_frame: int = 0
func transition_to(new_state: FighterState) -> void:
exit_state(current_state)
current_state = new_state
state_frame = 0
enter_state(new_state)
func is_actionable() -> bool:
return current_state in [
FighterState.IDLE,
FighterState.WALKING,
FighterState.CROUCHING
]---
Netcode Considerations
Rollback Essentials
class_name GameState
extends Resource
# Serialize complete game state for rollback
func save_state() -> Dictionary:
return {
"frame": frame_count,
"fighters": fighters.map(func(f): return f.serialize()),
"projectiles": projectiles.map(func(p): return p.serialize())
}
func load_state(state: Dictionary) -> void:
frame_count = state["frame"]
for i in fighters.size():
fighters[i].deserialize(state["fighters"][i])
# Reconstruct projectiles...---
Balance Guidelines
| Element | Guideline |
|---|---|
| Health | 10,000-15,000 for ~20 second rounds |
| Combo damage | Max 30-40% of health per touch |
| Fastest moves | 3-5 frames startup (jabs) |
| Slowest moves | 20-40 frames (supers, overheads) |
| Throw range | Short but reliable |
| Meter gain | Full bar in ~2 combos received |
---
Common Pitfalls
| Pitfall | Solution |
|---|---|
| Infinite combos | Implement hitstun decay and gravity scaling |
| Unblockable setups | Ensure all attacks have counterplay |
| Lag input drops | Robust input buffering (8+ frames) |
| Desync in netplay | Deterministic physics, rollback netcode |
---
Godot-Specific Tips
1. Use `_physics_process` sparingly - implement your own frame-based loop 2. AnimationPlayer: Tie hitbox activation to animation frames 3. Custom collision: May need custom hitbox system rather than physics engine 4. Save/Load for rollback: Keep state serializable
Advanced Fighting Game Meta-Systems
Professional implementation of move-set management, editor-side debugging, and roster balance.
1. Command-List JSON Schema (Move-set Definitions)
Decouple move-sets from character logic by using an external JSON schema. This allows designers to iterate on frame data and inputs without modifying GDScript.
class_name MoveSetLoader extends Node
var move_data: Dictionary = {}
func load_from_json(path: String) -> void:
var file := FileAccess.open(path, FileAccess.READ)
if file:
var json_string := file.get_as_text()
var result = JSON.parse_string(json_string)
if result is Dictionary:
move_data = result
file.close()
# Example JSON Schema structure:
# {
# "hadouken": {
# "input": ["down", "down_forward", "forward", "punch"],
# "startup": 12,
# "active": 3,
# "recovery": 25,
# "damage": 800
# }
# }2. Visual Frame Advancer (Editor Debugger)
Use @tool scripts to create editor-side debugging tools that allow scrubbing through animation frames to verify hitbox alignment.
@tool
class_name FrameAdvancer extends Node
@export var current_frame: int = 0:
set(value):
current_frame = value
if Engine.is_editor_hint():
_sync_animation_to_frame()
@export var step_forward: bool = false:
set(value):
if value:
current_frame += 1
step_forward = false # Reset toggle
func _sync_animation_to_frame() -> void:
var anim_player: AnimationPlayer = get_node_or_null("../AnimationPlayer")
if anim_player:
anim_player.seek(current_frame * (1.0/60.0), true)
# Force a redraw of debug shapes
get_parent().queue_redraw()3. Character-Specific Scaling (Balance Resources)
Encapsulate roster balance variables in Resource files. This allows for profile-based scaling (e.g., HeavyWeight vs. GlassCannon) that can be swapped instantly.
class_name FighterBalanceProfile extends Resource
@export_group("Damage Scaling")
@export var base_damage_mult: float = 1.0
@export var combo_proration_rate: float = 0.9 # Lower means faster damage drop-off
@export_group("Movement Scaling")
@export var walk_speed_mult: float = 1.0
@export var dash_distance_mult: float = 1.0
@export_group("Defense Scaling")
@export var max_health: int = 10000
@export var guts_threshold: float = 0.3 # Damage reduction kicks in at 30% HP
# Usage in Fighter script:
# @export var balance_profile: FighterBalanceProfile
# func take_damage(amount: int) -> void:
# health -= int(amount * balance_profile.damage_reduction_curve)Anti-Pattern: NEVER hardcode balance numbers in the Fighter base class. Strictly use delegated Resource profiles to maintain a clean, maintainable roster.
Reference
- Master Skill: godot-master
# bitwise_state_flags.gd
# Efficiently combining and tracking fighter states
extends Node
# EXPERT NOTE: Bitwise flags are the fastest way to check
# complex conditions (e.g. Can I block? Are we airborne AND stun?)
enum FighterState {
IDLE = 1 << 0,
ATTACKING = 1 << 1,
AIRBORNE = 1 << 2,
STUNNED = 1 << 3
}
var current_state: int = FighterState.IDLE
func can_block() -> bool:
# If NOT airborne AND NOT stunned
return !(current_state & FighterState.AIRBORNE) and !(current_state & FighterState.STUNNED)
func add_state(state: FighterState):
current_state |= state
# deterministic_physics_loop.gd
# Locking gameplay logic to fixed timesteps
extends CharacterBody2D
# EXPERT NOTE: NEVER use _process() for fighting logic.
# Determinism requires fixed _physics_process() execution.
func _physics_process(delta):
_apply_fighter_logic(delta)
move_and_slide()
func _apply_fighter_logic(_d):
# Input polling and state transitions happen here
pass
# direct_hitbox_query.gd
# Bypassing Area2D for frame-perfect intersection tests
extends Node2D
# EXPERT NOTE: For precision, query the PhysicsServer directly.
# Area2D overlaps are only updated once per physics frame.
func check_hitbox(query_pos: Vector2, radius: float) -> Array:
var space_state = get_world_2d().direct_space_state
var query = PhysicsShapeQueryParameters2D.new()
var shape = CircleShape2D.new()
shape.radius = radius
query.shape_rid = shape.get_rid()
query.transform = Transform2D(0, query_pos)
return space_state.intersect_shape(query)
# skills/genre-fighting/scripts/fighting_input_buffer.gd
extends Node
## Fighting Input Buffer
## Deterministic input buffering and history for fighting games.
## Stores inputs frame-by-frame to allow complex motion inputs (Hadoken, etc).
class_name FightingInputBuffer
# Config
const BUFFER_SIZE: int = 60 # 1 second at 60fps
const LENIENCY_FRAMES: int = 5 # Buffer window
# Frame Data
var input_history: Array[Dictionary] = [] # Array of {frame: int, inputs: int (bitmask)}
var current_frame: int = 0
var game_running: bool = false
# Input Flags (Bitmask)
enum Buttons {
UP = 1,
DOWN = 2,
LEFT = 4,
RIGHT = 8,
LIGHT_PUNCH = 16,
HEAVY_PUNCH = 32,
LIGHT_KICK = 64,
HEAVY_KICK = 128
}
func _ready() -> void:
# Use physics process for fixed timestep (critical for fighting games)
set_physics_process(true)
func _physics_process(_delta: float) -> void:
current_frame += 1
var current_inputs = _read_hardware_inputs()
# Store history
input_history.push_back({
"frame": current_frame,
"inputs": current_inputs
})
if input_history.size() > BUFFER_SIZE:
input_history.pop_front()
_check_special_moves(current_frame)
func _read_hardware_inputs() -> int:
var mask = 0
if Input.is_action_pressed("fight_up"): mask |= Buttons.UP
if Input.is_action_pressed("fight_down"): mask |= Buttons.DOWN
if Input.is_action_pressed("fight_left"): mask |= Buttons.LEFT
if Input.is_action_pressed("fight_right"): mask |= Buttons.RIGHT
if Input.is_action_just_pressed("fight_lp"): mask |= Buttons.LIGHT_PUNCH
# ... etc
return mask
func _check_special_moves(frame_now: int) -> void:
# Example: Quarter Circle Forward (Down -> Down+Forward -> Forward + Punch)
# Simplified check logic
if is_button_just_pressed(Buttons.LIGHT_PUNCH, frame_now):
# Look back for motion
if check_motion_sequence([Buttons.DOWN, Buttons.DOWN | Buttons.RIGHT, Buttons.RIGHT], frame_now, 15):
print("HADOKEN!")
func is_button_just_pressed(btn_mask: int, frame: int) -> bool:
# Check if pressed this frame but NOT last frame
var curr = get_input_at(frame)
var prev = get_input_at(frame - 1)
return (curr & btn_mask) and not (prev & btn_mask)
func get_input_at(frame: int) -> int:
# Iterate history backwards
for i in range(input_history.size() - 1, -1, -1):
if input_history[i].frame == frame:
return input_history[i].inputs
return 0
func check_motion_sequence(sequence: Array, end_frame: int, window: int) -> bool:
var seq_idx = sequence.size() - 1
var current_search_frame = end_frame
# Trace back
while current_search_frame > end_frame - window and seq_idx >= 0:
var inputs = get_input_at(current_search_frame)
var target = sequence[seq_idx]
# Fuzzy match: does functionality hold?
# (Simply checking if the bitmask contains the target bits)
if (inputs & target) == target:
seq_idx -= 1
current_search_frame -= 1
return seq_idx < 0
## EXPERT USAGE:
## Autoload this node. Bind your fighter's state machine to check `FightingInputBuffer.is_button_just_pressed(...)`.
# hit_stop_controller.gd
# Simulating fighter "impact freeze" via time scale
extends Node
# EXPERT NOTE: Hit-stop adds "juice" and impact feel.
# Temporarily slowing Engine.time_scale provides immediate feedback.
func apply_hit_stop(duration: float = 0.1):
Engine.time_scale = 0.0
await get_tree().create_timer(duration, true, false, true).timeout # Process during pause
Engine.time_scale = 1.0
# skills/genre-fighting/scripts/hitbox_component.gd
extends Area2D
## Hitbox Component Expert Pattern
## Modular hitbox/hurtbox system for fighting games.
## Separates data (Attack Resource) from logic.
class_name HitboxComponent
enum Type { HITBOX, HURTBOX, GRAB_BOX }
@export var type: Type = Type.HITBOX
@export var attack_data: Resource # Holds damage, frame data, knockback
@export_flags("Player", "Enemy") var target_team: int = 1
signal hit_confirmed(target: Node2D)
signal hurt_confirmed(attacker: Node2D, data: Resource)
func _ready() -> void:
area_entered.connect(_on_area_entered)
# Default state: disabled until animation frames enable it
if type == Type.HITBOX:
monitoring = false
monitorable = true
elif type == Type.HURTBOX:
monitoring = true
monitorable = true
func _on_area_entered(area: Area2D) -> void:
if type == Type.HITBOX:
# We hit someone
if area is HitboxComponent and area.type == Type.HURTBOX:
# Check team (simple mask comparison)
# Assuming area owner has a 'team' property or we use flags
_process_hit(area)
func _process_hit(hurtbox: HitboxComponent) -> void:
# Notify myself
hit_confirmed.emit(hurtbox.owner)
# Notify them
if hurtbox.has_signal("hurt_confirmed"):
hurtbox.hurt_confirmed.emit(owner, attack_data)
## EXPERT USAGE:
## Attach to Fighter bone/sprite. Use AnimationPlayer to toggle 'monitoring'.
## When Hitbox enters Hurtbox, signals fire carrying 'attack_data'.
# input_accumulation_control.gd
# Disabling OS input merging for raw frame-perfect polling
extends Node
# EXPERT NOTE: Input accumulation merges events to the framerate.
# Disabling it is vital for frame-perfect Fighting game inputs.
func _ready():
# Ensuring every button press is parsed exactly as it arrived
Input.use_accumulated_input = false
func _physics_process(_delta):
# Optional: flush if you need absolute immediate state
# Input.flush_buffered_events()
pass
# manual_animation_advancer.gd
# Syncing animations strictly to physics/rollback frames
extends AnimationMixer
# EXPERT NOTE: For determinism, stop child AnimationPlayers
# and manually call advance() inside _physics_process().
func _ready():
callback_mode_process = ANIMATION_CALLBACK_MODE_PROCESS_MANUAL
func step_animation(delta: float):
# Advancing by fixed delta to ensure frame-sync with logic
advance(delta)
# raw_byte_network_sync.gd
# Bypassing RPCs for low-level rollback UDP packets
extends Node
# EXPERT NOTE: send_bytes() with UNRELIABLE mode is the
# fastest way to transmit raw input frames.
func send_input_frame(frame_data: PackedByteArray):
if multiplayer.has_multiplayer_peer():
multiplayer.multiplayer_peer.put_packet(frame_data)
func _on_packet_received(data: PackedByteArray):
# Parse raw bytes directly into the rollback buffer
_handle_input_data(data)
func _handle_input_data(_d): pass
# rollback_state_serializer.gd
# High-speed serialization for frame snapshots
extends Node
# EXPERT NOTE: Rollback requires snapshots every frame.
# Pre-allocating PackedByteArray ensures zero allocation stutters.
var state_buffer := PackedByteArray()
func _ready():
state_buffer.resize(1024) # Reserve memory for the fighter state
func save_state() -> PackedByteArray:
# Serialize position, health, inputs into binary
return state_buffer # Return current snapshot
# round_timer_logic.gd
# Deterministic round management without Node timers
extends Node
# EXPERT NOTE: In fighting games, the timer must stay in sync
# with the frames, not the wall-clock time.
var frames_remaining: int = 60 * 60 # 60 seconds at 60fps
func _physics_process(_delta):
if frames_remaining > 0:
frames_remaining -= 1
if frames_remaining == 0:
_on_time_out()
func _on_time_out():
print("Round Ended by Frame Count")
# string_name_optimization.gd
# Speeding up move-lookups via hashed strings
extends Node
# EXPERT NOTE: Use StringName (&"name") for high-frequency
# lookups. It uses an internal hash for near O(1) matching.
var move_set: Dictionary = {
&"punch": 10,
&"kick": 15
}
func execute_move(move_name: StringName):
if move_set.has(move_name):
print("Executing ", move_name)