Skip to content

Instantly share code, notes, and snippets.

@Toqozz

Toqozz/Rope.cs Secret

Created July 13, 2021 02:40
Show Gist options
  • Save Toqozz/23726eae4b39c0f7e314a83987f9a911 to your computer and use it in GitHub Desktop.
Save Toqozz/23726eae4b39c0f7e314a83987f9a911 to your computer and use it in GitHub Desktop.
rope with thickness parameter on circle
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
namespace Rope {
class VerletNode {
public Vector2 position;
public Vector2 oldPosition;
public VerletNode(Vector2 position) {
this.position = position;
this.oldPosition = position;
}
}
enum ColliderType {
Circle,
Box,
None,
}
class CollisionInfo {
public int id;
public ColliderType colliderType;
public Vector2 colliderSize;
public Vector2 position;
public Vector2 scale;
public Matrix4x4 wtl;
public Matrix4x4 ltw;
public int numCollisions;
public int[] collidingNodes;
public CollisionInfo(int maxCollisions) {
this.id = -1;
this.colliderType = ColliderType.None;
this.colliderSize = Vector2.zero;
this.position = Vector2.zero;
this.scale = Vector2.zero;
this.wtl = Matrix4x4.zero;
this.ltw = Matrix4x4.zero;
this.numCollisions = 0;
this.collidingNodes = new int[maxCollisions];
}
}
public class Rope : MonoBehaviour {
// Maximum number of colliders hitting the rope at once.
private const int MAX_ROPE_COLLISIONS = 32;
// Size of the collider buffer, also the maximum number of colliders that a single node can touch at once.
private const int COLLIDER_BUFFER_SIZE = 8;
// IF YOU CHANGE THIS, REMEMBER TO CHANGE IT IN THE SHADER AS WELL.
private const int MAX_RENDER_POINTS = 256;
// Number of triangles present in each line segment between nodes.
// 4 = 2 per quad, one quad for the line and one for the start cap.
private const int TRIANGLES_PER_NODE = 4;
// Number of vertices present in each line segment between nodes.
// 8 = 2 quads.
private const int VERTICES_PER_NODE = 8;
[Min(2)]
public int totalNodes = 200;
public int iterations = 80;
public float nodeDistance = 0.1f;
[Min(0.001f)]
public float stepTime = 0.01f;
public float maxStep = 0.1f;
public float thickness;
public float drawWidth = 0.025f;
public Vector2 gravity = new Vector2(0, -20f);
public float collisionRadius = 0.5f; // Collision radius around each node. Set high to avoid tunneling.
private VerletNode[] nodes;
private float timeAccum;
private CollisionInfo[] collisionInfos;
private int numCollisions;
private bool shouldSnapshotCollision;
private Camera cam;
private Material material;
private Collider2D[] colliderBuffer;
private Vector4[] renderPositions;
private void Awake() {
if (totalNodes > MAX_RENDER_POINTS) {
Debug.LogError("Total nodes is more than MAX_RENDER_POINTS, so won't be able to render the entire rope.");
}
nodes = new VerletNode[totalNodes];
collisionInfos = new CollisionInfo[MAX_ROPE_COLLISIONS];
for (int i = 0; i < collisionInfos.Length; i++) {
collisionInfos[i] = new CollisionInfo(totalNodes);
}
// Buffer for OverlapCircleNonAlloc.
colliderBuffer = new Collider2D[COLLIDER_BUFFER_SIZE];
renderPositions = new Vector4[totalNodes];
// Spawn nodes starting from the transform position and working down.
Vector2 pos = transform.position;
for (int i = 0; i < totalNodes; i++) {
nodes[i] = new VerletNode(pos);
renderPositions[i] = new Vector4(pos.x, pos.y, 1, 1);
pos.y -= nodeDistance;
}
// Mesh setup.
Mesh mesh = new Mesh();
{
Vector3[] vertices = new Vector3[totalNodes * VERTICES_PER_NODE];
int[] triangles = new int[totalNodes * TRIANGLES_PER_NODE * 3];
for (int i = 0; i < totalNodes; i++) {
// 4 triangles per node, 3 indices per triangle.
int idx = i * TRIANGLES_PER_NODE * 3;
// 8 vertices per node.
int vIdx = i * VERTICES_PER_NODE;
// Unity uses a CLOCKWISE WINDING ORDER -- clockwise tri indices are facing the camera.
triangles[idx + 0] = vIdx; // v1 top
triangles[idx + 1] = vIdx + 1; // v2 bottom
triangles[idx + 2] = vIdx + 2; // v1 bottom
triangles[idx + 3] = vIdx; // v1 top
triangles[idx + 4] = vIdx + 3; // v2 top
triangles[idx + 5] = vIdx + 1; // v2 bottom
triangles[idx + 6] = vIdx + 4; // tl
triangles[idx + 7] = vIdx + 7; // br
triangles[idx + 8] = vIdx + 6; // bl
triangles[idx + 9] = vIdx + 4; // tl
triangles[idx + 10] = vIdx + 5; // tr
triangles[idx + 11] = vIdx + 7; // br
}
// We only really care about the number of vertices, not what they actually are -- the positions aren't used.
mesh.vertices = vertices;
mesh.triangles = triangles;
// Since we pretty much want the rope to always render (it's always going to be on screen if it's active), we
// just set the bounds super large to avoid recalculating the bounds when the rope changes.
mesh.bounds = new Bounds(Vector3.zero, Vector3.one * 100f);
}
GetComponent<MeshFilter>().mesh = mesh;
material = GetComponent<MeshRenderer>().material;
material.SetFloat("_Width", drawWidth);
}
private void Update() {
if (shouldSnapshotCollision) {
SnapshotCollision();
}
// Fixed timestep.
timeAccum += Time.deltaTime;
timeAccum = Mathf.Min(timeAccum, maxStep);
while (timeAccum >= stepTime) {
Simulate();
for (int i = 0; i < iterations; i++) {
ApplyConstraints();
AdjustCollisions();
}
timeAccum -= stepTime;
}
}
private void LateUpdate() {
for (int i = 0; i < nodes.Length; i++) {
renderPositions[i].w = 1;
Vector2 pos = nodes[i].position;
renderPositions[i].x = pos.x;
renderPositions[i].y = pos.y;
renderPositions[i].z = 1;
}
material.SetVectorArray("_Points", renderPositions);
}
private void FixedUpdate() {
shouldSnapshotCollision = true;
}
private void SnapshotCollision() {
Profiler.BeginSample("Snapshot");
numCollisions = 0;
// Loop through each node and get collisions within a radius.
for (int i = 0; i < nodes.Length; i++) {
int collisions =
Physics2D.OverlapCircleNonAlloc(nodes[i].position, collisionRadius, colliderBuffer);
for (int j = 0; j < collisions; j++) {
Collider2D col = colliderBuffer[j];
int id = col.GetInstanceID();
int idx = -1;
for (int k = 0; k < numCollisions; k++) {
if (collisionInfos[k].id == id) {
idx = k;
break;
}
}
// If we didn't have the collider, we need to add it.
if (idx < 0) {
// Record all the data we need to use into our classes.
CollisionInfo ci = collisionInfos[numCollisions];
ci.id = id;
ci.wtl = col.transform.worldToLocalMatrix;
ci.ltw = col.transform.localToWorldMatrix;
ci.scale.x = ci.ltw.GetColumn(0).magnitude;
ci.scale.y = ci.ltw.GetColumn(1).magnitude;
ci.position = col.transform.position;
ci.numCollisions = 1; // 1 collision, this one.
ci.collidingNodes[0] = i;
switch (col) {
case CircleCollider2D c:
ci.colliderType = ColliderType.Circle;
ci.colliderSize.x = ci.colliderSize.y = c.radius;
break;
case BoxCollider2D b:
ci.colliderType = ColliderType.Box;
ci.colliderSize = b.size;
break;
default:
ci.colliderType = ColliderType.None;
break;
}
numCollisions++;
if (numCollisions >= MAX_ROPE_COLLISIONS) {
Profiler.EndSample();
return;
}
// If we found the collider, then we just have to increment the collisions and add our node.
} else {
CollisionInfo ci = collisionInfos[idx];
if (ci.numCollisions >= totalNodes) {
continue;
}
ci.collidingNodes[ci.numCollisions++] = i;
}
}
}
shouldSnapshotCollision = false;
Profiler.EndSample();
}
private void Simulate() {
for (int i = 0; i < nodes.Length; i++) {
VerletNode node = nodes[i];
Vector2 temp = node.position;
node.position += (node.position - node.oldPosition) + gravity * stepTime * stepTime;
node.oldPosition = temp;
}
}
private void ApplyConstraints() {
Profiler.BeginSample("Constraints");
for (int i = 0; i < nodes.Length - 1; i++) {
VerletNode node1 = nodes[i];
VerletNode node2 = nodes[i + 1];
// First node follows the mouse, for debugging.
if (i == 0 && Input.GetMouseButton(0)) {
node1.position = Camera.main.ScreenToWorldPoint(Input.mousePosition);
}
// Current distance between rope nodes.
float diffX = node1.position.x - node2.position.x;
float diffY = node1.position.y - node2.position.y;
float dist = Vector2.Distance(node1.position, node2.position);
float difference = 0;
// Guard against divide by 0.
if (dist > 0) {
difference = (nodeDistance - dist) / dist;
}
Vector2 translate = new Vector2(diffX, diffY) * (.5f * difference);
node1.position += translate;
node2.position -= translate;
}
/*
// Distance constraint which reduces iterations, but doesn't handle stretchyness in a natural way.
VerletNode first = nodes[0];
VerletNode last = nodes[nodes.Length-1];
// Same distance calculation as above, but less optimal.
float distance = Vector2.Distance(first.position, last.position);
if (distance > 0 && distance > nodes.Length * nodeDistance) {
Vector2 dir = (last.position - first.position).normalized;
last.position = first.position + nodes.Length * nodeDistance * dir;
}
*/
Profiler.EndSample();
}
private void AdjustCollisions() {
Profiler.BeginSample("Collision");
for (int i = 0; i < numCollisions; i++) {
CollisionInfo ci = collisionInfos[i];
switch (ci.colliderType) {
case ColliderType.Circle: {
float radius = ci.colliderSize.x * Mathf.Max(ci.scale.x, ci.scale.y);
for (int j = 0; j < ci.numCollisions; j++) {
VerletNode node = nodes[ci.collidingNodes[j]];
float distance = Vector2.Distance(ci.position, node.position);
// Early out if we're not colliding.
if (distance - radius > thickness) {
continue;
}
Vector2 dir = (node.position - ci.position).normalized;
Vector2 hitPos = ci.position + dir * (radius + thickness);
node.position = hitPos;
}
}
break;
case ColliderType.Box: {
for (int j = 0; j < ci.numCollisions; j++) {
VerletNode node = nodes[ci.collidingNodes[j]];
Vector2 localPoint = ci.wtl.MultiplyPoint(node.position);
// If distance from center is more than box "radius", then we can't be colliding.
Vector2 half = ci.colliderSize * .5f;
Vector2 scalar = ci.scale;
float dx = localPoint.x;
float px = half.x - Mathf.Abs(dx);
if (px <= 0) {
continue;
}
float dy = localPoint.y;
float py = half.x - Mathf.Abs(dy);
if (py <= 0) {
continue;
}
// Need to multiply distance by scale or we'll mess up on scaled box corners.
if (px * scalar.x < py * scalar.y) {
float sx = Mathf.Sign(dx);
localPoint.x = half.x * sx;
} else {
float sy = Mathf.Sign(dy);
localPoint.y = half.y * sy;
}
Vector2 hitPos = ci.ltw.MultiplyPoint(localPoint);
node.position = hitPos;
}
}
break;
}
}
Profiler.EndSample();
}
/*
private void OnDrawGizmos() {
if (!Application.isPlaying) {
return;
}
for (int i = 0; i < nodes.Length - 1; i++) {
if (i % 2 == 0) {
Gizmos.color = Color.green;
} else {
Gizmos.color = Color.white;
}
Gizmos.DrawLine(nodes[i].position, nodes[i + 1].position);
}
}
*/
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment