using System.Collections.Generic; using UnityEngine; using Mirror; using System.Linq; using UnityEngine.UI; public class SpaceshipController : NetworkBehaviour { [SyncVar(hook=nameof(OnPnameChanged))] public string pname; [SyncVar(hook=nameof(OnScoresChanged))] public int Scores; [SyncVar(hook=nameof(OnTrailTimeChanged))] public float trailTime; public float trailIncrementRate = 0.5f; [SyncVar] public bool dead; [SyncVar] public float speed; [SyncVar(hook=nameof(OnScaleChanged))] public float scaleMultiplier=1; public Text pnameTxt; public Transform body; public TrailMgr trailMgr; public float movingSpeed = 0.1f; public float turningSmoothFactor = 0.1f; public Joystick joystick; public Vector2 joyInput; [SyncVar] public bool boosting; [Header("Client Prediction")] public SortedDictionary serverStateBuffer = new SortedDictionary(); public Vector3 DetourError = new Vector3(0,0.2f,0); public Vector3 Detour = Vector3.zero; public Quaternion RotationDetour = Quaternion.identity; public float DetourCorrectionFactor = 0.5f; public float timeDelayErrorFix = 1f; public bool showDebugHUD = false; public float distanceFromCenter= 0; [Command] void CmdSetPname(string value){ pname = value; } void OnPnameChanged(string oldName, string newName){ pnameTxt.text = newName; } void OnScoresChanged(int oldScores, int newScores){ Debug.Log($"Add scores { newScores - oldScores}, (total: {newScores})"); } void OnTrailTimeChanged(float oldValue, float newValue){ trailMgr.trail.time = newValue; } void OnBoostDown(){ if(isLocalPlayer){ if(isServer){ boosting=true; }else{ CmdSetBoosting(true); } } } void OnBoostUp(){ if(isLocalPlayer){ if(isServer){ boosting=false; }else{ CmdSetBoosting(false); } } } void OnScaleChanged(float oldScale, float newScale){ if(isLocalPlayer){ SceneData.holder.boostBtn.gameObject.SetActive(newScale>1); } } [Command] void CmdSetBoosting(bool value){ boosting=value; } [SyncVar] double startedTime = 0; void Start() { scaleMultiplier=1; if (isLocalPlayer) { if (joystick == null) { joystick = FindObjectOfType(); } FindObjectOfType().SetTarget(transform); string myName = PlayerPrefs.GetString("username"); SceneData.localPlayer = gameObject; SceneData.OnBoostDown.AddListener(OnBoostDown); SceneData.OnBoostUp.AddListener(OnBoostUp); if(isServer){pname=myName;}else{ CmdSetPname(myName); } pnameTxt.text = myName; pnameTxt.gameObject.SetActive(false); } if (isServer) { startedTime = NetworkTime.time; } } // Update is called once per frame int timeInMillis => (int)(NetworkTime.time * 1000); int roundedTime => Mathf.FloorToInt((float)timeInMillis / 100f) * 100; int lastClientUpdateTime = 0; void FixedUpdate() { distanceFromCenter = Vector3.Distance(transform.position, Vector3.zero); pnameTxt.rectTransform.rotation = Quaternion.Euler(Vector3.zero); SceneData.SetTimerTxt(startedTime-NetworkTime.time); //Update size of trail and spaceship transform.localScale = Vector3.Lerp(transform.localScale,new Vector3(scaleMultiplier,scaleMultiplier,scaleMultiplier),0.1f); trailMgr.trail.startWidth = Mathf.Lerp(trailMgr.trail.startWidth,scaleMultiplier,0.1f); if(dead){return;} if (isLocalPlayer) { joyInput = joystick.input; //Cheats => TODO: Remove this if(Input.GetKeyDown(KeyCode.F)){ CmdCheatKills(); } } //Simulate on both client and server if (isLocalPlayer || isServer) { body.Translate(new Vector3(0, speed), body); if (joyInput != Vector2.zero) { Turn(joyInput); } } if(isServer){ //boost check if(boosting && scaleMultiplier > 1){ speed= movingSpeed*2; scaleMultiplier-=Time.deltaTime; if(scaleMultiplier<1){scaleMultiplier=1;} //Clamp in case gets lower }else{ speed=movingSpeed; } } ///Diff = rot1 . rot2 ///1 = rot1 . rot2 / Diff ///1 * Diff * Diff = rot1.rot2. Diff if (isLocalPlayer) { CmdUpdateJoyInput(joyInput); //Fix Detours if (Mathf.Abs(Detour.magnitude) > 0.1f || true) { Vector3 newPosition = body.position + Detour; Quaternion newRotation = body.rotation * RotationDetour; if(Detour.magnitude > 0.5f){ trailMgr.trail.emitting =false; } body.position = Vector3.Lerp(body.position, newPosition, (Mathf.Abs(Detour.magnitude) > 0.2f) ? DetourCorrectionFactor * 2 * Detour.magnitude : DetourCorrectionFactor); Detour = newPosition - body.position; body.rotation = Quaternion.Lerp(body.rotation, newRotation, DetourCorrectionFactor * ((joystick.touchDown) ? 0.1f : 1)); RotationDetour = Quaternion.Inverse(transform.rotation) * newRotation; trailMgr.trail.emitting=true; } } //Fill input and position buffer for client predictions StatePayload currentState = new StatePayload(timeInMillis, transform.position, transform.rotation, joyInput); if (isLocalPlayer) { if (lastClientUpdateTime < roundedTime) { CmdValidateMovement(roundedTime, currentState.Position, currentState.Rotation, currentState.Input); lastClientUpdateTime = roundedTime; } } else if (isServer) { int lastUpdatedTime = serverStateBuffer.Count > 0 ? serverStateBuffer.Keys.Last() : 0; if (timeInMillis >= lastUpdatedTime + 100) { serverStateBuffer.Add(roundedTime, currentState); if (serverStateBuffer.Count > 1024) { serverStateBuffer.Remove(serverStateBuffer.Keys.First()); } } RpcUpdatePosition(joyInput, transform.position, transform.rotation, timeInMillis); RpcUpdateTrail(trailMgr.positions); }else{ transform.position = Vector3.Lerp(transform.position, targetPosition, 0.1f); transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, 0.1f); } } Vector3 targetPosition; Quaternion targetRotation; void Turn(Vector2 input) { body.rotation = Quaternion.Lerp(body.rotation, getTurnAngle(input), turningSmoothFactor * input.magnitude); } Quaternion getTurnAngle(Vector2 input) { var angle1 = Mathf.Atan2(-input.y, -input.x) * Mathf.Rad2Deg; return Quaternion.AngleAxis(angle1 + 90, Vector3.forward); } [Command] void CmdUpdateJoyInput(Vector2 input) { joyInput = input; } [Command] void CmdValidateMovement(int time, Vector3 position, Quaternion rotation, Vector2 input) { StatePayload payload = new StatePayload(time, position, rotation, input); StatePayload lastServerState = serverStateBuffer.Values.Last(); StatePayload payloadToCompare; if (serverStateBuffer.Count < 1) { Debug.Log("still Initiating server buffer"); return; } joyInput = payload.Input; if (!serverStateBuffer.ContainsKey(time)) { payloadToCompare = lastServerState; if (lastServerState.Time < time) { // Debug.Log("Server doesn't have that data yet\nYou asked for " + time + " best I can do is " + lastServerState.Time); } else { for (int i = 0; i < 50; i++) { if (serverStateBuffer.ContainsKey(time - i)) { payloadToCompare = serverStateBuffer[time - i]; Debug.Log($"Found closest state at {payloadToCompare.Time}, requested {time}"); break; } } } } else { payloadToCompare = serverStateBuffer[time]; } Vector3 positionDiff = payloadToCompare.Position - payload.Position; int timeDiff = lastServerState.Time - time; if (Mathf.Abs(positionDiff.magnitude) < 0.2f && lastServerState.Time - time < 500) { //Validated, move on } else { // Debug.Log($"RB : {positionDiff.magnitude} [{timeDiff}ms]"); RpcRubberBand(joyInput, serverStateBuffer.Values.Last().Position, serverStateBuffer.Values.Last().Rotation, trailMgr.positions, timeInMillis); } } [ClientRpc] void RpcUpdatePosition(Vector2 input, Vector3 position, Quaternion rotation, double sentTime) { if(isLocalPlayer || isServer){return;} double delay = (timeInMillis - sentTime) * timeDelayErrorFix; int numberOfFrames = (int)((float)(delay/1000f) *50f); Quaternion newRotation = rotation; Vector3 direction = (rotation * Vector3.forward).normalized; Vector3 newPosition = position + ((direction * speed) * numberOfFrames); for (int i = 0; i < numberOfFrames; i++) { newRotation = Quaternion.Lerp(newRotation, getTurnAngle(input), turningSmoothFactor * input.magnitude); } targetPosition = newPosition - DetourError; targetRotation = newRotation; } double lastRubberBandTime = 0; [ClientRpc] void RpcRubberBand(Vector2 input, Vector3 position, Quaternion rotation, Vector3[] trailPositions, double sentTime) { if (true) { if (sentTime < lastRubberBandTime) { Debug.Log("Old rubber band rpc, ignoree..."); return; } //Lag comprehension double delay = (timeInMillis - sentTime) * timeDelayErrorFix; int numberOfFrames = (int)((float)(delay/1000f) * 50f); Quaternion newRotation = rotation; Vector3 direction = (rotation * Vector3.forward).normalized; Vector3 newPosition = position + ((direction * speed) * numberOfFrames); for (int i = 0; i < numberOfFrames; i++) { newRotation = Quaternion.Lerp(newRotation, getTurnAngle(input), turningSmoothFactor * input.magnitude); } int distanceSinceSent = (int)Vector3.Distance(newPosition, position); #region trailSyncOld // Vector3[] newTrailPositions = new Vector3[trailPositions.Length]; // for(int i=0; i < trailPositions.Length; i++){ // if(i > trailPositions.Length - distanceSinceSent-1){ // //fill with current data // newTrailPositions[i] = trailMgr.positions[i]; // }else{ // Vector3 newTrailPoint = trailPositions[i-distanceSinceSent]; // if(trailMgr.positions.Length > i){ // if(Vector3.Distance(newTrailPoint, trailMgr.positions[i]) < 0.4f){ // newTrailPoint = trailMgr.positions[i]; // } // } // newTrailPositions[i] = newTrailPoint; // } // } // trailMgr.trail.SetPositions(newTrailPositions); #endregion // Vector3 newPosition = position + new Vector3(0, movingSpeed * ); // Vector3 newPosition = position; Detour = newPosition - transform.position - DetourError; // RotationDetour = rotation; RotationDetour = Quaternion.Inverse(transform.rotation) * newRotation; lastRubberBandTime = sentTime; // Debug.Log($"Rubber banded (Detour of pos:{Detour}, rotation: {RotationDetour}) you to {transform.position}, @ {sentTime} (delay: {delay}"); } } [ClientRpc] void RpcUpdateTrail(Vector3[] positions){ //trailMgr.trail.SetPositions(positions); } void OnGUI() { if (!isLocalPlayer) { return; } if (showDebugHUD) { GUI.Label(new Rect(Screen.width - 120, 10, 100, 20), transform.position.ToString()); GUI.Label(new Rect(Screen.width - 120, 30, 100, 20), timeInMillis.ToString()); GUI.Label(new Rect(Screen.width - 100, Screen.height - 30, 50, 20), (NetworkTime.rtt * 1000).ToString() + " ms"); } } public float Angle(Vector2 vector2) { return 360 - (Mathf.Atan2(vector2.x, vector2.y) * Mathf.Rad2Deg * Mathf.Sign(vector2.x)); } //Auto assign default variables [Editor only] void OnValidate() { if (body == null) { body = transform; } if(trailMgr==null){ trailMgr = GetComponent(); } } public void TrailCollided(Collider2D hit){ if(!isServer){ // Debug.Log("This cannot run on client, That's illegal!"); // <-- What this log says return; } SpaceshipController deadPlayer = hit.GetComponent(); if(deadPlayer!=null && !deadPlayer.dead){ // <-- okay we killed someone | KILLCODE deadPlayer.Die(pname); Debug.Log($"{pname} killed {deadPlayer.pname}"); OnKill(); } } void OnKill(){ Scores+= 10; //TODO: Need to change Scores on kills? scaleMultiplier+=0.05f; OnScaleChanged(scaleMultiplier,scaleMultiplier); IncreaseTrail(trailIncrementRate); } void IncreaseTrail(float rate){ trailTime = trailMgr.trail.time+ rate; trailMgr.trail.time = trailTime; Debug.Log("Increasing trail of" + pname); } [Command] void CmdCheatKills(){ OnKill(); } public void CollectPickup(PickupItem.PickupType type){ if(isClient){Debug.Log("Server function ran on client. That's illegal!");} // <-- What this log says switch(type){ case PickupItem.PickupType.Moon: IncreaseTrail(trailIncrementRate); break; case PickupItem.PickupType.Star: IncreaseTrail(trailIncrementRate/2f); break; } RpcCollectPickup(type); } [ClientRpc] void RpcCollectPickup(PickupItem.PickupType type){ if(isLocalPlayer){ MinigameManager.instance.GainMetals((type==PickupItem.PickupType.Star) ? 10 : 30); } } public void Die(string killer){ MinigameManager.instance.SpawnLeftoverPickups(transform.position, Scores); Debug.Log($"Sending death signal to {pname} by {killer}"); //Handle Respawning OnScaleChanged(scaleMultiplier,1); scaleMultiplier=1; dead=true; Scores=0; trailTime = 1; trailMgr.trail.time = trailTime; RpcDie(killer); MinigameManager.instance.SetRespawn(gameObject); } [ClientRpc] public void RpcDie(string killer){ Debug.Log($"{killer} killed {pname} : isItMe? -> {isLocalPlayer}"); KillfeedMgr.instance.AddNewEntry($"{pname} was killed by {killer}"); gameObject.SetActive(false); if(isLocalPlayer){ //TODO: Death message goes here SceneData.holder.deadScreen.SetActive(true); } } public void Respawn(Vector3 respawnPoint){ dead=false; trailMgr.trail.emitting =false; trailMgr.trail.Clear(); RpcRespawn(respawnPoint); transform.position = respawnPoint; trailMgr.trail.emitting=true; gameObject.SetActive(true); } [ClientRpc] public void RpcRespawn(Vector3 respawnPoint){ GetComponent().enableValidation=false; trailMgr.trail.Clear(); trailMgr.positions = new Vector3[0]; trailMgr.trail.emitting = false; transform.position = respawnPoint; trailMgr.trail.emitting=true; GetComponent().enableValidation=true; gameObject.SetActive(true); if(isLocalPlayer){ SceneData.holder.deadScreen.SetActive(false); } } } public class StatePayload { public int Time; public Vector3 Position; public Quaternion Rotation; public Vector2 Input; public StatePayload(int time, Vector3 position, Quaternion rotation, Vector2 input) { Time = time; Position = position; Rotation = rotation; Input = input; } }