snakes_mp/Assets/Scripts/SnakeController.cs
2025-05-30 21:24:50 +05:30

672 lines
19 KiB
C#

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<SnakePiece> snakePieces = new List<SnakePiece>();
public List<BendData> bends = new List<BendData>();
[SyncVar]
private List<BendData> serverBends = new List<BendData>();
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<Vector2> predictedPositions = new List<Vector2>();
private List<Vector2> predictedDirections = new List<Vector2>();
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<NetworkIdentity> oldPieces, List<NetworkIdentity> 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<SnakePiece>();
if (piece != null)
{
snakePieces.Add(piece);
}
}
}
}
public void StartGame()
{
if (!isServer) return;
snakePieces = new List<SnakePiece>();
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<SnakePiece>();
piece.userId = userId;
snakePieces.Add(piece);
RpcAddSnakePiece(piece.netIdentity);
}
isKickstarted = true;
isDead = false;
}
[ClientRpc]
void RpcAddSnakePiece(NetworkIdentity netId)
{
var piece = netId.GetComponent<SnakePiece>();
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<Vector2> moveInputQueue = new List<Vector2>();
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<SnakePiece>();
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<SnakePiece>() != 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<Fruit>() != 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;
}