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
LinearVelocitymakes 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!