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 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(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(FindObjectsSortMode.None).Select(p => new Vector3(p.transform.position.x, p.transform.position.y, p.GetComponent().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); } } }