using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; using Mirror; public class SnakeController : NetworkBehaviour { [SyncVar] public string userId; [SyncVar(hook = nameof(OnScoreChange))] public int Score = 0; public Vector2 startOffset = new Vector3(-10, -10); public float movingInterval = 0.2f; [SyncVar] public Vector2 curDirection = Vector3.right; public List snakePieces = new List(); public List bends = new List(); [SyncVar] private List serverBends = new List(); public GameObject snakePiecePrefab; public int startingLength = 3; public Vector2 fieldSize = new Vector2(160, 90); float moveTimer; [SyncVar] private float serverMoveTimer; public float topEdge => transform.position.y + fieldSize.y / 2f; public float botEdge => transform.position.y - fieldSize.y / 2f; public float leftEdge => transform.position.x - fieldSize.x / 2f; public float rightEdge => transform.position.x + fieldSize.x / 2f; // Add new fields for client prediction private List predictedPositions = new List(); private List predictedDirections = new List(); private float lastServerUpdateTime; private const float MAX_PREDICTION_TIME = 0.5f; // Maximum time to keep predictions private const float PREDICTION_TOLERANCE = 0.1f; // Tolerance for position matching private const float SYNC_THRESHOLD = 1f; // Distance threshold for considering out of sync void Start() { ButtonSet.OnUp += OnUp; ButtonSet.OnDown += OnDown; ButtonSet.OnLeft += OnLeft; ButtonSet.OnRight += OnRight; #if UNITY_EDITOR GameData.user_id = "user" + Random.Range(0, 100); #endif if (!isServer && isLocalPlayer) { CmdSetUserId(GameData.user_id); } if (isLocalPlayer) { lastServerUpdateTime = Time.time; playerType = GameData.isMaster ? PlayerType.Master : PlayerType.Client; } } [Command] void CmdSetUserId(string userId) { if (GameData.betData == null) { startOffset.y += Random.Range(0, 10); this.userId = userId; } else { if (userId == GameData.betData.owner_id) { startOffset.y += 10; } this.userId = userId; playerType = userId == GameData.betData.owner_id ? PlayerType.Master : PlayerType.Client; } } private void OnDestroy() { ButtonSet.OnUp -= OnUp; ButtonSet.OnDown -= OnDown; ButtonSet.OnLeft -= OnLeft; ButtonSet.OnRight -= OnRight; } [SyncVar] public bool isKickstarted = false; void OnSnakePiecesChanged(List oldPieces, List newPieces) { if (!isClient) return; Debug.Log("Server added a new piece with id: " + newPieces[newPieces.Count - 1].netId); // Clear existing pieces foreach (var piece in snakePieces) { if (piece != null) { Destroy(piece.gameObject); } } snakePieces.Clear(); // Add new pieces foreach (var netId in newPieces) { if (netId != null) { var piece = netId.GetComponent(); if (piece != null) { snakePieces.Add(piece); } } } } public void StartGame() { if (!isServer) return; snakePieces = new List(); for (int i = 0; i < startingLength; i++) { Vector3 pos = ((Vector2)transform.position - startOffset) - (Vector2.right * i); GameObject newPiece = Instantiate(snakePiecePrefab, pos, Quaternion.identity); NetworkServer.Spawn(newPiece); SnakePiece piece = newPiece.GetComponent(); piece.userId = userId; snakePieces.Add(piece); RpcAddSnakePiece(piece.netIdentity); } isKickstarted = true; isDead = false; } [ClientRpc] void RpcAddSnakePiece(NetworkIdentity netId) { var piece = netId.GetComponent(); if (piece != null) { snakePieces.Add(piece); } } public bool controlling = false; void Update() { if (isLocalPlayer) { ProcessInput(); } if (NetGameManager.instance.gameOver) { return; } if (!isKickstarted) { return; } if (isServer) { CheckHead(); if (isDead) { return; } Move(); } else if (isLocalPlayer) { // Client-side prediction for local player if (moveTimer > 0) { moveTimer -= Time.deltaTime; } else { moveTimer = movingInterval; PredictMove(); } } } void ProcessInput() { if (Input.GetKeyDown(KeyCode.RightArrow)) { OnRight(); } else if (Input.GetKeyDown(KeyCode.LeftArrow)) { OnLeft(); } else if (Input.GetKeyDown(KeyCode.UpArrow)) { OnUp(); } else if (Input.GetKeyDown(KeyCode.DownArrow)) { OnDown(); } } void OnRight() { ChangeDir(Vector2.right); } void OnLeft() { ChangeDir(-Vector2.right); } void OnUp() { ChangeDir(Vector2.up); } void OnDown() { ChangeDir(-Vector2.up); } public void AddScore(int amount = 1) { Score += amount; if (isServer) { RpcOnScore(); } else { OnScore(); } } [ClientRpc] void RpcOnScore() { OnScore(); } void OnScoreChange(int oldScore, int newScore) { NetGameManager.instance.UpdateScore(isLocalPlayer, newScore); } void OnScore() { SFXManager.instance.PlayCrunch(); } List moveInputQueue = new List(); void ChangeDir(Vector2 newDir) { if (isServer) { changeDir(newDir); } else { CmdChangeDir(newDir); } SFXManager.instance.PlayTurn(); } void changeDir(Vector2 newDir) { if (curDirection == -newDir) { return; } moveInputQueue.Add(newDir); } [Command] void CmdChangeDir(Vector2 newDir) { changeDir(newDir); } bool queueNewPiece = false; void PredictMove() { if (moveInputQueue.Count > 0) { Vector2 dir = Vector2.zero; if (curDirection.x != 0) { dir = new Vector2(-curDirection.x, moveInputQueue[0].y); } else { dir = new Vector2(moveInputQueue[0].x, -curDirection.y); } curDirection = moveInputQueue[0]; moveInputQueue.RemoveAt(0); // Add bend to both local and server lists BendData newBend = new BendData() { position = snakePieces[0].transform.position, direction = dir }; bends.Add(newBend); CmdAddBend(newBend); } // Store current state for prediction Vector2 curPosition = snakePieces[0].transform.position; Vector2 lastPosition = snakePieces[snakePieces.Count - 1].transform.position; // Update snake pieces for (int i = snakePieces.Count - 1; i > 0; i--) { Vector3 dir = (snakePieces[i - 1].transform.position - snakePieces[i].transform.position).normalized; snakePieces[i].direction = dir; snakePieces[i].isHead = false; snakePieces[i].transform.position = snakePieces[i - 1].transform.position; BendData bendToRemove = null; foreach (BendData bend in bends) { if (bend.position == snakePieces[i].transform.position) { snakePieces[i].direction = bend.direction; if (i == snakePieces.Count - 1) { bendToRemove = bend; } break; } } if (bendToRemove != null) { bends.Remove(bendToRemove); CmdRemoveBend(bendToRemove); } } // Move head snakePieces[0].transform.position = (Vector3)curPosition + (Vector3)curDirection; snakePieces[0].direction = curDirection; snakePieces[0].isHead = true; // Handle wrapping if (snakePieces[0].transform.position.x > rightEdge) { snakePieces[0].transform.position = new Vector2(leftEdge, snakePieces[0].transform.position.y); } else if (snakePieces[0].transform.position.x < leftEdge) { snakePieces[0].transform.position = new Vector2(rightEdge, snakePieces[0].transform.position.y); } if (snakePieces[0].transform.position.y > topEdge) { snakePieces[0].transform.position = new Vector2(snakePieces[0].transform.position.x, botEdge); } else if (snakePieces[0].transform.position.y < botEdge) { snakePieces[0].transform.position = new Vector2(snakePieces[0].transform.position.x, topEdge); } // Store prediction predictedPositions.Add(snakePieces[0].transform.position); predictedDirections.Add(curDirection); // Request server sync periodically if (Time.time - lastServerUpdateTime > 0.1f) // Request sync every 100ms { CmdRequestSync(); } } void Move() { if (moveTimer > 0) { moveTimer -= Time.deltaTime; return; } moveTimer = movingInterval; if (moveInputQueue.Count > 0) { Vector2 dir = Vector2.zero; if (curDirection.x != 0) { dir = new Vector2(-curDirection.x, moveInputQueue[0].y); } else { dir = new Vector2(moveInputQueue[0].x, -curDirection.y); } curDirection = moveInputQueue[0]; moveInputQueue.RemoveAt(0); // Add bend to both local and server lists BendData newBend = new BendData() { position = snakePieces[0].transform.position, direction = dir }; bends.Add(newBend); CmdAddBend(newBend); } Vector2 curPosition = snakePieces[0].transform.position; Vector2 lastPosition = snakePieces[snakePieces.Count - 1].transform.position; // Update snake pieces for (int i = snakePieces.Count - 1; i > 0; i--) { Vector3 dir = (snakePieces[i - 1].transform.position - snakePieces[i].transform.position).normalized; snakePieces[i].direction = dir; snakePieces[i].isHead = false; snakePieces[i].transform.position = snakePieces[i - 1].transform.position; BendData bendToRemove = null; foreach (BendData bend in bends) { if (bend.position == snakePieces[i].transform.position) { snakePieces[i].direction = bend.direction; if (i == snakePieces.Count - 1) { bendToRemove = bend; } break; } } if (bendToRemove != null) { bends.Remove(bendToRemove); CmdRemoveBend(bendToRemove); } } if (queueNewPiece) { GameObject newPiece = Instantiate(snakePiecePrefab, lastPosition, Quaternion.identity); NetworkServer.Spawn(newPiece); SnakePiece piece = newPiece.GetComponent(); piece.userId = userId; snakePieces.Add(piece); RpcAddSnakePiece(piece.netIdentity); SFXManager.instance.PlayCrunch(); AddScore(); queueNewPiece = false; } // Move head snakePieces[0].transform.position = (Vector3)curPosition + (Vector3)curDirection; snakePieces[0].direction = curDirection; snakePieces[0].isHead = true; // Handle wrapping if (snakePieces[0].transform.position.x > rightEdge) { snakePieces[0].transform.position = new Vector2(leftEdge, snakePieces[0].transform.position.y); } else if (snakePieces[0].transform.position.x < leftEdge) { snakePieces[0].transform.position = new Vector2(rightEdge, snakePieces[0].transform.position.y); } if (snakePieces[0].transform.position.y > topEdge) { snakePieces[0].transform.position = new Vector2(snakePieces[0].transform.position.x, botEdge); } else if (snakePieces[0].transform.position.y < botEdge) { snakePieces[0].transform.position = new Vector2(snakePieces[0].transform.position.x, topEdge); } // After server movement, send state to clients if (isServer) { Vector2[] positions = new Vector2[snakePieces.Count]; Vector2[] directions = new Vector2[snakePieces.Count]; for (int i = 0; i < snakePieces.Count; i++) { positions[i] = snakePieces[i].transform.position; directions[i] = snakePieces[i].direction; } RpcReconcileState(positions, directions, serverMoveTimer); } } [Command] void CmdRequestSync() { Vector2[] positions = new Vector2[snakePieces.Count]; Vector2[] directions = new Vector2[snakePieces.Count]; for (int i = 0; i < snakePieces.Count; i++) { positions[i] = snakePieces[i].transform.position; directions[i] = snakePieces[i].direction; } RpcReconcileState(positions, directions, serverMoveTimer); } [ClientRpc] void RpcReconcileState(Vector2[] serverPositions, Vector2[] serverDirections, float serverTimer) { if (!isLocalPlayer && !isServer) { // For non-local players, directly update positions from server for (int i = 0; i < snakePieces.Count; i++) { if (i < serverPositions.Length) { snakePieces[i].transform.position = serverPositions[i]; snakePieces[i].direction = serverDirections[i]; snakePieces[i].isHead = (i == 0); } } return; } if (!isLocalPlayer) return; // Sync move timer with server moveTimer = serverTimer; // Find the oldest prediction that matches the server state int matchIndex = -1; for (int i = 0; i < predictedPositions.Count; i++) { if (Vector2.Distance(predictedPositions[i], serverPositions[0]) < PREDICTION_TOLERANCE) { matchIndex = i; break; } } if (matchIndex >= 0) { // Remove all predictions up to the matching one predictedPositions.RemoveRange(0, matchIndex + 1); predictedDirections.RemoveRange(0, matchIndex + 1); } else { // Check if we're significantly out of sync bool needsSync = false; float maxDist = 0; for (int i = 0; i < snakePieces.Count; i++) { if (i < serverPositions.Length) { float distance = Vector2.Distance(snakePieces[i].transform.position, serverPositions[i]); if (distance > SYNC_THRESHOLD) { maxDist = Mathf.Max(maxDist, distance); needsSync = true; } } } if (needsSync) { Debug.Log("Syncing local player, max dist: " + maxDist); // Only update positions if significantly out of sync for (int i = 0; i < snakePieces.Count; i++) { if (i < serverPositions.Length) { snakePieces[i].transform.position = serverPositions[i]; snakePieces[i].direction = serverDirections[i]; snakePieces[i].isHead = (i == 0); } } // Clear predictions after sync predictedPositions.Clear(); predictedDirections.Clear(); } } lastServerUpdateTime = Time.time; } bool isDead = false; public PlayerType playerType; void CheckHead() { Collider2D[] obstacles = Physics2D.OverlapCircleAll(snakePieces[0].transform.position, 0.25f); foreach (Collider2D obstacle in obstacles) { if (obstacle.GetComponent() != null) { if (obstacle.transform == snakePieces[0].transform) { } else { Debug.Log($"Crashed into {obstacle.name}", gameObject); isDead = true; NetGameManager.instance.GameOver(playerType == PlayerType.Master ? PlayerType.Client : PlayerType.Master); SFXManager.instance.PlayDeath(); } } if (obstacle.GetComponent() != null) { queueNewPiece = true; NetGameManager.instance.DestroyFruit(obstacle.transform.position); } } } private void OnDrawGizmos() { Gizmos.DrawWireCube(transform.position, new Vector3(fieldSize.x, fieldSize.y)); if (snakePieces.Count > 0) { Gizmos.DrawWireSphere(snakePieces[0].transform.position, 0.25f); } } [Command] void CmdAddBend(BendData bend) { serverBends.Add(bend); RpcAddBend(bend); } [Command] void CmdRemoveBend(BendData bend) { serverBends.Remove(bend); RpcRemoveBend(bend); } [ClientRpc] void RpcAddBend(BendData bend) { if (!isLocalPlayer) { bends.Add(bend); } } [ClientRpc] void RpcRemoveBend(BendData bend) { if (!isLocalPlayer) { bends.Remove(bend); } } } [System.Serializable] public class BendData { public Vector3 position; public Vector2 direction; }