I am trying to make a scooter move and rotate appropriately relative to the camera given some move vector, but I just can’t get it to work! I’ve been at this for days so I am just so frustrated with this. I made it work using a LinearVelocity constraint but that gave me many physics-related problems and so I’ve been trying to use a VectorForce constraint but I’m not sure how to make it work.
NOTICE: I have edited the original post on January 7, 2024 to include the latest code
Just to be clear, I am open to suggestions on other ways to accomplish this because I am having a difficult time with my methods.
These are the relevant game objects:
-- get local player
local player = Players.LocalPlayer
-- get the master control module script which roblox puts into player scripts
-- by default
local PlayerModule = require(player.PlayerScripts.PlayerModule)
local MasterControl = PlayerModule:GetControls()
-- get scooter model
local scooter: Model = workspace:WaitForChild("Scooter-" .. player.Name)
-- get the physics box which is the part of the scooter that interacts with
-- the environment; basically the hitbox
local scooterPhysicsBox: Part = scooter:WaitForChild("ScooterPhysicsBox")
-- get the LinearVelocity constraint
local linearVelocity: LinearVelocity = scooter:WaitForChild("ScooterLinearVelocity")
I have some configurable values:
local maxMoveSpeed = 55
-- this should be interpreted as reaching the maxMoveSpeed from rest in 1.3 seconds
-- it is also the rate at which the scooter should decelerate
local acceleration = maxMoveSpeed / 1.3
-- this should be interpreted as a :Lerp() alpha of 0.05 at 60 FPS
local lerpInterpolationAlpha = 0.05 * 60
Every frame, I calculate some target linear velocity as well as the projected move vector, where the move vector is just the value I get from the MasterControl module’s :GetMoveVector()
. By projecting it relative to the camera, I mean making the move vector’s Z component align with the camera’ look vector and the move vector’s X component align with the camera’ right vector. Note that the VectorForce is set to be relative to the Attachment0 so it is always inline with the scooter’s look vector.
local movement = {
throttle = 0, -- represents whether the scooter should move forward, backward, or neither
projectedMoveVector = Vector3.zero -- the projected move vector
}
local function calculateMovement()
local cameraLookVector = workspace.CurrentCamera.CFrame.LookVector
local cameraRightVector = workspace.CurrentCamera.CFrame.RightVector
local scooterLookVector = scooterPhysicsBox.CFrame.LookVector
local moveVector = MasterControl:GetMoveVector()
-- project move vector in 3D space to align with camera look and right vector
projectedMoveVector = -moveVector.X * cameraRightVector + moveVector.Z * cameraLookVector
-- determine whether scooter should move forward (1), backward (-1), or become at rest (0)
movement.throttle = -math.sign(projectedMoveVector:Dot(scooterLookVector))
-- update projected move vector
movement.projectedMoveVector = projectedMoveVector
end
Based on the calculated projectedMoveVector
, I want to rotate the scooter. This is also done every frame. In case you are interested, I first align the scooter with the surface it is on with the method I learned from this post on the DevForum:
-- helper function to align with surface as per the post
local function calculateReorientationAngle(alignment: Vector3): number
return math.acos(scooterPhysicsBox.CFrame.UpVector:Dot(alignment))
end
-- helper function to align with surface as per the post
local function determineReorientationAxis(alignment: Vector3, angle: number): Vector3
local axis: Vector3
if angle < 0.00001 or angle > math.pi - 0.00001 then
axis = Vector3.xAxis
else
axis = scooterPhysicsBox.CFrame.UpVector:Cross(alignment)
end
return axis
end
Using those methods, I attempt to rotate the scooter:
local function updateScooterOrientation(deltaTime: number, surfaceNormal: Vector3?)
-- declare some target CFrame
local targetCFrame: CFrame
-- align with surface as per the post
if surfaceNormal ~= nil then
local angle = calculateReorientationAngle(surfaceNormal)
local axis = determineReorientationAxis(surfaceNormal, angle)
targetCFrame = CFrame.fromAxisAngle(axis, angle) * scooterPhysicsBox.CFrame.Rotation + scooterPhysicsBox.Position
else
local targetUpVector = Vector3.new(0, 1, 0)
local angle = calculateReorientationAngle(targetUpVector)
local axis = determineReorientationAxis(targetUpVector, angle)
targetCFrame = CFrame.fromAxisAngle(axis, angle) * scooterPhysicsBox.CFrame.Rotation + scooterPhysicsBox.Position
end
-- change target CFrame to be the CFrame after a lerp
targetCFrame = scooterPhysicsBox.CFrame:Lerp(targetCFrame, lerpInterpolationAlpha * deltaTime)
if movement.projectedMoveVector ~= movement.projectedMoveVector or movement.projectedMoveVector.Magnitude == 0 then
-- if projected move vector is NaN or has magnitude of zero, just align with the surface. don't rotate any more
alignOrientation.CFrame = targetCFrame
else
-- rotate the scooter using the projected move vector
local lookVectorYComponent = (-movement.projectedMoveVector.X * targetCFrame.UpVector.X - movement.projectedMoveVector.Z * targetCFrame.UpVector.Z) / targetCFrame.UpVector.Y
local lookVector = Vector3.new(movement.projectedMoveVector.X, lookVectorYComponent, movement.projectedMoveVector.Z)
local rightVector = targetCFrame.UpVector:Cross(lookVector)
alignOrientation.CFrame = CFrame.fromMatrix(
scooterPhysicsBox.Position, -- position
rightVector, -- right vector
targetCFrame.UpVector, -- up vector
-lookVector -- -look vector
)
end
end
However, the results are completely off as seen in this clip and I have no idea how to fix it: