Custom 2D Movement: Jumpsquat

Hello! I’ve been recently trying to recreate a movement system in Roblox similar to that of Smash Melee, I have been able to get pretty far on my own, but I am currently lost on what to do about Jump Squat (you can find out more about it with a simple search) but it essentially is a delay between when you press the jump button, and when the character jumps, the delay is also used to decide whether a character may do a shorthop, or a full jump.

But my main point here is that id like to support the Roblox FPS Unlocker as best I can, but you can easily see why this becomes a problem as Jumpsquat is counted in frames, the original game (Smash Melee) runs at 60 FPS, you can easily make the shorthop when desired, but when you unlock the FPS to lets say, 240? It becomes basically impossible to hit at all as the time frame was not designed for such a high frame rate.

Any ideas? I’m quite stumped here.

movementTest.rbxl (63.8 KB)

Control Script (inside StarterPlayerScripts)

local RunService = game:GetService("RunService")
local ContextActionService = game:GetService("ContextActionService")
local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local localPlayer = Players.LocalPlayer

local playerGui = localPlayer:WaitForChild("PlayerGui")
local debugFrame = playerGui:WaitForChild("DebugGui").DebugFrame
local debugValues = debugFrame:GetDescendants()

local humanoidRootPart = localPlayer.Character:WaitForChild("HumanoidRootPart")
--Im using a custom character (its literally just a part named "HumanoidRootPart" inside a model which is set as the startercharacter) not sure if it works with regular player characters, ¯\_(ツ)_/¯

--input related variables, easily modularized:tm:
local leftInput:number, rightInput:number = 0, 0
local isJumping:boolean = false
local jumpSuqatFramesSinceJump:number = 0
local doubleJumpPossible:boolean = true
local velocity:Vector3 = Vector3.zero
local currentFallingSpeed:number = 0

--statics, currently based off of converted values from fox in melee, should be modularized, but thats a problem for later me :)
local walkingSpeed = 960/28 --Studs/Frame
local runningSpeed = 1320/28 --Studs/Frame
local jumpForce = (3.68/2.8)*60 --(Studs/Frame)*60 || Studs/Frame = (SU (Smash Unit)/Frame)/2.8
local shortHopForce = (2.1/2.8)*60 --(Studs/Frame)*60 || Studs/Frame = (SU (Smash Unit)/Frame)/2.8
local gravity = (0.23/2.8)*60 --(Studs/Frame)*60 || Studs/Frame = (SU (Smash Unit)/Frame)/2.8
local fallingSpeed = (2.8/2.8)*60 --(Studs/Frame)*60 || Studs/Frame = (SU (Smash Unit)/Frame)/2.8
local jumpSquatFrames = 3

--debug
local lastJump = "none"

local function decrementJumpSquat()
	if isJumping then
		if jumpSuqatFramesSinceJump > 1 then
			jumpSuqatFramesSinceJump -= 1 --Count down the player's jumpsquat
		else
			isJumping = false
			jumpSuqatFramesSinceJump = 0
			lastJump = "Full"
		end
	end
end

local function isGrounded()
	local raycastResult = workspace:Raycast(humanoidRootPart.Position, Vector3.new(0, -5, 0), RaycastParams.new())

	if raycastResult ~= nil then
		return true
	end

	return false
end

local function left(_actionName, inputState, _inputObject:InputObject)
	leftInput = (inputState == Enum.UserInputState.Begin) and -1 or 0
end

local function right(_actionName, inputState, _inputObject:InputObject)
	rightInput = (inputState == Enum.UserInputState.Begin) and 1 or 0
end

local function onJump(_actionName, inputState, _inputObject:InputObject)
	local currentGroundedStatus = isGrounded()

	if inputState == Enum.UserInputState.Begin and (currentGroundedStatus or doubleJumpPossible) then
		doubleJumpPossible = (currentGroundedStatus) and true or false
		isJumping = true
		jumpSuqatFramesSinceJump = jumpSquatFrames --Set the player's jumpsquat to begin counting down
	elseif inputState == Enum.UserInputState.End and isJumping then
		isJumping = false
		lastJump = "Short"
	end
end

local function horizontalInput()
	return leftInput + rightInput
end

local function handleMovementX()
	local currentInputX = horizontalInput()

	if math.abs(currentInputX) >= 0.2875 or UserInputService:IsKeyDown(Enum.KeyCode.Space) == true then --The keycode should be user configureable later on
		return walkingSpeed * currentInputX
	elseif math.abs(currentInputX) >= 0.8 and UserInputService:IsKeyDown(Enum.KeyCode.Space) == false then --The keycode should be user configureable later on
		return runningSpeed * currentInputX
	end

	return 0
end

local function handleMovementY()

end

local function updateVelocity(delta:number)
	velocity = Vector3.new(handleMovementX() * delta)--, handleMovementY() * delta)
end

local function updatePosition()
	humanoidRootPart:PivotTo(humanoidRootPart.CFrame * CFrame.new(velocity)) --multiplying cframes is like adding regular numebers, confusing, but "it is what it is"
end

local function debugDisplay()
	for index, value:TextLabel in pairs(debugValues) do
		if value.Parent.ClassName == "TextLabel" then
			if index == 2 then
				value.Text = leftInput
			elseif index == 4 then
				value.Text = rightInput
			elseif index == 6 then
				value.Text = tostring(isJumping)
			elseif index == 8 then
				value.Text = tostring(jumpSuqatFramesSinceJump)
			elseif index == 10 then
				value.Text = tostring(velocity)
			elseif index == 12 then
				value.Text = lastJump
			elseif index == 14 then
				value.Text = currentFallingSpeed
			elseif index == 16 then
				value.Text = tostring(isGrounded())
			end
		end
	end
end

local function onRenderStep(delta)
	decrementJumpSquat() --Runs every frame, counts down player's jumpsquat every frame
	updateVelocity(delta)
	updatePosition()
	debugDisplay()
end

ContextActionService:BindAction("Left", left, false, Enum.KeyCode.A)
ContextActionService:BindAction("Right", right, false, Enum.KeyCode.D)
ContextActionService:BindAction("Jump", onJump, false,Enum.KeyCode.W)

RunService:BindToRenderStep("CharacterController", Enum.RenderPriority.Camera.Value - 1, onRenderStep)

PS: anyone got a better way to handle that debug display stuff :smile: and also any general scripting tips are appreciated

image

So assuming you know how many frames gives you a short jump (which looks like 3 to me glossing over your script), you can use the 60fps benchmark to determine a duration in seconds that will be the threshold in which your short jump will be performed. We will use this to determine how long the button was held in seconds instead of counting the frames.

We know at 60fps, 1 frame is 0.0166 seconds, so 3 frames is 0.05 seconds, so if the player taps W and the duration they held the button is 0.05 seconds or less, we perform the short jump. Otherwise, we perform the long jump.

1- rename jumpSquatFrames to jumpSquatDurationSecs which will be 0.05:

local jumpSquatDurationSecs = 0.05

2- rename jumpSuqatFramesSinceJump to jumpSquatTimeJumped

3- in onJump, make the following changes:

local function onJump(_actionName, inputState, _inputObject:InputObject)
	local currentGroundedStatus = isGrounded()

	if inputState == Enum.UserInputState.Begin and (currentGroundedStatus or doubleJumpPossible) then
		doubleJumpPossible = (currentGroundedStatus) and true or false
		isJumping = true
		jumpSquatTimeJumped = os.clock() --Set the player's time jumped to now
	elseif inputState == Enum.UserInputState.End and isJumping then
		-- how long have they held the jump button?
		local now = os.clock()
		
		if now - jumpSquatTimeJumped < jumpSquatDurationSecs then -- below our short jump threshold
			lastJump = 'Short'
		else -- above our short jump threshold
			lastJump = 'Full'
		end
		
		isJumping = false
	end
end

Since we’re going based on the time they started and ended their input, not based on frames (whose durations can vary) this will work regardless of whether or not players have their FPS unlocked. We also remove the need for the decrementJumpSquat function

There are a couple ways:
1- You could store all your values in a dictionary, change the names of the labels to be the keys that correspond to that value, then you could set your text like so:

local function debugDisplay()
    for index, value:TextLabel in pairs(debugValues) do
        if value.Parent.ClassName == "TextLabel" then
            value.Text = values[value.Name]
        end
    end
end

2- You could use a dictionary to hold functions that set text:

local set = {
    [2] = function(label) label.Text = leftInput end;
    [4] = function(label) label.Text = rightInput end;
    -- etc
}

local function debugDisplay()
    for index, value:TextLabel in pairs(debugValues) do
        if value.Parent.ClassName == "TextLabel" then
            set[index](value)
        end
    end
end

doing it this way would avoid the need for the 8 elseif’s in the worst case, but visually I’m not sure whether it’s more readable.

I think the best solution overall though, would be to modularize all of these controller vars and then in a separate script/module set the text of the labels

This worked really well, I probably would’ve never thought of just using the 60 FPS frame times.
Will also work on modularization of that debug function when I’m ready, its quite a mess.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.