How can I achieve this?

I’m trying to make a sphere that a player can control, like how they do in those old marble games. I tried using an Angular Velocity, but the issue with that is that

  • setting the MaxTorque to 50 (the base max speed of the sphere) causes the sphere to not roll at all, it only works if I set it to an extremely high number.

  • When I set it to a high number and stop holding down a key, the sphere stops angular movement but continues linear movement, making controlling it feel more mechanic-like instead of physics-based.

Are there any other constraints or ways that allow me to achieve

  • Having a max speed for the sphere (50) without setting the max force of the attachment/velocity to an extremely high number.

  • Allow the sphere to be in constant angular motion, and not just constant linear motion.

function module:createPlayerVelocity(char : Model, sphere : BasePart, properties)
	local AngularVelocity = Instance.new("AngularVelocity")
	AngularVelocity.Parent = sphere
	
	AngularVelocity.Attachment0 = Instance.new("Attachment")
	AngularVelocity.Attachment0.Name = "AngularVelocityAttatchment"
	AngularVelocity.Attachment0.Parent = sphere
	
	local humanoid = char:FindFirstChildOfClass("Humanoid")
	
	while task.wait() do
		AngularVelocity.Torque = Vector3.new(humanoid.MoveDirection.Z * 10, 0, humanoid.MoveDirection.X * -10)
		AngularVelocity.MaxTorque = 50 --/ properties.Size
	end
end

Is the character welded to the sphere? A humanoid object usually attempts to keep the character upright. If the sphere is welded to the character, this will cause the humanoid to resist the sphere’s rotation. You can fix this by changing the humanoid’s state to Enum.HumanoidStateType.Physics.

Also, is 50 supposed to be the maximum linear speed? Why are you setting MaxTorque equal to max speed? Torque causes angular acceleration, it’s not angular speed or linear speed.

If I understood correctly what you want, I’d recommend setting the AngularVelocity only around one axis but updating this axis every frame when humanoid.MoveDirection is not a zero vector. In order to update the rotation axis every frame, you’ll also need Attachment1.

The AngularVelocity property only needs to be set once. You’ll have to calculate the desired angular speed, negate it so that the rotation is clockwise, and then set it as the AngularVelocity around the x-axis of Attachment1. Angular speed of a rolling sphere in radians per second is its linear speed divided by its radius (assuming that there’s enough friction to keep it from sliding). Here’s the code for setting AngularVelocity properties that only need to be set once.

-- I put math.min just in case the sphere doesn't have the same size on all axes for whatever reason.
local sphereRadius: number = .5 * math.min(sphere.Size.X, sphere.Size.Y, sphere.Size.Z)
local angularSpeed: number = maxLinearVelocity / sphereRadius
angularVelocity.RelativeTo = Enum.ActuatorRelativeTo.Attachment1
angularVelocity.AngularVelocity = Vector3.new(-angularSpeed, 0, 0)

You can parent Attachment1 to terrain. Set its CFrame every frame such that its RightVector is parallel to the desired axis of rotation (perpendicular to the desired plane of rotation). CFrame.lookAt(Vector3.zero, Humanoid.MoveDirection) is such a CFrame. In order to prevent the AngularVelocity from accelerating the ball when the player isn’t holding a movement key, multiply the max torque with the magnitude of humanoid.MoveDirection (because torque causes angular acceleration). Here’s the code that should be run every frame.

angularVelocity.MaxTorque = humanoid.MoveDirection.Magnitude * maxTorque
if humanoid.MoveDirection.Magnitude > 1e-4 --[[just an arbitrary small number]] then
	attachment1.CFrame = CFrame.lookAt(Vector3.zero, humanoid.MoveDirection)
end
Here's some testing code I wrote. It's client-only, though. The ball created by it won't exist on the server and for some reason the character position doesn't replicate either.
--!strict
--https://devforum.roblox.com/t/how-can-i-achieve-this/3035272
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local sphereTransparency: number = .5
local sphereColor: Color3 = Color3.new(1, 0, 0)
local sphereMaterial: Enum.Material = Enum.Material.Marble
local sphereDiameter: number = 10

local maxLinearVelocity: number = 50
local maxTorque: number = 50_000

local humanoid: Humanoid
local sphere: Part
local attachment1: Attachment
local angularVelocity: AngularVelocity
local runServiceConnection: RBXScriptConnection?

local function createInstances(humanoidRootPart: Part): ()
	sphere = Instance.new("Part")
	sphere.Transparency = sphereTransparency
	sphere.Color = sphereColor
	sphere.Material = sphereMaterial
	sphere.Shape = Enum.PartType.Ball
	sphere.BottomSurface, sphere.TopSurface = Enum.SurfaceType.Smooth, Enum.SurfaceType.Smooth
	sphere.Size = sphereDiameter * Vector3.one
	sphere.CFrame = humanoidRootPart.CFrame
	
	local weldConstraint: WeldConstraint = Instance.new("WeldConstraint")
	weldConstraint.Part0, weldConstraint.Part1 = humanoidRootPart, sphere
	weldConstraint.Parent = sphere
	
	local attachment0: Attachment = Instance.new("Attachment")
	attachment1 = Instance.new("Attachment")
	attachment0.Name, attachment1.Name = "Attachment0", "Attachment1"
	attachment0.Parent, attachment1.Parent = sphere, workspace.Terrain
	
	angularVelocity = Instance.new("AngularVelocity")
	angularVelocity.RelativeTo = Enum.ActuatorRelativeTo.Attachment1
	angularVelocity.AngularVelocity = Vector3.new(-2 * maxLinearVelocity / sphereDiameter, 0, 0)
	angularVelocity.Attachment0, angularVelocity.Attachment1 = attachment0, attachment1
	angularVelocity.Parent = sphere
	
	sphere.Parent = workspace
end

local function onRunServiceEvent(): ()
	angularVelocity.MaxTorque = humanoid.MoveDirection.Magnitude * maxTorque
	if humanoid.MoveDirection.Magnitude > 1e-4 then
		attachment1.CFrame = CFrame.lookAt(Vector3.zero, humanoid.MoveDirection)
	end
	--print(humanoid.MoveDirection)
	print(sphere.AssemblyLinearVelocity.Magnitude)
end

local function onCharacterAdded(char: Model)
	local humanoidRootPart: Part = char:WaitForChild("HumanoidRootPart") :: Part
	humanoid = char:WaitForChild("Humanoid") :: Humanoid
	humanoid:ChangeState(Enum.HumanoidStateType.Physics)
	createInstances(humanoidRootPart)
	if runServiceConnection ~= nil then
		runServiceConnection:Disconnect()
	end
	runServiceConnection = RunService.Heartbeat:Connect(onRunServiceEvent)
end

Players.LocalPlayer.CharacterAdded:Connect(onCharacterAdded)

Additionally, in the code you posted, you’ve accidentally written AngularVelocity.Torque instead of AngularVelocity.AngularVelocity.

I just assumed that limiting the MaxTorque to the linear speed I wanted for the marble (to go at) would achieve just that.

Should I just try to use another constraint or?

If I understood correctly what you want, I’d recommend calculating the AngularVelocity property in the way it’s done in this code snippet in my original reply, and also doing the other things I mentioned in my original reply.

This way, on a horizontal surface, the maximum linear speed will be equal to the variable maxLinearVelocity, which you can set to be 50.

MaxTorque determines how quickly the sphere accelerates to the max speed. When the sphere reaches the max speed, the constraint will stop applying torque until the sphere needs to accelerate again. Unless the sphere is very small, you’ll need a much higher MaxTorque than 50. I used 50000 in the testing code that I posted, and if I remember correctly, it still took many seconds to accelerate to the max speed.

The AngularVelocity property defines the angular velocities around the axes of a CFrame. So when RelativeTo is set to Attachment1, changing the CFrame of Attachment1 changes the desired worldspace rotation plane without having to change the AngularVelocity property. I’ve written about this in my original reply as well. I suggest this because I don’t know how to convert angular velocity around axes of one coordinate system to angular velocity around axes of another coordinate system but doing this Attachment1 CFrame change means that there is no need for such a conversion.

Also, the HumanoidState change needs to be done on the client if the client is the network owner of their character (which is the case by default).

1 Like

I tried to implement this in a server script (haven’t changed the humanoid state on the client yet) but it didn’t work. I can’t roll around or anything, although attatchment1 seems to be turning just fine.

function module:createPlayerVelocity(char : Model, sphere : BasePart, properties)
local maxLinearVelocity : number = 50 / properties.Size
local maxTorque : number = (50 * 10^3) / properties.Size

local angularVelocity = Instance.new("AngularVelocity")
angularVelocity.Parent = sphere

angularVelocity.Attachment0 = Instance.new("Attachment")
angularVelocity.Attachment0.Name = "angularVelocityAttatchment0"
angularVelocity.Attachment0.Parent = sphere

angularVelocity.Attachment1 = Instance.new("Attachment")
angularVelocity.Attachment1.Name = "angularVelocityAttatchment1"
angularVelocity.Attachment1.Parent = sphere

local humanoid = char:FindFirstChildOfClass("Humanoid")

local sphereRadius : number = .5 * math.min(sphere.Size.X, sphere.Size.Y, sphere.Size.Z)
local angularSpeed : number = maxLinearVelocity / sphereRadius
angularVelocity.RelativeTo = Enum.ActuatorRelativeTo.Attachment1
angularVelocity.AngularVelocity = Vector3.new(-angularSpeed, 0, 0)

while task.wait() do
	angularVelocity.MaxTorque = humanoid.MoveDirection.Magnitude * maxTorque
	if humanoid.MoveDirection.Magnitude > 1e-4 then
		angularVelocity.Attachment1.CFrame = CFrame.lookAt(Vector3.zero, humanoid.MoveDirection)
	end
end

end

The worldspace rotation of Attachment1 should only be dependent on Humanoid.MoveDirection. If you parent it to the sphere, it’s rotation is also dependent on the rotation of the sphere which will cause unpredictable behavior.

Thus, Attachment 1 should be parented to a non-rotating object whose CFrame has the default rotation (same as the rotation of the identity CFrame). workspace.Terrain is such an object. Terrain is a BasePart although it’s kind off weird considering that it differs from other BaseParts in significant ways and many BasePart properties are practically meaningless in it.

Also, even after setting the parent of Attachment1 correctly, I’m not sure if the code will work without changing the humanoid state.

Edit: Actually, after reading some of the documentation for the deprecated BodyAngularVelocity, I noticed that the way your original code sets the AngularVelocity is actually correct if the radius of the sphere is 5 (10 = 50 / 5) and could easily be modified to work with any radius and max linear speed. I didn’t know that the direction of the AngularVelocity vector is always the direction of the rotation axis because the documentation of AngularVelocity doesn’t mention this. I thought it might only be the case when the vector defines rotation around one axis only (I didn’t think of AngularVelocity as a direction vector but just a way to store rotations around different directions) but apparently its direction is always the direction of rotation axis. So apparently Attachment1 is not needed, although doing it in the way I suggested does also work.

1 Like

It ended up working, although the marble feels stiff whenever I turn to the left or right. Of course it loses a lot of speed when going to the opposite direction (i.e forwards → backwards), but it doesn’t feel right whenever I simply turn to the left or the right, it’s like I’m locked in on a certian direction.

I’d be fine with it controling more slowly if the sphere was big, but it’s dimensions is only 10 studs in every direction, and making it massless doesn’t change anything. I don’t know if I’m doing something wrong or not, but controling it just isn’t fun.

So do you want it to turn to left and right faster while still having the same max speed (max magnitude of AssemblyLinearVelocity). If you want to make all direction changes faster, just increase MaxTorque. If you want to keep the time needed to change direction by 180 degrees the same as before but reduce the time needed to change it by 90 degrees, you can add a direction dependent multiplier for the max torque.

Here’s an example of how you can calculate MaxTorque (calculate it every frame using this function).

local maxBaseTorque: number = 50_000
local maxDirectionDependentTorqueMultiplier: number = 5

local function getTorque(): number
	if humanoid.MoveDirection.Magnitude <= 1e-4 then
		return 0
	end
	local baseTorque: number = humanoid.MoveDirection.Magnitude * maxBaseTorque
	
	-- currentRotationAxis is not the current desired rotation axis unless the part is moving in
	-- the desired direction at the moment.
	local currentRotationAxis: Vector3 = Vector3.yAxis:Cross(sphere.AssemblyLinearVelocity).Unit
	local directionCoefficient: number = math.abs(humanoid.MoveDirection.Unit:Dot(currentRotationAxis))
	return baseTorque * (1 + directionCoefficient * (maxDirectionDependentTorqueMultiplier - 1))
end

If you want very quick direction changes, you should possibly also increase the friction of the marble by setting CustomPhysicalProperties that have a higher friction. Having too little friction will cause the part to slide in which case the linear speed won’t be directly proportional to the angular speed anymore.

1 Like

Increasing the friction was all I had to do to make it turn more, thanks a lot for the help though, and the math because I’d never be able to figure this out myself :woozy_face:

One last question though, is there any way that I can allow for the sphere to gain speed when going down slopes like how a ball would when it goes down slopes because although it does, the force bringing the ball down the slope gets mitigated once I begin controlling it again, and then continues like normal once I stop controlling the sphere.

Unfortunately I don’t know what to do about that.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.