I did however figure this out by getting the difference between the 2 CFrame’s Y orientations and then multiplying them by that difference to get them to line up while still aligning with the terrain in all other ways; I’m not sure if the original post title is still relevant to this solution but it’s exactly what I wanted to figure out! I think I know how to code the rest of the movement system now that I have this basic logic
local TerrainAlignment = CFrame.fromMatrix(Shared.PrimaryPart.CFrame.Position, alignRightVector, alignLookVector:Cross(alignRightVector), -alignLookVector)
local CameraLookAt = CFrame.lookAt(Shared.PrimaryPart.Position, Shared.PrimaryPart.Position + game.Workspace.CurrentCamera.CFrame.LookVector)
-- ^ The CFrame that I want to align the Y orientation and only the Y orientation with
local x, y, z = TerrainAlignment:ToOrientation()
local x2, y2, z2 = CameraLookAt:ToOrientation()
local dif = y - y2
TerrainAlignment = TerrainAlignment * CFrame.Angles(0, dif, 0)
TerrainTiltGyro.CFrame = TerrainAlignment
Here’s the disorganized as butt prototype coding in case anyone is tackling a similar confusion (obviously a lot to still change about it; I’m just tackling the basics at this stage). GroundSensor.run() is every frame
local GroundSensor = {}
local Raycasts = require(game.ReplicatedStorage:WaitForChild("Raycasts"))
local Shared = require(script.Parent:WaitForChild("Shared"))
local PrimaryPart: BasePart = Shared.PrimaryPart
local PrimaryPartWidth = PrimaryPart.Size.X
local PrimaryPartThickness = PrimaryPart.Size.Y -- Vertical thickness
local PrimaryPartLength = PrimaryPart.Size.Z
-- The params used to search for a platform
local FindPlatformParams = RaycastParams.new()
FindPlatformParams.CollisionGroup = "Default"
FindPlatformParams.FilterDescendantsInstances = {PrimaryPart.Parent}
FindPlatformParams.FilterType = Enum.RaycastFilterType.Exclude
local SLOPE_DIFFERENTIATION_LIMIT = 2
-- Ordered: RGBV color
local frontRep = Instance.new("Part", game.Workspace)
frontRep.Position = PrimaryPart.Position
frontRep.CanCollide = false
frontRep.Anchored = true
frontRep.CanTouch = false
frontRep.Size = Vector3.new(2, 2, 2)
frontRep.Transparency = .5
frontRep.Color = Color3.new(1, 0, 0)
local frontHit = frontRep:Clone()
frontHit.Parent = game.Workspace
frontHit.Size = Vector3.new(1, 1, 1)
local hindRep = frontRep:Clone()
hindRep.Parent = game.Workspace
hindRep.Position = PrimaryPart.Position
hindRep.Color = Color3.new(0, 0, 1)
local hindHit = hindRep:Clone()
hindHit.Parent = game.Workspace
hindHit.Size = Vector3.new(1, 1, 1)
local hindHit = hindRep:Clone()
hindHit.Parent = game.Workspace
hindHit.Size = Vector3.new(1, 1, 1)
local rightRep = frontRep:Clone()
rightRep.Parent = game.Workspace
local rightHit = rightRep:Clone()
rightHit.Size = Vector3.new(1, 1, 1)
rightHit.Parent = game.Workspace
rightHit.Color = Color3.new(0, 1, 0.0666667)
local leftRep = frontRep:Clone()
leftRep.Parent = game.Workspace
local leftHit = leftRep:Clone()
leftHit.Parent = game.Workspace
leftHit.Size = Vector3.new(1, 1, 1)
leftHit.Color = Color3.new(0.933333, 0, 1)
-- Height to hover off the ground
local hipHeight = .5
local sensorRaycastDistance = hipHeight * 1.5
local Speed = {
CurrentSpeed = 0,
TargetSpeed = .5
}
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Setup
local RootPartAttachment = Instance.new("Attachment")
RootPartAttachment.Name = "RootPartAttachment"
RootPartAttachment.Position = Vector3.new(0, 0, 0)
RootPartAttachment.Parent = PrimaryPart
-- Controls vertical velocity to position character above terrain
local AlignPosition = Instance.new("AlignPosition")
AlignPosition.Parent = PrimaryPart
AlignPosition.Mode = Enum.PositionAlignmentMode.OneAttachment
AlignPosition.Attachment0 = RootPartAttachment
AlignPosition.ForceLimitMode = Enum.ForceLimitMode.PerAxis
AlignPosition.MaxAxesForce = Vector3.new(1000000, 1000000, 1000000)
AlignPosition.Responsiveness = 200
local TerrainTiltGyro = Instance.new("BodyGyro")
TerrainTiltGyro.Parent = PrimaryPart
TerrainTiltGyro.MaxTorque = Vector3.new(1000, 1000, 1000)
TerrainTiltGyro.P = 1000
TerrainTiltGyro.Name = "TerrainTiltGyro"
local BodyVelocity = Instance.new("BodyVelocity")
BodyVelocity.Parent = PrimaryPart
BodyVelocity.MaxForce = Vector3.new(10000,10000,10000)
BodyVelocity.P = 10000
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private
-- // The function which reports a successful hit to a raycast searching for a standing platform
local function raycastSuccessPlatformFunction(Result: RaycastResult)
local Hit = Result.Instance
if Hit == game.Workspace.Terrain or (Hit:IsA("Part") and Hit.CanCollide) then
return true
end
end
local function raycastFront(lookVector: Vector3, raycastVector: Vector3, upVector: Vector3)
local origin = PrimaryPart.Position + ((PrimaryPartLength/2) * lookVector) + (upVector * 6)
frontRep.Position = origin
local Result: RaycastResult? = Raycasts.send(origin, raycastVector, 500, FindPlatformParams, raycastSuccessPlatformFunction)
if Result then
frontHit.Position = Result.Position
end
return if Result then Result.Position else nil
end
local function raycastBack(lookVector: Vector3, raycastVector: Vector3, upVector: Vector3)
local origin = PrimaryPart.Position - ((PrimaryPartLength/2) * lookVector) + (upVector * 6)
hindRep.Position = origin
local Result: RaycastResult? = Raycasts.send(origin, raycastVector, 500, FindPlatformParams, raycastSuccessPlatformFunction)
if Result then
hindHit.Position = Result.Position
end
return if Result then Result.Position else nil
end
local function raycastRight(rightVector: Vector3, raycastVector: Vector3, upVector: Vector3)
--raycastVector = Vector3.new(0, -1, 0)
local origin = PrimaryPart.Position + ((PrimaryPartWidth/2) * rightVector) + (upVector * 6)
local Result: RaycastResult? = Raycasts.send(origin, raycastVector, 500, FindPlatformParams, raycastSuccessPlatformFunction)
if Result then
rightHit.Position = Result.Position
end
return if Result then Result.Position else nil
end
local function raycastLeft(rightVector: Vector3, raycastVector: Vector3, upVector: Vector3)
--raycastVector = Vector3.new(0, -1, 0)
local origin = PrimaryPart.Position - ((PrimaryPartWidth/2) * rightVector) + (upVector * 6)
local Result: RaycastResult? = Raycasts.send(origin, raycastVector, 500, FindPlatformParams, raycastSuccessPlatformFunction)
if Result then
leftHit.Position = Result.Position
end
return if Result then Result.Position else nil
end
local function getSizeOfTable(t)
local count = 0
for k, v in pairs(t) do
count += 1
end
return count
end
local function applyForwardVelocity(Condition: boolean?)
if Condition == true then
local speedValue = 15
local rotation = PrimaryPart.Orientation.Y
local Xdirection = math.sin(0.0174532925*rotation)*(speedValue * -1)
local Zdirection = math.cos(0.0174532925*rotation)*(speedValue * -1)
BodyVelocity.Velocity = Vector3.new(Xdirection, 0, Zdirection) + Vector3.new(0, 0, 0)
else -- Cease forward movement
BodyVelocity.Velocity = Vector3.new(0, 0, 0)
end
end
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Public
local Forward_Pressed = false
local ContextActionService = game:GetService("ContextActionService")
ContextActionService:BindAction("Forward", function(actionName, inputState)
if inputState == Enum.UserInputState.Cancel then
return
end
if inputState == Enum.UserInputState.Begin then
Forward_Pressed = true
Speed.CurrentSpeed = 0
--applyForwardVelocity(true)
else
Forward_Pressed = false
--applyForwardVelocity(false)
end
end, false, Enum.KeyCode.W)
local Left = false
ContextActionService:BindAction("Test", function(actionName, inputState)
if inputState == Enum.UserInputState.Cancel then
return
end
if inputState == Enum.UserInputState.Begin then
Left = true
else
Left = false
end
end, false, Enum.KeyCode.A)
local Right = false
ContextActionService:BindAction("Test2", function(actionName, inputState)
if inputState == Enum.UserInputState.Cancel then
return
end
if inputState == Enum.UserInputState.Begin then
Right = true
else
Right = false
end
end, false, Enum.KeyCode.D)
function GroundSensor.run()
-- Reset raycast filter list
FindPlatformParams.FilterDescendantsInstances = {PrimaryPart.Parent}
-- // Get Platform Hit Vectors // --
-- Send vectors downwards to orientate player with the terrain
local lookVector: Vector3 = Shared.PrimaryPart.CFrame.LookVector
local rightVector: Vector3 = Shared.PrimaryPart.CFrame.RightVector
local upVector: Vector3 = Shared.PrimaryPart.CFrame.UpVector
local downVector: Vector3 = Shared.PrimaryPart.CFrame.UpVector * -1
local PlatformHits = {
Front = raycastFront(lookVector, downVector, upVector),
Back = raycastBack(lookVector, downVector, upVector),
Right = raycastRight(rightVector, downVector, upVector),
Left = raycastLeft(rightVector, downVector, upVector)
}
local totalPlatformHits = getSizeOfTable(PlatformHits)
local averagePlatformHit = Vector3.new(0,0,0)
for i, p in pairs(PlatformHits) do
averagePlatformHit += p
end
averagePlatformHit /= totalPlatformHits
-- Position character based off the averagePlatformHitd of the sensored positions
if totalPlatformHits > 0 then
local height = averagePlatformHit.Y + hipHeight
local positionVector = PrimaryPart.Position
-- // Move forward // --
if Forward_Pressed then
Speed.CurrentSpeed += 0.003
if Speed.CurrentSpeed > Speed.TargetSpeed then
Speed.CurrentSpeed = Speed.TargetSpeed
end
positionVector += (lookVector * Speed.CurrentSpeed)
--applyForwardVelocity(true)
end
-- // Determine Alignment // --
-- Align to platform
local alignLookVector = (PlatformHits.Front - PlatformHits.Back).unit
local alignRightVector = (PlatformHits.Left - PlatformHits.Right).unit
--local Align = CFrame.fromMatrix(Shared.PrimaryPart.CFrame.Position, Shared.PrimaryPart.CFrame.RightVector, Shared.PrimaryPart.CFrame.UpVector, -Shared.PrimaryPart.CFrame.LookVector) -- THIS IS CORRECT
local TerrainAlignment = CFrame.fromMatrix(Shared.PrimaryPart.CFrame.Position, alignRightVector, alignLookVector:Cross(alignRightVector), -alignLookVector)
local CameraLookAt = CFrame.lookAt(Shared.PrimaryPart.Position, Shared.PrimaryPart.Position + game.Workspace.CurrentCamera.CFrame.LookVector)
local x, y, z = TerrainAlignment:ToOrientation()
local x2, y2, z2 = CameraLookAt:ToOrientation()
local dif = y - y2
TerrainAlignment = TerrainAlignment * CFrame.Angles(0, dif, 0)
TerrainTiltGyro.CFrame = TerrainAlignment
AlignPosition.Position = Vector3.new(positionVector.X, height, positionVector.Z)
else
end
end
return GroundSensor