- Genre: Fighting Game
- Expert blueprint for 2D/3D fighters emphasizing frame-perfect combat and competitive balance.
- NEVER Do
- NEVER use variable frame rates
- — Fighting games require fixed 60fps. Implement custom frame-based loop, not
- _physics_process(delta)
- .
- NEVER skip input buffering
- — Without 5-10 frame buffer, players miss inputs. Command inputs feel unresponsive.
- NEVER forget damage scaling
- — Infinite combos break competitive play. Apply 10% damage reduction per hit in combo.
- NEVER make all moves safe on block
- — If all attacks have +0 or better advantage on block, defense has no value. Mix safe and unsafe moves.
- NEVER use client-side hit detection for netplay
- — Client predicts, server validates. Peer-to-peer needs rollback netcode or desyncs occur.
- Available Scripts
- MANDATORY
- Read the appropriate script before implementing the corresponding pattern. fighting_input_buffer.gd Frame-locked input polling with motion command detection. Stores 20-frame history, fuzzy-matches QCF/DP inputs, uses _physics_process for deterministic timing. 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
= 0 Frame-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 false Motion 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.0 Cancel 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
- Use
- _physics_process
- sparingly
- - implement your own frame-based loop
- AnimationPlayer
-
- Tie hitbox activation to animation frames
- Custom collision
-
- May need custom hitbox system rather than physics engine
- Save/Load for rollback
- Keep state serializable Reference Master Skill: godot-master