497 lines
14 KiB
C#
497 lines
14 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;
|
|
public Vector2 curDirection = Vector3.right;
|
|
public List<SnakePiece> snakePieces = new List<SnakePiece>();
|
|
public List<BendData> bends = new List<BendData>();
|
|
public GameObject snakePiecePrefab;
|
|
public int startingLength = 3;
|
|
|
|
public Vector2 fieldSize = new Vector2(160, 90);
|
|
float moveTimer;
|
|
|
|
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
|
|
|
|
void Start()
|
|
{
|
|
ButtonSet.OnUp += OnUp;
|
|
ButtonSet.OnDown += OnDown;
|
|
ButtonSet.OnLeft += OnLeft;
|
|
ButtonSet.OnRight += OnRight;
|
|
|
|
|
|
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(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;
|
|
}
|
|
|
|
bool isKickstarted = false;
|
|
public void StartGame()
|
|
{
|
|
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);
|
|
}
|
|
isKickstarted = true;
|
|
isDead = false;
|
|
}
|
|
|
|
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
|
|
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);
|
|
|
|
bends.Add(new BendData()
|
|
{
|
|
position = snakePieces[0].transform.position,
|
|
direction = dir
|
|
});
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// Move head
|
|
snakePieces[0].transform.position = curPosition + 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);
|
|
}
|
|
|
|
void Move()
|
|
{
|
|
if (moveTimer > 0)
|
|
{
|
|
moveTimer -= Time.deltaTime;
|
|
}
|
|
else
|
|
{
|
|
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);
|
|
|
|
bends.Add(new BendData()
|
|
{
|
|
position = snakePieces[0].transform.position,
|
|
direction = dir
|
|
});
|
|
}
|
|
|
|
Vector2 curPosition = snakePieces[0].transform.position;
|
|
Vector2 lastPosition = snakePieces[snakePieces.Count - 1].transform.position;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (queueNewPiece)
|
|
{
|
|
GameObject newPiece = Instantiate(snakePiecePrefab, lastPosition, Quaternion.identity);
|
|
NetworkServer.Spawn(newPiece);
|
|
SnakePiece piece = newPiece.GetComponent<SnakePiece>();
|
|
piece.userId = userId;
|
|
snakePieces.Add(piece);
|
|
SFXManager.instance.PlayCrunch();
|
|
AddScore();
|
|
|
|
queueNewPiece = false;
|
|
}
|
|
|
|
snakePieces[0].transform.position = curPosition + curDirection;
|
|
snakePieces[0].direction = curDirection;
|
|
snakePieces[0].isHead = true;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
[ClientRpc]
|
|
void RpcReconcileState(Vector2[] serverPositions, Vector2[] serverDirections)
|
|
{
|
|
if (!isLocalPlayer) return;
|
|
|
|
// 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]) < 0.1f)
|
|
{
|
|
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
|
|
{
|
|
// No match found, reset to server state
|
|
predictedPositions.Clear();
|
|
predictedDirections.Clear();
|
|
|
|
// Update snake pieces to match server state
|
|
for (int i = 0; i < snakePieces.Count; i++)
|
|
{
|
|
snakePieces[i].transform.position = serverPositions[i];
|
|
snakePieces[i].direction = serverDirections[i];
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class BendData
|
|
{
|
|
public Vector3 position;
|
|
public Vector2 direction;
|
|
} |