Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Help  Extending Grappling Hook & Collision Detection
#1
Hello!



I am having a problem where I'm extending a rope like ExtendableGrapplingHook.cs example.



Mine is a little different in that the rope is a fixed length. If the end particle (i.e. Hook) doesn't hit anything while the rope is extending it will just flop to the ground.



When first extending, I am using the same method of extending the rope.. basically LayParticlesInStraightLine() function from the Grappling Hook example.



I'm using the OnCollision callback to check if the rope's last/end particle hit something while extending.



If the end particle collides with something it can "Grapple", I add an ObiParticleAttachment to the last particle, and connect it to the object it collided with.



I will then stop LayParticlesInStraightLine, and just finish deploying the rope from the base until it's fully extended.



The problem is that the end of the rope sometimes passes through the object before the attachment can happen. It should connect at the exact point it first touched the object, but it sometimes attaches half-way through or even sometimes on the opposite side.



I have tried increasing the Collision constraint Iterations but it doesn't seem to help much. I suspect I'm doing something wrong..



Below are the functions I'm using to extend my rope (aka grappling hook) and where I check OnCollision callback for that last particle contact..



Any help would be appreciated as always!!

   



Code:
IEnumerator ExtendTetherCR()
{

    yield return null; //skip a step. then go.

    extending = true;
    extended = false;
    retracting = false;
    retracted = false;
    lastParticleContact = false;


    // Procedurally generate the rope path (just a short segment, as we will extend it over time):
    int filter = ObiUtils.MakeFilter(collidesWithMask, collisionCategory);
    blueprint.path.Clear();
    blueprint.path.AddControlPoint(Vector3.zero, Vector3.zero, Vector3.zero, Vector3.up, sectionMass, sectionRotMass, 0.1f, filter, Color.white, "Start");
    blueprint.path.AddControlPoint(localDirection * 0.1f, Vector3.zero, Vector3.zero, Vector3.up, sectionMass, sectionRotMass, 0.1f, filter, Color.white, "End");
    blueprint.path.FlushEvents();

    // Generate the particle representation of the rope (wait until it has finished):
    yield return blueprint.Generate();

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

    yield return new WaitForFixedUpdate();
    yield return null;

    shipPA = rope.gameObject.AddComponent<ObiParticleAttachment>();
    ObiParticleGroup startPG = rope.ropeBlueprint.groups[0];
    shipPA.target = shipAttachment.transform;
    shipPA.particleGroup = startPG;
    shipPA.attachmentType = ObiParticleAttachment.AttachmentType.Dynamic;

    cursor = rope.gameObject.AddComponent<ObiRopeCursor>();
    cursor.cursorMu = 0.0f; //Mu=0 means the start of the rope. .05 is the middle of the rope.
    cursor.direction = true; //true means we extend from start to end.

    //Debug.Log("Entend: rope.elements.Count: " + rope.elements.Count);
    Vector3 direction = tetherDirectionHandle.transform.localPosition.normalized;

    while (true )
    {

        Vector3 origin = solver.transform.InverseTransformPoint(rope.transform.position);
        
        

        float length = 0;

        if(!lastParticleContact)
        {
            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;
            }

        }

        float distanceLeft = tetherTargetLength - cursor.ChangeLength(tetherShootSpeed * Time.deltaTime);
        //Debug.Log("distanceLeft: " + distanceLeft);
        
        if (distanceLeft < 0 )
        {
            cursor.cursorMu = 1.0f;
            cursor.ChangeLength(distanceLeft);
            break;
        }

        
        yield return null;

        
    }


    yield return new WaitForFixedUpdate();
    yield return null;

    extending = false;
    extended = true;
    retracting = false;
    retracted = false;


}

Code:
    void Solver_OnCollision(object sender, ObiNativeContactList e)
    {

        ///no need to check collisions if the tether is currently connected to something or in the process of retracting..
        if (otherPA != null || retracting)
            return;

        //Debug.Log("Sender name: " + sender.ToString());

        var world = ObiColliderWorld.GetInstance();

        //iterate over all contacts in the current frame:
        foreach (Oni.Contact contact in e)
        {

            // if this one is an actual collision:
            if (contact.distance < collisionDistance)
            {
                ObiColliderBase col = world.colliderHandles[contact.bodyB].owner;
                if (col != null)
                {//if we are here we know particles are colliding with something..
                                        

                    //Debug.Log("ObiRope Colliding with something: " + col.gameObject.name);
                    if (rope.solver.simplices[contact.bodyA] == rope.elements[rope.elements.Count - 1].particle2)
                    {//if we are here we know it's the last particle in the rope that collided with something. Stop exending in the handle direction now!
                        //Debug.Log("Rope Last Particle Contacted: " + col.gameObject.name);
                        lastParticleContact = true;

                        //if (col.gameObject.layer == 15)
                        if ((attachmentMask & (1 << col.gameObject.layer)) != 0)
                        {//if we are here, we know the last particle in the rope should attach to what it collided with!

                            otherPA = rope.gameObject.AddComponent<ObiParticleAttachment>();
                            ObiParticleGroup endPG = rope.ropeBlueprint.groups[1];

                            otherPA.target = col.transform;
                            otherPA.particleGroup = endPG;
                            otherPA.attachmentType = ObiParticleAttachment.AttachmentType.Dynamic;
                            //Debug.Log("Tether attached!");

                        }
                        
                    }
                }
            }
        }
    }
Reply
#2
I am wondering if this could be caused by the rope being extended manually using LayParticlesInStraightLine() rather than physics?

Would the result be any different if I attached a small Rigidbody2D object to the end of the rope using an ObiParticleAttachment? Then lengthened the rope while putting an outward force on that attached Rigidbody2D?

This way I can just use the Unity Physics system to detect the collision with attachable object?
Reply
#3
Hi!

In your collision callback code you’re not using the contact point at all (contact.pointB) - instead you assume the particle’s current position is the contact point, whatever it may be. This generally won’t be true since collision detection happens during physics (at a fixed timestep) but the rope is extending every frame.

Use contact.pointB as the attachment point, keep in mind it’s expressed in the solver’s local space. See:
https://obi.virtualmethodstudio.com/manu...sions.html

Also keep in mind you will have to deal with tunneling: setting the position of particles directly while extending the rope means they have zero velocity so CCD won’t work: they will pass right through objects if moving fast enough. This is why we use raycasts in the example scene instead of relying on static collision detection.

Assuming the example approach won’t work for you (as it raycasts just once before shooting the rope, so any objects moving in the way of the rope as it extends will be ignored) a much simpler approach imho that doesn’t have either of the above problems and is more efficient is to raycast from the tip of the rope during the previous frame to the tip of the rope in the current frame as it extends. This doesn’t require iterating through all contacts to find the one you’re interested in, and won’t suffer from tunneling since you cover the distance moved each frame.

Kind regards,
Reply
#4
Hello again!

Thank you so much for the advice. I figured there was a better way of doing this.

I tried your suggestion of using the raycast and that has improved things. I am still seeing some tunnelling but that is now a function of the extend speed. Basically the faster the tether extends the greater the distance from the last particle's position previous frame vs current frame..

For the benefit of the other folks out there, here is my code.

Code:
IEnumerator ExtendTetherCR()
{

    yield return null; //skip a step. then go.

    extending = true;
    extended = false;
    retracting = false;
    retracted = false;
    lastParticleCollided = false;
    lastParticleAttached = false;


    // Procedurally generate the rope path (just a short segment, as we will extend it over time):
    int filter = ObiUtils.MakeFilter(collidesWithMask, collisionCategory);
    blueprint.path.Clear();
    blueprint.path.AddControlPoint(Vector3.zero, Vector3.zero, Vector3.zero, Vector3.up, sectionMass, sectionRotMass, 0.1f, filter, Color.white, "Start");
    blueprint.path.AddControlPoint(localDirection * 0.1f, Vector3.zero, Vector3.zero, Vector3.up, sectionMass, sectionRotMass, 0.1f, filter, Color.white, "End");
    blueprint.path.FlushEvents();

    // Generate the particle representation of the rope (wait until it has finished):
    yield return blueprint.Generate();

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

    yield return new WaitForFixedUpdate();
    yield return null;

    ObiParticleGroup startPG = rope.ropeBlueprint.groups[0];
    shipPA.target = shipAttachment.transform;
    shipPA.particleGroup = startPG;
    shipPA.attachmentType = ObiParticleAttachment.AttachmentType.Dynamic;

    cursor.cursorMu = 0.0f;  //cursorMu controls where new particles will be added to or removed from. curosorMu=0 means start of rope, cursorMu=1 means end of rope
    cursor.direction = true; //true means we extend from start to end.

    Vector3 direction = tetherDirectionHandle.transform.localPosition.normalized;

    Vector3 lastParticlePosOld = Vector3.zero;

    while (true)
    {

        Vector3 origin = solver.transform.InverseTransformPoint(rope.transform.position);

        float length = 0;

        if (!lastParticleCollided)
        {

            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;
            }

            //after positioning the particles in a line, we want to cast a ray from the last particle to see if we hit anything. if we do, we stop extending in a straight line and let the physics take over.
            int lastParticle = rope.elements[rope.elements.Count - 1].particle2;  //first step is to get the index for the last particle in the rope. this is the one we will cast from.
            Vector3 lastParticlePos = solver.transform.TransformPoint(solver.positions[lastParticle]); //then we get the world position for that particle from the solver.

            //if lastParticlePosOld is zero, it means this is the first step and we haven't positioned the particles yet, so we skip the raycast.
            if (lastParticlePosOld != Vector3.zero)
            {
                Vector3 rayDirection = lastParticlePos - lastParticlePosOld;

                RaycastHit2D hit = Physics2D.Raycast(lastParticlePosOld, rayDirection, rayDirection.magnitude, collisionMask); //cast the ray and see if it hits anything in the collision mask.

                if (hit.collider != null)
                {
                    //if we hit something, we stop extending in a straight line and let the physics take over
                    //Debug.Log("Hit something! Stopping straight extension. Hit point: " + hit.point);
                    lastParticleCollided = true;

                    if (((1 << hit.collider.gameObject.layer) & attachmentMask) != 0)
                    {
                        //Debug.Log("Hit something we can attach to!");
                                                                                
                        ObiParticleGroup endPG = rope.ropeBlueprint.groups[1];

                        otherPA = rope.gameObject.AddComponent<ObiParticleAttachment>();
                        otherPA.target = hit.collider.transform;
                        
                        otherPA.particleGroup = endPG;
                        otherPA.attachmentType = ObiParticleAttachment.AttachmentType.Dynamic;
                        lastParticleAttached = true;

                    }
                }
            }
                        
            lastParticlePosOld = lastParticlePos;  // record the last particle position for the next step..
        }

        float distanceLeft = tetherTargetLength - cursor.ChangeLength(tetherShootSpeed * Time.deltaTime);
        //Debug.Log("distanceLeft: " + distanceLeft);

        if (distanceLeft < 0)
        {
            cursor.ChangeLength(distanceLeft);
            break;
        }


        yield return null;


    }


    yield return new WaitForFixedUpdate();
    yield return null;

    extending = false;
    extended = true;
    retracting = false;
    retracted = false;


}
Reply
#5
(14-05-2026, 02:08 AM)docgonzzo Wrote: Hello again!

Thank you so much for the advice. I figured there was a better way of doing this.

I tried your suggestion of using the raycast and that has improved things. I am still seeing some tunnelling but that is now a function of the extend speed. Basically the faster the tether extends the greater the distance from the last particle's position previous frame vs current frame..

To get rid of the speed dependant tunneling, do the raycast *before* actually extending the rope. Your code extends the rope and then checks if it hit anything, if it did, the rope has already been extended and has left the raycast hit point behind it.

Do not explicitly store lastParticlePosOld: instead use solver.positions[tipParticle] before setting it to origin + direction * length as the old position, and use origin + direction * length as the new position  (which is where we will be moving the particle this frame). If you hit something, reduce length to only extend the rope up to the raycast hit. This way you ensure the rope always ends up extending the right amount.

kind regards,
Reply
#6
Thanks so much for all the help it is much appreciated. Excellent support here for this asset, as always.

It took some time, but I think I understand what you were suggesting. I implemented it and it seems to be working well.

On the final element of the rope, before moving that last particle, I do the raycast check and if it hits something I reset length.

Code:
    IEnumerator ExtendTetherCR()
    {

        yield return null; //skip a step. then go.

        extending = true;
        extended = false;
        retracting = false;
        retracted = false;
        lastParticleCollided = false;
        lastParticleAttached = false;

        // Procedurally generate the rope path (just a short segment, as we will extend it over time):
        int filter = ObiUtils.MakeFilter(collidesWithMask, collisionCategory);
        blueprint.path.Clear();
        blueprint.path.AddControlPoint(Vector3.zero, Vector3.zero, Vector3.zero, Vector3.up, sectionMass, sectionRotMass, 0.1f, filter, Color.white, "Start");
        blueprint.path.AddControlPoint(localDirection * 0.1f, Vector3.zero, Vector3.zero, Vector3.up, sectionMass, sectionRotMass, 0.1f, filter, Color.white, "End");
        blueprint.path.FlushEvents();

        // Generate the particle representation of the rope (wait until it has finished):
        yield return blueprint.Generate();

        // Set the blueprint (this adds particles/constraints to the solver and starts simulating them).
        rope.ropeBlueprint = blueprint;

        yield return new WaitForFixedUpdate();
        yield return null;

        ObiParticleGroup startPG = rope.ropeBlueprint.groups[0];
        shipPA.target = shipAttachment.transform;
        shipPA.particleGroup = startPG;
        shipPA.attachmentType = ObiParticleAttachment.AttachmentType.Dynamic;

        cursor.cursorMu = 0.0f;  //cursorMu controls where new particles will be added to or removed from. curosorMu=0 means start of rope, cursorMu=1 means end of rope
        cursor.direction = true; //true means we extend from start to end.

        Vector3 direction = tetherDirectionHandle.transform.localPosition.normalized;
        
        while (true)
        {
            Vector3 origin = solver.transform.InverseTransformPoint(rope.transform.position);

            float length = 0;

            if (!lastParticleCollided)
            {
                //extend the rope in a straight line by moving the positions of the active particles.
                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;

                    if(i == rope.elements.Count - 1)
                    {///before we place the last particle of the last element, check if there is anything in that path using a raycast.

                        Vector3 rayStart = solver.transform.TransformPoint(solver.positions[p2]);
                        Vector3 rayEnd = solver.transform.TransformPoint(origin + direction * length);
                        Vector3 rayDirection = rayEnd - rayStart;

                        RaycastHit2D hit = Physics2D.Raycast(rayStart, rayDirection, rayDirection.magnitude, collisionMask); //cast the ray and see if it hits anything in the collision mask.

                        if (hit.collider != null)
                        {
                            //Debug.Log("Hit something! hit.collider.name: " + hit.collider.name);
                            lastParticleCollided = true;

                            //length needs to be reset to be the distance from the rope's origin to the point the raycast hit something.
                            length = ( (Vector3)hit.point - rope.transform.position ).magnitude;

                            if (((1 << hit.collider.gameObject.layer) & attachmentMask) != 0)
                            {
                                //Debug.Log("Hit something we can attach to!");
                                lastParticleAttached = true;
                                //add a particle attachment, and set it's target to be the collider we hit in the raycast.
                                otherPA = rope.gameObject.AddComponent<ObiParticleAttachment>();
                                otherPA.target = hit.collider.transform;
                            }
                        }
                    }
                    //move the last particle of the rope into position (before attaching it, if applicable).
                    solver.prevPositions[p2] = solver.positions[p2] = origin + direction * length;

                    if(lastParticleAttached)
                    {
                        ObiParticleGroup endPG = rope.ropeBlueprint.groups[1];
                        otherPA.particleGroup = endPG;
                        otherPA.attachmentType = ObiParticleAttachment.AttachmentType.Dynamic;

                    }
                        


                        

                }
                                

                
            }

            float distanceLeft = tetherTargetLength - cursor.ChangeLength(tetherShootSpeed * Time.deltaTime);

            


            if (distanceLeft < 0)
            {
                cursor.ChangeLength(distanceLeft);
                break;
            }


            yield return null;


        }


        yield return new WaitForFixedUpdate();
        yield return null;


        extending = false;
        extended = true;
        retracting = false;
        retracted = false;


    }
Reply