fgm24/Assets/Scripts/Rope/RopeSimulator.cs

299 lines
9.9 KiB
C#
Raw Normal View History

2024-02-03 00:15:07 +01:00
using System;
2024-02-02 21:03:44 +01:00
using System.Collections;
using System.Collections.Generic;
2024-02-03 00:15:07 +01:00
using System.Linq;
using Unity.VisualScripting;
using UnityEditor.ShaderGraph.Drawing;
2024-02-02 21:03:44 +01:00
using UnityEngine;
2024-02-03 00:15:07 +01:00
using UnityEngine.Analytics;
using UnityEngine.Rendering;
2024-02-02 21:03:44 +01:00
using UnityUtils;
public class RopeSimulator : MonoBehaviour
{
[SerializeField]
private float gravity = 10;
[SerializeField]
2024-02-03 00:15:07 +01:00
private int solveIterations = 10;
2024-02-02 21:03:44 +01:00
[SerializeField]
2024-02-03 00:15:07 +01:00
private bool constrainStickMinLength;
[SerializeField]
Transform start, end;
[SerializeField]
int subDivision = 50;
[SerializeField]
float collisionCheckDist = 0.5f;
[SerializeField, Range(0f, 1f)]
float distBetweenRopePoints = 0.1f;
[SerializeField, Range(0.01f, 1f)]
float ropeRadius;
[SerializeField]
float ignoreResolveThreshold = 0.08f;
[SerializeField]
Transform ropeCollidersParent;
[SerializeField]
LayerMask staticColliderMask;
[SerializeField, Range(0f, 100f)]
float pullForce = 20f;
2024-02-02 21:03:44 +01:00
2024-02-03 02:47:40 +01:00
[SerializeField]
float xyGravityDampScalor = 1f;
2024-02-03 01:25:34 +01:00
[Header("Rendering")]
[SerializeField] LineRenderer lineRenderer;
2024-02-02 21:03:44 +01:00
int[] order;
public Vector2 testPos;
Rope rope;
2024-02-03 01:14:34 +01:00
Dictionary<Collider2D, float> colliderToSquezeForce = new();
2024-02-02 21:03:44 +01:00
private void Start()
{
2024-02-03 00:15:07 +01:00
//rope = new RopeBuilder()
// .AddPoint(new Point(testPos, locked: true))
// .AddPoint(new Point(testPos.Add(x:5f)))
// .AddPoint(new Point(testPos.Add(x: 10f)))
// .AddPoint(new Point(testPos.Add(x: 15f)))
// .AddPoint(new Point(testPos.Add(x: 20f)))
// .ConnectPoints(0, 1)
// .ConnectPoints(1, 2)
// .ConnectPoints(2, 3)
// .ConnectPoints(3, 4)
// .Build();
RopeBuilder builder = new RopeBuilder();
builder.AddPoint(new Point(start.position, locked: true));
for (int i = 1; i < subDivision; i++)
{
2024-02-03 02:47:40 +01:00
Vector3 pointPos = Vector3.Lerp(start.position, end.position, (float)i / (float)subDivision);
2024-02-03 00:15:07 +01:00
//Debug.Log($"pos: {pointPos}, t={i / subDivision}");
Debug.DrawRay(pointPos, (end.position - start.position).normalized);
builder.AddPoint(new Point(pointPos));
}
builder.AddPoint(new Point(end.position, locked: true));
for (int i = 0; i < subDivision; i++)
{
builder.ConnectPointsWithDesiredLength(i, i + 1, desiredLength: distBetweenRopePoints);
}
rope = builder.Build();
foreach (var point in rope.points)
{
GameObject ropeCollider = new GameObject("Rope Collider");
ropeCollider.transform.parent = ropeCollidersParent;
2024-02-03 02:47:40 +01:00
ropeCollider.transform.position = point.position;
2024-02-03 00:15:07 +01:00
ropeCollider.layer = LayerMask.NameToLayer("Rope");
var colliderComponent = ropeCollider.AddComponent<CircleCollider2D>();
colliderComponent.radius = ropeRadius;
}
2024-02-02 21:03:44 +01:00
CreateOrderArray();
}
private void Update()
{
2024-02-03 01:14:34 +01:00
colliderToSquezeForce.Clear();
2024-02-03 00:15:07 +01:00
rope.points.First().position = start.position;
rope.points.Last().position = end.position;
2024-02-02 21:03:44 +01:00
Simulate();
2024-02-03 00:15:07 +01:00
// Update the rope collider positions
for (int i = 0; i < rope.points.Count; i++)
{
ropeCollidersParent.GetChild(i).position = rope.points[i].position;
}
// Handle static colliders
foreach (var point in rope.points)
{
if (point.locked) continue;
HandleStaticCollidersOfPoint(point);
}
// Constrain start transform based on overshoot
float overshoot = rope.CalculateLengthOvershoot();
if (overshoot > 0)
{
//start.position = prevStartPos;
2024-02-03 02:47:40 +01:00
Vector2 pullDirection = (rope.points.ElementAt(1).position - start.position).normalized;
2024-02-03 00:15:07 +01:00
start.gameObject.GetComponent<Rigidbody2D>().AddForce(pullDirection * overshoot * pullForce);
}
2024-02-03 01:14:34 +01:00
// Handle squeze kills
foreach (var collider in colliderToSquezeForce)
{
ISquezeDamageReceiver squezeDamageReceiver = collider.Key.GetComponent<ISquezeDamageReceiver>();
if (squezeDamageReceiver == null) continue;
squezeDamageReceiver.TakeSquezeDamage(collider.Value);
}
2024-02-03 01:25:34 +01:00
// Update line renderer
var positions = rope.points.Select(p => new Vector3(p.position.x, p.position.y, 0f)).ToArray();
lineRenderer.positionCount = positions.Length;
lineRenderer.SetPositions(positions);
2024-02-03 02:47:40 +01:00
// Handle xy dampening on z gravity
foreach (var point in rope.points)
{
if (point.position.z >= 0f) continue;
Vector2 newXYPos = Vector2.MoveTowards(new Vector2(point.position.x, point.position.y), new Vector2(point.prevPosition.x, point.prevPosition.y), Mathf.Abs(point.position.z * xyGravityDampScalor));
point.position.Set(newXYPos.x, newXYPos.y, 0f);
}
2024-02-02 21:03:44 +01:00
}
private void OnDrawGizmos()
{
if (!Application.isPlaying) return;
foreach (var point in rope.points)
{
2024-02-03 00:15:07 +01:00
//Debug.Log($"pos: {point.position}");
Gizmos.DrawSphere(point.position, ropeRadius);
2024-02-02 21:03:44 +01:00
}
}
void Simulate()
{
foreach (Point p in rope.points)
{
if (!p.locked)
{
2024-02-03 02:47:40 +01:00
Vector3 positionBeforeUpdate = p.position;
2024-02-02 21:03:44 +01:00
p.position += p.position - p.prevPosition;
2024-02-03 02:47:40 +01:00
p.position.z -= gravity * Time.deltaTime * Time.deltaTime;
2024-02-02 21:03:44 +01:00
p.prevPosition = positionBeforeUpdate;
}
}
for (int i = 0; i < solveIterations; i++)
{
for (int s = 0; s < rope.sticks.Count; s++)
{
Stick stick = rope.sticks[order[s]];
if (stick.dead)
{
continue;
}
2024-02-03 02:47:40 +01:00
Vector3 stickCentre = (stick.A.position + stick.B.position) / 2;
Vector3 stickDir = (stick.A.position - stick.B.position).normalized;
2024-02-02 21:03:44 +01:00
float length = (stick.A.position - stick.B.position).magnitude;
2024-02-03 00:15:07 +01:00
if (length > stick.desiredLength || constrainStickMinLength)
2024-02-02 21:03:44 +01:00
{
if (!stick.A.locked)
{
2024-02-03 00:15:07 +01:00
TryMovePointToPosition(stick.A, stickCentre + stickDir * stick.desiredLength / 2);
2024-02-02 21:03:44 +01:00
}
if (!stick.B.locked)
{
2024-02-03 00:15:07 +01:00
TryMovePointToPosition(stick.B, stickCentre - stickDir * stick.desiredLength / 2);
2024-02-02 21:03:44 +01:00
}
}
}
}
}
2024-02-03 02:47:40 +01:00
private void TryMovePointToPosition(Point point, Vector3 position)
2024-02-03 00:15:07 +01:00
{
2024-02-03 02:47:40 +01:00
Vector2 moveDir = new Vector2(position.x, position.y) - new Vector2(point.position.x, point.position.y);
2024-02-03 00:15:07 +01:00
int stepsRequired = (int) Mathf.Ceil(moveDir.magnitude / collisionCheckDist);
moveDir.Normalize();
2024-02-03 02:47:40 +01:00
Vector2 initialPos = new Vector2(point.position.x, point.position.y);
2024-02-03 00:15:07 +01:00
for (int i = 0 ; i < stepsRequired; i++)
{
2024-02-03 02:47:40 +01:00
Vector2 newPos = Vector2.MoveTowards(new Vector2(point.position.x, point.position.y), new Vector2(position.x, position.y), collisionCheckDist);
point.position.Set(newPos.x, newPos.y, point.position.z);
2024-02-03 00:15:07 +01:00
Collider2D collider = Physics2D.OverlapCircle(point.position, ropeRadius, staticColliderMask);
if (collider == null) continue;
// A static collider was met, dont move any further
Vector2 resolvedPos = collider.ClosestPoint(initialPos);
if (Vector2.Distance(initialPos, resolvedPos) < ignoreResolveThreshold) continue;
2024-02-03 02:47:40 +01:00
Vector2 penetrationDir = (resolvedPos - new Vector2(point.position.x, point.position.y)).normalized;
2024-02-03 00:15:07 +01:00
Vector2 finalPos = resolvedPos - penetrationDir * ropeRadius;
//Debug.Log($"resolved pos: {point.position}->{finalPos}");
2024-02-03 02:47:40 +01:00
point.position.Set(finalPos.x, finalPos.y, point.position.z);
2024-02-03 00:15:07 +01:00
break;
}
2024-02-03 02:47:40 +01:00
// Move z position
point.position.z = position.z;
2024-02-03 00:15:07 +01:00
}
private void HandleStaticCollidersOfPoint(Point p)
{
Collider2D hitCollider = Physics2D.OverlapCircle(p.position, ropeRadius, staticColliderMask);
if (hitCollider == null) return;
2024-02-03 01:14:34 +01:00
// Register the squeze force this rope particle is squezing the collider
2024-02-03 02:47:40 +01:00
Vector2 pointPos = new Vector2(p.position.x, p.position.y);
Vector2 resolvedPos = hitCollider.ClosestPoint(pointPos);
Vector2 penetration = resolvedPos - pointPos;
2024-02-03 01:14:34 +01:00
Vector2 finalPos = resolvedPos - penetration.normalized * ropeRadius;
float squezeForce;
if (!colliderToSquezeForce.TryGetValue(hitCollider, out squezeForce))
colliderToSquezeForce.Add(hitCollider, squezeForce + penetration.magnitude);
else
colliderToSquezeForce[hitCollider] = squezeForce + penetration.magnitude;
2024-02-03 02:47:40 +01:00
p.position.Set(finalPos.x, finalPos.y, p.position.z);
2024-02-03 00:15:07 +01:00
}
2024-02-02 21:03:44 +01:00
void CreateOrderArray()
{
order = new int[rope.sticks.Count];
for (int i = 0; i < order.Length; i++)
{
order[i] = i;
}
ShuffleArray(order, new System.Random());
}
public static T[] ShuffleArray<T>(T[] array, System.Random prng)
{
int elementsRemainingToShuffle = array.Length;
int randomIndex = 0;
while (elementsRemainingToShuffle > 1)
{
// Choose a random element from array
randomIndex = prng.Next(0, elementsRemainingToShuffle);
T chosenElement = array[randomIndex];
// Swap the randomly chosen element with the last unshuffled element in the array
elementsRemainingToShuffle--;
array[randomIndex] = array[elementsRemainingToShuffle];
array[elementsRemainingToShuffle] = chosenElement;
}
return array;
}
}