How do I make the Player.Character remain upright while welded/constrained to a rolling part?

Hello, I am currently working on a heavily Monkey Ball inspired experience wherein the character travels through a stage while inside a physics ball and is unable to jump. I can implement this by welding the character to the part that is spawned at the same time as the player and turning off collision for the character, while manipulating it via increasing or decreasing rotational velocity while a key is held down.

Here are the scripts:

BallGive (Server side)

game:GetService('Players').PlayerAdded:Connect(function(Player)
	local GiveBall = function()
		local Ball = Instance.new("Part");
		Ball.Name = "Ball"
		local Weld = Instance.new("Weld");
		Weld.Name = "Giver"
		local Character = Player.Character or Player.CharacterAdded:Wait();
		local Humanoid = Character:WaitForChild('Humanoid');

		Weld.Parent = Ball;
		Weld.Part1 = Ball;
		Weld.Part0 = Character.HumanoidRootPart;

		Ball.Parent = Character;
		Ball.Anchored = false;
		Ball.Shape = Enum.PartType.Ball;
		Ball.Size = Vector3.new(7, 7, 7.375);
		Ball.Material = Enum.Material.Plastic;
		Ball.CFrame = Player.Character.HumanoidRootPart.CFrame;
		Ball.Transparency = 0.85;
		Ball.Color = Color3.new(1, 0.32549, 0.337255);
		Ball.CustomPhysicalProperties = PhysicalProperties.new(1, 2, 0.2, 1, 1)
	end

	GiveBall();

	Player.CharacterAdded:Connect(function()
		GiveBall();
	end)
end)

BallMove (LocalScript)

local Player = game:GetService('Players').LocalPlayer;
local InputService = game:GetService('UserInputService');
local Camera = workspace.CurrentCamera;
local Settings = {
	Speed = 50,
	Size = Vector3.new(7, 7, 7.25)
}

local Character = Player.Character or Player.CharacterAdded:Wait();
local Humanoid = Character:WaitForChild('Humanoid');
local RayParams = RaycastParams.new();
local Ball = Character:WaitForChild('Ball');
local Connection;

Camera.CameraSubject = Ball
RayParams.FilterType = Enum.RaycastFilterType.Exclude;
RayParams.FilterDescendantsInstances = {Character};

for _, v in next, Player.Character:GetChildren() do
	if v:IsA('BasePart') and v ~= Ball then
		v.Massless = true;
		v.CanCollide = false;
	end
end

Humanoid.Died:Connect(function() Connection:Disconnect() end);
Connection = game:GetService('RunService').RenderStepped:Connect(function(delta)
	Humanoid.PlatformStand = true;
	Ball.CanCollide = true;

	if InputService:IsKeyDown('W') then
		Ball.RotVelocity -= Camera.CFrame.RightVector * delta * Settings.Speed
	end

	if InputService:IsKeyDown('A') then
		Ball.RotVelocity -= Camera.CFrame.LookVector * delta * Settings.Speed
	end

	if InputService:IsKeyDown('S') then
		Ball.RotVelocity += Camera.CFrame.RightVector * delta * Settings.Speed
	end

	if InputService:IsKeyDown('D') then
		Ball.RotVelocity += Camera.CFrame.LookVector * delta * Settings.Speed
	end
end)

Thing is, I want the player character to eventually be upright and performing animations based on the velocity of the ball. I don’t know how to do that, since welding forces both position and rotation to match. I have tried to use ball&socket constraints to maintain position separate from orientation but I cannot get the character to spawn inside the ball.

My latest attempt is below:

LocalScript is identical. ServerScript is as follows:

game:GetService('Players').PlayerAdded:Connect(function(Player)
	local GiveBall = function()
		local Ball = Instance.new("Part");
		Ball.Name = "Ball"
		local BallSocket = Instance.new("BallSocketConstraint")
		BallSocket.Name = "BallSocket"
		local Character = Player.Character or Player.CharacterAdded:Wait();
		local Humanoid = Character:WaitForChild('Humanoid');
		
		Ball.Parent = Character;
		Ball.Anchored = false;
		Ball.Shape = Enum.PartType.Ball;
		Ball.Size = Vector3.new(7, 7, 7.375);
		Ball.Material = Enum.Material.Plastic;
		Ball.CFrame = Player.Character.HumanoidRootPart.CFrame;
		Ball.Transparency = 0.85;
		Ball.Color = Color3.new(1, 0.32549, 0.337255);
		Ball.CustomPhysicalProperties = PhysicalProperties.new(1, 2, 0.2, 1, 1)
		
		Character.HumanoidRootPart.Position = Ball.CFrame.Position
		
		BallSocket.Parent = Ball
		BallSocket.Attachment0 = Ball
		BallSocket.Attachment1 = Character.HumanoidRootPart
		BallSocket.Attachment0.Position = Ball.CFrame.Position -- adjust if needed
		BallSocket.Attachment1.Position = Ball.Player.Character.HumanoidRootPart.CFrame.Position -- adjust if needed
		BallSocket.LimitsEnabled = false
		BallSocket.MaxFrictionTorque = 0
	end

	GiveBall()

	Player.CharacterAdded:Connect(function()
		GiveBall()
	end)
end)

Can I still theoretically do this with Ball and Socket? Should I try using weldconstraint or something else entirely?

1 Like

Maybe you could use AlignOrientation? Or if that messes with the physics, then try setting the orientation using runService or something like that. Might be a little underqualified to answer this one.

You should anchor the HRP and in RenderStepped set the CFrame at the ball and looking forward.

One of the easier ways is to create 2 collision groups - for the ball and for the body parts. Set these 2 collision groups to be non-collidable and instead of using a Weld, use AlignPosition to align the HumanoidRootPart to the ball.

local PhysicsService = game:GetService("PhysicsService")

PhysicsService:RegisterCollisionGroup("PlayerBodyPart")
PhysicsService:RegisterCollisionGroup("PlayerBall")
PhysicsService:CollisionGroupSetCollidable("PlayerBodyPart", "PlayerBall", false)

game:GetService('Players').PlayerAdded:Connect(function(Player)
	local GiveBall = function()
		local Character = Player.Character or Player.CharacterAdded:Wait();
		local Humanoid = Character:WaitForChild('Humanoid');
		local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart");
		local RootAttachment = HumanoidRootPart:WaitForChild("RootAttachment")
		
		local Ball = Instance.new("Part");
		Ball.Name = "Ball"
		Ball.Anchored = false;
		Ball.Shape = Enum.PartType.Ball;
		Ball.Size = Vector3.new(7, 7, 7.375);
		Ball.Material = Enum.Material.Plastic;
		Ball.CFrame = Player.Character.HumanoidRootPart.CFrame;
		Ball.Transparency = 0.85;
		Ball.Color = Color3.new(1, 0.32549, 0.337255);
		Ball.CustomPhysicalProperties = PhysicalProperties.new(1, 2, 0.2, 1, 1);
		Ball.CollisionGroup = "PlayerBall";
		
		local BallAttachment = Instance.new("Attachment")
		BallAttachment.Name = "BallAttachment"
		BallAttachment.Parent = Ball
		
		local RootAlignPosition = Instance.new("AlignPosition")
		RootAlignPosition.Name = "RootAlignPosition"
		RootAlignPosition.Attachment0 = RootAttachment
		RootAlignPosition.Attachment1 = BallAttachment
		RootAlignPosition.MaxForce = math.huge
		RootAlignPosition.MaxVelocity = math.huge
		RootAlignPosition.Parent = HumanoidRootPart
		
		-- always parent after setting properties/children so there is
		-- only one replication to the client as opposed to
		-- x amount of property manipulation/children
		Ball.Parent = Character;
		Ball:SetNetworkOwner(Player);
		
		local function onChildAdded(child)
			if child:IsA("BasePart") == true and child ~= Ball then
				child.CollisionGroup = "PlayerBodyPart"
			end
		end

		for _, child in Character:GetChildren() do
			task.spawn(onChildAdded, child)
		end
		
		Character.ChildAdded:Connect(onChildAdded)
	end

	GiveBall();

	Player.CharacterAdded:Connect(function()
		GiveBall();
	end)
end)

The collision group implementation isn’t working on my end, the character ends up getting stuck to the front of the ball aggressively trying to align.

It seems the onChildAdded function isn’t declaring the body parts as non-collidable with the ball. That, and the ball won’t respond to inputs; I believe it may need to be parented to the player.

Can you check if your body parts’ CollisionGroup to PlayerBodyPart and the ball to PlayerBall? If they are, it might be accessories that are still collidable, in which case you can switch to GetDescendants and DescendantAdded instead. Also ensure you are actually registering your collision groups and collidable behavior properly.

I tested the script before sending and works just fine on my end.

Figured it out! Your implementation DOES work, it just needed a single line declaring the ball’s parent as the character, then it works exactly as intended! Just inserted this at line 25 and it worked like a charm.

Ball.Parent = Character

Pretty sure this is because the ball is identified elsewhere as a child of the character, so it only acts accordingly when they’re properly parented. Thank you for the help!!!

Spoke too soon, new problem: it’s inconsistent, my line change did nothing

I don’t know quite why but my hunch is that the ball is spawning in after the character and the function is run late; it always breaks after respawning once.

I’ve tried a couple of different ways to implement this, but nothing seems to be getting every descendant part of the player model. I don’t know what’s not colliding correctly, since as I said before, results are inconsistent.

What does your new code look like?

local function onDescendantAdded(descendant)
			if descendant:IsA("BasePart") and descendant ~= Ball then
				descendant.CollisionGroup = "PlayerBodyPart"
			end
		end

		for _, descendant in Character:GetDescendants() do
			task.spawn(onDescendantAdded, descendant)
		end

		Character.DescendantAdded:Connect(onDescendantAdded)
	end

I’ve swapped out everything and even checked for changes to the parent at some point. Nada.

I meant your entire code. I’m running near similar code from what I posted before and I have no issues with it, even when respawning. Client script never changed from your original.

local PhysicsService = game:GetService("PhysicsService")

PhysicsService:RegisterCollisionGroup("PlayerBodyPart")
PhysicsService:RegisterCollisionGroup("PlayerBall")
PhysicsService:CollisionGroupSetCollidable("PlayerBodyPart", "PlayerBall", false)

game:GetService('Players').PlayerAdded:Connect(function(Player)
	local function onCharacterAdded(Character)
		local Humanoid = Character:WaitForChild('Humanoid');
		local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart");
		local RootAttachment = HumanoidRootPart:WaitForChild("RootAttachment")
		
		-- setup ball and collision group
		local Ball = Instance.new("Part");
		Ball.Name = "Ball"
		Ball.Anchored = false;
		Ball.Shape = Enum.PartType.Ball;
		Ball.Size = Vector3.new(7, 7, 7.375);
		Ball.Material = Enum.Material.Plastic;
		Ball.CFrame = Character.HumanoidRootPart.CFrame;
		Ball.Transparency = 0.85;
		Ball.Color = Color3.new(1, 0.32549, 0.337255);
		Ball.CustomPhysicalProperties = PhysicalProperties.new(1, 2, 0.2, 1, 1);
		Ball.CollisionGroup = "PlayerBall";
		
		-- setup ball attachment, parent, and network ownership
		local BallAttachment = Instance.new("Attachment")
		BallAttachment.Name = "BallAttachment"
		BallAttachment.Parent = Ball
		Ball.Parent = Character;
		Ball:SetNetworkOwner(Player);
		
		-- setup root AlignPosition
		local RootAlignPosition = Instance.new("AlignPosition")
		RootAlignPosition.Name = "RootAlignPosition"
		RootAlignPosition.Attachment0 = RootAttachment
		RootAlignPosition.Attachment1 = BallAttachment
		RootAlignPosition.MaxForce = math.huge
		RootAlignPosition.MaxVelocity = math.huge
		RootAlignPosition.Responsiveness = math.huge
		RootAlignPosition.Parent = HumanoidRootPart
		
		-- set limbs/accessories to player collision group
		local function onDescendantAdded(descendant)
			if descendant:IsA("BasePart") == true and descendant:FindFirstAncestorOfClass("Tool") == nil and descendant ~= Ball then
				descendant.CollisionGroup = "PlayerBodyPart"
			end
		end

		for _, descendant in Character:GetDescendants() do
			task.spawn(onDescendantAdded, descendant)
		end

		Character.DescendantAdded:Connect(onDescendantAdded)
	end

	Player.CharacterAdded:Connect(onCharacterAdded)
	
	if Player.Character ~= nil then
		task.spawn(onCharacterAdded, Player.Character)
	end
end)

Pretty much only the child to descendant stuff was changed. aside from that, I can’t really say what will make a difference. I tried the script you posted here and was met with the same inconsistency.

Plus, when I respawn after it does work, the character no longer spawns in the ball.

In your explorer while you’re in game, can you double check these following things for me:

  1. The ball’s CollisionGroup is set to PlayerBall.
  2. All the parts’ CollisionGroup (excluding the ball), including accessories, are set to PlayerBodyPart.
  3. The ball has a BallAttachment in the center (you can view it by clicking the Visible property or clicking the move tool).
  4. The HumanoidRootPart has a RootAttachment and a RootAlignPosition.
  5. The RootAlignPosition’s Attachment0 is set to the RootAttachment and the Attachment1 is set to BallAttachment (this is very important it is in this order).
  6. The RootAlignPosition is Enabled with the MaxForce, MaxVelocity, and Responsiveness set to inf.

Let me know the results from the list above and send me your current server/client script.

1 Like

ball.rbxl (54.7 KB)
Here is my place file that is working completely fine.

Ok, your place file works perfectly on my end, so something’s up with mine.

All the body parts above the legs don’t have their collisions changed, and the rootpart is missing its alignposition. Don’t know why it’s not getting assigned. Does this place being r6 have anything to do with it?

It shouldn’t, since both R6 and R15 rigs have a Humanoid, HumanoidRootPart, and a RootAttachment inside the HumanoidRootPart (double check that for me since I don’t want to publish just to switch rig types), and all limbs are BaseParts. Double check if there are other scripts somehow interfering as well. Also, you still have not sent your version of the script, so I cannot help you at this point in time.

Right, sorry. I swapped out the old one with your implement but there are a couple here, one of which may be interfering. There’s a custom respawn behavior I put in that when disabled makes the place unplayable, but here it is.

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(Player)

	Player.CharacterAdded:Connect(function(Character)

		Character.Humanoid.Died:Connect(function()
			wait(3)
			Player:LoadCharacter()
		end)
	end)

	Player:LoadCharacter()

end)

I suspect this may be causing problems, but I can’t disable it wholesale. I’m currently trying other scripts in your place, along with r6, and it all works.

Check the output, are there any errors? In your current place, is your workspace.SignalBehavior set to Default or Immediate? If so, set it to Deferred and try again.

1 Like

Setting it to deferred did the trick!! Output was giving absolutely nothing which is why I was having such a hard time pinpointing the problem. That should do it for now.

I don’t understand why the default SignalBehavior would cause the problem, was the workspace loading stuff in prematurely?