fgm24/Assets/Scripts/Rope/RopeSimulator.cs

232 lines
8.3 KiB
C#

using UnityEngine;
using UnityEngine.Assertions;
using System.Linq;
using Unity.Netcode;
using System.Collections;
public class RopeSimulator : NetworkBehaviour
{
[Header("Solver")]
[SerializeField] int solveIterations = 10;
Transform ropeStart, ropeEnd;
[Header("Rope Settings")]
[SerializeField] int subDivision = 20;
[SerializeField] float distBetweenRopePoints = 0.2f;
[Header("Rope Renderer")]
[SerializeField] LineRenderer lineRenderer;
[SerializeField] Rope rope;
[Header("Networking")]
[SerializeField] float reconciliateThreshold = 1f;
[SerializeField] int reconciliateLerpFrameDuration = 5;
[SerializeField] CircularBuffer<RopeState> stateBuffer;
[SerializeField] float reconciliateCooldownDuration = 1f;
float reconciliateCooldown = -3f; // First reconciliate after n seconds after its begun to populate stateBuffer
private const int k_stateBufferSize = 128;
private int currentTick => NetworkManager.Singleton.NetworkTickSystem.LocalTime.Tick;
bool initted = false;
private void Awake()
{
this.rope = null;
stateBuffer = new CircularBuffer<RopeState>(k_stateBufferSize);
}
private void OnEnable()
{
GameManager.OnPlayersReady += Init;
}
private void OnDisable()
{
GameManager.OnPlayersReady += Init;
if (NetworkManager.Singleton != null)
NetworkManager.Singleton.NetworkTickSystem.Tick -= NetworkTick;
}
private void Update()
{
if (!initted) return;
reconciliateCooldown += Time.deltaTime;
rope.points.First().position = ropeStart.position;
rope.points.Last().position = ropeEnd.position;
Simulate(this.rope, Time.deltaTime, this.distBetweenRopePoints, this.solveIterations);
DisplayRope();
}
private void Init(GameObject[] players)
{
this.ropeStart = players[0].transform;
this.ropeEnd = players[1].transform;
this.rope = RopeSimulator.BuildRope(this.ropeStart, this.ropeEnd, this.subDivision, this.distBetweenRopePoints);
NetworkManager.Singleton.NetworkTickSystem.Tick += NetworkTick;
initted = true;
}
private void NetworkTick()
{
RopeState ropeState = new()
{
tick = currentTick,
nrope = Rope.ToNetworkRope(this.rope),
playerPositions = GameObject.FindObjectsByType<PlayerMovement>(FindObjectsSortMode.None).Select(p => new Vector3(p.transform.position.x, p.transform.position.y, p.GetComponent<NetworkObject>().NetworkObjectId)).ToArray()
};
stateBuffer.Add(ropeState, currentTick);
if (IsServer)
{
ServerToClientRopeStateRpc(ropeState);
}
}
[Rpc(SendTo.NotServer)]
private void ServerToClientRopeStateRpc(RopeState serverState)
{
RopeState clientState = this.stateBuffer.Get(serverState.tick);
Rope serverRope = Rope.FromNetworkRope(serverState.nrope, this.distBetweenRopePoints);
// Not enough information on client to reconcile
if (clientState.nrope.Equals(default(NetworkRope)))
return;
Rope previousClientRope = Rope.FromNetworkRope(clientState.nrope, this.distBetweenRopePoints);
float diff = Rope.CalcDiff(previousClientRope, serverRope);
// Debug.Log($"Diff: {diff}, ({serverState.tick})/({currentTick})");
if (diff > reconciliateThreshold && reconciliateCooldown >= reconciliateCooldownDuration)
{
reconciliateCooldown = 0f;
// StopCoroutine("ReconciliateLerp");
// StartCoroutine(ReconciliateLerp(serverRope));
Rope localCopy = serverRope.Copy(this.distBetweenRopePoints);
int ticksToResimulate = currentTick - serverState.tick;
Debug.Log($"Resimulating {ticksToResimulate} ticks");
for (int i = 1; i <= ticksToResimulate; i++)
{
RopeState intermediateState = stateBuffer.Get(serverState.tick + i);
Vector3 intermediateRopeStart = new Vector3(intermediateState.playerPositions[0].x, intermediateState.playerPositions[0].y, ropeStart.position.z);
Vector3 intermediateRopeEnd = new Vector3(intermediateState.playerPositions[1].x, intermediateState.playerPositions[1].y, ropeEnd.position.z);
rope.points.First().position = intermediateRopeStart;
rope.points.Last().position = intermediateRopeEnd;
Simulate(localCopy, 1f / (float) NetworkManager.Singleton.NetworkTickSystem.TickRate, this.distBetweenRopePoints, this.solveIterations);
}
localCopy.points.First().position = ropeStart.position;
localCopy.points.Last().position = ropeEnd.position;
StopCoroutine("LerpRope");
LerpRope(this.rope, localCopy);
}
}
IEnumerator LerpRope(Rope from, Rope to)
{
for (int i = 1; i <= reconciliateLerpFrameDuration; i++)
{
float t = (float) i / (float) reconciliateLerpFrameDuration;
for (int j = 0; j < this.rope.points.Length; j++)
{
from.points[j].position = Vector3.Lerp(this.rope.points[j].position, to.points[j].position, t);
from.points[j].prevPosition = Vector3.Lerp(this.rope.points[j].prevPosition, to.points[j].prevPosition, t);
}
yield return new WaitForEndOfFrame();
}
}
private void DisplayRope()
{
if (!lineRenderer) return;
lineRenderer.positionCount = rope.points.Length;
lineRenderer.SetPositions(rope.points.Select(point => point.position).ToArray());
}
public static Rope BuildRope(Transform ropeStart, Transform ropeEnd, int subDivision, float distBetweenRopePoints)
{
Assert.IsNotNull(ropeStart);
Assert.IsNotNull(ropeEnd);
RopeBuilder builder = new();
builder.AddPoint(new Point(ropeStart.position, locked: true));
for (int i = 0; i < subDivision; i++)
{
Vector3 pointPos = Vector3.Lerp(ropeStart.position, ropeEnd.position, (float) i / (float) subDivision);
builder.AddPoint(new Point(pointPos, locked: false));
}
builder.AddPoint(new Point(ropeEnd.position, locked: true));
for (int i = 0; i <= subDivision; i++)
{
builder.ConnectPointsWithDesiredLength(i, i + 1, desiredLength: distBetweenRopePoints);
}
return builder.Build();
}
public static void Simulate(Rope rope, float dt, float desiredLength, int solveIterations)
{
Assert.IsNotNull(rope);
Assert.IsTrue(dt > 0f);
foreach (var point in rope.points)
{
if (point.locked) continue;
// Verlet
Vector3 prevPos = point.position;
point.position += point.position - point.prevPosition;
point.prevPosition = prevPos;
}
// Stick resolution
for (int solveIter = 0; solveIter < solveIterations; solveIter++)
{
for (int stickIdx = 0; stickIdx < rope.sticks.Length; stickIdx++)
{
var stick = rope.sticks[stickIdx];
Vector3 stickDir = (stick.B.position - stick.A.position);
Vector3 stickCentre = stick.A.position + stickDir / 2f;
float length = stickDir.magnitude;
if (length > stick.desiredLength)
{
float distribution = stick.A.locked || stick.B.locked ? 1f : 2f;
float distanceToMove = (length - stick.desiredLength) / distribution;
if (!stick.A.locked)
{
Vector3 prevPos = stick.A.position;
stick.A.position += stickDir.normalized * distanceToMove;
stick.A.prevPosition = prevPos;
}
if (!stick.B.locked)
{
Vector3 prevPos = stick.B.position;
stick.B.position -= stickDir.normalized * distanceToMove;
stick.B.prevPosition = prevPos;
}
}
}
}
}
private void OnDrawGizmos()
{
Gizmos.color = Color.green;
foreach (var p in rope.points)
{
Gizmos.DrawSphere(p.position, 0.5f);
}
}
}