Why do players get sent flying under the map

Hey everyone, I have a tool called batons in my game which are supposed to be used to simply knock players back and players are supposed to fight with it to push each other in the water it was made with a module script due to how many batons there are in the game (Its a cosemetic)

In studio it works great and players get pushed back a few studs but in game it works horribly and as soon as a player is hit they are just sent flying ive tried many methods to fix it but nothing works and if it does it just breaks the batons

This is the module

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local players = game:GetService("Players")
local RunService = game:GetService("RunService")

local Baton = {}
Baton.__index = Baton

local function GetAnimation(id: number): Animation
	local Animation = Instance.new("Animation")
	Animation.AnimationId = "rbxassetid://" .. id
	return Animation
end

function Baton.New(name: string, tool: Tool, idleID: number, force: number, swing1ID: number, swing2ID: number): {any}
	local self = setmetatable({}, Baton)

	self.Name = name
	self.Cooldown = 1
	self.Tool = tool
	self.Equipped = false
	self.Wielder = nil
	self.Swinging = false
	self.Force = force
	self.Idle = GetAnimation(idleID)
	self.Swing1 = GetAnimation(swing1ID)
	self.Swing2 = GetAnimation(swing2ID)
	self.Run = GetAnimation(113913581766221)
	self._Connections = {}
	self._Animations = {}

	return self
end

function Baton:HitPlayer(targetCharacter: Model): nil
	if not self.Wielder then return end

	local targetHRP = targetCharacter:FindFirstChild("HumanoidRootPart")
	if not targetHRP then return end

	local attackerHRP = self.Tool.Parent:FindFirstChild("HumanoidRootPart")
	if not attackerHRP then return end

	-- Clean up old attachment if it exists
	local oldAttach = targetHRP:FindFirstChild("KnockbackAttachment")
	if oldAttach then
		local oldLV = oldAttach:FindFirstChildOfClass("LinearVelocity")
		if oldLV then oldLV:Destroy() end
		oldAttach:Destroy()
	end

	-- Compute strictly horizontal direction
	local direction = (targetHRP.Position - attackerHRP.Position)
	direction = Vector3.new(direction.X, 0, direction.Z)
	if direction.Magnitude < 0.1 then return end
	direction = direction.Unit

	-- Attach to HRP center
	local attach = Instance.new("Attachment")
	attach.Name = "KnockbackAttachment"
	attach.Position = Vector3.zero -- ensure centered
	attach.Parent = targetHRP

	local linearVelocity = Instance.new("LinearVelocity")
	linearVelocity.Attachment0 = attach
	linearVelocity.RelativeTo = Enum.ActuatorRelativeTo.World
	linearVelocity.VelocityConstraintMode = Enum.VelocityConstraintMode.Vector
	linearVelocity.VectorVelocity = direction * math.clamp(self.Force, 0, 20000)
	linearVelocity.MaxForce = math.huge
	linearVelocity.Parent = targetHRP

	task.delay(0.2, function()
		if linearVelocity then linearVelocity:Destroy() end
		if attach then attach:Destroy() end
	end)
end

function Baton:StartDetection(): nil
	local debounce = {}

	local function BatonTouched(part)
		local character = part:FindFirstAncestorWhichIsA("Model")
		if not character or not self.Swinging then return end

		local humanoid = character:FindFirstChildWhichIsA("Humanoid")
		if not humanoid then return end

		for _, check in pairs(debounce) do
			if check == character then return end
		end

		table.insert(debounce, character)
		self:HitPlayer(character)
		task.wait(1)
		table.remove(debounce, table.find(debounce, character))
	end

	for index, object in pairs(self.Tool:GetDescendants()) do
		if object:IsA("BasePart") then
			self._Connections[index .. "touchevent"] = object.Touched:Connect(BatonTouched)
		end
	end
end

function Baton:Setup(): nil
	local function EquipTool()
		self.Equipped = true
		local character = self.Tool.Parent
		local humanoid = character:FindFirstChild("Humanoid")
		if not humanoid then return end

		local player = players:GetPlayerFromCharacter(character)
		self.Wielder = player

		self._Animations.Idle = humanoid:LoadAnimation(self.Idle)
		self._Animations.Swing1 = humanoid:LoadAnimation(self.Swing1)
		self._Animations.Swing2 = humanoid:LoadAnimation(self.Swing2)
		self._Animations.Run = humanoid:LoadAnimation(self.Run)

		local idleAlr = false
		local runAlr = false

		self._Animations.Idle:Play()

		task.spawn(function()
			RunService.Heartbeat:Connect(function()
				if self.Equipped then
					if humanoid.MoveDirection.Magnitude > 0 then
						if not runAlr then
							runAlr = true
							idleAlr = false
							self._Animations.Idle:Stop()
							self._Animations.Run:Play()
						end
					else
						if not idleAlr then
							idleAlr = true
							runAlr = false
							self._Animations.Idle:Play()
							self._Animations.Run:Stop()
						end
					end
				else
					self._Animations.Idle:Stop()
					self._Animations.Run:Stop()
				end
			end)
		end)

		local activationSequence = 0
		local cooldown = false

		local function Swing()
			if not humanoid or cooldown then return end
			cooldown = true
			self.Swinging = true
			activationSequence += 1

			if activationSequence >= 2 then
				activationSequence = 0
				self._Animations.Swing2:Play()
			else
				self._Animations.Swing1:Play()
			end

			task.wait(self.Cooldown)
			self.Swinging = false
			cooldown = false
		end

		self._Connections["Swing"] = self.Tool.Activated:Connect(Swing)
	end

	self._Connections["Equip"] = self.Tool.Equipped:Connect(EquipTool)

	local function DequipTool()
		if not self.Equipped then return end

		self.Equipped = false
		self.Wielder = nil

		if self._Animations.Idle then self._Animations.Idle:Stop() end
		if self._Connections["Swing"] then self._Connections["Swing"]:Disconnect() end
	end

	self._Connections["Dequip"] = self.Tool.Unequipped:Connect(DequipTool)

	local function RemoveConnections()
		for _, connection in pairs(self._Connections) do
			connection:Disconnect()
		end

		for _, anim in pairs(self._Animations) do
			anim:Stop()
		end

		table.clear(self._Connections)
		table.clear(self._Animations)
	end

	self.Tool.Destroying:Connect(RemoveConnections)
end

function Baton:ReplicateSword(char)
	local GetPlayer = players:GetPlayerFromCharacter(char)
	if not GetPlayer or not GetPlayer:FindFirstChild("BatonSelected") then return end
	local CurrentBaton = GetPlayer.BatonSelected

	local Sword = ReplicatedStorage.BatonModels:FindFirstChild(CurrentBaton.Value .. " Replica")
	if Sword then
		local SwordReplica = Sword:Clone()
		local Handle = SwordReplica.Handler
		local RootPart = char:FindFirstChild("Torso")

		if RootPart then
			local weld = Instance.new("Weld")
			weld.Part0 = RootPart
			weld.Part1 = Handle
			weld.C1 = CFrame.new(0, .45, -.3) * CFrame.Angles(math.rad(90), 0, math.rad(-40))
			weld.Parent = Handle
			SwordReplica.Parent = char
		else
			warn("HumanoidRootPart not found in character.")
		end
	else
		warn("Sword model not found in BatonModels.")
	end
end

function Baton:RemoveSword(char: Model, baton)
	for _, v: Model in pairs(char:GetChildren()) do
		if v.Name == baton.Name .. " Replica" then
			v:Destroy()
		end
	end
end

return Baton

and this is the script that goes in the actual tool

local replicatedStorage = game:GetService("ReplicatedStorage")
local classes = replicatedStorage:WaitForChild("Classes")
local tool = script.Parent 
local Baton = require(classes.Baton)
local NewBaton = Baton.New(tool.Name, tool, 134226851422062, 30, 130210959577138, 81841947512068)

NewBaton:Setup() 
NewBaton:StartDetection()

tool.Equipped:Connect(function()
	Baton:RemoveSword(tool.Parent, tool)
end)

tool.Unequipped:Connect(function()
	Baton:ReplicateSword(tool.Parent.Parent.Character, tool)
end)

Any help would be greatly appreciated launch is in a few hours

1 Like

Try significantly lowering the knockback strength and see if it fixes your problem. Make sure you are testing in game and not in studio

I tried but after I lower the knockback it barely knocks the player back its already pretty low

1 Like

Great. That means my solution is the answer to your problem. You will need to take your time and adjust it until it works the way you want it to :slightly_smiling_face:

Ive just tried it and they still get flung under the map at the lowest knockback i could put it where the player still moved back

The issue where players are being knocked under the map is likely caused by how LinearVelocity is applied in the Baton:HitPlayer() function. The knockback force you’re applying is horizontal, but nothing prevents the player from going downward if collisions or physics misbehave, especially if they are close to the ground or interacting with other forces.

:white_check_mark: Problem Areas:

  1. Attachment placement and force direction:
  • You’re attaching the LinearVelocity to HumanoidRootPart and applying strictly horizontal force, but Roblox physics may still result in downward movement if the character is already slightly floating or clipping.
  1. Missing upward force or raycast grounding checks.
  • No check ensures that the character remains grounded or lifted slightly to avoid collision with the ground.

:white_check_mark: Recommended Fixes:

:hammer_and_wrench: 1. Add slight upward force

Modify your VectorVelocity to include a small upward Y component to prevent the character from clipping into the ground.

Replace:

direction = Vector3.new(direction.X, 0, direction.Z)

With:

direction = Vector3.new(direction.X, 0.25, direction.Z) -- slight upward push

Then normalize and scale:

direction = direction.Unit
linearVelocity.VectorVelocity = direction * math.clamp(self.Force, 0, 20000)

:hammer_and_wrench: 2. Optional – use BodyVelocity instead

If LinearVelocity continues to cause issues, consider replacing it with BodyVelocity, which often gives more predictable short-term physics results for knockbacks:

local bv = Instance.new("BodyVelocity")
bv.Velocity = direction * math.clamp(self.Force, 0, 20000)
bv.MaxForce = Vector3.new(1e5, 1e5, 1e5)
bv.P = 1250
bv.Parent = targetHRP
task.delay(0.2, function()
	if bv then bv:Destroy() end
end)

:hammer_and_wrench: 3. Ensure characters are not stuck in falling state

You might also want to briefly disable and re-enable PlatformStand to keep them from ragdolling or falling through terrain:

local humanoid = targetCharacter:FindFirstChildOfClass("Humanoid")
if humanoid then
	humanoid.PlatformStand = true
	task.delay(0.3, function()
		if humanoid then humanoid.PlatformStand = false end
	end)
end

:white_check_mark: Summary of Safe LinearVelocity Knockback

Here’s a better direction calculation to ensure safe physics behavior:

local direction = (targetHRP.Position - attackerHRP.Position)
direction = Vector3.new(direction.X, 0.25, direction.Z) -- adds slight upward push
if direction.Magnitude < 0.1 then return end
direction = direction.Unit

If the problem persists even with the adjusted vector, try using BodyVelocity instead.

Let me know if you’d like a fully rewritten HitPlayer() method based on these improvements.