How would I be able to achieve movement of objects on a sphere from a point to another?

Currently, in my roblox real time strategy game in development, I am trying to achieve a system in which players can control units that move across the surface of a globe. The globe will represent a planet, and players should be able to interact with and navigate their units seamlessly across its surface. However, I’m facing some challenges in terms of how to approach the technical aspects of implementing this system effectively.

  • How would I make units move in a set speed on the entire time they are moving thru the surface of the sphere,
  • Can the units height be always around the sphere relative to the cere
  • Is it possible to keep it optimized and not have millions of calculations a second,
  • Or even, what are the neccesary mathematical calculations to account in the proccess?

Here is a script that receives the local input of the player with its selected units so the units sent are moved to the desired location, the ‘hitpos’. It uses methods such as linear velocity which I believe wouldnt really work on a sphere if they werent to be set constantly.

-- Dictionary to store active movement loop connections for each unit
local moveLoops = {}

-- Connect to the event triggered when movement needs to be halted
game.ReplicatedStorage.HaltMovement.OnServerEvent:Connect(function(player, unit)
    if unit and unit.Parent then
        unit:SetAttribute('uHalting', true)  -- Set the attribute to indicate that movement should halt
    end
end)

-- Connect to the event triggered when a unit needs to be moved
game.ReplicatedStorage.MoveUnit.OnServerEvent:Connect(function(player, hitpos, unit)
    -- Disconnect any existing movement loop for this unit, if it exists
    if moveLoops[unit] then
        moveLoops[unit]:Disconnect()
    end

    unit:SetAttribute('uHalting', false)  -- Reset the attribute to allow movement
    -- Calculate and set the new velocity for the unit based on the hit position and speed attribute
    unit.PrimaryPart.Vel.PlaneVelocity = Vector2.new(hitpos.X - unit.PrimaryPart.Position.X, hitpos.Z - unit.PrimaryPart.Position.Z).Unit * unit:GetAttribute('Speed')

    game.ReplicatedStorage.CreateBeam:FireClient(player, hitpos, unit, false)  -- Inform client to create a beam effect

    -- Create a new movement loop for the unit
    moveLoops[unit] = game:GetService("RunService").Heartbeat:Connect(function()
        local distanceToHitPos = (unit.PrimaryPart.Position - Vector3.new(hitpos.X, unit.PrimaryPart.Position.Y, hitpos.Z)).Magnitude
        -- Check if the unit should keep moving or if it's close enough to the hit position
        if distanceToHitPos > 1 and not unit:GetAttribute('uHalting') then
            unit:SetAttribute('uRepeating', true)  -- Set attribute to indicate repeating movement
            -- Update the velocity for continued movement towards the hit position
            unit.PrimaryPart.Vel.PlaneVelocity = Vector2.new(hitpos.X - unit.PrimaryPart.Position.X, hitpos.Z - unit.PrimaryPart.Position.Z).Unit * unit:GetAttribute('Speed')
        else
            unit:SetAttribute('uRepeating', false)  -- Reset attribute indicating repeating movement
            unit.PrimaryPart.Vel.PlaneVelocity = Vector2.new(0, 0)  -- Stop horizontal movement
            game.ReplicatedStorage.CreateBeam:FireClient(player, hitpos, unit, true)  -- Inform client to create a beam effect

            if unit:GetAttribute('uHalting') then
                unit:SetAttribute('uHalting', false)  -- Reset halting attribute if movement was halted
            else
                game.ReplicatedStorage.CreateBeam:FireClient(player, hitpos, unit, true)  -- Inform client to create a beam effect
            end

            moveLoops[unit]:Disconnect()  -- Disconnect the movement loop since movement is complete
            moveLoops[unit] = nil  -- Remove the reference to the disconnected loop
        end
    end)
end)


Here is a example if anything to clarify what I am seeking. (a really bad example)

I know it is a very hard question and I understand it might not even be achievable, however, if anyone with the understand could spread its knowledge, it would be greatly appreciated.

3 Likes

Do the units need to be physically simulated? Are there obstacles they need to avoid and is it necessary to make sure that the units don’t move through each other or stand inside each other? Moving a single unit on a sphere with no physics isn’t very complicated. However, if the answer to any of the above questions is yes, then the movement will be more complicated.

No physics
The shortest path on a sphere surface from one point to another goes along the great circle that contains these points. A great circle on a sphere is a circle whose center is the same as the center of the sphere. This circle has the same radius as the sphere does.

Let’s define initialOffset as a vector that tells the initial offset of the unit from the center of the sphere and targetOffset as a vector that tells the offset of the unit from the center of the sphere after it has reached the desired location. With offset of the unit I mean the offset of the part that is used to move the unit like HumanoidRootPart is used to move a normal roblox character. This part is probably the primary part if the unit is a model.

The vectors initialOffset and targetOffset are parallel to the plane in which the correct great circle is (and in which the character moves). Thus, the cross product of these vectors gives a vector that is perpendicular to the plane (a normal vector of the plane).

local greatCirclePlaneNormalUnitVector = initialOffset:Cross(targetOffset).Unit

Movement along the great circle can be achieved by rotating initialOffset around the normal vector of the plane that contains the great circle.

local newOffset = CFrame.fromAxisAngle(greatCirclePlaneNormalUnitVector, amountOfRotationInRadians) * initialOffset.

To get the initial offset, we need

  • the center of the sphere
  • the surface point (in world space) above which the character is from the center of the sphere
  • and the height at which the position of the part equivalent to HumanoidRootPart should be from the surface of the sphere. (for normal roblox characters, this height would be Humanoid.HipHeight + HumanoidRootPart.Size / 2).
local initialSurfacePointOffsetFromCenter = initialSphereSurfacePoint - sphereCenter
local initialOffset = initialSurfacePointOffsetFromCenter + initialSurfacePointOffsetFromCenter.Unit * heightOffsetOfUnitPrimaryPartPositionFromSphereSurface

Alternatively, we could calculate it if we knew

  • the radius of the sphere
  • the direction of the character from the center of the sphere
  • and the aforementioned height at which the position of the part equivalent to HumanoidRootPart should be from the surface of the sphere.

In addition to moving the unit we also need it to be correctly oriented. The UpVector of the primary part (part equivalent to HumanoidRootPart) of the unit should be equal to the normal vector of the sphere surface, which has the same direction as the offset of the character from the sphere center.

local lookVector = currentOffset.Unit

The right vector should be perpendicular to the movement direction (and parallel to the sphere surface below the character) so it should be equal to the normal vector of the plane in which the great circle is. Calculating this normal vector (variable greatCirclePlaneNormalUnitVector) is already explained.

local rightVector = greatCirclePlaneNormalVector

Now, the final code for moving the unit (not tested):

local RunService = game:GetService("RunService")

local runServiceConnections = {}

local function startMovementTowardsTarget(unit, unitWalkSpeed, targetSphereSurfacePoint, heightOffsetOfUnitPrimaryPartPositionFromSphereSurface, sphereCenter)
	local targetSurfacePointOffsetFromCenter = targetSphereSurfacePoint - sphereCenter
	local targetOffset = targetSurfacePointOffsetFromCenter + targetSurfacePointOffsetFromCenter.Unit * heightOffsetOfUnitPrimaryPartPositionFromSphereSurface

	local sphereRadius = targetSurfacePointOffsetFromCenter.Magnitude
	local angularSpeed = unitWalkSpeed / sphereRadius -- tells how many radians of rotation happens in second
	if runServiceConnections[unit] ~= nil then
		runServiceConnections[unit]:Disconnect()
	end
	runServiceConnections[unit] = RunService.Heartbeat:Connect(function(deltaTime)
		local currentOffset = unit.PrimaryPart.Position - sphereCenter
		local greatCirclePlaneNormalUnitVector = currentOffset:Cross(targetOffset).Unit
		local angleBetweenCurrentAndTarget = math.acos(targetOffset.Unit:Dot(currentOffset.Unit))
		
		local amountOfRotationInRadiansDuringDeltaTime = angularSpeed * deltaTime
		local newOffset
		if amountOfRotationInRadiansDuringDeltaTime >= angleBetweenCurrentAndTarget then
			newOffset = targetOffset
			runServiceConnections[unit]:Disconnect()
			runServiceConnections[unit] = nil
		else
			newOffset = CFrame.fromAxisAngle(greatCirclePlaneNormalUnitVector, amountOfRotationInRadiansDuringDeltaTime) * currentOffset
		end
		local newPosition = sphereCenter + newOffset

		local upVector = newOffset.Unit
		local rightVector = greatCirclePlaneNormalUnitVector
		unit.PrimaryPart.CFrame = CFrame.fromMatrix(newPosition, rightVector, upVector)
	end)
end

Physics
If you need physics, I guess you could, for each unit, make a part that is located in the center of the planet, connected to the unit and rotated using an AngularVelocity instance. It should be attached to the center using a BallSocketConstraint. If I have correctly understood how AngularVelocity works, I think you should also have one attachment (Attachment0) parented to the aforementioned part and another attachment (Attachment1) that is rigidly attached to the center of the planet and rotated by changing its CFrame such that it will make the rotation direction of the AngularVelocity correct. By configuring the MaxTorque of the AngularVelocity, you can change how much physics affect the movement of the unit.

A simple way to connect the unit to the rotating part in the center of the planet would be welding. However, you might want the unit to be able to move vertically (in the direction perpendicular to the surface of the planet). Thus, it might be a better idea to constrain the movement of the unit and the rotation of the rotating part such that the position of the unit is always on the line that goes through the center of the planet in the direction of the up vector of the rotating part. By using two plane constraints, you can constrain movement to a line without constraining rotation.

You can calculate the desired rotation of the character based on its position and target position as explained for the no physics case. You could use an align orientation to have the unit attempt to achieve this orientation in the physics case.

You can disable the default gravity with a VectorForce instance parented to the primary part of the unit or by setting workspace.Gravity to 0. If you use a VectorForce for this, the direction of its force should be upwards and the magnitude should be (mass of unit) * (workspace.Gravity). Then, to create gravity towards the center of the planet, you just need a VectorForce whose force is towards the center of the planet.

I haven’t tested this, so I’m not sure if it works.

2 Likes

Thank you for your time, I’ll be reading it and I’ll inform you to show the progress.

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