You've got a lot of the necessary pieces already in place. This whole question is mostly a matter of making sure that the chain of events happen correctly, and the different events are observed.
The main piece that you are missing how to "grab" another player. And to do that, all you have to do is :
- Create a weld or constraint between the grabber and the grabbed.
- Set the grabbed humanoid's state to Ragdoll or Physics.
- Set the network ownership of grabbed character model to the server.
And that should be enough for you to be able to manipulate the other player's character. The important thing to know is where these events need to be processed. Some things like animations need to be played on the clientside, while other things like collision detection need to be managed by the server.
Here is a breakdown of each of the actions from your question :
| Event |
Action |
Client-side |
Server-side |
| A player on the Smiler team clicks their mouse... |
spammed input is debounced |
x |
|
|
an animation plays |
x |
|
|
collisions with players on the other team are detected |
|
x |
|
a cooldown starts to debounce additional clicks |
x |
|
| A player is touched... |
their controls are disabled |
x |
|
|
their character ragdolls |
x |
x |
|
a constraint between the grabber and the player is created |
|
x |
|
network ownership over their character is given to the grabber |
|
x |
|
their health decreases |
|
x |
|
they are released after a little while |
|
x |
| A player's health decreases... |
if it drops too low, they switch teams |
|
x |
| A player is released from the grab... |
their character no longer ragdolls |
x |
x |
|
the constraint connecting them to the grabber is deleted |
|
x |
|
network ownership is returned to them |
|
x |
|
they regain control of their character |
x |
|
Each step in the process isn't difficult on its own, but it can be a little confusing when you put it all together. So let's dig into what that might look like.
This example has a workspace that looks like this :
| Workspace |
Notes |
 |
- In ReplicatedStorage, there are two RemoteEvents for communicating between the server and client. There is an Animation instance holding onto the grab animation.
- In ServerScriptService, there is a Script hosting the server-side logic.
- In StarterPlayer > StarterCharacterScripts, there is a LocalScript hosting the client-side logic.
- In Teams, there are two Team objects defined, one for the Smilers and one for the other team. |
In the LocalScript :
-- Services
local ContextActionService = game:GetService("ContextActionService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Teams = game:GetService("Teams")
-- Remote Events
local GrabPlayerEvent = ReplicatedStorage:FindFirstChild("GrabPlayerEvent")
local TogglePlayerControlsEvent = ReplicatedStorage:FindFirstChild("TogglePlayerControlsEvent")
-- Animations
local GrabAnimation = ReplicatedStorage:FindFirstChild("GrabAnimation")
-- define some variables
local player = Players.LocalPlayer
local playerControlModule = require(player.PlayerScripts.PlayerModule)
local cooldown = false
local COOLDOWN_TIMER = 2.5 --seconds
-- TODO : implement a UI so that people can see this cooldown
-- load the animation
local humanoid = player.Character.Humanoid
local animator = humanoid:FindFirstChildOfClass("Animator")
if not animator then
error("Could not find Animator on character model")
end
local animationTrack = animator:LoadAnimation(GrabAnimation)
animationTrack.Looped = false
animationTrack.Stopped:Connect(function()
-- tell the server that we're done playing the animation
GrabPlayerEvent:FireServer(false)
end)
function onAction(actionName, inputState, inputObject)
-- escape if the character doesn't exist
if player.Character == nil then
return
end
-- escape if the player isn't on the Smiler team
if player.TeamColor ~= Teams.Smilers.TeamColor then
return
end
-- wait for the mouseUp event
if inputState ~= Enum.UserInputState.End then
return
end
-- escape if on cooldown
if cooldown then
return
end
-- Play the animation, and tell the server we're doing it
GrabPlayerEvent:FireServer(true)
animationTrack:Play()
-- start the cooldown
cooldown = true
wait(COOLDOWN_TIMER)
cooldown = false
end
-- Listen for Mouse clicks to know when to play the grab animation
game.ContextActionService:BindAction("Grab", onAction, false, Enum.UserInputType.MouseButton1)
-- Listen for when the server demands that we enable/disable player controls
TogglePlayerControlsEvent.OnClientEvent:Connect(function(enabled)
-- disable player controls
local controls = playerControlModule:GetControls()
if enabled then
controls:Enable()
else
controls:Disable()
end
-- ragdoll the character
local character = player.Character
if not character then
return
end
local humanoid = character.Humanoid
if not enabled then
humanoid:ChangeState(Enum.HumanoidStateType.Physics)
else
humanoid:ChangeState(Enum.HumanoidStateType.GettingUp)
end
end)
And in the server Script :
-- Services
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Teams = game:GetService("Teams")
-- script connections
local animationConnections = {} -- array<playerId, array<RBXScriptConnection>>
-- remote events
local GrabPlayerEvent = ReplicatedStorage:FindFirstChild("GrabPlayerEvent")
local TogglePlayerControlsEvent = ReplicatedStorage:FindFirstChild("TogglePlayerControlsEvent")
-- helper functions
local function constrainTwoParts(partA, partB, constraintType)
local a0, a1 = Instance.new("Attachment"), Instance.new("Attachment")
a0.Parent = partA
a1.Parent = partB
local b = Instance.new(constraintType)
b.Attachment0 = a0
b.Attachment1 = a1
b.Parent = game.Workspace
return b
end
local function destroyConstraintsAndAttachments(constraint)
constraint.Enabled = false
constraint.Attachment0:Destroy()
constraint.Attachment1:Destroy()
constraint:Destroy()
end
local function createPlayerKey(player)
-- helper function to ensure dictionary key consistency
return tostring(player.UserId)
end
local function getPlayerHands(characterModel)
-- find the player's hands, regardless of R6, R15, or Rthro model type
local hands = {}
local expectedHandNames = { "LeftHand", "LeftArm", "RightHand", "RightArm" }
for _, partName in ipairs(expectedHandNames) do
local hand = characterModel:FindFirstChild(partName, false)
if hand then
table.insert(hands, hand)
end
end
return hands
end
local function toggleNetworkOwnership(characterModel, isEnabled, owner)
for _, child in ipairs(characterModel:GetDescendants()) do
if child:IsA("BasePart") then
if child.Anchored then
child.Anchored = false
end
if isEnabled then
child.Massless = false
child:SetNetworkOwner(owner)
else
child.Massless = true
child:SetNetworkOwner(nil)
end
end
end
end
local function enableRagdoll(characterModel, player, enabled)
if enabled then
if player then
-- disable the opponent's controls temporarily and ragdoll them
TogglePlayerControlsEvent:FireClient(player, false)
-- set the network ownership to the server to allow better ragdolling
toggleNetworkOwnership(characterModel, false, player)
end
-- create a bunch of temporary attachments and constraints so they flop around
for _, v in pairs(characterModel:GetDescendants()) do --ragdoll
if v:IsA("Motor6D") then
local a0, a1 = Instance.new("Attachment"), Instance.new("Attachment")
a0.CFrame = v.C0
a1.CFrame = v.C1
a0.Parent = v.Part0
a1.Parent = v.Part1
local b = Instance.new("BallSocketConstraint")
b.Attachment0 = a0
b.Attachment1 = a1
b.Parent = v.Part0
-- disable the existing character motors
v.Enabled = false
end
end
else
-- remove the ragdoll constraints and connections
for _,v in pairs(characterModel:GetDescendants()) do --unragdoll
if v:IsA('Motor6D') then
v.Enabled = true
end
if v.Name == 'BallSocketConstraint' then
v:Destroy()
end
if v.Name == 'Attachment' then
v:Destroy()
end
end
if player then
-- tell the player to stop ragdolling and restore controls
TogglePlayerControlsEvent:FireClient(player, true)
-- return network ownership to the original player
toggleNetworkOwnership(characterModel, true, player)
end
end
end
local function stopObservingHands(player)
-- clean up the listeners on the player's hands
local playerKey = createPlayerKey(player)
for _, connection in ipairs(animationConnections[playerKey]) do
connection:Disconnect()
end
end
local function createHandTouchListener(hand, player)
return function(otherPart)
local character = otherPart.Parent
local humanoid = character:FindFirstChild("Humanoid")
if humanoid then
local otherPlayer = Players:GetPlayerFromCharacter(character)
if otherPlayer then
-- check what team the other player is so you don't grab people on your own team
if otherPlayer.Team == Teams.Smilers then
return
end
end
-- disable the collision detectors on the hands
stopObservingHands(player)
-- ragdoll the other player
enableRagdoll(character, otherPlayer, true)
-- add a temporary weld between the hand and the otherPart so we grab them
local constraint = constrainTwoParts(hand, otherPart, "RodConstraint")
constraint.Enabled = true
constraint.Length = constraint.CurrentDistance
constraint.Thickness = 2
-- TODO : figure out how long to hold on here
-- define some constants
local TIME_TO_HOLD_PLAYER = 5.0 -- seconds
local DAMAGE_PER_TICK = 1.0
local TICKS_PER_SECOND = 5.0
local TIME_TO_WAIT = 1.0 / TICKS_PER_SECOND
local TEAM_SWITCH_THRESHOLD = 0 -- health
-- start holding the other player and subtracting health
local startingTime = tick()
local timePassed = 0.0
local otherPlayerDead = false
while (timePassed < TIME_TO_HOLD_PLAYER and otherPlayerDead == false) do
timePassed = tick() - startingTime
-- every tick subtract some health from the opponent
local humanoid = character.Humanoid
if humanoid then
-- if this tick will kill the player, release the weld so we also don't die
if humanoid.Health - DAMAGE_PER_TICK <= TEAM_SWITCH_THRESHOLD then
destroyConstraintsAndAttachments(constraint)
constraint = nil
end
humanoid:TakeDamage(DAMAGE_PER_TICK)
-- check if the other player should switch teams
if (humanoid.Health <= TEAM_SWITCH_THRESHOLD) then
-- change the player's team to be the smilers
if otherPlayer then
otherPlayer.Team = Teams.Smilers
end
otherPlayerDead = true
break
end
else
otherPlayerDead = true
break
end
wait(TIME_TO_WAIT)
end
-- let go of the other player
if constraint then
destroyConstraintsAndAttachments(constraint)
end
-- tell the other player to stand up
enableRagdoll(character, otherPlayer, false)
end
end
end
-- Listen for when the client starts to do the grab animation
GrabPlayerEvent.OnServerEvent:Connect(function(player, isStart)
local playerKey = createPlayerKey(player)
local playerCharacterModel = player.Character
if playerCharacterModel == nil then
error(string.format("%s's character model should exist, but it doesn't", player.Name))
end
-- if this is the start of the animation...
if isStart then
-- set up listeners in case we touch the other players
for _, hand in ipairs(getPlayerHands(playerCharacterModel)) do
local touchListener = createHandTouchListener(hand, player)
local connection = hand.Touched:Connect(touchListener)
table.insert(animationConnections[playerKey], connection)
end
else
stopObservingHands(player)
end
end)
-- listen for when players join to initialize some variables
Players.PlayerAdded:Connect(function(player)
-- add an entry in the connections and animation tables for this player
local key = createPlayerKey(player)
animationConnections[key] = {}
player.CharacterAdded:Connect(function(character)
character.Humanoid.BreakJointsOnDeath = false
character.Humanoid.RequiresNeck = false
end)
end)
-- listen for when players leave to clean up some variables
Players.PlayerRemoving:Connect(function(player)
-- clean up any connections
local key = createPlayerKey(player)
for _, connection in ipairs(animationConnections[key]) do
connection:Disconnect()
end
end)