Trying to find a way to do smooth knockback correctly

  1. What do you want to achieve? Keep it simple and clear!
    I want to achieve a smooth knockback for both dummies and players for later use in an anime game.

  2. What is the issue?
    The issue is that YouTube videos and old forum topics use either BodyVelocity (which is deprecated now) or showcase excessive/crazy knockback.
    My current knockback implemented with AssemblyLinearVelocity is not smooth at all (on both client and server). I already tried ApplyImpulse, but I feel there are too many parameters to manage, like mass. In an anime game, this is a bit excessive because I just want everybody to get knocked back the exact same distance. Also, sometimes the character would just fall down on the ground before getting back up, which is extremely annoying (even though I don’t encounter this specific issue anymore).

  3. What solutions have you tried so far?
    First, I tried this script where I fire a RemoteEvent for players, and handle dummies on the server using Heartbeat to manually set their velocity and calculate the distance.

local look = attackerRoot.CFrame.LookVector
local dir = Vector3.new(look.X, 0, look.Z)
if dir.Magnitude < 0.01 then return end
dir = dir.Unit
local speed = KNOCKBACK_STUDS / KNOCKBACK_TIME

local targetChar = targetHumanoid.Parent :: Model
local targetPlayer = Players:GetPlayerFromCharacter(targetChar)

if targetPlayer then
    KnockbackRemote:FireClient(targetPlayer, {
        dir = dir,
        speed = speed,
        up = KNOCKBACK_UP,
        distance = KNOCKBACK_STUDS,
        duration = KNOCKBACK_TIME,
    })
else
    targetRoot:SetNetworkOwner(nil)

    local startFlat = Vector3.new(targetRoot.Position.X, 0, targetRoot.Position.Z)
    local elapsed = 0
    local upApplied = false

    local conn: RBXScriptConnection
    conn = RunService.Heartbeat:Connect(function(dt)
        elapsed += dt
        local p = Vector3.new(targetRoot.Position.X, 0, targetRoot.Position.Z)
        local travelled = (p - startFlat).Magnitude

        if travelled >= KNOCKBACK_STUDS or elapsed >= KNOCKBACK_TIME or targetRoot.Parent == nil then
            conn:Disconnect()
            if targetHumanoid.Parent and targetHumanoid.Health > 0 then
                --targetHumanoid:ChangeState(Enum.HumanoidStateType.GettingUp)
            end
            return
        end

        --targetHumanoid:ChangeState(Enum.HumanoidStateType.Physics)
        local v = targetRoot.AssemblyLinearVelocity
        local vy = v.Y
        if not upApplied then
            vy = KNOCKBACK_UP
            upApplied = true
        end
        targetRoot.AssemblyLinearVelocity = Vector3.new(dir.X * speed * 1.1, vy, dir.Z * speed * 1.1)
    end)
end

Even with the network owner of the dummy set to nil, manually setting AssemblyLinearVelocity in Heartbeat is super stuttery at the start.

I also tried setting PlatformStanding to true on the humanoid during the knockback, but it feels too excessive and just makes the dummy fall over immediately. I tried changing states using Humanoid:ChangeState(Enum.HumanoidStateType.Physics) instead, but it didn’t really change much and the overall applied effect was still worse than PlatformStanding.

After that, I tried switching to the new LinearVelocity constraint with this script:

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Debris = game:GetService("Debris")

local knockbackEvent = ReplicatedStorage:WaitForChild("KnockbackEvent")

local dummy = workspace:WaitForChild("Dummy")
local dummyHRP = dummy:WaitForChild("HumanoidRootPart")

knockbackEvent.OnServerEvent:Connect(function(player)
	if dummyHRP:FindFirstChild("KnockbackAttachment") then return end

	local playerHRP = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
	if not playerHRP then return end

	local direction = (dummyHRP.Position - playerHRP.Position).Unit
	local power = 80

	local attachment = Instance.new("Attachment")
	attachment.Name = "KnockbackAttachment"
	attachment.Parent = dummyHRP

	local lv = Instance.new("LinearVelocity")
	lv.Name = "KnockbackVelocity"

	lv.ForceLimitsEnabled = true
	lv.ForceLimitMode = Enum.ForceLimitMode.PerAxis
	lv.MaxAxesForce = Vector3.new(math.huge, 0, math.huge) 

	lv.VectorVelocity = Vector3.new(direction.X, 0, direction.Z).Unit * power
	lv.Attachment0 = attachment
	lv.Parent = dummyHRP

	Debris:AddItem(lv, 0.25)
	Debris:AddItem(attachment, 0.25)
end)

This actually works and looks incredibly smooth! But there is a huge catch: whenever the dummy collides with a wall during the knockback, it either gets violently launched into the air (I assume the physics solver is trying to resolve the collision and panics), or it doesn’t fly up but instead starts glitching and moving uncontrollably in all directions.

How can I keep this smooth LinearVelocity knockback but prevent the dummy from flying into orbit or glitching whenever it hits a wall? Any help is highly appreciated!

Here are some videos of the LinearVelocity method :


Here are some videos of the AssemblyLinearVelocity method :


the LinearVelocity is probably better

i think using math.huge literally tells the engine that it WILL move that speed NO MATTER THE CASE, so it will cause some weird stuff when colliding with objects that do NOT allow it move that speed

the fix: just make it a high number, but not math.huge
idk just try some random number like 60k or something, just something high but not insane
i could be completely wrong, I’m not great with linear/assembly velocities yet

2 Likes

I’ve already found this solution, and it works flawlessly.

For example, with a target speed of 20 studs per second and a lifetime of 0.5 seconds before garbage collection (Debris), I successfully get exactly 10 studs of distance traveled.

lv.MaxAxesForce = Vector3.new(15000, 0, 15000)
lv.MaxAxesForce = Vector3.new(100000, 0, 100000) -- same as 15000
lv.MaxAxesForce = Vector3.new(math.huge, 0, math.huge) -- same as 15000

If I set this to 15,000 or higher (or even math.huge), the dummy gets pushed back exactly 10 studs every single time.

But here’s the thing: 15,000 works perfectly, but 10,000 doesn’t quite cut it. Why is that? Is there an actual formula to calculate this minimum force threshold? The Roblox docs don’t really explain this, and honestly, relying on pure guesswork and trial-and-error makes me a bit uncomfortable when trying to understand how the physics engine actually behaves under the hood.

On top of that, the fact that the values we put in the Vector3 for MaxAxesForce don’t have any units doesn’t help at all to understand the ‘how’ and the ‘why’ behind the numbers.

If I’m understanding how this works correctly, with zero friction (and zero gravity), the only thing resisting the movement should be the internal forces of the dummy (I assume the Humanoid’s internal state forces trying to keep it standing/upright). Even when I enabled the Massless property on every single part of the dummy, it barely changed the result at all.

The dummy in the background (which has ground friction) is less affected by the knockback than the dummy in the foreground :


So, why is it that starting right around 15,000, the character travels exactly 10 studs away over 0.5s at 20 studs/s?

i think massless doesn’t really change it because of how Assembly’s work, for an Assembly SOMETHING needs mass, so the HRP (the heaviest part) retains its mass for the assembly and it barely makes a difference

honestly I’m not to sure, I’m still trying to learn and understand roblox physics lol

Here’s a script I made a few weeks back which can use either raw velocity or LinearVelocities. I understand the topic has already been solved, but it’s a good opportunity to share something I think may work well for others just as it does for me.

--RAW VELOCITY VERSION (EX)
--[[local Players = game:GetService("Players")

_G.knock = function(character_to_knock, position_to_knock_from, velocity_value)
	task.spawn(function()
	if not character_to_knock or not character_to_knock:IsA("Model") then
		return
	end

	local humanoid = character_to_knock:FindFirstChildOfClass("Humanoid")
	local root = character_to_knock:FindFirstChild("HumanoidRootPart")

	if not humanoid or not root then
		return
	end

	local origin

	if typeof(position_to_knock_from) == "Vector3" then
		origin = position_to_knock_from
	elseif typeof(position_to_knock_from) == "Instance"
		and position_to_knock_from:IsA("BasePart") then
		origin = position_to_knock_from.Position
	else
		return
	end

	humanoid:ChangeState(Enum.HumanoidStateType.FallingDown)

	local direction = root.Position - origin

	if direction.Magnitude < 0.01 then
		direction = Vector3.zAxis
	end

	direction = (direction.Unit + Vector3.new(0, 0.35, 0)).Unit

	local desiredVelocity = direction * velocity_value
	humanoid.RootPart:SetNetworkOwner(nil)
	humanoid.RootPart.Velocity = desiredVelocity
	task.spawn(function()
		repeat task.wait() until humanoid.RootPart.Velocity.Magnitude < desiredVelocity.Magnitude * .1
		humanoid.RootPart:SetNetworkOwner(game.Players:GetPlayerFromCharacter(character_to_knock))
		end)
end)]]
--LINEARVELOCITY VERSION (NEO)
local Debris = game:GetService("Debris")
local Players = game:GetService("Players")

_G.knock = function(character_to_knock, position_to_knock_from, velocity_value)
	task.spawn(function()
		if not character_to_knock or not character_to_knock:IsA("Model") then
			return
		end

		local humanoid = character_to_knock:FindFirstChildOfClass("Humanoid")
		local root = character_to_knock:FindFirstChild("HumanoidRootPart")

		if not humanoid or not root then
			return
		end

		local origin

		if typeof(position_to_knock_from) == "Vector3" then
			origin = position_to_knock_from
		elseif typeof(position_to_knock_from) == "Instance"
			and position_to_knock_from:IsA("BasePart") then
			origin = position_to_knock_from.Position
		else
			return
		end

		humanoid:ChangeState(Enum.HumanoidStateType.FallingDown)

		local direction = root.Position - origin

		if direction.Magnitude < 0.01 then
			direction = Vector3.zAxis
		end

		direction = (direction.Unit + Vector3.new(0, 0.35, 0)).Unit

		local desiredVelocity = direction * velocity_value

		-- Preserve any existing upward/downward motion if desired.
		-- Remove this line if you want a complete override.
		desiredVelocity += Vector3.yAxis * math.max(root.AssemblyLinearVelocity.Y, 0)

		local attachment = Instance.new("Attachment")
		attachment.Parent = root

		local linearVelocity = Instance.new("LinearVelocity")
		linearVelocity.Attachment0 = attachment
		linearVelocity.RelativeTo = Enum.ActuatorRelativeTo.World
		linearVelocity.VectorVelocity = desiredVelocity
		linearVelocity.ForceLimitsEnabled = false
		linearVelocity.Parent = root
		linearVelocity.Name = "PushForce"
		Debris:AddItem(attachment, 0.1)
		Debris:AddItem(linearVelocity, 0.1)
	end)
end

--This will knock players back from nearby explosions.
workspace.ChildAdded:Connect(function(desc)
	if desc:IsA("Explosion") then
		for _, player in ipairs(Players:GetPlayers()) do
			local character = player.Character
			if not character then
				continue
			end

			local root = character:FindFirstChild("HumanoidRootPart")
			if not root then
				continue
			end

			if (desc.Position - root.Position).Magnitude <= desc.BlastRadius then
				_G.knock(character, desc.Position, math.clamp(desc.BlastPressure, 0, 512))
			end
		end
	end
end)

It’s important you remove

“humanoid:ChangeState(Enum.HumanoidStateType.FallingDown)”

if you don’t want it.

1 Like

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