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)