您已经准备好许多必要的部件。整个问题主要是确保事件链正确发生,并观察到不同的事件。
您缺少的主要部分是如何“抓住”另一个玩家。而要做到这一点,你所要做的就是:
- 在抓取器和被抓取物之间创建焊缝或约束。
- 将抓取的人形机器人的状态设置为 Ragdoll 或 Physics。
- 设置抓取人物模型的网络所有权给服务器。
这应该足以让你能够操纵其他玩家的角色。重要的是要知道这些事件需要在哪里处理。有些东西,比如动画,需要在客户端播放,而其他的东西,比如碰撞检测,需要由服务器来管理。
以下是您问题中每个操作的细分:
| 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 |
|
流程中的每个步骤本身并不困难,但如果将它们放在一起,可能会有些混乱。因此,让我们深入研究一下它可能是什么样子。
此示例的工作区如下所示:
| 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. |
在本地脚本中:
-- 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)
并在服务器脚本中:
-- 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)