Smooth weld between 2 characters

Hi
Welding two player characters together is an issue I’ve seen a lot on the devforums (sadly without good solutions). It’s difficult to do this in a way that’s smooth for both players. I want to give an example method of doing this. This uses a simple assembly as ‘middle-man’ to connect the players together.
Here’s the result:

Technical explanation

We use the same principle as having a driver and passenger(s) in a vehicle: weld them both to a central assembly and give the driver network ownership of the assembly. The driver then controls the assembly using bodyforces.

Our assembly consist of 2 parts:


The ‘BasePart’ is welded to the ‘Part’, and ‘Part’ is welded to the target character’s rootpart. Then the position/orientation of ‘Basepart’ (and hence, the entire assembly + the target character), is updated by the controlling character to their rootpart’s CFrame.

Here is a placefile that demonstrates the weld:
character_weld_test.rbxl (63.0 KB)
Use the playtest with 2 players for it to work.

The code itself is not too complex, so you should be able to modify it to suit your needs.
Basic module usage:

WeldPlayers.Setup(player1, player2, CFrame.new(0, 0, 5)*CFrame.Angles(0, math.rad(90), 0))

player1 is the controlling player, player2 is the player that should be welded to player1, and the third parameter is a CFrame offset compared to player1’s CFrame.
To undo the weld:

WeldPlayers.Cleanup(player2)

The reason I’m using ‘player2’ for cleanup, has to do with our game (simplifies some things). If you prefer, you can rewrite some of the module’s code so it keeps track of the weld under ‘player1’.

Streaming is supported (using CollectionService). Been using this for our game, and so far it seems to work nicely. Let me know if you find any bugs.

35 Likes

Fixed a typo in the module: ‘player1’ where it should have been ‘player2’ for cleanup.
This caused the model to not be removed automatically when either player died.

Didn’t notice this, since I had an external function that calls cleanup if someone dies.

Here are some adjustemnts i made for people who want it to work with Player-NPC and not just Player-Player

  • 1- Take the Client script under the WeldPlayersTogether Module and parent it to StarterPlayer.StarterPlayerScripts instead of inserting it through script
    image

  • 2- Edited the module a bit to work with Player-NPC tasks ( The player has to be the movement controller not the NPC)

--[[
	'Weld' two characters together.
	This is done by welding both characters to an assembly and giving the first player network ownership over both.
	
	A localscript is inserted to playerscripts for all players that join.
	This handles the movement for all welds that are owned by that player.
	
	Advantages:
		* Smooth movement (similar to vehicle assemblies with seats)
		* Server doesn't have to handle updates every frame; clients handle it
	Disadvantages:
		* Weld 'owner' is given full movement control over the target character, so succeptible to exploits
]]
local TAG = "CustomPlayerWeldAssemblyPart"

local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")

local weldModel = script.WeldModel

-- Update collisions enabled/disabled for the character (char that doesn't have network ownership)
local function updateCollisions(char, canCollide)
	if not canCollide then
		-- Disable collisions, and keep track of which we disabled using an attribute, so we can re-enable collisions later
		for _, v in pairs(char:GetDescendants()) do
			if (v:IsA("BasePart") or v:IsA("MeshPart")) and v.CanCollide == true then
				v.CanCollide = false
				v:SetAttribute("WasCanCollideEnabledBeforeWeld", true)
			end
		end
	else
		-- Enable collisions; only for parts that had collisions enabled originally
		for _, v in pairs(char:GetDescendants()) do
			if v:GetAttribute("WasCanCollideEnabledBeforeWeld") then
				v.CanCollide = true
				v:SetAttribute("WasCanCollideEnabledBeforeWeld", nil)
			end
		end
	end
end


local module = {}
module.CurrentWelds = {
	-- Format: player1 = weldModel, ...
}
module.ConnectionsPerAssembly = {
	-- Format: weldModel = { conn1, conn2, ... }, ...
}

-- Weld 'part' and 'to' using a WeldConstraint
local function weld(part, to)
	local weld = Instance.new("WeldConstraint")
	weld.Name = "PlayerWeldConstraint"
	weld.Part0 = to
	weld.Part1 = part
	weld.Parent = to
end

-- Add weld between two players
-- 'offsetCFrame' is an offset starting from player1's rootpart
-- 'player1' gets network ownership of both characters; 'player2' is the player that initiated the weld
function module.Setup(Character1: Model, Character2: Model, offsetCFrame)
	if not Character1 or not Character2 then
		return
	end
	local char1, char2 = Character1, Character2
	if not char1 or not char2 then
		return
	end
	local root1, root2 = char1:FindFirstChild("HumanoidRootPart"), char2:FindFirstChild("HumanoidRootPart")
	if not root1 or not root2 then
		return
	end
	local humanoid1, humanoid2 = char1:FindFirstChild("Humanoid"), char2:FindFirstChild("Humanoid")
	if not humanoid1 or not humanoid2 then
		return
	end
	
	-- Disable collisions on the char that is moved around by the assembly forces (prevent collision issues and physics flings)
	updateCollisions(char2, false)
	
	local model = weldModel:Clone()
	
	-- For some reason we can't change the relative cframes when the weldconstraint is enabled, so we briefly disable it here...
	model.BasePart.WeldConstraint.Enabled = false
	
	-- Setup defaults: assembly floats at char1's root position
	model.BasePart.CFrame = root1.CFrame
	model.BasePart.AlignOrientation.CFrame = model.BasePart.CFrame
	model.BasePart.AlignPosition.Position = model.BasePart.Position
	
	-- Set relative cframe of 'BasePart' and 'Part'
	model.Part.CFrame = offsetCFrame
	
	model.BasePart.WeldConstraint.Enabled = true
	
	-- Instantly teleport second character towards dest and weld them to assembly
	char2:SetPrimaryPartCFrame(model.Part.CFrame)
	weld(root2, model.Part)
	
	model.Parent = workspace
	
	local PlayerOwner = game.Players:GetPlayerFromCharacter(Character1)
	
	-- Set NetworkOwner of assembly to be 'char1'
	model.BasePart:SetNetworkOwner(PlayerOwner)
	
	-- Set attribute and tag, so 'player1' knows they can control the assembly to their rootpart
	model.BasePart:SetAttribute("UserId", PlayerOwner.UserId)
	CollectionService:AddTag(model.BasePart, TAG)
		
	-- Save assembly so we can cleanup later (save to 'player2' instead of 'player1'!)
	module.CurrentWelds[Character2] = model
	
	-- Cleanup if either character dies (humanoid.Died or removed from workspace)
	module.ConnectionsPerAssembly[model] = {}
	table.insert(module.ConnectionsPerAssembly[model], humanoid1:GetPropertyChangedSignal("Health"):Connect(function()
		if not humanoid1 or humanoid1.Health <= 0 then
			module.Cleanup(Character2)
		end
	end))
	table.insert(module.ConnectionsPerAssembly[model], humanoid2:GetPropertyChangedSignal("Health"):Connect(function()
		if not humanoid2 or humanoid2.Health <= 0 then
			module.Cleanup(Character2)
		end
	end))
	table.insert(module.ConnectionsPerAssembly[model], char1:GetPropertyChangedSignal("Parent"):Connect(function()
		if not char1 or not char1.Parent then
			module.Cleanup(Character2)
		end
	end))
	table.insert(module.ConnectionsPerAssembly[model], char2:GetPropertyChangedSignal("Parent"):Connect(function()
		if not char2 or not char2.Parent then
			module.Cleanup(Character2)
		end
	end))
end

-- Cleanup assembly, and release both players from welds
-- NOTE: Pass 'player2'; NOT the player that has network ownership!
function module.Cleanup(Character2)
	if not Character2 then
		return
	end

	updateCollisions(Character2, true)
	
	local model = module.CurrentWelds[Character2]
	if model then
		-- Cleanup connections for this assembly
		if module.ConnectionsPerAssembly[model] then
			for _, v in pairs(module.ConnectionsPerAssembly[model]) do
				v:Disconnect()
			end
			module.ConnectionsPerAssembly[model] = nil
		end
		-- Cleanup assembly model
		model:Destroy()
		module.CurrentWelds[Character2] = nil
	end
end

return module


  • 3- Changed the third argument of module:Setup() from offsetCFrame to actual CFrame for more flexible use.

Here’s the server script

local ServerStorage = game:GetService("ServerStorage")
local Players = game:GetService("Players")

local WeldPlayers = require(ServerStorage.WeldPlayersTogether)

local CHARACTER1, CHARACTER2 = workspace:WaitForChild("Musab_YB"), workspace:WaitForChild("Dummy")

task.wait(3)
WeldPlayers.Setup(CHARACTER1, CHARACTER2,
	CFrame.lookAt( CHARACTER1.PrimaryPart.CFrame * (CFrame.new(0,0,-5)).Position ,CHARACTER1.PrimaryPart.Position) -- Don't worry about this it's just a CFrame
)
task.wait(3)
WeldPlayers.Cleanup(CHARACTER2)

Thanks @apenzijncoolenleuk1 Great work!! i love the idea behind it

5 Likes

Why is the dummy’s going over there?
this only rarely happens, normally they would stick to the player but sometimes they go to a random specific point.

While writing this i reset my character and it works now (idk if thats because i reset or just waited a bit) but i know it’s gonna happen again.
Any tips?


Also i haven’t edited the module just copied ur tweaks and i put the client script in starterplayerscripts btw.

Perhaps your attack applies physics effects on the dummy?
Seems to break when you start an attack.
Or do you only connect the weld as the attack begins?

The only thing im doing is stunning them which sets their WalkSpeed/JumpPower to 0.

But i actually found out why it puts them there, For some reason the “WeldModel” goes to that specific point.

You can see it in the video


Any idea on how to fix this?

Okay this is getting pretty weird, So idk if this is a coincidence but the Weld-Model keeps getting positioned at on of the spawn points and no matter how many times i weld the Weld-Model it won’t go to a different spawn point unless i reset my character which fixes the bug.

RobloxStudioBeta_NdZhh0VLgt

both of these are different spawn points

RobloxStudioBeta_Nhx92g5IJW

I tried to fix it and i think it’s the “AlignOrientation” and “AlignPosition” but i’m not sure still.
i really need help fixing this :pray:

Are you sure there’s not other code that interferes with the weld?
Try it in a baseplate game.

Might be a spawning related function?