How to make an object stick inside another realistically (Like roots in soil)

I’m trying to model roots getting pulled out of the ground, but in order to do that they need to have some kind of resistance to movement up to some threshold to model static vs dynamic friction. It’s that resistance to movement that’s really got me stuck (pun definitely intended)

Dynamic friction I can do, that’s just adding a whole bunch of drag. But I can’t for the life of me see how to do static friction if I can’t access the amount of force that’s being applied to an object each physics cycle and counter act/cancel it if it’s below a threshold.

Whatever the solution, it has to be very performant since there’s going to be a lot of these objects all the time, so an expensive check on OnCollisionStay is not possible.

Does anyone have any ideas?
I hope I don’t have to build a separate physics system to cope with this requirement… If so, does anyone know where to begin with that? I’d still like these objects to be a part of the general physics environment so I guess I’d have to include everything in the new system

Below are my classes for dynamic friction. Thank you in advance for any ideas!

using UnityEngine;

[RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))]
public class StickingObject : MonoBehaviour {

    public float StuckDrag = 100;
    public float StuckAngularDrag = 100;

    Rigidbody2D rb;
   
    float drag;
    float angularDrag;
    float gravity;

    void Start() {
        rb = GetComponent<Rigidbody2D>();
        drag = rb.drag;
        angularDrag = rb.angularDrag;
        gravity = rb.gravityScale;
    }
   
    public void EnterObject() {
        rb.drag = StuckDrag;
        rb.angularDrag = StuckAngularDrag;
        rb.gravityScale = 0;
    }

    public void ExitObject() {
        rb.drag = drag;
        rb.angularDrag = angularDrag;
        rb.gravityScale = gravity;
    }

}
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))]
public class GroundObject : MonoBehaviour {

    Rigidbody2D rb;
    Collider2D c;

    void Start() {
        rb = GetComponent<Rigidbody2D>();
        c = GetComponent<Collider2D>();
        c.isTrigger = true;
    }
   
    void OnTriggerEnter2D(Collider2D other) {
        StickingObject obj = other.GetComponent<StickingObject>();
        if (obj == null)
            return;

        obj.EnterObject();
    }

    void OnTriggerExit2D(Collider2D other) {
        StickingObject obj = other.GetComponent<StickingObject>();
        if (obj == null)
            return;

        obj.ExitObject();
    }

}

I think you should use joints for simulating that. The damper is a resistance to the movement that matches what you’re looking for.

1 Like

It’s a good idea!
I had a bit of a go with a spring joint 2D and couldn’t quite find out how to make it work, but I found that a fixed joint with the damping ratio set high works really well. There’s a few optimisations to do, but I think it works pretty well.
I’ve included a testing class below as well if anyone wants to try it out. It’s a dragging class that lets you apply a force and torque (not calculated perfectly, but good enough for testing)

Thanks for the hint, I really appreciate it :smile:

using UnityEngine;

[RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))]
public class StickingObject : MonoBehaviour {

    public float StuckDrag = 20;
    public float StuckAngularDrag = 20;

    Rigidbody2D rb;
    ObjectStuckButMoving dynamicFriction;
    ObjectStuckAndStill staticFriction;
   
    float drag;
    float angularDrag;
    float gravity;

    void Start() {
        rb = GetComponent<Rigidbody2D>();
        dynamicFriction = GetComponent<ObjectStuckButMoving>();
        staticFriction = GetComponent<ObjectStuckAndStill>();

        if (dynamicFriction == null)
            dynamicFriction = gameObject.AddComponent<ObjectStuckButMoving>();
        if (staticFriction == null)
            staticFriction = gameObject.AddComponent<ObjectStuckAndStill>();

        // Save normal values
        drag = rb.drag;
        angularDrag = rb.angularDrag;
        gravity = rb.gravityScale;

        // Disable both types of friction
        dynamicFriction.enabled = false;
        staticFriction.enabled = false;
    }
   
    // When entering a GroundObject
    public void EnterObject() {
        rb.drag = StuckDrag;
        rb.angularDrag = StuckAngularDrag;
        rb.gravityScale = 0;

        // Enable dynamic friction to start with
        TransitionToDynamicFriction();
    }

    // On exitting a GroundObject
    public void ExitObject() {
        rb.drag = drag;
        rb.angularDrag = angularDrag;
        rb.gravityScale = gravity;

        // Disable both types of friction
        dynamicFriction.enabled = false;
        staticFriction.enabled = false;
    }

    public void TransitionToStaticFriction() {
        dynamicFriction.DisableDynamicFriction();
        staticFriction.EnableStaticFriction();
        Debug.Log("Static");
    }

    public void TransitionToDynamicFriction() {
        staticFriction.DisableStaticFriction();
        dynamicFriction.EnableDynamicFriction();
        Debug.Log("Dynamic");
    }

}
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D), typeof(Collider2D))]
public class GroundObject : MonoBehaviour {

    void Start() {
        Collider2D collider = GetComponent<Collider2D>();
        collider.isTrigger = true;
    }
   
    // If a StickingObject collides with this, tell it
    void OnTriggerEnter2D(Collider2D other) {
        StickingObject obj = other.GetComponent<StickingObject>();
        if (obj != null)
            obj.EnterObject();
    }

    // If a StickingObject exits this, tell it
    void OnTriggerExit2D(Collider2D other) {
        StickingObject obj = other.GetComponent<StickingObject>();
        if (obj != null)
            obj.ExitObject();
    }

}
using UnityEngine;

[RequireComponent(typeof(StickingObject))]
public class ObjectStuckButMoving : MonoBehaviour {

    public float StoppedPositionalThreshold = 0.001f; // World units
    public float StoppedAngularThreshold = 0.1f; // Angular degrees
    public float RequiredStoppedFrames = 10;

    StickingObject StickingObject;
    Vector2 lastPos;
    float lastRot;
    bool hasStopped = false;
    int framesStopped = 0;

    void Start() {
        StickingObject = GetComponent<StickingObject>();
        lastPos = transform.position;
        lastRot = transform.eulerAngles.z;
    }

    void Update() {
        float distanceMoved = Vector2.Distance(lastPos, transform.position);
        lastPos = transform.position;

        float angleMoved = Mathf.DeltaAngle(transform.eulerAngles.z, lastRot);
        lastRot = transform.eulerAngles.z;

        if (distanceMoved <= StoppedPositionalThreshold && angleMoved <= StoppedAngularThreshold) { // Is stopped

            if (hasStopped) { // Has been stopped for at least one frame now
                framesStopped++;

                if (framesStopped >= RequiredStoppedFrames)
                    StickingObject.TransitionToStaticFriction();

            } else
                hasStopped = true;
           
        } else if (hasStopped) { // Was stopped, but started moving again

            hasStopped = false;
            framesStopped = 0;

        }

    }

    public void EnableDynamicFriction() {
        enabled = true;
        hasStopped = false;
        framesStopped = 0;
    }

    public void DisableDynamicFriction() {
        enabled = false;
    }

}
using UnityEngine;

[RequireComponent(typeof(StickingObject))]
public class ObjectStuckAndStill : MonoBehaviour {

    public float breakingForce = 4;
    public float breakingTorque = 4;

    StickingObject StickingObject;
    FixedJoint2D fixedJoint;
   
    void Start() {
        StickingObject = GetComponent<StickingObject>();
    }
   
    void OnJointBreak2D(Joint2D brokenJoint) {
        Debug.Log("The broken joint exerted a reaction force of " + brokenJoint.reactionForce.magnitude);
        Debug.Log("The broken joint exerted a reaction torque of " + brokenJoint.reactionTorque);
        StickingObject.TransitionToDynamicFriction();
    }

    public void EnableStaticFriction() {
        enabled = true;

        // Find or create a Fixed Joint
        fixedJoint = GetComponent<FixedJoint2D>();
        if (fixedJoint == null)
            fixedJoint = gameObject.AddComponent<FixedJoint2D>();

        // Set the appropriate force thresholds
        fixedJoint.enabled = true;
        fixedJoint.breakForce = breakingForce;
        fixedJoint.breakTorque = breakingTorque;
        fixedJoint.dampingRatio = 1;
        fixedJoint.frequency = 7;

    }

    public void DisableStaticFriction() {
        enabled = false;
        if (fixedJoint != null)
            fixedJoint.enabled = false;
    }

}

Testing class (Mouse offset calculation not perfect but whatever):

using UnityEngine;

public class MouseDragger : MonoBehaviour {

    Transform obj;
    Rigidbody2D rb;
    Vector3 offset;

    Vector3 lastPos;

    void Update() {
        var ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        if (Input.GetMouseButtonDown(0)) {
            if (obj == null) {  
                RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction);
                if (hit.collider != null) {
                    obj = hit.transform;
                    rb = obj.GetComponent<Rigidbody2D>();
                    lastPos = ray.origin;
                    //offset = obj.position - lastPos;
                    offset = 0.2f * obj.InverseTransformPoint(hit.point);
                }
            }
        } else if (Input.GetMouseButtonUp(0)) {
            obj = null;
        }

        if (obj != null) {
            Vector2 diff = ray.origin - lastPos;
            rb.AddForceAtPosition(diff, offset);
            Debug.DrawLine(offset + obj.position, ray.origin);
            Debug.DrawRay(obj.position, offset, Color.magenta);
        }

    }

}
1 Like