Spiderman Swing System Physics

I want to create a Spiderman Swing System similar to the Insomniac Spiderman games. The issue is I don’t know how to create the pendulum motion of the swinging. I tried to come up with some stuff that could work, but I am really bad at physics math. I searched on the internet, Devforum, and even asked AI for solutions, but nothing has helped or at least made sense with the Roblox physics objects. The version I have now is garbage, but it is very glitchy and does not feel like Spiderman’s motion.

Right now, I’m using LinearVelocity on the HumanoidRootPart and a Beam to a point found above the player. I manually track a Momentum attribute and update VectorVelocity every RenderStepped. The code behind it feels very wrong, and the result is not smooth.

I’d really appreciate:

  • Feedback on whether my approach to pendulum motion with LinearVelocity makes sense.
  • I’m unsure whether I should simulate the swing manually or rely on Roblox constraints
  • Also, if anyone can help me do this with server authority it would be much appreciated.

LocalScript

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

local PointFinder = require(ReplicatedStorage.PointFinder)

local Inputs = ReplicatedStorage.Inputs
local DefaultInput = Inputs.Default
local Swing = DefaultInput.Swing

local Player = game.Players.LocalPlayer
local Camera = workspace.CurrentCamera

local Character:Model = Player.Character or Player.CharacterAdded:Wait()
local HumanoidRootPart:BasePart = Character:WaitForChild("HumanoidRootPart")
local RootAttachment = HumanoidRootPart.RootAttachment

local Animator:Animator = Character:WaitForChild("Humanoid"):WaitForChild("Animator")

local InitSwing = false
local Swinging = false
local Boosting = false
local CanFall = false

local Attachment0:Attachment?
local Attachment1:Attachment?
local Beam:Beam
local PositionPart:Part?
local EdgePosition = Vector3.zero

local PlayerLinearVelocity
local BoostLinearVelocity

local Distance = 0
const MAX_DISTANCE = 200

const ShootAnimId = "rbxassetid://137388960004987"
const HoldAnimId = "rbxassetid://88050919699548"

local ShootAnim = Instance.new("Animation")
ShootAnim.AnimationId = ShootAnimId
local ShootTrack = Animator:LoadAnimation(ShootAnim)

local HoldAnim = Instance.new("Animation")
HoldAnim.AnimationId = HoldAnimId
local HoldTrack = Animator:LoadAnimation(HoldAnim)

local function CreatePostionPart(Position:Vector3)
	local Part = Instance.new("Part")
	Part.CanCollide = false
	Part.Transparency = 1
	Part.Size = Vector3.new(0.1, 0.1, 0.1)
	Part.Position = Position
	Part.Anchored = true
	Part.Parent = workspace
	return Part
end

local function CreateBeam()
	Attachment0 = Instance.new("Attachment")
	Attachment1 = Instance.new("Attachment")
	Beam = Instance.new("Beam")
	Beam.Attachment0 = Attachment0
	Beam.Attachment1 = Attachment1
	Beam.Width0 = 0.2
	Beam.Width1 = 0.2
	Beam.FaceCamera = true
	Beam.LightInfluence = 0.5
	Beam.Transparency = NumberSequence.new(0.5)
	return Attachment0, Attachment1, Beam
end

local function SetMomentum(Value:number)
	local Attribute = HumanoidRootPart:GetAttribute("Momentum")
	if not Attribute then 
		HumanoidRootPart:SetAttribute("Momentum", Value)
		return
	end
	HumanoidRootPart:SetAttribute("Momentum", Value)
end

local function GetMomentum()
	local Attribute = HumanoidRootPart:GetAttribute("Momentum")
	if not Attribute then 
		SetMomentum(0)
		return 0
	end
	return Attribute 
end

local function CleanupVisuals()
	Attachment0:Destroy()
	Attachment1:Destroy()
	Beam:Destroy()
	PositionPart:Destroy()
	EdgePosition = nil
	PositionPart = nil
	Attachment0 = nil
	Attachment1 = nil
	Beam = nil
	HoldTrack:Stop()
end

local function CreateBoost()
	local Momentum = GetMomentum()
	local Force = Vector3.new(HumanoidRootPart.CFrame.LookVector.X * 10, 10, HumanoidRootPart.CFrame.LookVector.Z * 10) * Momentum
	local LinearVelocity = Instance.new("LinearVelocity")
	LinearVelocity.VelocityConstraintMode = Enum.VelocityConstraintMode.Vector
	LinearVelocity.MaxForce = math.huge
	LinearVelocity.VectorVelocity = Force
	LinearVelocity.Attachment0 = RootAttachment
	LinearVelocity.Parent = HumanoidRootPart
	return LinearVelocity
end

local function StartSwing()
	if InitSwing or Swinging then return end
	InitSwing = true
	CanFall = false

	local PlayerPosition = HumanoidRootPart.Position
	local _
	_, EdgePosition = PointFinder.FindClosestEdgePointAbove(PlayerPosition, HumanoidRootPart.CFrame.LookVector, MAX_DISTANCE)
	if not EdgePosition then InitSwing = false return end

	PositionPart = CreatePostionPart(EdgePosition)
	Attachment0, Attachment1, Beam = CreateBeam()
	Beam.Parent = HumanoidRootPart
	Attachment0.Parent = PositionPart
	Attachment1.Parent = Character.RightHand
	ShootTrack:Play()

	SetMomentum(5)
	BoostLinearVelocity = BoostLinearVelocity ~= nil and BoostLinearVelocity or CreateBoost()
	Boosting = true

	task.delay(.1, function()
		if BoostLinearVelocity then BoostLinearVelocity:Destroy() end
		Boosting = false
		if InitSwing == false then return end

		Distance = (HumanoidRootPart.Position - EdgePosition).Magnitude

		PlayerLinearVelocity = Instance.new("LinearVelocity")
		PlayerLinearVelocity.VelocityConstraintMode = Enum.VelocityConstraintMode.Vector
		PlayerLinearVelocity.MaxForce = math.huge
		PlayerLinearVelocity.Attachment0 = RootAttachment
		PlayerLinearVelocity.Parent = HumanoidRootPart

		SetMomentum(10)
		InitSwing = false
		Swinging = true
		HoldTrack:Play()
	end)
end

local function StopSwing()
	if Swinging or InitSwing then 
		CleanupVisuals()
		InitSwing = false
		Swinging = false
		CanFall = false
		Boosting = true
		BoostLinearVelocity = BoostLinearVelocity ~= nil and BoostLinearVelocity or CreateBoost()
		task.delay(.3, function()
			if BoostLinearVelocity then BoostLinearVelocity:Destroy() end
			Boosting = false
		end)
	end
end

Swing.Pressed:Connect(StartSwing)
Swing.Released:Connect(StopSwing)

local function IsOnGround()
	local RaycastParams = RaycastParams.new()
	RaycastParams.FilterDescendantsInstances = {Character}
	RaycastParams.FilterType = Enum.RaycastFilterType.Exclude
	local RaycastResult = workspace:Raycast(HumanoidRootPart.Position, Vector3.new(0, -1, 0) * 15, RaycastParams)
	return RaycastResult ~= nil
end

RunService.RenderStepped:Connect(function(dt)
	if not Swinging and not InitSwing and not Boosting then CanFall = true end

	if not Swinging then
		if not PlayerLinearVelocity or not CanFall then return end
		if IsOnGround() then 
			PlayerLinearVelocity:Destroy()
			PlayerLinearVelocity = nil
			SetMomentum(0)
			return 
		end
		SetMomentum(GetMomentum() + (5 * dt))
		local Momentum = GetMomentum()
		local Mass = HumanoidRootPart.AssemblyMass
		PlayerLinearVelocity.VectorVelocity = Vector3.new(HumanoidRootPart.CFrame.LookVector.X*10, -10, HumanoidRootPart.CFrame.LookVector.Z*10) * ((10 * Momentum) / Mass)
		return  
	end

	if not PositionPart or not Attachment0 or not Attachment1 then return end

	local Momentum = GetMomentum()
	SetMomentum(math.clamp(Momentum + (Momentum * 1 * dt), 0, 25))
	local Mass = HumanoidRootPart.AssemblyMass
	PlayerLinearVelocity.VectorVelocity = Vector3.new(HumanoidRootPart.CFrame.LookVector.X*10, 10, HumanoidRootPart.CFrame.LookVector.Z*10) * ((10 * Momentum) / Mass)

	if HumanoidRootPart.Position.Y >= EdgePosition.Y then 
		StopSwing()
	end
end)

PointFinder Module

local module = {}

module.SourceFolder = workspace:WaitForChild("SwingPoints")

-- closest part whose TOP is above the input position AND within range AND in front of player
function module.FindClosestPartAbove(position: Vector3, playerForward: Vector3, range: number?)
	range = range or math.huge

	local closestPart = nil
	local closestDist = math.huge

	for _, part in ipairs(module.SourceFolder:GetDescendants()) do
		if part:IsA("BasePart") then
			local topY = part.Position.Y + (part.Size.Y / 2)

			-- Must be above player
			if topY > position.Y then

				-- Direction from player to part
				local dirToPart = (part.Position - position).Unit

				-- Dot product: >0 means in front, <0 means behind
				local dot = playerForward:Dot(dirToPart)
				if dot <= 0 then
					continue -- ignore parts behind player
				end

				-- Horizontal distance
				local dx = part.Position.X - position.X
				local dz = part.Position.Z - position.Z
				local dist = math.sqrt(dx*dx + dz*dz)

				if dist <= range and dist < closestDist then
					closestDist = dist
					closestPart = part
				end
			end
		end
	end

	return closestPart
end



-- Closest edge point on top face
function module.ClosestEdgePointOnTopFace(part: BasePart, worldPos: Vector3)
	local cf = part.CFrame
	local size = part.Size

	local localPos = cf:PointToObjectSpace(worldPos)

	local topLocal = Vector3.new(
		math.clamp(localPos.X, -size.X/2, size.X/2),
		size.Y/2,
		math.clamp(localPos.Z, -size.Z/2, size.Z/2)
	)

	local distToXEdge = math.min(
		math.abs(topLocal.X - size.X/2),
		math.abs(topLocal.X + size.X/2)
	)

	local distToZEdge = math.min(
		math.abs(topLocal.Z - size.Z/2),
		math.abs(topLocal.Z + size.Z/2)
	)

	if distToXEdge < distToZEdge then
		if topLocal.X > 0 then
			topLocal = Vector3.new(size.X/2, topLocal.Y, topLocal.Z)
		else
			topLocal = Vector3.new(-size.X/2, topLocal.Y, topLocal.Z)
		end
	else
		if topLocal.Z > 0 then
			topLocal = Vector3.new(topLocal.X, topLocal.Y, size.Z/2)
		else
			topLocal = Vector3.new(topLocal.X, topLocal.Y, -size.Z/2)
		end
	end

	return cf:PointToWorldSpace(topLocal)
end


function module.FindClosestEdgePointAbove(position: Vector3, playerForward: Vector3, range: number?)
	local part = module.FindClosestPartAbove(position, playerForward, range)
	if not part then
		return nil, nil, nil
	end

	local edgePoint = module.ClosestEdgePointOnTopFace(part, position)

	return part, edgePoint
end

return module

If you need me to elaborate on this please ask me. Also, thank you in advance!

3 Likes

Bumping post to improve visibility!

I just would suggest you to learn how the real Pendulum physics work, then apply adjustment forces to it. Think real physics knowledge as roblox avatar the Bacon man, and the adjustments as avatar customization. Assist the Pendulum to feel like spider web by applying hidden forces to it and customizing it. Mod the Pendulum physics like people modding games!

For this type of project I recommend just rendering physics calculation rather than relying on Roblox’s velocity instances. After that you can translate it to Roblox physics instances. Because you don’t really know, what you need, you just have set of instances you have to deal and you’re trying to make recipe out of them without addressing the core physics logic. Right now you don’t even know if its your scripts fault that player falls like that or velocity instance doing it, this is simple example who knows what else they’re making it hard on you.

What are your thoughts on using physics constraints Roblox provides? I did some research and I think that a ball and socket constraint with a rod could probably emulate some pendulum motion.

I see them as tools that can provide my physics based systems smoothness since constraints run at 240 hz instead of 60 hz or whatever player set their fps. Imagine calculation based as prototype or core of it and physics instances as a polish on it. Its like sketch vs rendered drawing, render is nothing without proper sketch. Its easier to customize and polish it while its in sketch stage, and gives you control and predictability. Even though I say those things I am not that great at physics constraints and math behind them I still learning and I think it would give me less headache if I first start with raw physics calculations than constraints. Doesn’t it sound more comfortable to work with a part you know Roblox doesn’t apply physics?

1 Like

Bumping because we need more cool stuff like this

1 Like

Agreed. Hoping that some mastermind at physics hopefully catches sight on this.

Yeah, it does. I just wish I understood the math better because I could do other things with these principals that are cool.