Input Processing for Fighting Game has a delay

Hello everyone, so recently I’ve been reworking my input system for a traditional fighting game. My current system goes as follows:

  1. Create a variable called “tickAtInput” which will store the current tick when the player enters an input.
  2. Read and store (translated) player inputs alongside the tickAtInput variable, thus resetting the tickAtInput variable.
  3. Use RunService and constantly check if (tick() - tickAtInput) >= MAX_TIME_BEFORE_PROCESSING
  4. If the condition is met, check if there’s any actual inputs in the inputs table, if so follow the next steps
  5. Sort the input table into moves and times. Store the difference between the first input’s time and the last as howLong
  6. Run through all possible sequences in a determined moveset, check if any of the sequences match the inputs in the inputs table
  7. If there’s one or more valid sequences, check their priority. Find the highest priority move and check to see if their leniency variable is greater than howLong, if not then execute the move’s function. If their leniency variable is lower than howLong, then find the lowest priority move and execute their function.

Now that you know the process, what’s the actual problem? Well, when I input proper commands such as {"Down","Down_Forward","Forward","LightPunch"}, it takes four frames (yes I’ve counted) before the move’s function actually ran. This might seem not too bad, but other fighting games such as street fighter have their moves executed on the exact same frame.

Here is an example script that uses the process listed above:

local Melee = require(game.ReplicatedStorage.Melee).new()
local Translator = Melee.Translator.new({
	W = Vector2.new(0,1),
	D = Vector2.new(1,0),
	A = Vector2.new(-1,0),
	S = Vector2.new(0,-1)
})
local InputProcessor = Melee.InputProcessor.new()
local UIS = game:GetService("UserInputService")
local RunService = game:GetService("RunService")

local inputs = {}
local tickAtInput: number = tick()

local testMoveset = {
	["Uppercut"] = {
		Sequences = {"Forward", "Down", "Forward", "LightPunch"},
		Priority = 6,
		TimeWindow = 15,
		Activation = function()
			print("Uppercut!")
		end,
	}
}

UIS.InputBegan:Connect(function(input: InputObject, gameProcessedEvent: boolean) 
	if gameProcessedEvent then return end
	if input.UserInputType == Enum.UserInputType.Keyboard then
		
		local translatedInput = Translator:Translate(input)
		
		if translatedInput then
			tickAtInput = tick()
			table.insert(inputs, {translatedInput, tickAtInput})
		end
		
	end	
end)


RunService.RenderStepped:Connect(function(deltaTime: number) 
	if InputProcessor:TickToFrames(tickAtInput, tick()) >= 12 then
		
		if #inputs >= 1 then
			local nameOfInputs = {}
			local validMoves = {}
			local howLong = InputProcessor:TickToFrames(inputs[1][2], inputs[#inputs][2])
			
			for _, inputData in pairs(inputs) do
				table.insert(nameOfInputs, inputData[1])
			end
			
			for name, moveData: {Sequences: {}, Priority: number} in pairs(testMoveset) do
				if InputProcessor:Search(nameOfInputs, moveData.Sequences) then
					table.insert(validMoves, moveData.Priority)
				end
			end
			
			if #validMoves > 1 then
				
				local highestPriorityMove = InputProcessor:FindMoveByPriority(testMoveset, InputProcessor:FindMaxValue(validMoves))
				local lowestPriorityMove = InputProcessor:FindMoveByPriority(testMoveset, InputProcessor:FindMinValue(validMoves))
				
				if highestPriorityMove.TimeWindow > howLong then
					highestPriorityMove.Activation()
				else
					lowestPriorityMove.Activation()
				end
				
			else
				local move = InputProcessor:FindMoveByPriority(testMoveset, validMoves[1])
				move.Activation()
			end

		end
		
	end
end)

Here is the searching algorithm:

local function computePrefixTable(sequence)
	local m = #sequence
	local pi = {}
	local k = 0

	pi[1] = 0 -- pi[1] is always 0

	for i = 2, m do
		while k > 0 and sequence[k + 1] ~= sequence[i] do
			k = pi[k]
		end
		if sequence[k + 1] == sequence[i] then
			k = k + 1
		end
		pi[i] = k
	end

	return pi
end

-- Function to check if sequence exists in inputTable using KMP algorithm

function InputProcessor:Search(inputTable, sequence)
	local n = #inputTable
	local m = #sequence
	local pi = computePrefixTable(sequence)
	local q = 0

	for i = 1, n do
		while q > 0 and sequence[q + 1] ~= inputTable[i] do
			q = pi[q]
		end
		if sequence[q + 1] == inputTable[i] then
			q = q + 1
		end
		if q == m then
			return true
		end
	end

	return false
end