How does one go about making it so that you can grab other players by clicking on them whilst near them?

Viewed 1473

(Banned from DevForums, asking here.)

Basically, a game called Infectious Smile has a grabbing system when you become a "Smiler". The longer you are holding them, the more they are converted. When their health gets to 0, they will transform into a smiler, just like the player converting them, and convert others.

How would I make a system/grabbing system that does just that but just ragdolls them. (I want to make it so that the player is dropped after some time, obviously!)

I have a ragdoll system that I'm working on too so I'm not being fully spoonfed.

Ragdoll Script: Done!

Grab and Ragdoll Script: Shows grab animation on click but no grabbing.

Grab script that just plays animation (LocalScript):

local userInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")

local player = Players.LocalPlayer
local character = player.Character
if not character or not character.Parent then
    character = player.CharacterAdded:Wait()
end
local humanoid = character:WaitForChild("Humanoid")
local Animator = humanoid:WaitForChild("Animator")
local cooldown = false
local cooldowntime = 1

local grab = Instance.new("Animation")
grab.Name = "GrabAnim"
grab.AnimationId = "rbxassetid://7691396275"

game:GetService("ContentProvider"):PreloadAsync({grab})

local grabTrack = Animator:LoadAnimation(grab)
grabTrack.Priority = Enum.AnimationPriority.Core
grabTrack.Looped = false
grabTrack.Name = "GrabAnimTrack"

if(cooldown) then 
    wait(cooldowntime) 
    cooldown = false
    return
end


game:GetService("UserInputService").InputBegan:Connect(function(inputObject, gameProcessed)
    if inputObject.UserInputType == Enum.UserInputType.MouseButton1 then
        if(cooldown) then 
            return 
        end
        
        -- It'll return "Handle" half the time.
        local Head = player.Character.Head
        local D = Head.CFrame.LookVector * 10
        local RayCast = workspace:Raycast(Head.Position,D)
        if RayCast then
            script.Parent.Lock:FireServer(RayCast.Instance)
        end
        cooldown = true
        grabTrack:Play(0.09, 1, 1.2)
    end
    
    if(cooldown) then
        wait(cooldowntime)
        cooldown = false
        return
    end
end)

For the Event Called script (Script):

local event = script.Parent:WaitForChild('Lock')
local arrested = false
local weld

event.OnServerEvent:Connect(function(plr, Target)
    print(Target)
    if Target.Name == "UpperTorso" or Target.Name == "LowerTorso" or Target.Parent:IsA("Accessory") then
        arrested = true
        coroutine.wrap(function()
            while arrested == true do
                Target.Anchored = true
                Target.CFrame = plr.Character.HumanoidRootPart.CFrame * CFrame.new(0,0,-3)
                wait()
            end
        end)()
    elseif arrested then
        arrested = false
    end
end)

The grab seems to return "Handle" instead of like "UpperTorso" so I don't know how to get rid of the handle thing.

3 Answers

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
see notes in next column. - 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)

Inside an accessory is a handle. So if you don't want the handle, just remove part.Parent:IsA("Accessory"). Also if you want the raycast to ignore or go right trough the accessory then first make a table that gets all the accessories and then make a raycast param and add the table to the FilterDescendantsInstances. Make sure the FilterType is Blacklist (Its default so don't really worry about it). OR if you want to make if the head looks at the accessory then select the torso then first test if there IS a torso. If there is then make it local upperTorso = Target.Parent.Parent.UpperTorso.

I'm a beginner here, but I use love2d and have noticed when I call mouse:IsDown() it calls my functions many times, because it captured the mouse button being down for many frames.

So I made a function:

Leftclick = 1

function mouseClicker ()
if mouse:IsDown() then

Leftclick = Leftclick -1 
else

 Leftclick =1 
end 

When I use this:

if Leftclick ==0 then

end 

It prevents a lot of madness. Maybe you can use something like

if leftclick < 0 then

I don't know if that would help with the spamming issue you mentioned.

Related