Skip to content

Instantly share code, notes, and snippets.

@Fenikkel
Last active November 15, 2023 15:37
Show Gist options
  • Save Fenikkel/c9f44a1a1c35098eb8c4c627614afd97 to your computer and use it in GitHub Desktop.
Save Fenikkel/c9f44a1a1c35098eb8c4c627614afd97 to your computer and use it in GitHub Desktop.
SpringJoint drag 2D

SpringJoint drag 2D for Unity

 

Notes

Simple tu use and foolproof. Permit drag with a SpringJoint2D the desired Layers that have a Rigidbody2D. You can edit freely the values of the LineRenderer, SpringJoint2D and the Rigidboy2D. It works using the mouse or on multi-touch screens.

 

Usage

  1. Create an empty GameObject and add as component SpringJointDrag2D_Mouse. Edit the variables or leave the default values.

  2. Create a GO with a Rigidbody2D and a Collider2D. Make sure that the GO has at least one layer in the drag LayerMask of SpringJointDrag2D_Mouse.

  3. Press play and drag the GO. Check for any warning about your configuration.

  Same instructions for SpringJointDrag2D_Touch.cs

 

Project configuration

  1. Go to Project Settings -> Player -> Other Settings -> Configuration -> Active Input Handling -> Both or Input System Package

  2. Go to Package Manager -> Unity Registry -> Input System -> Install

 

Compatibility

  • Any Unity version
  • Any pipeline (Build-in, URP, HDRP, etc)
  • Input System Package

 

Support

⭐ Star if you like it
❤️️ Follow me for more

#if UNITY_STANDALONE || UNITY_EDITOR // Code specific to PC platforms (Windows, macOS, Linux)
using UnityEngine;
using System.Collections;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.UI;
using UnityEngine.Events;
public class SpringJointDrag2D_Mouse : MonoBehaviour
{
/* Fallback SpringJoint2D config */
const float DISTANCE = 0.1f;
const float DAMPING_RATIO = 1.0f;
const float FREQUENCY = 1.5f;
/* Recommended config for the connected */
const float RECOMMENDED_LINEAR_DRAG = 4.0f;
const float RECOMMENDED_ANGULAR_DRAG = 4.0f;
[SerializeField] SpringJoint2D _SpringJoint2D;
[SerializeField] LineRenderer _LineRenderer; // Leave empty if you don't want to draw a LineRenderer
[Header("Connected config")]
[SerializeField] LayerMask _DragLayerMask = 1 << 0;
[SerializeField] bool _AnchorToCenterOfMass = false;
[SerializeField] bool _UseRecommendedValues = false;
[Space(20)]
public UnityEvent<Vector3> OnStartDrag;
[Space]
public UnityEvent<Vector3> OnEndDrag;
InputAction _MouseInputAction;
InputSystemUIInputModule _InputSystemUIInputModule;
Coroutine _MouseDragCoroutine;
private void Awake()
{
CameraConfigCheck();
SpringJointConfigCheck();
LineRendererCheck();
_InputSystemUIInputModule = FindAnyObjectByType<InputSystemUIInputModule>();
if (_InputSystemUIInputModule)
{
_MouseInputAction = _InputSystemUIInputModule.actionsAsset.FindAction("Click");
}
else // Independent InputAction
{
_MouseInputAction = new InputAction(name: "MouseLeftClick", type: InputActionType.Button);
_MouseInputAction.AddBinding("<Mouse>/leftButton");
}
}
private void OnEnable()
{
_MouseInputAction.performed += OnMouseLeftClick;
if (!_InputSystemUIInputModule)
{
_MouseInputAction.Enable();
}
}
private void OnDisable()
{
_MouseInputAction.performed -= OnMouseLeftClick;
if (!_InputSystemUIInputModule)
{
_MouseInputAction?.Disable();
}
}
private void OnMouseLeftClick(InputAction.CallbackContext context)
{
//Debug.Log($"Click performed: {context.ReadValueAsButton()}");
if (context.ReadValueAsButton())
{
CheckMouseGrabbing();
}
}
#region Spring Joint 2D
private void CheckMouseGrabbing() // Should be private
{
Vector3 mouseScreenPoint = Mouse.current.position.ReadValue();
mouseScreenPoint.z = Camera.main.nearClipPlane;
Vector3 mouseWorldPoint = Camera.main.ScreenToWorldPoint(mouseScreenPoint);
Collider2D collider2dFound = Physics2D.OverlapPoint(mouseWorldPoint, _DragLayerMask);
if (!collider2dFound)
{
//Debug.Log($"Nothing clicked mith the mouse");
return;
}
if (!collider2dFound.attachedRigidbody)
{
Debug.Log($"The <b>{collider2dFound.name}</b> doesn't have a Rigidbody2D. Add this component to make the {this.GetType().Name} work.");
return;
}
if (!_SpringJoint2D)
{
Debug.LogError($"Unexpected unasigned SpringJoint2D");
return;
}
mouseWorldPoint.z = collider2dFound.transform.position.z;
_SpringJoint2D.connectedBody = collider2dFound.attachedRigidbody;
_SpringJoint2D.transform.position = mouseWorldPoint;
if (_AnchorToCenterOfMass) // Set the connected anchor to the point of equilibrium
{
_SpringJoint2D.connectedAnchor = collider2dFound.attachedRigidbody.centerOfMass;
}
else // Set the connected anchor to the mouse position
{
Vector2 nonRotatedAnchor = (Vector2)mouseWorldPoint - collider2dFound.attachedRigidbody.position;
Vector2 rotatedAnchor = RotateAroundPivot(nonRotatedAnchor, Vector2.zero, -collider2dFound.attachedRigidbody.rotation);
_SpringJoint2D.connectedAnchor = rotatedAnchor;
}
if (_MouseDragCoroutine != null)
{
Debug.LogError("MouseSpringJoint2dDrag already working!");
return;
}
OnStartDrag.Invoke(mouseWorldPoint);
_MouseDragCoroutine = StartCoroutine(MouseSpringJoint2dDrag(_SpringJoint2D));
}
private IEnumerator MouseSpringJoint2dDrag(SpringJoint2D springJoint2d)
{
// Preserve the original body values
float originalDrag = springJoint2d.connectedBody.drag;
float originalAngularDrag = springJoint2d.connectedBody.angularDrag;
// Use the recommended values while connected
if (_UseRecommendedValues)
{
springJoint2d.connectedBody.drag = RECOMMENDED_LINEAR_DRAG;
springJoint2d.connectedBody.angularDrag = RECOMMENDED_ANGULAR_DRAG;
}
// Performance variables
Camera cam = Camera.main;
Vector3 mouseScreenPoint;
Vector3 mouseWorldPoint = Vector3.zero;
Mouse mouse = Mouse.current;
Vector2 connectedForcePosition;
while (mouse.leftButton.isPressed)
{
mouseScreenPoint = mouse.position.ReadValue();
mouseScreenPoint.z = cam.nearClipPlane;
mouseWorldPoint = cam.ScreenToWorldPoint(mouseScreenPoint);
mouseWorldPoint.z = springJoint2d.transform.position.z;
springJoint2d.transform.position = mouseWorldPoint;
if (_AnchorToCenterOfMass)
{
connectedForcePosition = springJoint2d.connectedBody.worldCenterOfMass;
}
else
{
Vector2 nonRotatedPos = springJoint2d.connectedBody.position + springJoint2d.connectedAnchor;
Vector2 rotatedPos = RotateAroundPivot(nonRotatedPos, springJoint2d.connectedBody.position, springJoint2d.connectedBody.rotation);
connectedForcePosition = rotatedPos;
}
DrawLineRenderer(connectedForcePosition, mouseWorldPoint);
yield return null;
}
// Restore the original body values
if (springJoint2d.connectedBody)
{
springJoint2d.connectedBody.drag = originalDrag;
springJoint2d.connectedBody.angularDrag = originalAngularDrag;
springJoint2d.connectedBody = null;
}
ResetLineRenderer();
OnEndDrag.Invoke(mouseWorldPoint);
_MouseDragCoroutine = null;
}
#endregion
#region Line renderer
private void DrawLineRenderer(Vector3 startPoint, Vector2 endPoint)
{
if (_LineRenderer == null)
{
return;
}
_LineRenderer.positionCount = 2;
_LineRenderer.SetPosition(0, startPoint);
_LineRenderer.SetPosition(1, endPoint);
}
private void ResetLineRenderer()
{
if (_LineRenderer == null)
{
return;
}
_LineRenderer.positionCount = 0;
}
#endregion
#region Configuration check
private void CameraConfigCheck()
{
if (!Camera.main)
{
Debug.LogWarning($"Tag your Main Camera. Disabling <b>{this.GetType().Name}</b>");
this.enabled = false;
return;
}
if (!Camera.main.orthographic)
{
Debug.LogWarning($"2D physics only works with a camera with an <b>Orthographic</b> projection. Disabling <b>{this.GetType().Name}</b>");
this.enabled = false;
return;
}
if (Camera.main.transform.rotation != Quaternion.identity)
{
Debug.Log($"2D physics only works with a camera <b>without any rotation</b>. Disabling <b>{this.GetType().Name}</b>");
this.enabled = false;
return;
}
}
private void SpringJointConfigCheck()
{
if (_SpringJoint2D)
{
Rigidbody2D rigidbody2D = _SpringJoint2D.GetComponent<Rigidbody2D>();
if (rigidbody2D)
{
if (!rigidbody2D.isKinematic)
{
rigidbody2D.isKinematic = true;
Debug.LogWarning($"<b>{rigidbody2D.name}</b> needs a kinematic Rigidbody2D to work.");
}
}
else
{
rigidbody2D = _SpringJoint2D.gameObject.AddComponent<Rigidbody2D>();
rigidbody2D.isKinematic = true;
Debug.LogWarning($"<b>{_SpringJoint2D.name}</b> need a Rigidbody2D to work.");
}
if (_SpringJoint2D.autoConfigureDistance)
{
_SpringJoint2D.autoConfigureDistance = false;
Debug.LogWarning($"Disabling autoConfigureDistance of <b>{_SpringJoint2D.name}</b>");
}
if (_SpringJoint2D.autoConfigureConnectedAnchor)
{
_SpringJoint2D.autoConfigureConnectedAnchor = false;
Debug.LogWarning($"Disabling autoConfigureConnectedAnchor of <b>{_SpringJoint2D.name}</b>");
}
}
else // If the SpringJoint2D it's null, create a fallback (plan B)
{
GameObject go = new GameObject("Spring Joint 2D (Fallback)");
Rigidbody2D body = go.AddComponent<Rigidbody2D>();
body.isKinematic = true;
_SpringJoint2D = go.AddComponent<SpringJoint2D>();
_SpringJoint2D.autoConfigureDistance = false;
_SpringJoint2D.autoConfigureConnectedAnchor = false;
_SpringJoint2D.distance = DISTANCE;
_SpringJoint2D.dampingRatio = DAMPING_RATIO;
_SpringJoint2D.frequency = FREQUENCY;
}
}
private void LineRendererCheck()
{
if (_LineRenderer == null)
{
Debug.Log("No LineRenderer asigned. The behaviour won't show it");
return;
}
if (!_LineRenderer.useWorldSpace)
{
Debug.LogWarning($"Changing to WorldSpace on {_LineRenderer.name}");
_LineRenderer.useWorldSpace = true;
}
ResetLineRenderer();
}
#endregion
#region Utils
private Vector2 RotateAroundPivot(Vector2 point, Vector2 pivot, float rotationDegrees)
{
/* Anti-clockwise rotation */
// Convert the rotation angle from degrees to radians
float rotationRadians = rotationDegrees * Mathf.Deg2Rad;
// Calculate the sin and cos of the rotation angle
float sinTheta = Mathf.Sin(rotationRadians);
float cosTheta = Mathf.Cos(rotationRadians);
// Calculate the new coordinates after rotating by the given angle
float x = point.x - pivot.x;
float y = point.y - pivot.y;
float newX = x * cosTheta - y * sinTheta + pivot.x;
float newY = x * sinTheta + y * cosTheta + pivot.y;
return new Vector2(newX, newY);
}
#endregion
}
#endif
#if UNITY_IOS || UNITY_ANDROID || UNITY_EDITOR
using UnityEngine;
using System.Collections;
using UnityEngine.InputSystem.EnhancedTouch;
using UnityEngine.Events;
public class SpringJointDrag2D_Touch : MonoBehaviour
{
const int MAX_TOUCH = 10;
/* Fallback SpringJoint2D config */
const float DISTANCE = 0.1f;
const float DAMPING_RATIO = 1.0f;
const float FREQUENCY = 1.5f;
/* Recommended config for the connected */
const float RECOMMENDED_LINEAR_DRAG = 4.0f;
const float RECOMMENDED_ANGULAR_DRAG = 4.0f;
[Header("Connected config")]
[SerializeField] LayerMask _DragLayerMask = 1 << 0;
[SerializeField] bool _AnchorToCenterOfMass = false;
[SerializeField] bool _UseRecommendedValues = false;
[Space(20)]
[SerializeField] SpringJoint2D[] _SpringJoints2D = new SpringJoint2D[MAX_TOUCH];
[Space]
[SerializeField] LineRenderer[] _LineRenderers = new LineRenderer[MAX_TOUCH]; // Leave empty if you don't want to draw a LineRenderer
[Space]
public UnityEvent<Vector3>[] OnDrag = new UnityEvent<Vector3>[MAX_TOUCH];
[Space]
public UnityEvent<Vector3>[] OnEndDrag = new UnityEvent<Vector3>[MAX_TOUCH];
Coroutine[] _TouchDragCoroutine = new Coroutine[MAX_TOUCH];
private void Awake()
{
CameraConfigCheck();
SpringJointConfigCheck();
LineRenderersCheck();
}
private void OnEnable()
{
EnhancedTouchSupport.Enable();
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerDown += FingerDownAction;
}
private void OnDisable()
{
UnityEngine.InputSystem.EnhancedTouch.Touch.onFingerDown -= FingerDownAction;
EnhancedTouchSupport.Disable();
}
#region Spring Joint 2D
private void FingerDownAction(Finger finger)
{
if (finger == null)
{
Debug.LogWarning("Null finguer!");
return;
}
if (!finger.currentTouch.began)
{
Debug.LogWarning($"Incorrect touch phase: <b>{finger.currentTouch.phase}</b>");
return;
}
if (finger.index < 0 || MAX_TOUCH <= finger.index)
{
Debug.LogWarning($"Finger index <b>{finger.index}</b>: out of range.");
return;
}
if (_SpringJoints2D.Length <= finger.index)
{
Debug.LogWarning($"Increase the SpringJoints2D list to play with more than <b>{_SpringJoints2D.Length}</b> fingers");
return;
}
Vector3 fingerScreenPoint = finger.screenPosition;
fingerScreenPoint.z = Camera.main.nearClipPlane;
Vector3 fingerWorldPoint = Camera.main.ScreenToWorldPoint(fingerScreenPoint);
Collider2D collider2dFound = Physics2D.OverlapPoint(fingerWorldPoint, _DragLayerMask);
if (!collider2dFound)
{
Debug.Log($"Nothing touched with finguer index <b>{finger.index}</b>");
return;
}
if (!collider2dFound.attachedRigidbody)
{
Debug.Log($"The <b>{collider2dFound.name}</b> doesn't have a Rigidbody2D. Add this component to make the {this.GetType().Name} work.");
return;
}
fingerWorldPoint.z = collider2dFound.transform.position.z;
if (!_SpringJoints2D[finger.index])
{
Debug.LogError($"Unexpected unasigned SpringJoint2D at index <b>{finger.index}</b>");
return;
}
_SpringJoints2D[finger.index].connectedBody = collider2dFound.attachedRigidbody;
_SpringJoints2D[finger.index].transform.position = fingerWorldPoint;
if (_AnchorToCenterOfMass) // Set the connected anchor to the point of equilibrium
{
_SpringJoints2D[finger.index].connectedAnchor = collider2dFound.attachedRigidbody.centerOfMass;
}
else // Set the connected anchor to the mouse position
{
Vector2 nonRotatedAnchor = (Vector2)fingerWorldPoint - collider2dFound.attachedRigidbody.position;
Vector2 rotatedAnchor = RotateAroundPivot(nonRotatedAnchor, Vector2.zero, -collider2dFound.attachedRigidbody.rotation);
_SpringJoints2D[finger.index].connectedAnchor = rotatedAnchor;
}
if (_TouchDragCoroutine[finger.index] != null)
{
Debug.LogError($"Finger index <b>{finger.index}</b>: Coroutine already working!");
return;
}
_TouchDragCoroutine[finger.index] = StartCoroutine(TouchSpringJoint2dDrag(_SpringJoints2D[finger.index], finger));
OnDrag[finger.index].Invoke(fingerWorldPoint);
}
private IEnumerator TouchSpringJoint2dDrag(SpringJoint2D springJoint2d, Finger finger)
{
// Preserve the original body values
float originalDrag = springJoint2d.connectedBody.drag;
float originalAngularDrag = springJoint2d.connectedBody.angularDrag;
// Use the new values while connected
if (_UseRecommendedValues)
{
springJoint2d.connectedBody.drag = RECOMMENDED_LINEAR_DRAG;
springJoint2d.connectedBody.angularDrag = RECOMMENDED_ANGULAR_DRAG;
}
// Performance variables
Camera cam = Camera.main;
Vector3 fingerScreenPoint;
Vector3 fingerWorldPoint = Vector3.zero;
Vector2 connectedForcePosition;
while (!finger.currentTouch.ended)
{
fingerScreenPoint = finger.screenPosition;
fingerScreenPoint.z = cam.nearClipPlane;
fingerWorldPoint = cam.ScreenToWorldPoint(fingerScreenPoint);
fingerWorldPoint.z = springJoint2d.transform.position.z;
springJoint2d.transform.position = fingerWorldPoint;
if (_AnchorToCenterOfMass)
{
connectedForcePosition = springJoint2d.connectedBody.worldCenterOfMass;
}
else
{
Vector2 nonRotatedPos = springJoint2d.connectedBody.position + springJoint2d.connectedAnchor;
Vector2 rotatedPos = RotateAroundPivot(nonRotatedPos, springJoint2d.connectedBody.position, springJoint2d.connectedBody.rotation);
connectedForcePosition = rotatedPos;
}
DrawLineRenderer(finger.index, connectedForcePosition, fingerWorldPoint);
yield return null;
}
// Restore the original body values
if (springJoint2d.connectedBody)
{
springJoint2d.connectedBody.drag = originalDrag;
springJoint2d.connectedBody.angularDrag = originalAngularDrag;
springJoint2d.connectedBody = null;
}
ResetLineRenderer(finger.index);
OnEndDrag[finger.index].Invoke(fingerWorldPoint);
_TouchDragCoroutine[finger.index] = null;
}
#endregion
#region Line renderer
private void DrawLineRenderer(int index, Vector3 startPoint, Vector2 endPoint)
{
if (_LineRenderers.Length <= index)
{
return;
}
LineRenderer lineRenderer = _LineRenderers[index];
if (lineRenderer == null)
{
return;
}
lineRenderer.positionCount = 2;
lineRenderer.SetPosition(0, startPoint);
lineRenderer.SetPosition(1, endPoint);
}
private void ResetLineRenderer(int index)
{
if (_LineRenderers.Length <= index)
{
return;
}
LineRenderer lineRenderer = _LineRenderers[index];
if (lineRenderer == null)
{
return;
}
lineRenderer.positionCount = 0;
}
#endregion
#region Configuration check
private void CameraConfigCheck()
{
if (!Camera.main)
{
Debug.LogWarning($"Tag your Main Camera. Disabling <b>{this.GetType().Name}</b>");
this.enabled = false;
return;
}
if (!Camera.main.orthographic)
{
Debug.LogWarning($"2D physics only works with a camera with an <b>Orthographic</b> projection. Disabling <b>{this.GetType().Name}</b>");
this.enabled = false;
return;
}
if (Camera.main.transform.rotation != Quaternion.identity)
{
Debug.Log($"2D physics only works with a camera <b>without any rotation</b>. Disabling <b>{this.GetType().Name}</b>");
this.enabled = false;
return;
}
}
private void SpringJointConfigCheck()
{
for (int i = 0; i < _SpringJoints2D.Length; i++)
{
if (_SpringJoints2D[i] == null) // If there is no SpringJoint2D assigned, create a fallback (plan B)
{
GameObject go = new GameObject($"Spring Joint 2D Fallback ({i})");
Rigidbody2D body = go.AddComponent<Rigidbody2D>();
body.isKinematic = true;
_SpringJoints2D[i] = go.AddComponent<SpringJoint2D>();
_SpringJoints2D[i].autoConfigureDistance = false;
_SpringJoints2D[i].autoConfigureConnectedAnchor = false;
_SpringJoints2D[i].distance = DISTANCE;
_SpringJoints2D[i].dampingRatio = DAMPING_RATIO;
_SpringJoints2D[i].frequency = FREQUENCY;
}
else // Check the assigned
{
Rigidbody2D rigidbody2D = _SpringJoints2D[i].GetComponent<Rigidbody2D>();
if (rigidbody2D)
{
if (!rigidbody2D.isKinematic)
{
rigidbody2D.isKinematic = true;
Debug.LogWarning($"<b>{rigidbody2D.name}</b> needs a kinematic Rigidbody2D to work.");
}
}
else
{
rigidbody2D = _SpringJoints2D[i].gameObject.AddComponent<Rigidbody2D>();
rigidbody2D.isKinematic = true;
Debug.LogWarning($"<b>{_SpringJoints2D[i].name}</b> need a Rigidbody2D to work.");
}
if (_SpringJoints2D[i].autoConfigureDistance)
{
_SpringJoints2D[i].autoConfigureDistance = false;
Debug.LogWarning($"Disabling autoConfigureDistance of <b>{_SpringJoints2D[i].name}</b>");
}
if (_SpringJoints2D[i].autoConfigureConnectedAnchor)
{
_SpringJoints2D[i].autoConfigureConnectedAnchor = false;
Debug.LogWarning($"Disabling autoConfigureConnectedAnchor of <b>{_SpringJoints2D[i].name}</b>");
}
}
}
}
private void LineRenderersCheck()
{
for (int i = 0; i < _LineRenderers.Length; i++)
{
if (_LineRenderers[i] == null)
{
Debug.Log($"No LineRenderer asigned in index <b>{i}</b>. The behaviour won't show it");
return;
}
if (!_LineRenderers[i].useWorldSpace)
{
Debug.LogWarning($"Changing to WorldSpace on {_LineRenderers[i].name}");
_LineRenderers[i].useWorldSpace = true;
}
ResetLineRenderer(i);
}
}
#endregion
#region Utils
private Vector2 RotateAroundPivot(Vector2 point, Vector2 pivot, float rotationDegrees)
{
/* Anti-clockwise rotation */
// Convert the rotation angle from degrees to radians
float rotationRadians = rotationDegrees * Mathf.Deg2Rad;
// Calculate the sin and cos of the rotation angle
float sinTheta = Mathf.Sin(rotationRadians);
float cosTheta = Mathf.Cos(rotationRadians);
// Calculate the new coordinates after rotating by the given angle
float x = point.x - pivot.x;
float y = point.y - pivot.y;
float newX = x * cosTheta - y * sinTheta + pivot.x;
float newY = x * sinTheta + y * cosTheta + pivot.y;
return new Vector2(newX, newY);
}
#endregion
}
#endif
@Fenikkel
Copy link
Author

Connected rigidbody recommended setup:
    - Mass: 1
    - Linear Drag: 4
    - Angular Drag: 5
    - Gravity Scale: 0

Spring Joint 2D recommended setup:
    - Body type: Kinematic
    - Auto config: false (both)
    - Distance: 0.1
    - Damping ratio: 1.0
    - Frequency: 1.5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment