Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Help  Issues After Upgrading to Obi Rope 7: Bounciness, Jitter, and Rope Generation Problem
#1
Hello,

I recently upgraded to Obi Rope 7 and encountered a few issues issues with the ropes not behaving as they did in Obi Rope 6. I've kept the same settings from Obi 6 and am using the default settings for the new solver options (except for substeps, which I set to match the value I used in the Obi Fixed Updater).

In my game, the player can shoot ropes. I originally based this mechanic on the Extendable Grappling Hook script provided with Obi Rope, although I've modified it over time. After upgrading to Obi 7, I found that I could no longer shoot ropes at all. I noticed the Extendable Grappling Hook script had changed since Obi 6, so I tried to apply similar changes to my custom rope generation script. This allowed rope generation to work again, but I'm not sure if I implemented it correctly, and I suspect this might be related to some of the issues I'm experiencing (particularly issues 2 & 3 below).

I've attached a video showing the differences between Obi 6 and Obi 7.



There are the issues I'm having:
  1. Increased Rope Bounciness:
    • When lifting certain rigidbodies, the ropes are much more bouncy than they were in Obi 6.
  2. Rope Flashing in the Wrong Location:
    • Occasionally, when generating a rope, it momentarily appears to connect from the player’s hand to the world origin for one frame.
  3. Unpredictable Behavior When Shooting Ropes at Rigidbodies:
    • When shooting ropes at rigidbodies (e.g., a box), the behavior is inconsistent. Sometimes the box is lifted into the air or pushed around unexpectedly. I think this might be related to the issue mentioned above.
  4. Increased Rope Jitter:
    • When ropes are left to settle, there seems to be more residual movement in Obi 7 compared to Obi 6. I'm using the same sleep threshold in both versions.
This is my Rope Generation script. (For now I've modified the Change length method in ObiCursor so that I can just pass in the new length rather than the length change)
Code:
public class RopeGenerator : MonoBehaviour{
        public float breakThreshhold = 0.1f;
        public float extraRopeLengthOnSpawn = 0;
        [SerializeField] private bool _useTearing;
        [SerializeField] private float _tearResistance = 800;

        [Range(0, 2)]
        [SerializeField] private float _stretchingScale = 1;
       
        public ObiSolver solver;
        public Material material;
        public ObiRopeSection section;

        [Range(0, 1)]
        public float hookResolution = 0.5f;

        //public float hookExtendRetractSpeed = 2;

        [FormerlySerializedAs("hookShootSpeed")]
        public float ropeShootSpeed = 1;

        public int particlePoolSize = 100;

        public float ropeParticleInverseMass = 100.0f;

        //public ObiRope rope;
        [SerializeField] private float ropeThickness = 0.5f;
        [SerializeField] private float cursorParticle = 4f;
        [SerializeField] private float sourceParticle = 2f;
        [SerializeField] private bool particleRendererOn = false;
        [SerializeField] private bool _useSurfaceCollision;
       
        public void InitialiseRope(RopeData ropeData, Action ropeInitialised = null) {
            if (ropeData.IsInitialised) {
                return;
            }
            // Setup a blueprint for the rope:
            ropeData.Blueprint = ScriptableObject.CreateInstance<ObiRopeBlueprint>();
            ropeData.Blueprint.resolution = hookResolution;
            ropeData.Blueprint.pooledParticles = particlePoolSize;

            // Create both the rope and the solver:    
            ropeData.ObiRope = ropeData.RopeObject.AddComponent<ObiRope>();
            ropeData.RopeObject.AddComponent<ObiParticleRenderer>().enabled = particleRendererOn;

            ObiRopeExtrudedRenderer ropeRenderer = ropeData.RopeObject.AddComponent<ObiRopeExtrudedRenderer>();
            ropeRenderer.section = section;
            ropeRenderer.uvScale = new Vector2(1, 4);
            ropeRenderer.normalizeV = false;
            ropeRenderer.uvAnchor = 1;
            ropeRenderer.material = material;
            //ropeData.RopeObject.GetComponent<MeshRenderer>().material = material;

            // Tweak rope parameters:
            ropeData.ObiRope.maxBending = 0.02f;

            // Add a cursor to be able to change rope length:
            ropeData.Cursor = ropeData.RopeObject.AddComponent<ObiRopeCursor>();
            ropeData.Cursor.cursorMu = 0;
            ropeData.Cursor.direction = false;

            ropeData.ObiRope.tearingEnabled = _useTearing;
            if (_useTearing) {
                ropeData.ObiRope.tearResistanceMultiplier = _tearResistance;
            }

            ropeData.ObiRope.surfaceCollisions = _useSurfaceCollision;

            ropeData.ObiRope.stretchingScale = _stretchingScale;
            StartCoroutine(InitialiseRopeRoutine(ropeData, ropeInitialised));
        }

        IEnumerator InitialiseRopeRoutine(RopeData ropeData, Action ropeInitialised = null) {
            var localHit = Vector3.forward;

            // Procedurally generate the rope path (just a short segment, as we will extend it over time):
            int filterCollision = ObiUtils.MakeFilter(ObiUtils.CollideWithEverything, 0);
            int filterNoCollision = ObiUtils.MakeFilter(ObiUtils.CollideWithNothing, 0);

            ropeData.Blueprint.path.Clear();
            ropeData.Blueprint.path.AddControlPoint(Vector3.zero, Vector3.zero, Vector3.zero, Vector3.up, 1 / ropeParticleInverseMass, 0.1f, ropeThickness, filterCollision, Color.white,
                "Rope start");
            ropeData.Blueprint.path.AddControlPoint(localHit.normalized * 0.5f, Vector3.zero, Vector3.zero, Vector3.up, 1 / ropeParticleInverseMass, 0.1f, ropeThickness, filterCollision,
                Color.white, "Rope end");

            ropeData.Blueprint.path.FlushEvents();

            // Generate the particle representation of the rope (wait until it has finished):
            yield return ropeData.Blueprint.Generate();
            ropeData.IsInitialised = true;
            yield return null;
            ropeInitialised?.Invoke();
        }

        private void LayParticlesInStraightLine(Vector3 origin, Vector3 direction, ObiRope rope)
        {
            // placing all particles in a straight line, respecting rope length
            float length = 0;
            for (int i = 0; i < rope.elements.Count; ++i)
            {
                int p1 = rope.elements[i].particle1;
                int p2 = rope.elements[i].particle2;
                solver.prevPositions[p1] = solver.positions[p1] = origin + direction * length;
                length += rope.elements[i].restLength;
                solver.prevPositions[p2] = solver.positions[p2] = origin + direction * length;
            }
        }
       
        public void GenerateRope(RopeData ropeData, RopeControlPoint sourceRcp, RopeControlPoint endRcp, Action<RopeData> onRopeGenerated, bool generateImmediately) {
            StartCoroutine(GenerateRopeRoutine());

            IEnumerator GenerateRopeRoutine() {
                var endRcpPosition = endRcp.transform.position;
                yield return null;
                // Clear pin constraints:
                var pinConstraints = ropeData.ObiRope.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;
                pinConstraints.Clear();

                if (generateImmediately) {
                    InitialiseRope(ropeData);
                }

                // Set the blueprint (this adds particles/constraints to the solver and starts simulating them).
                ropeData.ObiRope.ropeBlueprint = ropeData.Blueprint;
                ropeData.ObiRope.GetComponent<ObiRopeExtrudedRenderer>().enabled = true;

                // wait for the solver to load the rope, after the next physics step:
                yield return new WaitForFixedUpdate();
                yield return null;
                // set masses to zero, as we're going to override positions while we extend the rope:
                if (!generateImmediately) {
                    for (int i = 0; i < ropeData.ObiRope.activeParticleCount; ++i)
                        solver.invMasses[ropeData.ObiRope.solverIndices[i]] = 0;
                }

                float currentLength = 0;
                const float maxPercentOfDistance = 0.75f; // the max percent of the distance between the source and the hit that the rope can extend to in one frame
               
                //move definitions out of loop
                Vector3 origin;
                Vector3 direction;
               
                // while the last particle hasn't reached the hit, extend the rope:
                do {
                    //--Set Cursor and source to a set number of particles away from the end of the rope
                    SetCursorAndSourceMu(ropeData, cursorParticle, sourceParticle);

                    // calculate rope origin in solver space:
                    origin = solver.transform.InverseTransformPoint(sourceRcp.transform.position); //spawn from rope origin
                    // update direction and distance to hook point:
                    direction = endRcpPosition - origin;
                    float distance = direction.magnitude;
                    direction.Normalize();
                   
                    LayParticlesInStraightLine(origin, direction, ropeData.ObiRope);
                   
                    if (generateImmediately) {
                        ropeData.Cursor.ChangeLength(distance + extraRopeLengthOnSpawn);
                    }
                    else {
                        // calculate the max increment for this frame to stop the increment from being larger than the distance.
                        float maxIncrement = distance * maxPercentOfDistance;
                        var increment = ropeShootSpeed;

                        increment = Mathf.Min(maxIncrement, increment);
                        // increase length:
                        currentLength += increment;
                        // if we have reached the desired length, break the loop:
                        if (currentLength >= distance) {
                            ropeData.Cursor.ChangeLength(distance + extraRopeLengthOnSpawn);
                            break;
                        }

                        // change rope length (clamp to distance between rope origin and hook to avoid overshoot)
                        ropeData.Cursor.ChangeLength(Mathf.Min(distance, currentLength));
                    }
                    yield return null;
                } while (!generateImmediately);

                // wait for the last length change to take effect, and ensure the rope is straight:
                yield return new WaitForFixedUpdate();
                yield return null;
                LayParticlesInStraightLine(origin, direction, ropeData.ObiRope);
               
                // restore masses so that the simulation takes over now that the rope is in place:
                for (int i = 0; i < ropeData.ObiRope.activeParticleCount; ++i)
                    solver.invMasses[ropeData.ObiRope.solverIndices[i]] = ropeParticleInverseMass; // 1/0.1 = 10

                // Pin both ends of the rope (this enables two-way interaction between character and rope):
                var batch = new ObiPinConstraintsBatch();
                batch.AddConstraint(ropeData.ObiRope.elements[0].particle1, sourceRcp.ObiCollider, Vector3.zero, Quaternion.identity, 0, 1000, breakThreshhold);
                batch.AddConstraint(ropeData.ObiRope.elements[^1].particle2, endRcp.ObiCollider, Vector3.zero, Quaternion.identity, 0, 1000, breakThreshhold);
                batch.activeConstraintCount = 2;
                pinConstraints.AddBatch(batch);

                ropeData.ObiRope.SetConstraintsDirty(Oni.ConstraintType.Pin);

                //--Set Cursor and source to a set number of particles away from the end of the rope
                SetCursorAndSourceMu(ropeData, cursorParticle, sourceParticle);

                //Add Rope Data to both Control points
                endRcp.RopeActivated(ropeData);     // must happen before onRopeGenerated Electrics needs to know the RCPs Rope Data
                endRcp.InitialisePointOnRopeGenerated(ropeData);     // must happen before onRopeGenerated Electrics needs to know the RCPs Rope Data
                onRopeGenerated?.Invoke(ropeData); // calls hand wait finished, source.RopeActivated, RopeManager.RopeGenerated and RcpAttachPoint.AttachRcp
            }
        }
        public void SetCursorAndSourceMu(RopeData ropeData, float cursorParticlesFromEnd, float sourceParticlesFromEnd) {
            cursorParticlesFromEnd = Mathf.Clamp(cursorParticlesFromEnd, 2.0f, (float)ropeData.ObiRope.activeParticleCount - 2);
            sourceParticlesFromEnd = Mathf.Clamp(sourceParticlesFromEnd, 2.0f, (float)ropeData.ObiRope.activeParticleCount - 2);
            ropeData.Cursor.cursorMu = 1 - cursorParticlesFromEnd / ropeData.ObiRope.activeParticleCount;
            ropeData.Cursor.sourceMu = 1 - sourceParticlesFromEnd / ropeData.ObiRope.activeParticleCount;
        }
    }


Any advice or suggestions on how to resolve these issues would be greatly appreciated!
Thanks in advance!
Reply
#2
Hi!

1) Solvers in Obi 7 have two execution modes: synchronous and asynchronous. Async mode is used by default, it is much more performant however it introduces one frame of latency w.r.t systems that run their entire update cycle in a single frame, like rigidbody physics. If your game heavily relies on interaction with rigidbodies (specially via attachments) you should use synchronous mode. In this particular case, it seems like the rigidbody and the rope are playing catch-up with each other, resulting in a force feedback type of situation.

See: https://obi.virtualmethodstudio.com/manu...tsnew.html

2) Could not reproduce this using your script or the original RopeGrapplingHook one. Would it be possible for you to share a scene/project that exhibits this issue (by sending it to support(at)virtualmethodstudio.com) so that I can take a closer look?

3) and 4) - and maybe also 1)- look to me like the attached end of the rope is colliding against the object it is attached to, leading to jittering as the rope is trying to escape the collider but can't since it is attached to it. This is a physically impossible to solve situation and has always led to undefined behavior in Obi (see "attachments inside colliders" in the manual) but Obi 7 is less tolerant to it due to its faster convergence speed.

I see your code uses ObiUtils.CollideWithEverything as the collision filter for the entire rope, so this is likely the issue. Typically, you'd want a few particles at the end of the rope to use a filter that doesn't allow for collisions against the object they're going to be attached to.

"Collision filters" at the end of the scripting collisions page might also be relevant, as it explains how to create and set up collision filters at runtime: https://obi.virtualmethodstudio.com/manu...sions.html

Let me know if you need further help!

kind regards,
Reply
#3
Thanks, I will try these suggestions later today. I'll also try to put together a project that shows the issue. I have previously tried switching to synchronous mode although only during runtime I haven't started in synchronous mode.

Thanks Again
Reply
#4
Sorry for the delay but, I've now sent the project files to support@virtualmethodstudio.com
Reply
#5
(03-09-2024, 12:55 PM)Destro26 Wrote: Sorry for the delay but, I've now sent the project files to support@virtualmethodstudio.com

Hi Scott,

Just received your project. Will take a look at it asap and get back to you.

kind regards,
Reply