using System.Collections.Generic; using UnityEngine; using Mirror; using System.Linq; using UnityEngine.UI; using System.Collections; public class SpaceshipController : NetworkBehaviour { [SyncVar] public bool ready; public SpriteRenderer playerImg; public SkinsData skins; [SyncVar(hook = nameof(OnSkinChanged))] public string skinName = null; [SyncVar(hook = nameof(OnPnameChanged))] public string pname; [SyncVar(hook = nameof(OnScoresChanged))] public int Scores; [SyncVar(hook = nameof(OnKillsChanged))] public int Kills; [SyncVar(hook = nameof(OnTrailTimeChanged))] public float trailTime; public float minTrailTime; public float maxTrailTime; [SyncVar] public int moonsCollected; public float trailIncrementRate = 0.5f; [SyncVar] public bool dead; [SyncVar] public float speed; [SyncVar] public float speedMultiplier; [SyncVar] public float boostConsumption; [SyncVar] public float energyGain; 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; RocketLevel rocketStats; public void SetReady(bool value){ if(isServer){ ready =value; }else{ CmdSetReady(value); } } [Command] void CmdSetReady(bool value){ ready= value; } [Command] void CmdSetPname(string value) { pname = value; } void OnPnameChanged(string oldName, string newName) { pnameTxt.text = newName; Debug.Log(pname + " Joined the game"); } [Command] void CmdSetSkin(string newSkin) { skinName = newSkin; } [Command] void CmdSetStats(float _speed, float _energy, float _boost){ speedMultiplier = _speed; energyGain = _energy; boostConsumption = _boost; } void OnSkinChanged(string oldSkin, string newSkin) { ChangeSkin(newSkin); } void ChangeSkin(string name) { Sprite newSprite = SkinManagerHelper.getSkinSprite(name, skins); playerImg.sprite = newSprite; } void OnScoresChanged(int oldScores, int newScores) { Debug.Log($"Add scores {newScores - oldScores}, (total: {newScores})"); } void OnTrailTimeChanged(float oldValue, float newValue) { trailMgr.trail.time = newValue; if (isLocalPlayer) { bool isBoostAvailable= trailTime > minTrailTime; SceneData.holder.boostBtn.gameObject.SetActive(isBoostAvailable); if(!isBoostAvailable&& boosting){ CmdSetBoosting(false); } } } void OnBoostDown() { if (isLocalPlayer) { if (isServer) { boosting = true; } else { CmdSetBoosting(true); } } } void OnBoostUp() { if (isLocalPlayer) { if (isServer) { boosting = false; } else { CmdSetBoosting(false); } } } [Command] void CmdSetBoosting(bool value) { boosting = value; } [SyncVar] double startedTime = 0; void ResetStats() { startedXp = DBmanager.Xp; startedMetal = DBmanager.Metal; } void Start() { if (isLocalPlayer) { ResetStats(); if(MinigameManager.instance.isRanked){ DBmanager.SetMetal(DBmanager.Metal + DBmanager.CurrentRank.energyReward); DBmanager.SetMetal(DBmanager.Metal - DBmanager.CurrentRank.entryFee); } if (joystick == null) { joystick = FindObjectOfType(); } FindObjectOfType().SetTarget(transform); string myName = PlayerPrefs.GetString("username"); SceneData.localPlayer = gameObject; SceneData.OnBoostDown.AddListener(OnBoostDown); SceneData.OnBoostUp.AddListener(OnBoostUp); //Set username if (isServer) { pname = myName; } else { CmdSetPname(myName); } //Set Skin if (DBmanager.GetSkinIdByName(SkinShopManager.GetEquipedSkin()) < 0) { //False skin purchase } else { string skin = SkinShopManager.GetEquipedSkin(); SkinType rarity = SkinShopManager.GetRarityFromName(skin); PurchasedSkin purchasedSkin = DBmanager.GetSkinByName(skin); float m_speed = SkinShopManager.GetStatsForRarity(rarity)[purchasedSkin.speedLevel].speedMultiplier; float m_energy = SkinShopManager.GetStatsForRarity(rarity)[purchasedSkin.energyLevel].moonMultiplier; float m_boost = SkinShopManager.GetStatsForRarity(rarity)[purchasedSkin.boostLevel].boostConsumption; if (isServer) { skinName = skin; speedMultiplier = m_speed; energyGain = m_energy; boostConsumption = m_boost; } else { CmdSetSkin(skin); CmdSetStats(m_speed, m_energy, m_boost); } } pnameTxt.text = myName; pnameTxt.gameObject.SetActive(false); } if (isServer) { startedTime = NetworkTime.time; trailTime =0; DecreaseTrail(0); } } // Update is called once per frame int timeInMillis => (int)(NetworkTime.time * 1000); int roundedTime => Mathf.FloorToInt((float)timeInMillis / 100f) * 100; int lastClientUpdateTime = 0; float scale => Mathf.Clamp(1+(trailTime * _scaleMultiplier),1,10); void FixedUpdate() { if (MinigameManager.instance.isRanked && !MinigameManager.instance.RankedGameStarted) { return; } distanceFromCenter = Vector3.Distance(transform.position, Vector3.zero); pnameTxt.rectTransform.rotation = Quaternion.Euler(Vector3.zero); //Update size of trail and spaceship transform.localScale = Vector3.Lerp(transform.localScale, new Vector3(scale, scale, scale), 0.1f); trailMgr.trail.startWidth = Mathf.Lerp(trailMgr.trail.startWidth, scale, 0.1f); if (dead) { return; } if (isLocalPlayer) { joyInput = joystick.input; //Cheats => TODO: Remove this if (Input.GetKeyDown(KeyCode.F)) { CmdCheatKills(); } // Debug.Log(startedTime-NetworkTime.time); double survivalTime = (startedTime == 0) ? 0 : NetworkTime.time - ((MinigameManager.instance.isRanked)? MinigameManager.instance.startedTime :startedTime); if (DBmanager.MostTime < (int)survivalTime) { DBmanager.SetMostTime((int)survivalTime); } SceneData.SetTimerTxt(survivalTime); } //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 && scale > 1) { speed = movingSpeed * 2 * speedMultiplier; DecreaseTrail(Time.deltaTime * boostConsumption); // scaleMultiplier -= Time.deltaTime; // if (scaleMultiplier < 1) { scaleMultiplier = 1; } //Clamp in case gets lower } else { speed = movingSpeed* speedMultiplier; } } ///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("Got hit : " + hit.name); // Debug.Log("This cannot run on client, That's illegal!"); // <-- What this log says return; } SpaceshipController deadPlayer = hit.GetComponent(); Debug.Log("got hit by player? : " + deadPlayer != null); if (deadPlayer != null && !deadPlayer.dead && (NetworkTime.time- deadPlayer.startedTime) > 5) { // <-- okay we killed someone | KILLCODE deadPlayer.Die(pname); Debug.Log($"{pname} killed {deadPlayer.pname}"); OnKill(); } } void OnKill() { Kills++; // Scores += 10; //TODO: Need to change Scores on kills? // scaleMultiplier += 0.05f; // OnScaleChanged(scaleMultiplier, scaleMultiplier); IncreaseTrail(trailIncrementRate); RpcOnKill(); } [ClientRpc] void RpcOnKill() { } void OnKillsChanged(int oldVal, int newVal) { if (isLocalPlayer) { Debug.Log("Show kills for " + newVal); KillText.Show(newVal); } } void IncreaseTrail(float rate) { trailTime = Mathf.Clamp(trailMgr.trail.time + rate,minTrailTime,maxTrailTime); trailMgr.trail.time = trailTime; // Debug.Log("Increasing trail of" + pname); } public void DecreaseTrail(float rate) { trailTime = Mathf.Clamp(trailMgr.trail.time - rate,minTrailTime,maxTrailTime); trailMgr.trail.time = trailTime; // Debug.Log("Decreasing trail of" + pname); } [Command] void CmdCheatKills() { OnKill(); } public void CollectPickup(PickupItem.PickupType type) { if (!isServer) { Debug.Log("Server function ran on client. That's illegal!"); } // <-- What this log says switch (type) { case PickupItem.PickupType.Moon: IncreaseTrail(trailIncrementRate); // scaleMultiplier += 0.05f; moonsCollected++; Scores += 2; break; case PickupItem.PickupType.Star: IncreaseTrail(trailIncrementRate / 2f); Scores += 1; // scaleMultiplier += 0.025f; break; } RpcCollectPickup(type); } [ClientRpc] void RpcCollectPickup(PickupItem.PickupType type) { if (isLocalPlayer) { int gainedEnergy = (int)energyGain; if(type == PickupItem.PickupType.Star){ gainedEnergy = Mathf.RoundToInt((float)gainedEnergy/2f); } MinigameManager.instance.GainMetals(gainedEnergy); AudioManager.instnace.CollectPickup(); } } public void Die(string killer) { MinigameManager.instance.SpawnLeftoverPickups(transform.position, (int)((float)Scores/4f)); Debug.Log($"Sending death signal to {pname} by {killer}"); //Handle Respawning // OnScaleChanged(scaleMultiplier, 1); // scaleMultiplier = 1; dead = true; Scores = 0; Kills = 0; startedTime = NetworkTime.time; DecreaseTrail(trailTime); trailMgr.trail.time = trailTime; RpcDie(killer); gameObject.SetActive(false); //MinigameManager.instance.SetRespawn(gameObject); } private int startedXp; private int startedMetal; bool RewardedRankedMetal =false; [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.ShowDeadscreen(DBmanager.Xp - startedXp, DBmanager.Metal - startedMetal, NetworkTime.time - startedTime); if (MinigameManager.instance.isRanked) { // MinigameManager.instance.rankedSummary.ShowLoss(); // StartCoroutine(ShowRankedResults()); if(MinigameManager.instance.winnerId<=0){ Debug.LogError("Winner ID not synced yet"); } if (MinigameManager.instance.winnerId != netId) { //LOSER! MinigameManager.instance.rankedSummary.ShowLoss(); AudioManager.instnace.MinigameLost(); } else { MinigameManager.instance.rankedSummary.ShowWin(); AudioManager.instnace.MinigameWon(); } } } } IEnumerator ShowRankedResults() { while (MinigameManager.instance.winnerId <= 0) { yield return new WaitForFixedUpdate(); } } public void Respawn(Vector3 respawnPoint) { Debug.Log("Respawning " + pname); if (isServer) { respawn(respawnPoint); } else { CmdRespawn(respawnPoint); } } [Command] void CmdRespawn(Vector3 respawnPoint) { respawn(respawnPoint); } 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); ResetStats(); } } [Command] public void CmdWonRanked(){ WonRanked(); } [Command] public void CmdLostRanked(){ LostRanked(); } public void WonRanked() { if (!isServer) { return; } RpcWonRanked(); Die("server"); } [ClientRpc] void RpcWonRanked() { if (isLocalPlayer) { DBmanager.SetMetal(DBmanager.Metal+500); MinigameManager.instance.rankedSummary.ShowWin(); //Add trophies DBmanager.SetTrophies(DBmanager.Trophies + 30); } } public void LostRanked() { if (!isServer) { return; } RpcLostRanked(); Die("server"); } void RpcLostRanked() { if (isLocalPlayer) { DBmanager.SetMetal(DBmanager.Metal+250); MinigameManager.instance.rankedSummary.ShowLoss(); // DBmanager.SetTrophies(Mathf.Clamp(DBmanager.Trophies - 15, 0, int.MaxValue)); DBmanager.SetTrophies(Mathf.Clamp(DBmanager.Trophies - DBmanager.CurrentRank.trophieLoss, 0, int.MaxValue)); } } } 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; } }