Orientating a player while on a rope constraint

I haven’t had this problem solved for almost a year, so I’m gonna be as in-depth as I can with my issue.

I made a Spider-Man web swinging system. It works fine, but I want to orientate the player differently.

Look at my current system:


You can see the player is ALWAYS vertical.

Now look at Fortnite’s web swinging:


You can see that the player is rotated in the same direction and the web is.

Side by side you can see what I’m trying to achieve:

You can also clearly see that the player is always rotated the same orientation as the web.

I’ve tried using CFrame.lookat() and RunService to constantly make the player rotate towards the rope’s swing point (the attachment the rope is attached to), but the player rotates backwards halfway through the swing. Please, any help is appreciated. Thank you.

1 Like

Have you looked at the “Up” argument for CFrame.lookAt(), or are you calculating the LookVector from the UpVector you already have?

Can you elaborate on what that argument is?

This is what I’ve used to get the closest result I could.
humanoidRootPart.CFrame = CFrame.lookAt(humanoidRootPart.Position, currentRope.Attachment1.WorldPosition) * CFrame.Angles(math.rad(-90), math.rad(0), math.rad(0))

It’s actually pretty simple, I just didn’t know it existed til recently.
Basically you can give the CFrame.lookAt() function 3 arguments, the position, the look position, and the direction you want the UpVector to be. The issue is that if you’re looking away from the point you’re swinging from then the directions wouldn’t be perpendicular, so you have to use the HRP.CFrame.RightVector to make sure they are.

Try this:

local pos = HRP.Position
local upDirection = (ropeConstraint.Attachment0.WorldPosition-pos).Unit
local newLookVector = -HRP.CFrame.RightVector:Cross(upDirection)
HRP.CFrame = CFrame.lookAt(pos,pos+newLookVector,upDirection)

There’s a drift towards the lookvector facing out with this, if you prefer the drift to be facing left/right instead of out, use this code instead:

Code

local pos = HRP.Position
local upDirection = (ropeConstraint.Attachment0.WorldPosition-pos).Unit
local newRightVector = HRP.CFrame.LookVector:Cross(upDirection)
local newLookVector = -newRightVector:Cross(upDirection)
HRP.CFrame = CFrame.lookAt(pos,pos+newLookVector,upDirection)

If I can fix the drift I’ll update this.

For me, it’s a bit glitchy but it orients the player correctly, you might be able to find some ways to smooth it out. Maybe using AlignOrientation? There’s probably a lot of ways.

Edit: Changed it to actually work :skull:

4 Likes

WOW!! Thank you so much! I really appreciate it.

1 Like

Glad to help, also you’re making me wanna make a spiderman demo now :sob:

I wish you the best with this game, and hope to see it on the front page some day if you’re planning on making a full game from it!

Is it guaranteed that the horizontal orientation of HumanoidRootPart is correct (same direction as horizontal velocity) before the CFrame calculations (@Hqsuperlabgames’s code) are done? If it is possible that the horizontal orientation is incorrect, then I don’t think the code always works because the resulting CFrame is dependent on the RightVector of the CFrame before calculations, which means it’s dependent on the horizontal orientation before these calculations.

If, for example, this RightVector points in the direction in which the player is moving, then I think the RightVector of the resulting CFrame would be what’s supposed to be its LookVector and the LookVector of the resulting CFrame would be the counter vector of what is supposed to be its RightVector

If the code given by @Hqsuperlabgames works in every situation, then there’s probably no need to change it. However, if it doesn’t work in every situation, I have some alternative code that is based on how the character is rotated in the Fortnite video. The only information that my code uses from the CFrame the HumanoidRootPart has before this code is its position so the CFrame given by this code is not dependent on the orientation of the CFrame the HumanoidRootPart has before this code.

If I understood correctly (based on the Fortnite video), the UpVector of the CFrame of the HumanoidRootPart should be parallel with the rope and the LookVector of this CFrame should point to the direction in which the player is moving. The direction in which the player is moving is the direction of the velocity of the HumanoidRootPart.

When moving at a fixed distance (rope length) from the fixed point where the rope is attached (moving on the surface of a sphere) the velocity is perpendicular to the rope (because the tangent plane at a given sphere surface point is perpendicular to the line segment between this point and the center of the sphere). This means that the upVector and lookVector calculated in this code should be approximately perpendicular.

local upVector = (currentRope.Attachment1.WorldPosition-humanoidRootPart.Position).Unit
local lookVector = humanoidRootPart.AssemblyLinearVelocity.Unit
local rightVector = lookVector:Cross(upVector)

HRP.CFrame = CFrame.fromMatrix(HRP.Position, rightVector, upVector, lookVector)
2 Likes

Sorry for the very poorly drawn (and explained) example, but all I’m doing in my math is making a new lookvector from the rightvector of the HRP.

Both of our code has the same math behind it, and both should work, except mine can be used in the CFrame.lookAt() function instead of the CFrame.fromMatrix() function.
This is just because CFrame.lookAt() uses a lookvector and upvector and fills in the rightvector for you, instead of all three vectors.

Cross just finds the vector perpendicular to both angles. The vector it finds that in relates to the direction of the angles you’re crossing, I don’t really know much about that but I know that much is true.
Then, the vector perpendicular to both should be one of the vectors the character needs to face. Could be the right vector if you cross the lookvector and upvector, could be the lookvector if you cross the upvector and right vector, really just depends on what you’re crossing.
Here’s what mine does, yours basically does the same except finding the rightvector instead:

yellow = new upvector
red = look vector
green = up vector
grey arrow in the second frame is the old lookvector just for demonstration purposes so you can see the new one
blue = right vector

Basically, it doesn’t matter what the rightvector is, since two vectors in 3D space will always have a vector 90 degrees apart from them both.

I don’t understand your explanation after the picture, but here are some images to explain what I mean. Blue is LookVector, red is RightVector, green is UpVector and yellow is movement direction (direction of velocity).

Example of initial HumanoidRootPart CFrame and movement direction:
Initial

What I think your code does in this example situation:
WhatIThinkYourCodeDoes

What I think should happen in this example situation:
WhatIThinkShouldHappen

In short, I think your code creates a CFrame with the correct UpVector but not necessarily the correct LookVector and RightVector. Your code uses one vector that is known to be correct (up). My code uses two vectors that are known to be correct (up and look) assuming that I understood correctly what OP wants. Of course, I may have misunderstood that.

1 Like

RightVector in the case of mine is known to be correct, at least for the first frame.

What I’ve just realized is there is drift in a direction for mine, depending on how you calculate the new LookVector. That’s a reason that my RightVector isn’t known to be correct after the first frame.

But, your LookVector isn’t always correct either. AssemblyLinearVelocity can be 0, which would cause it to break, and substituting it for the actual LookVector of the HRP would be incorrect, as I’ve learned. You could probably store the AssemblyLinearVelocity in a value and use that if you’re not moving, and update it if you are moving.

But regardless, my solution of using CFrame.lookAt() works even with AssemblyLinearVelocity as the LookVector and the UpVector we know to be correct, so only needing two values is still a cleaner solution than CFrame.fromMatrix() in my opinion.

1 Like

Okay, so after further testing I wanna try and fix another problem. I’m using body movers (multiple body forces) to move around the player on the rope. With this, since it is rotating the whole HumanoidRootPart it’s messing with the body forces. Is there a way where I can rotate the character, but not move the HumanoidRootPart and keep it straight. I was thinking with a Motor6D but I’m not sure how to accomplish that.

Sorry to bother you.

1 Like

No worries about bothering me, you can probably achieve that with a motor6D.
I may need a refresher, but it works as an animatable weld right? So it should have C0 and C1 properties. If you set the C0 of the M6D that connects the HRP to the torso, it should turn the torso but not HRP. If it doesn’t do that, try setting the C1. Just set it to the CFrame you were setting the HRP to.

This should also fix the drift problem since the HRP LookVector stays the same.

C0 in a Motor6D doesn’t have a CFrame property though. How could I do that?

1 Like

C0 (which is a CFrame) defines an offset (position offset and rotation offset) from Part0. The CFrame properties of a Motor6D (C0, Transform and C1) satisfy the following equation:

Part0.CFrame * Motor6D.C0 * Motor6D.Transform = Part1.CFrame * Motor6D.C1

The CFrame given by the expression on either side of the equation can be though of as the world space CFrame of the Motor6D because Roblox animations rotate bodyparts around the position of this CFrame.

Roblox animations change the Transform property and I would recommend you to use the Transform property as well for this use case. The transform property must be updated on RunService.Stepped so that it won’t be overridden by Roblox animations.

This code with the necessary additions that are mentioned in comments should rotate the visible parts of the character as if the CFrame of HumanoidRootPart was calculatedCf without actually changing the CFrame of the HumanoidRootPart. Does it work?

-- Connect RunService.Stepped to this function when the player starts using the swing mechanic and disconnect when they stop using it.
local function updateCharacterOrientation()
	local calculatedCf = -- the CFrame calculated using the code posted earlier (I believe you use Hqsuperlabgames' code)
	local rootMotor6D = -- the Motor6D called "Root" connecting HumanoidRootPart and LowerTorso, it's a child of LowerTorso
	-- * rootMotor6D.Transform makes sure that Roblox animations are applied on top of this offset.	
	local motor6DTargetCf = calculatedCf * rootMotor6D.C0 * rootMotor6D.Transform
	local currentNonAnimatedMotor6DCf = HRP.CFrame * rootMotor6D.C0
	local targetMotor6DCfOffsetFromCurrentNonAnimatedMotor6DCf = currentNonAnimatedMotor6DCf:Inverse() * motor6DTargetCf
	rootMotor6D.Transform = targetMotor6DCfOffsetFromCurrentNonAnimatedMotor6DCf
end

An important thing to note is that Transform is not replicated so it needs to be set on each client.

1 Like

Like what @RoBoPoJu said, C0 basically is the CFrame property (it’s the CFrame for Part1 relative to Part0 if that makes sense)
I know nothing about welds, apparently :sob:

No, C0 is not the CFrame for Part1 relative to Part0 unless both Transform and C1 are identity matrices.

The CFrame of Part1 relative to Part0 is Part0.CFrame:Inverse() * Part1.CFrame. By solving this expression from the equation in my answer above we can get an expression for this relative CFrame in terms of C0, Transform and C1.

The equation:

Part0.CFrame * Motor6D.C0 * Motor6D.Transform = Part1.CFrame * Motor6D.C1

We can think of each side of the equation as a single matrix (which is the result of the operations written in the equation). When doing the exact same operation on both sides of the equation, the equation stays valid.

The addition needs to be made to the same side of the original matrix (expression) on both sides of the equation. This is because A * B is usually not equal to B * A when A and B are matrices. In this case I add it to the left of the original expressions on both sides.

Part0.CFrame:Inverse() * (Part0.CFrame * Motor6D.C0 * Motor6D.Transform) = Part0.CFrame:Inverse() * (Part1.CFrame * Motor6D.C1)

Although the rules for preserving equality in matrix operations are more strict than the ones for preserving equality in operations on numbers, matrices A, B and C satisfy the equation A * (B * C) = (A * B) * C. Because A * B * C has the same order of operations as (A * B) * C (which means they are equal), we can conclude that A * (B * C) = A * B * C.

Obviously, A * (B * C * D) = A * ((B * C) * D) because of same order of operations. From the equation that I wrote above (which was A * (B * C) = A * B * C) we can conclude that

A*((B*C)*D) = A*(B*C)*D = (A*(B*C))*D = (A*B*C)*D = A*B*C*D
A more detailed explanation of the above sequence of equations
  1. A * ((B * C) * D) = A * (B * C) * D follows from the equation A * (B * C) = A * B * C.
  2. A * (B * C) * D = (A * (B * C)) * D because these have the same order of operations.
  3. (A * (B * C)) * D = (A * B * C) * D follows from the equation A * (B * C) = A * B * C.
  4. (A * B * C) * D = A * B * C * D because these have the same order of operation.

Based on this, A * (B * C * D) = A * B * C * D, which means the Motor6D equation can be simplified to this:

Part0.CFrame:Inverse() * Part0.CFrame * Motor6D.C0 * Motor6D.Transform = Part0.CFrame:Inverse() * Part1.CFrame * Motor6D.C1

Multiplying a matrix and its inverse matrix gives an identity matrix, which can be left out of the equation (because multiplying a matrix by identity matrix gives the original matrix). Thus, the equation can be further simplified:

Motor6D.C0 * Motor6D.Transform = Part0.CFrame:Inverse() * Part1.CFrame * Motor6D.C1

Nextx we can add * Motor6D.C1:Inverse() to the end of both sides of the equation:

Motor6D.C0 * Motor6D.Transform * Motor6D.C1:Inverse() = Part0.CFrame:Inverse() * Part1.CFrame * Motor6D.C1 * Motor6D.C1:Inverse()

Based on the equation A * (B * C) = A * B * C we can write the Motor6D equation as follows:

Motor6D.C0 * Motor6D.Transform * Motor6D.C1:Inverse() = (Part0.CFrame:Inverse() * Part1.CFrame) * (Motor6D.C1 * Motor6D.C1:Inverse())

Here A = Part0.CFrame:Inverse() * Part1.CFrame, B = Motor6D.C1 and C = Motor6D.C1:Inverse()

Once again, we have an expression that gives an identity matrix. Thus, we can finally write the equation like this:

Motor6D.C0 * Motor6D.Transform * Motor6D.C1:Inverse() = Part0.CFrame:Inverse() * Part1.CFrame

Now, the right side is Part0.CFrame:Inverse() * Part1.CFrame which is the CFrame of Part1 relative to Part0. Thus, this relative CFrame in terms of C0, Transform and C1 is the following:

Motor6D.C0 * Motor6D.Transform * Motor6D.C1:Inverse()
Here's an explanation of why the relative CFrame is Part0.CFrame:Inverse() * Part1.CFrame

Let’s call the relative CFrame X, part0.CFrame A and Part1.CFrame B.
We can form the equation A*X = B. Let’s edit this equation:

A*X = B
A:Inverse()*(A*X) = A:Inverse()*B        -- A:Inverse()* added to both sides
A:Inverse()*A*X = A:Inverse()*B        -- equation A*(B*C) = A*B*C
X = A:Inverse()*B        -- Identity matrix can be left out.
2 Likes

That’s very useful to know, and I was wrong, but the math didn’t actually correct me. My response had nothing to do with the math behind CFrames and all to do with me misunderstanding how welds work.

I simply said “it’s [C0 is] the CFrame for Part1 relative to Part0

But in reality, it’s the inverse CFrame for Part0 relative to Part1. And that’s all you needed to say for me to get it :sob:

You were indeed correct in saying that Motor6D is an animatable weld and that C0 or C1 can be used to move Part1 relative to Part0 (torso relative to HRP). However, I don’t understand what you mean by “inverse CFrame for Part0 relative to Part1”. Neither C0 nor C1 alone tell the CFrame of one connected part relative to the other connected part. They are both needed for finding the relative CFrame of one part to the other (and Transform is needed too).

Animals including humans move their bodyparts by rotating bones around joints. C0 can be thought of as a CFrame defining

  • the position of the imaginary joint relative to Part0 and
  • the axes (relative to Part0) used in rotating Part1.

Likewise, C1 can be thought of as a CFrame defining

  • the position of the imaginary joint relative to Part1 and
  • the axes (relative to Part1) used in rotating Part1.

If you put a weld connecting two parts, then set the C0 position of one to Vector3.new(0,0,3), then the position of Part0 will be Part1.Position-Vector3.new(0,0,3)., unless I’m still misunderstanding something about CFrames (crying, if I am)
I’m not talking about finding CFrames, I’m talking about setting them.

The green part is Part0,
The blue part is Part1

image

1 Like

Thanks, I think I now understand what you mean. You are right that C0 is the CFrame for Part1 relative to Part0 when only C0 is set so your earlier reply is correct in this kind of situation (and “inverse CFrame for Part0 relative to Part1” now makes sense to me too because when only C0 is set, Part1.CFrame * C0:Inverse() = Part0.CFrame).

Perhaps your understanding of how welds work is entirely correct. I may have just misunderstood you when I thought you meant that C0 is the CFrame for Part1 relative to Part0 in every situation.

However, I’ll mention that in the case of a default Roblox character rig, both C0 and C1 are set by default so “C0 is the CFrame for Part1 relative to Part0” isn’t true in the case of a Roblox character unless you set C1 = CFrame.new().

1 Like