using System; using System.Collections.Generic; using System.Linq; using Unity.Netcode; using UnityEngine; using UnityEngine.Assertions; using UnityUtils; public class RopeSimulatorOld : NetworkBehaviour { [SerializeField] private float gravity = 10; [SerializeField] private int solveIterations = 10; [SerializeField] private bool constrainStickMinLength; [SerializeField] public RopeJoint start, end; [SerializeField] float subDivision = 50f; [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] LayerMask staticColliderMask; [SerializeField, Range(0f, 100f)] float pullForce = 20f; [SerializeField] float xyGravityDampScalor = 1f; [SerializeField, Range(0f, 20f)] public float ropeExtendSpeed, ropeShrinkSpeed; public float squezeDamage = 1f; public AnimationCurve swingSpeedToDamageMultiplier; [SerializeField] public float ropeMaxLength, ropeMinLength; [Header("Rope Colliders")] [SerializeField] GameObject colliderPrefab; [SerializeField] string colliderTag = "Rope"; [SerializeField] Transform ropeCollidersParent; [Header("Rendering")] [SerializeField] LineRenderer lineRenderer; [Header("Animaion")] [SerializeField] float pullAnimationOvershootThreshold = 0.2f; [Header("Netcode")] private const float k_serverTickRate = 60f; private const int k_rngSeed = 6969; public float k_ropeReconciliateThreshold = 1f; private System.Random rng = new System.Random(k_rngSeed); private int currentTick => NetworkManager.Singleton.NetworkTickSystem.LocalTime.Tick; [SerializeField] private CircularBuffer stateBuffer; private GameState lastReceivedServerGameState; private const int k_bufferSize = 512; private bool isReconciliating = false; private GameState currentRewindingState; [SerializeField] private float reconciliateDuration = 0.1f; private float rewindCounter = 0f; private int[] order; public float Overshoot => rope.CalculateLengthOvershoot(); public bool InSwingMode => start.locked || end.locked; public Rope rope; Dictionary colliderToSquezeForce = new(); public static RopeSimulatorOld instance; private bool IsInitialized => start != null || end != null; private void Awake() { if (instance == null) { instance = this; } else { Destroy(instance); } stateBuffer = new(k_bufferSize); } private void OnEnable() { GameManager.OnPlayersReady += PlayersReady; if (NetworkManager.Singleton != null) NetworkManager.Singleton.NetworkTickSystem.Tick += NetworkTick; } private void OnDisable() { GameManager.OnPlayersReady -= PlayersReady; if (NetworkManager.Singleton != null) NetworkManager.Singleton.NetworkTickSystem.Tick -= NetworkTick; } public void PlayersReady(GameObject[] players) { BuildRope(players[0].GetComponent(), players[1].GetComponent()); } public void BuildRope(RopeJoint start, RopeJoint end) { Assert.IsNotNull(start); Assert.IsNotNull(end); // Sanity check if rope simulator was initialized before - we are re-building the rope if (this.start != null) { this.start.playerInput.ropeLengthExtend -= ExtendRope; this.start.playerInput.ropeLengthShrinken -= ShrinkenRope; } if (this.end != null) { this.end.playerInput.ropeLengthExtend -= ExtendRope; this.end.playerInput.ropeLengthShrinken -= ShrinkenRope; } this.start = start; this.end = end; Rebuild(); this.start.playerInput.ropeLengthShrinken += ShrinkenRope; this.end.playerInput.ropeLengthShrinken += ShrinkenRope; this.start.playerInput.ropeLengthExtend += ExtendRope; this.end.playerInput.ropeLengthExtend += ExtendRope; } void ShrinkenRope(int playerNumber) { int prevSubDivision = (int)subDivision; subDivision -= ropeShrinkSpeed * Time.deltaTime; subDivision = Mathf.Clamp(subDivision, ropeMinLength, ropeMaxLength); // Only shrinken if the numeric value has changed if (prevSubDivision - (int)subDivision <= 0) return; // Shrink from rope point after start rope joint List newPoints = new(rope.points.Length - 1); for (int i = 0; i < (rope.points.Length - 1); i++) { newPoints.Add(rope.points[i]); } var builder = new RopeBuilder(newPoints, new List()); // Re-gen sticks for (int i = 0; i < (int)subDivision; i++) { builder.ConnectPointsWithDesiredLength(i, i + 1, distBetweenRopePoints); } rope = builder.Build(); RebuildRopeColliders(); CreateOrderArray(); } void ExtendRope(int playerNumber) { int prevSubDivision = (int)subDivision; subDivision += ropeExtendSpeed * Time.deltaTime; subDivision = Mathf.Clamp(subDivision, ropeMinLength, ropeMaxLength); // Only extend if the numeric value has changed if (prevSubDivision - (int)subDivision >= 0) return; // Extend from rope point after start rope point List newPoints = new(rope.points.Length + 1); newPoints.Add(new Point(rope.points[1].position)); for (int i = 1; i < rope.points.Length; i++) { newPoints.Add(rope.points[i]); } var builder = new RopeBuilder(newPoints, new List()); // Re-gen sticks for (int i = 0; i < (int)subDivision; i++) { //Debug.Log($"Reg-gen stick. from: {i} to {i + 1}, with dist: {distBetweenRopePoints}"); builder.ConnectPointsWithDesiredLength(i, i + 1, distBetweenRopePoints); } rope = builder.Build(); RebuildRopeColliders(); CreateOrderArray(); } public override void OnDestroy() { base.OnDestroy(); // May never have been initialized if (!IsInitialized) return; start.playerInput.ropeLengthShrinken -= ShrinkenRope; end.playerInput.ropeLengthShrinken -= ShrinkenRope; start.playerInput.ropeLengthExtend -= ExtendRope; end.playerInput.ropeLengthExtend -= ExtendRope; } private void Rebuild() { Debug.Log("rebuild"); RopeBuilder builder = new RopeBuilder(); builder.AddPoint(new Point(start.position, locked: true)); // Build rope points for (int i = 1; i < (int)subDivision; i++) { Vector3 pointPos = Vector3.Lerp(start.position, end.position, (float)i / Mathf.Floor(subDivision)); Debug.DrawRay(pointPos, (end.position - start.position).normalized); builder.AddPoint(new Point(pointPos)); } builder.AddPoint(new Point(end.position, locked: true)); // Connect rope points for (int i = 0; i < (int)subDivision; i++) { builder.ConnectPointsWithDesiredLength(i, i + 1, desiredLength: distBetweenRopePoints); } rope = builder.Build(); RebuildRopeColliders(); CreateOrderArray(); } private void RebuildRopeColliders() { for (int i = 0; i < ropeCollidersParent.childCount; i++) { Destroy(ropeCollidersParent.GetChild(i)); } foreach (var point in rope.points) { GameObject ropeCollider = Instantiate(colliderPrefab); ropeCollider.transform.parent = ropeCollidersParent; ropeCollider.transform.position = point.position; ropeCollider.tag = colliderTag; ropeCollider.layer = LayerMask.NameToLayer("Rope"); } } private void DrawRope() { // Update line renderer List positions = new List(this.rope.points.Length); for (int i = 0; i < this.rope.points.Length; i++) { positions.Add(this.rope.points[i].position); } lineRenderer.positionCount = positions.Count; lineRenderer.SetPositions(positions.ToArray()); } [Rpc(SendTo.NotServer)] private void ServerToClientGameStateRpc(GameState serverState) { this.lastReceivedServerGameState = serverState; Debug.Log($"Received server state. Server tick: {serverState.tick}, client: {currentTick}"); // Not enough information if (stateBuffer.Get(serverState.tick).Equals(default(GameState))) return; // TODO: investigate why this is zero at start of game sometimes if (stateBuffer.Get(serverState.tick).nrope.positions.Length == 0) return; Debug.Log($"client len: {stateBuffer.Get(serverState.tick).nrope.positions.Length}, server len {serverState.nrope.positions.Length}"); Rope serverRope = Rope.FromNetworkRope(serverState.nrope, distBetweenRopePoints); Rope oldLocalRope = Rope.FromNetworkRope(stateBuffer.Get(serverState.tick).nrope, distBetweenRopePoints); float serverLocalRopeError = Rope.CalcDiff(serverRope, oldLocalRope); if (serverLocalRopeError >= k_ropeReconciliateThreshold) { SimpleReconcile(serverState); } Debug.Log($"Server to client sync error: {serverLocalRopeError}"); } private void SimpleReconcile(GameState rewindState) { Debug.LogWarning("Reconciliate"); this.rewindCounter = 0f; this.isReconciliating = true; this.currentRewindingState = rewindState; } private void Reconciliate() { Physics2D.simulationMode = SimulationMode2D.Script; Debug.LogWarning("Reconciliate"); Debug.Break(); // Reset client to server state GameState serverState = this.lastReceivedServerGameState; this.rope = Rope.FromNetworkRope(serverState.nrope, distBetweenRopePoints); Dictionary enemyByIds = new(); Dictionary playerByIds = new(); IEnumerable enemies = GameObject.FindObjectsByType(FindObjectsSortMode.None); IEnumerable players = GameObject.FindObjectsByType(FindObjectsSortMode.None); foreach (var enemy in enemies) enemyByIds.Add(enemy.GetComponent().NetworkObjectId, enemy.gameObject); foreach (var player in players) playerByIds.Add(player.GetComponent().NetworkObjectId, player.gameObject); // Re-simulate up to this client's tick Debug.Log($"Needs to re-sim {currentTick - serverState.tick} steps"); for (int tick = serverState.tick; tick <= currentTick; tick++) { GameState intermediateState = tick == serverState.tick ? serverState : stateBuffer.Get(tick); foreach (var enemyPos in intermediateState.enemyPositions) { // Find corresponding client enemy with id (z-component) ulong enemyID = (ulong)enemyPos.z; GameObject enemy = enemyByIds.GetValueOrDefault(enemyID); Assert.IsNotNull(enemy, $"Server enemy with id: {enemyID} could not be found on client!"); enemy.transform.position = new Vector2(enemyPos.x, enemyPos.y); } foreach (var playerPos in intermediateState.playerPositions) { // Find corresponding client player with id (z-component) ulong playerID = (ulong)playerPos.z; GameObject player = playerByIds.GetValueOrDefault(playerID); Assert.IsNotNull(player, $"Server player with id: {playerID} could not be found on client!"); player.transform.position = new Vector2(playerPos.x, playerPos.y); } this.Simulate(Time.fixedDeltaTime); Physics2D.Simulate(Time.fixedDeltaTime); } Physics2D.simulationMode = SimulationMode2D.FixedUpdate; } private void NetworkTick() { stateBuffer.Add(ProcessGame(), currentTick); // Send to clients if is server if (IsServer) ServerToClientGameStateRpc(stateBuffer.Get(currentTick)); } private GameState ProcessGame() { GameState localState = new() { tick = currentTick, nrope = Rope.ToNetworkRope(this.rope), enemyPositions = GameObject.FindObjectsByType(FindObjectsSortMode.None).Select(e => new Vector3(e.transform.position.x, e.transform.position.y, e.GetComponent().NetworkObjectId)).ToArray(), playerPositions = GameObject.FindObjectsByType(FindObjectsSortMode.None).Select(p => new Vector3(p.transform.position.x, p.transform.position.y, p.GetComponent().NetworkObjectId)).ToArray() }; return localState; } private void Update() { if (!IsInitialized) return; if (isReconciliating) { this.rope.points.First().position = start.position; this.rope.points.First().prevPosition = start.position; this.rope.points.Last().position = end.position; this.rope.points.Last().prevPosition = end.position; Rope rewindRope = Rope.FromNetworkRope(currentRewindingState.nrope, distBetweenRopePoints); rewindCounter += Time.deltaTime; float t = rewindCounter / reconciliateDuration; for (int point = 1; point < rope.points.Length - 1; point++) { Vector3 newPos = Vector3.Slerp(this.rope.points[point].position, rewindRope.points[point].position, t); this.rope.points[point].position = newPos; this.rope.points[point].prevPosition = newPos; } Simulate(Time.deltaTime); foreach (var point in rope.points) { // point.prevPosition = point.position; } DrawRope(); if (t >= 1f) { rewindCounter = 0f; isReconciliating = false; currentRewindingState = default(GameState); } return; } colliderToSquezeForce.Clear(); rope.points.First().position = start.position; rope.points.Last().position = end.position; float ropeDiff = Simulate(Time.fixedDeltaTime); // Update the rope collider positions for (int i = 0; i < rope.points.Length; 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(); PlayerPullAnimation(overshoot); PullPlayers(overshoot); // Handle squeze kills foreach (var collider in colliderToSquezeForce) { ISquezeDamageReceiver squezeDamageReceiver = collider.Key.transform.root.GetComponent(); if (squezeDamageReceiver == null) squezeDamageReceiver = collider.Key.GetComponent(); if (squezeDamageReceiver == null) continue; float swingMultiplier = InSwingMode ? swingSpeedToDamageMultiplier.Evaluate((start.locked ? end : start).body.velocity.magnitude) : 1f; squezeDamageReceiver.TakeSquezeDamage(collider.Value * squezeDamage * swingMultiplier); } // 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); } DrawRope(); } private void PlayerPullAnimation(float overshoot) { //if (overshoot > pullAnimationOvershootThreshold) //{ // float startDot = Vector2.Dot((start.position - rope.points[1].position).normalized, start.playerInput.movement); // if (startDot > 0.35f) // { // start.playerAnimationHandler?.animator.SetBool("IsPulling", true); // } // float endDot = Vector2.Dot((end.position - rope.points[rope.points.Count - 2].position).normalized, end.playerInput.movement); // if (endDot > 0.35f) // { // end.playerAnimationHandler?.animator.SetBool("IsPulling", true); // } //} //else //{ // start.playerAnimationHandler?.animator.SetBool("IsPulling", false); // end.playerAnimationHandler?.animator.SetBool("IsPulling", false); //} } private void PullPlayers(float overshoot) { if (overshoot <= 0f) return; //start.position = prevStartPos; float divider = !start.locked && !end.locked ? 2f : 1f; if (!start.locked) { Vector2 pullDirection = (rope.points[1].position - start.position).normalized; Vector2 force = pullDirection * overshoot * (pullForce / divider); start.body.AddForce(force); } else { start.body.velocity *= 0; } if (!end.locked) { Vector2 pullDirection = (rope.points[rope.points.Length - 2].position - end.position).normalized; Vector2 force = pullDirection * overshoot * (pullForce / divider); end.body.AddForce(force); } else { end.body.velocity *= 0; } } private void OnDrawGizmos() { if (!IsInitialized) return; if (!Application.isPlaying) return; // Local rope Gizmos.color = Color.green; foreach (var point in rope.points) { Gizmos.DrawSphere(point.position, ropeRadius); } // Last received server rope if (lastReceivedServerGameState.Equals(default(GameState))) return; Gizmos.color = Color.red; foreach (var point in Rope.FromNetworkRope(lastReceivedServerGameState.nrope, distBetweenRopePoints).points) { Gizmos.DrawSphere(point.position, ropeRadius); } } float Simulate(float dt) { float diff = 0f; foreach (Point p in rope.points) { if (!p.locked) { Vector3 positionBeforeUpdate = p.position; p.position += p.position - p.prevPosition; p.position.z -= gravity * dt * dt; diff += Mathf.Abs(Vector3.Distance(p.prevPosition, p.position)); p.prevPosition = positionBeforeUpdate; } } for (int i = 0; i < solveIterations; i++) { for (int s = 0; s < rope.sticks.Length; s++) { Stick stick = rope.sticks[order[s]]; if (stick.dead) continue; Vector3 stickCentre = (stick.A.position + stick.B.position) / 2; Vector3 stickDir = (stick.A.position - stick.B.position).normalized; float length = Vector2.Distance(stick.A.position, stick.B.position); if (length > stick.desiredLength || constrainStickMinLength) { if (!stick.A.locked) { TryMovePointToPosition(stick.A, stickCentre + stickDir * stick.desiredLength / 2); } if (!stick.B.locked) { TryMovePointToPosition(stick.B, stickCentre - stickDir * stick.desiredLength / 2); } } } } return diff; } private void TryMovePointToPosition(Point point, Vector3 position) { Vector2 moveDir = new Vector2(position.x, position.y) - new Vector2(point.position.x, point.position.y); int stepsRequired = (int)Mathf.Ceil(moveDir.magnitude / collisionCheckDist); moveDir.Normalize(); Vector2 initialPos = new Vector2(point.position.x, point.position.y); bool shouldBreak = false; for (int i = 0; i < stepsRequired; i++) { 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); foreach (var collider in Physics2D.OverlapCircleAll(point.position, ropeRadius, staticColliderMask)) { if (collider == null) continue; if (collider.isTrigger) continue; // A static collider was met, dont move any further Vector2 resolvedPos = collider.ClosestPoint(initialPos); if (Vector2.Distance(initialPos, resolvedPos) < ignoreResolveThreshold) continue; Vector2 penetrationDir = (resolvedPos - new Vector2(point.position.x, point.position.y)).normalized; Vector2 finalPos = resolvedPos - penetrationDir * ropeRadius; //Debug.Log($"resolved pos: {point.position}->{finalPos}"); point.position.Set(finalPos.x, finalPos.y, point.position.z); shouldBreak = true; break; } if (shouldBreak) break; } // Move z position point.position.z = position.z; } private void HandleStaticCollidersOfPoint(Point p) { foreach (var hitCollider in Physics2D.OverlapCircleAll(p.position, ropeRadius * 1.1f, staticColliderMask)) { if (hitCollider == null) continue; if (hitCollider.isTrigger) continue; // Register the squeze force this rope particle is squezing the collider Vector2 pointPos = new Vector2(p.position.x, p.position.y); Vector2 resolvedPos = hitCollider.ClosestPoint(pointPos); Vector2 penetration = resolvedPos - pointPos; 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; p.position.Set(finalPos.x, finalPos.y, p.position.z); } } void CreateOrderArray() { order = new int[rope.sticks.Length]; for (int i = 0; i < order.Length; i++) { order[i] = i; } ShuffleArray(order, rng); } public static T[] ShuffleArray(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; } }