Alright, as I said, it only took 15-30 minutes. However, I sort of got carried away and expanded on the original idea. That took a few hours longer. Anyways, heres a model I made:
If you want, here’s the code it uses:
Code
-----------------------
-- Services -----------
local CollectionService: CollectionService = game:GetService("CollectionService")
local PhysicsService: PhysicsService = game:GetService("PhysicsService")
local Players: Players = game:GetService("Players")
local Debris: Debris = game:GetService("Debris")
-----------------------
-- Variables ----------
-- Types
type ferro = {target: Attachment, origin: Attachment, magnet: AlignPosition, bond: WeldConstraint, bonded: boolean}
type targetPoints = {magnet: BasePart, magnetPos: Vector3, ferroPos: Vector3}
-- Objects
local magnetFolder: Folder = script.Parent
local magnetCreator: BasePart = magnetFolder:WaitForChild("MagnetCreator")::BasePart
local magnetTemplate: BasePart = magnetCreator:Clone() do
magnetTemplate.Name = "Magnet"
CollectionService:AddTag(magnetTemplate, "ferromagnetic")
end
local emptyVector = Vector3.new()
-- Track
local ferroCache: {[BasePart]: ferro} = {}
-- Settings
local isRealistic: boolean = false -- whether to try and make it be somewhat more realistic
local isChainable: boolean = true -- whether to link ferromagnets together
local nearMass: number = 15 -- the maximum difference in mass between two parts to classify as 'equal'
local radius: number = 30 -- the distance the magnetic parts need to be from the character
local showWelds: boolean = true
local material: Enum.Material = Enum.Material.SmoothPlastic -- the material of the magnetic parts
local color: Color3 = Color3.fromRGB(163, 162, 165) -- the color of the magnetic parts
-- Params
local rayParams: RaycastParams = RaycastParams.new() do
rayParams.FilterType = Enum.RaycastFilterType.Whitelist
end
local overlapParams: OverlapParams = OverlapParams.new() do
overlapParams.CollisionGroup = if isChainable then "ferromagnetic" else "Default"
end
-- Collision groups
local magneticGroup: number = PhysicsService:CreateCollisionGroup("magnetic")
local ferromagneticGroup: number = PhysicsService:CreateCollisionGroup("ferromagnetic")
PhysicsService:CollisionGroupSetCollidable("magnetic", "ferromagnetic", false)
PhysicsService:CollisionGroupSetCollidable("ferromagnetic", "ferromagnetic", isChainable)
magnetTemplate.CollisionGroupId = ferromagneticGroup
-----------------------
-- Methods ------------
local abs: (number) -> number = math.abs
-- Returns if a point is in a parts bounds
local function isPointInPartBounds(part: BasePart, point: Vector3): boolean
local size: Vector3 = part.Size * 0.5
local relativePos: Vector3 = part.CFrame:PointToObjectSpace(point)
return (math.clamp(relativePos.X,-size.X,size.X) == relativePos.X and math.clamp(relativePos.Y,-size.Y,size.Y) == relativePos.Y and math.clamp(relativePos.Z,-size.Z,size.Z) == relativePos.Z)
end
-- Returns if the magnet and ferromagnet are touch and an array containing the magnet and attachment positions
local function getTargetPoints(ferroPart: BasePart, target: Vector3): (boolean,targetPoints)
local isTouching: boolean, points: targetPoints = false, {}::targetPoints
local origin: Vector3 = ferroPart.Position
rayParams.FilterDescendantsInstances = CollectionService:GetTagged("bonded")
local targetResult: RaycastResult? = workspace:Raycast(origin, (target-origin), rayParams)
if targetResult then
points.magnet = targetResult.Instance
points.magnetPos = targetResult.Position
target = points.magnetPos
end
rayParams.FilterDescendantsInstances = {ferroPart}
local originResult: RaycastResult? = workspace:Raycast(target, (origin-target), rayParams)
points.ferroPos = if originResult then originResult.Position else origin
isTouching = isPointInPartBounds(points.magnet, origin) or isPointInPartBounds(ferroPart, target)
return isTouching,points
end
-- Initializes and returns a magnet force
local function createMagneticForce(origin: BasePart, attachment0: Attachment, attachment1: Attachment): AlignPosition
local originMass: number, targetMass: number = origin.AssemblyMass, attachment1.Parent.AssemblyMass
local alignPosition: AlignPosition = Instance.new("AlignPosition") do
alignPosition.Name = "MagneticForce"
alignPosition.ReactionForceEnabled = isRealistic and nearMass >= abs(originMass-targetMass) -- apply force to both?
if origin.Anchored or (isRealistic and originMass > targetMass) then attachment0,attachment1 = attachment1,attachment0 end
alignPosition.Attachment0 = attachment0
alignPosition.Attachment1 = attachment1
alignPosition.Parent = origin
end
return alignPosition
end
-- Initializes and returns a magnet attachment at the relative position
local function createAttachment(part: BasePart, position: Vector3?): Attachment
local attachment: Attachment = Instance.new("Attachment") do
attachment.Name = "MagnetAttachment"
attachment.Position = if position then part.CFrame:PointToObjectSpace(position) else emptyVector
attachment.Parent = part
end
return attachment
end
-- Bonds a ferromagnetic and magnetic part together
local function createBond(ferroPart: BasePart, magnetPart: BasePart): WeldConstraint
local weld: WeldConstraint = Instance.new("WeldConstraint") do
weld.Name = "MagneticBond"
weld.Enabled = false
weld.Part0 = magnetPart
weld.Part1 = ferroPart
weld.Parent = ferroPart
end
return weld
end
local function highlight(part: BasePart): ()
local highlight: SelectionBox = Instance.new("SelectionBox") do
highlight.Adornee = part
highlight.Color3 = Color3.fromRGB(100,100,255)
highlight.SurfaceColor3 = Color3.fromRGB(100,100,200)
highlight.Parent = part
end
end
-- Magnetizes a part
local function attractFerromagnet(magneticOrigin: Vector3, ferroPart: BasePart): ()
local ferro: ferro = ferroCache[ferroPart] or {}::ferro
local isTouching: boolean, points: targetPoints = getTargetPoints(ferroPart, magneticOrigin)
ferro.target = ferro.target or createAttachment(points.magnet, points.magnetPos)
ferro.origin = ferro.origin or createAttachment(ferroPart, points.ferroPos)
ferro.magnet = ferro.magnet or createMagneticForce(ferroPart, ferro.origin, ferro.target)
ferro.bond = ferro.bond or createBond(ferroPart, points.magnet)
ferro.bonded = isTouching
ferro.target.Position = points.magnet.CFrame:PointToObjectSpace(points.magnetPos)
ferro.origin.Position = ferroPart.CFrame:PointToObjectSpace(points.ferroPos)
if isTouching then
ferro.bond.Enabled = isTouching
ferro.magnet.Enabled = not isTouching
if isChainable then CollectionService:AddTag(ferroPart, "bonded") end
if showWelds then highlight(ferroPart) end
end
ferroCache[ferroPart] = ferro
end
-- Create magnetic parts
local function CreateFerromagneticPart(): ()
local magnetPart: BasePart = magnetTemplate:Clone() do
magnetPart.Size = Vector3.new(math.random(5),math.random(5),math.random(5))
magnetPart.Material = material
magnetPart.Color = color
magnetPart.Anchored = false
magnetPart.Parent = magnetFolder
end
Debris:AddItem(magnetPart, 20)
end
-- Returns true if a PVInstance exist in 3D space and false if not
local function HasMass(pv: PVInstance): boolean
local exist: boolean = pv:IsA("BasePart") or (pv:FindFirstChildOfClass("BasePart") ~= nil)
if not exist then
for _, descendant: Instance in pairs(pv:GetDescendants()) do
exist = descendant:IsA("BasePart")
if exist then break end
end
end
return exist
end
-- Sets the character collision group
local function MagnetizeCharacter(character: Model): ()
task.defer(function()
for _, child: Instance in pairs(character:GetChildren()) do
if child:IsA("BasePart") then
child.CollisionGroupId = magneticGroup
if child ~= character.PrimaryPart then CollectionService:AddTag(child, "bonded") end
end
end
CollectionService:AddTag(character, "magnetic")
end)
end
-----------------------
-- Events -------------
-- Collision groups
for _, player: Player in pairs(Players:GetPlayers()) do -- set already loaded characters collision
MagnetizeCharacter(player.Character or player.CharacterAdded:Wait())
player.CharacterAdded:Connect(MagnetizeCharacter)
end
Players.PlayerAdded:Connect(function(player: Player) -- set loading characters collision
player.CharacterAdded:Connect(MagnetizeCharacter)
end)
-- Magnet creation
Instance.new("ClickDetector", magnetCreator).MouseClick:Connect(CreateFerromagneticPart)
-- Detect parts
game:GetService("RunService").Heartbeat:Connect(function(): ()
for _, magnet: PVInstance in pairs(CollectionService:GetTagged("magnetic")::{PVInstance}) do
if (magnet.Parent ~= nil) and HasMass(magnet) then -- check if it exist
local magneticOrigin: Vector3 = magnet:GetPivot().Position
for _, part: BasePart in pairs(workspace:GetPartBoundsInRadius(magneticOrigin, radius, overlapParams)) do
local ferro: ferro = ferroCache[part]
if (part.CollisionGroupId ~= ferromagneticGroup) or (ferro and ferro.bonded) or CollectionService:HasTag(part, "bonded") then continue end
task.spawn(attractFerromagnet, magneticOrigin, part)
end
else -- remove if it doesn't
CollectionService:RemoveTag(magnet, "magnetic")
end
end
end)
CollectionService:GetInstanceRemovedSignal("ferromagnetic"):Connect(function(ferroPart: BasePart) ferroCache[ferroPart] = nil end)
-----------------------
-- Execution ----------
for i=1, 30 do
CreateFerromagneticPart()
task.wait(2)
end
There are several settings that you can set, such as whether it will highlight welded parts (debugging purposes), whether parts can form a magnetic “chain”, and an option for semi-realistic magnetism (in relation to part mass). Also know that parts that are anchored will switch so that the player is pulled to them instead.