private void Simulate() { // For each node in rope. for (int i = 0; i < nodes.Length; i++) { VerletNode node = nodes[i]; // Acceleration is useful to have around if we want to apply external forces to rope nodes, just add the // force to `node.acceleration`. node.acceleration += gravity; float2 move = node.position - node.oldPosition; // Limiting maximum move gives the simulation more stability. if (math.length(move) > maxSimMove) { move = math.normalize(move) * maxSimMove; } // Calculate new position. float2 temp = node.position; node.position += move * (1f - node.friction) + stepTime * stepTime * node.acceleration; node.oldPosition = temp; node.acceleration = float2.zero; node.friction = 0; nodes[i] = node; } }
private void ApplyConstraints() { for (int i = 0; i < nodes.Length - 1; i++) { VerletNode node1 = nodes[i]; VerletNode node2 = nodes[i + 1]; Constraint constraint1 = constraints[i]; Constraint constraint2 = constraints[i + 1]; // If the node is constrained, we need to set its position early so that the connected node adjusts // accordingly, but we don't want it to move, so we multiply movement by 0 later if we're constrained. // For the most part, we don't actually need to check `constraint2` because it's overwritten in the // Next iteration of the loop. However, the last node never gets to be `node1`, so we have to check both. float mult1 = 1f, mult2 = 1f; if (constraint1.enabled) { node1.position = constraint1.position; mult1 = 0f; } if (constraint2.enabled) { node2.position = constraint2.position; mult2 = 0f; } // Get the current distance between rope nodes. float2 diff = math.float2(node1.position.x - node2.position.x, node1.position.y - node2.position.y); float dist = math.length(node1.position - node2.position); float difference = 0; // Guard against divide by 0. if (dist > 0) { difference = (dist - nodeDistance) / dist; } diff *= .5f * difference; // Apply correction. node1.position -= diff * mult1; node2.position += diff * mult2; nodes[i] = node1; nodes[i + 1] = node2; } }
private void AdjustCollisions() { // Loop through each collider. for (int i = 0; i < numCollisions; i++) { CollisionInfo nc = collisionInfos[i]; // Looping inside the switch statement is marginally faster than the other way around. switch (nc.colliderType) { case ColliderType.Circle: { float radius = nc.colliderSize.x * math.max(nc.scale.x, nc.scale.y); // Correct each node which is colliding with this collider. for (int j = 0; j < nc.numCollisions; j++) { VerletNode node = nodes[collidingNodes[i * nodes.Length + j]]; float distance = math.distance(nc.position, node.position); // Leave if we're not actually colliding. if (distance - radius > 0) { continue; } float2 dir = math.normalize(node.position - nc.position); float2 hitPos = nc.position + dir * radius; node.position = hitPos; // Colliding nodes should have some friction on them. node.friction = friction; nodes[collidingNodes[nodes.Length * i + j]] = node; } } break; case ColliderType.Box: { for (int j = 0; j < nc.numCollisions; j++) { VerletNode node = nodes[collidingNodes[i * nodes.Length + j]]; float4 localPoint = math.mul(nc.wtl, math.float4(node.position, 0, 1)); // If distance from center is more than box "radius", then we can't be colliding. Vector2 half = nc.colliderSize * .5f; Vector2 scalar = nc.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; } // Finally get the world space hit position. float4 hitPos = math.mul(nc.ltw, localPoint); node.position = hitPos.xy; node.friction = friction; nodes[collidingNodes[nodes.Length * i + j]] = node; } } break; } } }
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."); } // Buffer for OverlapCircleNonAlloc. colliderBuffer = new Collider2D[COLLIDER_BUFFER_SIZE]; // Jobs setup. // You could use `NativeList` for some of these instead, and not worry about all the constants. nodes = new NativeArray <VerletNode>(totalNodes, Allocator.Persistent); constraints = new NativeArray <Constraint>(totalNodes, Allocator.Persistent); collisionInfos = new NativeArray <CollisionInfo>(MAX_ROPE_COLLISIONS, Allocator.Persistent); collidingNodes = new NativeArray <int>(totalNodes * MAX_ROPE_COLLISIONS, Allocator.Persistent); renderPositions = new Vector4[totalNodes]; // Spawn nodes starting from the transform position and working down. Vector2 pos = transform.position; for (int i = 0; i < nodes.Length; 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); }