Hello! I'm not entirely new to godot, but I am still working my way around advanced tactics. I have a problem, and I wanted to ask if anyone could help with it. Fighting games have been my go to for playing video games. I played most of the King Of Fighters games, (KOF XV Hype!) Almost all the Street Fighter games, (3rd Strike is the best!) Mortal Kombat 9, X and 11, Dead or Alive 5, and some of the Tekken series. I've been making my own Fighting game in Godot, hopefully with some speed similar to that of 3rd Strike. But there's something that's always been messing up what I've been working on. The motion inputs. At first I thought it was gonna be a simple task to make a move that required multiple inputs to perform. However, upon testing the code, the inputs need to be held down for the game to register the move. This isn't a good things, as since controller moves are gonna be present, it would be impossible for some of the moves to be pulled off. What I have so far is the Hadoken (Fireball), LP, LK, left and right movement, Animations, and an Enemy in an Idle pose. I'm also working on the GUI for the game and health bars. The command moves consist of multiple inputs that are already present. The code I have for the Command moves is:

if(Input.is_action_pressed("move_forwards")): if(Input.is_action_pressed("lightpunch")): $AnimatedSprite.play("Shoryuken") isAttacking = true;

This isn't the true command for the Shoryuken, but it's a placeholder. The only problem is that it's not identical to the real command inputs. Does anyone have any tips for this? I thought about making a tutorial when this was all finished. I'm Currently using GDScript.

A few months back I did a little fighting game test project with Godot and here's what I learned out doing it. Others may have solved things differently, but I hope this will help you develop your project further. I'll first describe my basic node setup for the characters and then describe the "buffered input system" I used to make special moves possible.

I created a scene, "BaseCharacter", which has the basic buildingblocks each fighter in the game would need (and do bear in mind that this isn't a polished solution, just a test): - A state machine to handle the character state changes between movement, idle, attack, knocked down etc, and the input for the character - An animation player to hold all the animations for the character - Animation Tree that contains a state machine for controlling the animation - The 3D-model for the character - A timer to follow up on possible combos and special moves. - Some Areas and Collision shapes to make the hitboxes and to handle collision

The key component here is really the state machine, because it made it easy to implement context-based input. For instance, input during crouching does a different action than input when standing idle.

Then for the "buffered input system". The BaseCharacter node script will receive all the input and place it in a buffer, or a list, of inputs from the player. So, when ever the player pushes a button or a direction, it's placed in an input buffer as the latest input, but only if it is different from the input that was received during the previous frame. I decided to use a simple string as the stored input element, and it is a string with a number representing input direction, and x, y, a or b representing input buttons if a button is pressed. You can choose what ever way you want, of course, but this worked for me OK enough.

The timer mentioned above is needed to clear this buffer if the player doesn't give any input for a while, and is always reset when the player pushes some button.

Here's the code for this, and as said, it's attached to the BaseCharacter node. You will notice there's a function _interpret_movement_vector, and I'll describe that after the code block.

(note: for some reason some of the code doesn't stay inside the code tags in preview, but I hope the code is readable none the less)

 # Input related variables.
onready var input_timer = $Timer_Input # The timer for input of combos or moves
var input_buffer = [] # The list of the latest inputs.
var input_buffer_max_length = 16 # The max length of the input buffer
var input_previous_to_buffer = "N" # The last input accepted to the buffer (i.e. the last change from input state)
var input_latest_key = "N" # The latest input received  
var input_has_combo_timed_out = true # has the combo timed out

# Handles the input directions and punch-button inputs.
func _handle_move_input(delta):
	#
	# Get the input movement vector as a key 
	# for direction.
	#
	var movementKey = _interpret_movement_vector(delta)
	
	
	#
	# Check for action buttons.
	#
	var punch_l = false
	var punch_r = false
	var kick_l = false
	var kick_r = false
	
	if player_id == "1":
		punch_l = Input.is_action_just_pressed("player_1_lefthand")
		punch_r = Input.is_action_just_pressed("player_1_righthand")
		kick_l = Input.is_action_just_pressed("player_1_leftfoot")
		kick_r = Input.is_action_just_pressed("player_1_rightfoot")
	else:
		punch_l = Input.is_action_just_pressed("player_2_lefthand")
		punch_r = Input.is_action_just_pressed("player_2_righthand")
		kick_l = Input.is_action_just_pressed("player_2_leftfoot")
		kick_r = Input.is_action_just_pressed("player_2_rightfoot")
	
	
	if punch_l:
		movementKey = movementKey + "x"
		
	if punch_r:
		movementKey = movementKey + "y"
		
	if kick_l:
		movementKey = movementKey + "a"
		
	if kick_r:
		movementKey = movementKey + "b"
	
	
	#
	# Store the movement key for other use.
	#
	input_latest_key = movementKey 
	
	#
	# If the input changed store it in to the buffer.
	#
	if movementKey != input_previous_to_buffer:
		input_buffer.push_back(movementKey)
		
		#
		# Store this as the previous input.
		#
		input_previous_to_buffer = movementKey
		
		#
		# Set that a combo is available and reset 
		# the timeout-timer for combo.
		#
		input_has_combo_timed_out = false
		input_timer.start()
		
		
	
	#
	# If the input buffer is longer than the max length, pop the front 
	# of the list.
	#
	if input_buffer.size() > input_buffer_max_length:
		input_buffer = input_buffer.slice(input_buffer.size()-input_buffer_max_length, input_buffer.size() )

The last bit in the code limits the length of the input buffer to some maximum. You don't necessarily need it but I thought it was a good idea to have some max size for the buffer.

Now, to the _interpret_movement_vector function. In the game the characters may be facing left or right, so the inputs need to be converted from "pushed left" and "pushed up-right" to "pushed back" and "pushed up-forward" based on the character facing. This is how I did it:

# Interprets the movement direction input.
func _interpret_movement_vector(delta):
	#
	# Get the deltax and deltay of the movement vector.
	#
	var dx = 0.0
	var dy = 0.0
	var vMovement = Vector3(dx, 0, dy)
	
	if player_id == "1":
		dx = Input.get_action_strength("player_1_right") - Input.get_action_strength("player_1_left")
		dy = Input.get_action_strength("player_1_up") - Input.get_action_strength("player_1_down")
	else:
		dx = Input.get_action_strength("player_2_right") - Input.get_action_strength("player_2_left")
		dy = Input.get_action_strength("player_2_up") - Input.get_action_strength("player_2_down")
	
	vMovement = Vector3(dx, 0, dy)
	if vMovement.length() < 0.2:
		# This is neutral
		return "N"
	
	vMovement = Vector3(dx, 0, dy).normalized()
	
	#
	# Get what ever the direction is.
	#
	var angleMovement = v_forward.angle_to(vMovement)
	
	#
	# Check if the input is forward, up, backwards, etc.
	# Denote with numbers on the keyboard, with 6 being forward and 8 up.
	#
	# 7  8  9
	# 4  N  6
	# 1  2  3
	#
	# The direction depends on delta y.
	if dy >= 0:
		if angleMovement < PI/5.0: 
			return "6"
		if angleMovement < 2.0*PI/5.0:
			return "9"
		if angleMovement < 3.0*PI/5.0:
			return "8"
		if angleMovement < 4.0*PI/5.0:
			return "7"
		else: 
			return "4"
	
	if angleMovement < PI/5.0:
		return "6"
	if angleMovement < 2.0*PI/5.0:
		return "3"
	if angleMovement < 3.0*PI/5.0:
		return "2"
	if angleMovement < 4.0*PI/5.0:
		return "1"
	if angleMovement < PI:
		return "4"
	
	# This is forward again.
	return "6"

When the player pushes the directional keys or stick, it is stored in a movement vector. If no input is given, or the strength of input is inside some deadzone, I interpret it as "no directional input". This vector is compared to the forward vector of the character in the way you want. You can see that I decided to interpret the movement to 8 directions basically mapping each direction to the numpad keys, with 6 meaning forward and 8 meaning up, and N (neutral) as no directional input.

As a result of all this, you've now got a list of inputs from the player, which looks like this: [Ny] or [N,6,N,6,N,a] or maybe like this: [2,3,6a,N]. If several keys are pressed during a single frame, it'll look like this: [3abxy], or maybe something like this[N, 9, 8, 6xa].

N, or the neutral position isn't needed for the actual input, so I made a function to get rid of it and to format the input a bit further so that it actually returns a string. As a result the inputs above will come out as follows: [Ny] = "[y]" [N,6,N,6,N,a] = "[6][6][a]" [2,3,6a,N] = "[2][3][6a]"

(continued in a following post, seems this is getting to long for a single post)

(continued from the previous post)

The code for that function looks like this (again, the code tag seems to work a bit wonky, sorry about that):

#
# Returns the formatted input buffer.
#
func get_input_buffer() -> String:
	#
	# Add square brackets around the input buffer.
	#
	var str_inputbuffer = str(input_buffer)
	# Make it as a list of [N][Nx][6x], etc.
	str_inputbuffer = str_inputbuffer.replace(", ", "][")
	# Remove all N's to make each [N] to [] and others to have just the input, then remove all [] that have no content
	str_inputbuffer = str_inputbuffer.replace("N", "").replace("[]", "")
	
	#
	# Return the result.
	#
	return str_inputbuffer

The next step is to start interpreting what this description string means in the different states the character is in. And this is where the state machine comes in handy. Each state has a function to check for special moves, basic moves and movement, which will then trigger an animation and a state change for the state machine. So you can have "up" input meaning different things when the character is standing up, crouching or laying flat on the ground after being knocked down. As an example, the check_special_moves-function for my Idle-state (or standing up -state) looks like this:

#
# Checks for special moves that can be launched from this state.
#
func check_special_moves():
	#
	# To trigger a special move, we look at the input buffer of the parent.
	# If it matches some input, we'll trigger the attack by setting the chosen_attack
	# variable for the parent.
	#
	var str_inputbuffer = parent.get_input_buffer()
	
	#
	# Check the special moves from the longest to the shortest.
	#
	if str_inputbuffer.find("[4][6][4][6][a]") > -1 or str_inputbuffer.find("[4][6][4][6][6a]") > -1:
		# Move detected, choose the attack.
		parent.choose_attack("kick_left_high", 20, false, false, true, false, true)
		print_debug("SUPER CRACKER KICK!")
		return true
	
	if str_inputbuffer.find("[6][2][3][6][x]") > -1 or str_inputbuffer.find("[6][2][3][6][6x]") > -1:
		# Move detected, choose the attack.
		parent.choose_attack("punch_left_high", 20, true, false, false, false, true )
		print_debug("Super UPPERCUT!")
		
		return true
	
	if str_inputbuffer.find("[6][6][6][a]") > -1 or str_inputbuffer.find("[6][6][6][6a]") > -1:
		# Move detected, choose the attack.
		parent.choose_attack("punch_right_middle", 20, false, true, false, false, true )
		print_debug("POWER KICK!")
		return true
		
	
	#
	# No moves triggered.
	#
	return false

You can of course use some list or something to do these kinds of checks, but for this test I did, I just used some if-statements to try things out. The function returns true if some special move was triggered, and false if not. This is needed because if no special move was found, you want to check if some basic move, like a simple punch, is to be triggered instead. So I created another function called check_basic_moves which checks this:

#
# Checks for basic moves from the input buffer.
#
func check_basic_moves():
	
	#
	# Get the latest input.
	#
	var str_inputbuffer = get_last_input()
	
	#
	# Basic left high punch.
	#
	if str_inputbuffer.find("x") > -1:
		choose_attack("punch_left_high", 5, true, false, false, false, false )
		return true
	
	#
	# Basic right high punch.
	#
	if str_inputbuffer.find("y") > -1:
		choose_attack("punch_right_middle", 5, false, true, false, false, false )
		return true
	
	#
	# Basic left high kick.
	#
	if str_inputbuffer.find("a") > -1:
		choose_attack("kick_left_high", 5, false, false, true, false, false )
		return true
	
	#
	# Basic right high kick.
	#
	if str_inputbuffer.find("b") > -1:
		choose_attack("kick_left_high", 5, false, false, false, true, false )
		return true

    return false 

Again, the function returns true if some move was triggered and false otherwise. If there was no attack-related input, you then check for movement input, jumps, crouching, rolling and so on.

I'll not add the code for the choose_attack function as that's specific to my test, but I hope you can get the idea behind the system. As said, this was just me testing things out, and the way I got this working. I'm looking forward to hearing about the system you'll come up with when your project is done.

@Grr said: (continued from the previous post)

The code for that function looks like this (again, the code tag seems to work a bit wonky, sorry about that):

#
# Returns the formatted input buffer.
#
func get_input_buffer() -> String:
	#
	# Add square brackets around the input buffer.
	#
	var str_inputbuffer = str(input_buffer)
	# Make it as a list of [N][Nx][6x], etc.
	str_inputbuffer = str_inputbuffer.replace(", ", "][")
	# Remove all N's to make each [N] to [] and others to have just the input, then remove all [] that have no content
	str_inputbuffer = str_inputbuffer.replace("N", "").replace("[]", "")
	
	#
	# Return the result.
	#
	return str_inputbuffer

The next step is to start interpreting what this description string means in the different states the character is in. And this is where the state machine comes in handy. Each state has a function to check for special moves, basic moves and movement, which will then trigger an animation and a state change for the state machine. So you can have "up" input meaning different things when the character is standing up, crouching or laying flat on the ground after being knocked down. As an example, the check_special_moves-function for my Idle-state (or standing up -state) looks like this:

#
# Checks for special moves that can be launched from this state.
#
func check_special_moves():
	#
	# To trigger a special move, we look at the input buffer of the parent.
	# If it matches some input, we'll trigger the attack by setting the chosen_attack
	# variable for the parent.
	#
	var str_inputbuffer = parent.get_input_buffer()
	
	#
	# Check the special moves from the longest to the shortest.
	#
	if str_inputbuffer.find("[4][6][4][6][a]") > -1 or str_inputbuffer.find("[4][6][4][6][6a]") > -1:
		# Move detected, choose the attack.
		parent.choose_attack("kick_left_high", 20, false, false, true, false, true)
		print_debug("SUPER CRACKER KICK!")
		return true
	
	if str_inputbuffer.find("[6][2][3][6][x]") > -1 or str_inputbuffer.find("[6][2][3][6][6x]") > -1:
		# Move detected, choose the attack.
		parent.choose_attack("punch_left_high", 20, true, false, false, false, true )
		print_debug("Super UPPERCUT!")
		
		return true
	
	if str_inputbuffer.find("[6][6][6][a]") > -1 or str_inputbuffer.find("[6][6][6][6a]") > -1:
		# Move detected, choose the attack.
		parent.choose_attack("punch_right_middle", 20, false, true, false, false, true )
		print_debug("POWER KICK!")
		return true
		
	
	#
	# No moves triggered.
	#
	return false

You can of course use some list or something to do these kinds of checks, but for this test I did, I just used some if-statements to try things out. The function returns true if some special move was triggered, and false if not. This is needed because if no special move was found, you want to check if some basic move, like a simple punch, is to be triggered instead. So I created another function called check_basic_moves which checks this:

#
# Checks for basic moves from the input buffer.
#
func check_basic_moves():
	
	#
	# Get the latest input.
	#
	var str_inputbuffer = get_last_input()
	
	#
	# Basic left high punch.
	#
	if str_inputbuffer.find("x") > -1:
		choose_attack("punch_left_high", 5, true, false, false, false, false )
		return true
	
	#
	# Basic right high punch.
	#
	if str_inputbuffer.find("y") > -1:
		choose_attack("punch_right_middle", 5, false, true, false, false, false )
		return true
	
	#
	# Basic left high kick.
	#
	if str_inputbuffer.find("a") > -1:
		choose_attack("kick_left_high", 5, false, false, true, false, false )
		return true
	
	#
	# Basic right high kick.
	#
	if str_inputbuffer.find("b") > -1:
		choose_attack("kick_left_high", 5, false, false, false, true, false )
		return true

    return false 

Again, the function returns true if some move was triggered and false otherwise. If there was no attack-related input, you then check for movement input, jumps, crouching, rolling and so on.

I'll not add the code for the choose_attack function as that's specific to my test, but I hope you can get the idea behind the system. As said, this was just me testing things out, and the way I got this working. I'm looking forward to hearing about the system you'll come up with when your project is done.

Thank you so much! I knew I needed to use a timer some how, but I didn't have an idea how. The photos you showed were 3d, does that mean that this works for 2d as well? or should I try revising it to fit 2d?

It's only the graphics that are in 3D, the input logic itself is 2D.

@Grr said: It's only the graphics that are in 3D, the input logic itself is 2D. There's a problem with the code, the player_id isn't declared in the current scope. Is there a way to fix this?

You can add this to your main node (for me it is in the BaseCharacter node script): export var player_id = "1" # the ID of this player

8 days later

I got an idea to do the special move monitoring in another way, which seems to work a bit better than the way I described in the previous post. It doesn't need the input buffer and has a visual way to define the moves a fighter has.

I've done a bare-bones demo-project, in 2D, that contains the input code, a base node class for a fighter and an example fighter with a couple of moves (see the attachment).

Again, it uses a state machine for the fighter character to handle it's states, and the input is checked in the fighter base node and in the active state. Note that as this current version focuses on the handling of input, it doesn't have logic for all the states for a fighter. All fighters are created as inherited scenes of the FighterBase scene.

This new version utilizes two custom node types to describe the moves a character has: combo_move and combo_input. Combo_move is a node class that describes a single move (an uppercut, punch, kick, etc). Combo_inputs will be child-nodes for the combo_move-node and describe what inputs are required to trigger the move. The combo_move tracks also the timeouts for each move, if a player is too slow with their input. The timeout can be defined for each move. Each state that has moves must be of the state_baseinput-type (see the script state_baseinput.gd).

I've gone with a Tekken-style input, with left punch, right punch, left kick and right kick as the basic action buttons.

To try this out, use a game controller (I used an xbox-controller). The moves that have been defined for an example fighter are: - Left punch (x-button on the xbox-controller) - Forward, Down, Forward & Down, Left punch - Down, Forward & Down, Forward - Back, Forward, Back, Forward, Left punch & Left kick (x and a buttons on the xbox-controller)

There's some print-statements in the code so you can see how your input is interpreted in the Godot output-window.

I added a placeholder scene for the main menu and another placeholder for the character selection as well, so the (extremely, extremely programmer graphics) fighter will show up once the Start Combat button is pressed. This particular "fighter" has a couple of sprites and area2D's with collision shapes, and a a couple of animations that can be triggered with the moves. These area2D's don't do anything yet, as the focus is really on the input example, but in the game you would handle hit-testing with the area2D-nodes. Note that there is a animation state machine in the animation tree, so be sure to check that out.

When you open the project, go to the folder Fighters and open the FighterBase scene. Choose the FighterBase node and read the comments in the Editor Description field. Then check out the ExampleFighterA-scene.

I hope this is useful.

    4 months later

    Thanks for this thread, its very very useful and to @Grr , thanks i haven't go through the code yet, but as you wrote animation player to store the animation and animation tree with state machine and the input buffering systems, i know it will be useful. thanks.

    a year later
    2 months later

    Grr Hello, I created an account just to ask this. I am working on one as well and I saw this comment and I was wondering if I could receive the example as well.

    I have been trying to figure out the better method of handling inputs, whether on event or using polling, how to handle special input combinations, etc. I know traditionally games used to poll inputs per frame but I am trying to make this system flexible, (being able to include charged inputs and implementing attack > movement inputs on top of movement > attack inputs,) with an emphasis on responsiveness. That said, I would really appreciate to see an implementation of fighting game inputs as reference. Thank you.