Multiple Key Combo? (R + L + R + L)

For the longest time as an developer, I have seen many fighting games use an multi-key combo system in their games, I have experimented with this but have never ever come close to fully understanding it. I have made basic implements of this with if arguments but don’t use multiple keys.

For those unfamiliar to what I am saying, this multi-key combo system works like this:

  • R + L + R + L
  • L + L + R + L

In case you don’t understand R and L, it stands for right and left mouse clicks.

And so on. If you’re able to hit a player you carry on with the combo until you hit the last key, if you miss or don’t hit a player for a like 3 seconds, the combo gets canceled. I never really understood how this in general worked and I don’t even know if UserInputType is even able to do such a thing. I was hoping someone here more experienced is able to provide an in-depth example/explanation of how this works,

There’s no built-in way of detecting when a “combo” is executed, and it wouldn’t make sense if there was because the exact mechanics of how it works would be specific to every game.

A very simple way of detecting combos is to just allow a combo to grow when an action is performed within a small enough delay of the previous action. Here’s something I whipped up, it won’t work by itself but hopefully you can get some ideas for your own solution:


local comboPatterns = {
	{name = "One-two Punch", pattern = {"R", "L", "R", "L"}},
	{name = "Right Jab", pattern = {"L", "L", "R", "L"}}
}

do
	local combo = {} --A table describing the combo of actions the player has made
	local comboExpireTime = 0.5
	local lastActionTick = tick()

	--Adds an action to the combo, resetting the combo if it's expired, and returning the combo afterwards
	function addToCombo( action )
		local t = tick()
		
		--Reset the combo if it's expired (more time passed than comboExpireTime)
		if t - lastActionTick > comboExpireTime then
			combo = {}
		end
		lastActionTick = t --Reset the timer

		--Add the action to the combo
		table.insert(combo, action)

		return combo
	end
end

function compareCombo( pattern, combo )
	--Compare the last action of the pattern and combo, then the second-to-last, etc.
	local patternIndex = #pattern
	local comboIndex = #combo
	
	repeat
		if combo[comboIndex] ~= pattern[patternIndex] then
			--If they don't match, return early
			return false
		end

		patternIndex -= 1
		comboIndex -= 1
	until patternIndex == 0 or comboIndex == 0

	--If none of the actions failed to match the pattern, it's a match
	return true
end

function findLongestMatchingCombo( combo )
	--Find all combo patterns that match the current one
	local matchingCombos = {}
	for _, comboPattern do
		if compareCombo(pattern.pattern, combo) then
			table.insert(matchingCombos, comboPattern)
		end
	end

	--Sort them by length, and return the longest
	table.sort(matchingCombos, function ( a, b )
		return #a.pattern < #b.pattern
	end)

	return matchingCombos[#matchingCombos]
end

function onActionPerformed( combo )
	local longestMatchingCombo = findLongestMatchingCombo(combo)
	if longestMatchingCombo then
		print(longestCombo.name)
	end
end

function onMouse1Clicked()
	local combo = addToCombo("L")
	onActionPerformed(combo)
end

function onMouse2Clicked()
	local combo addToCombo("R")
	onActionPerformed(combo)
end
4 Likes

My idea is very simple, you just need to create a recursive function with the table parameter containing the combo after it is completed, delete it and call the function again.

function Combo(allCombo)
	local action
	local key
	for k,v in pairs(allCombo) do
		action=v
		key=k 
		break
	end
	if action==nil then
		return
	end
	local t=0
	local Do= false
	repeat
		t=t+dt
		wait(dt)
		if input==action["input"] then
			Do= true
			func['func']()-- doing combo
			break
		end
	until t>=input['time']
	if Do then
		allCombo[k]=nil
		Combo(allCombo)
	end
end

Note that you must create functions for each combo and the Combo table must look like this:

local combo={
	combo1={
		input="R",
		func=funcCombo1
	},
	combo2={
		input='L',
		func=funcCombo2
	}
}