using System.Collections.Generic; using UnityEngine; using Mirror; using System.Linq; using UnityEngine.UI; using System.Collections; using CustomExtensions; 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; [SyncVar] public bool boosting; [Header("Client Prediction")] private Vector2 input; [SyncVar] public Vector2 m_Input; [SerializeField]private float rubberbandSmoothness = 0.3f; private float timer; private int currentTick; private float minTimeBetweenTicks; private const float SERVER_TICK_RATE = 30f; private const int BUFFER_SIZE = 2048; private float ERROR_THRESHOLD =0.25f; private int MIN_ERROR_COUNT = 10; public bool showDebugHUD = false; private double clientNetworkRTT; public InputState[] clientInputBuffer; public InputState[] serverInputBuffer; public PlayerState[] clientStateBuffer; public PlayerState[] serverStateBuffer; [SerializeField]private int ErrorInputCount; [SerializeField]private int ErrorStateCount; [SerializeField]private int RubberBandsCount; public GameObject DeathEffect; public GameObject debrisEffect; // public GameObject boostStartEffect; public ParticleSystem[] boostStartEffect; public ParticleSystem[] boostEffects; // public GameObject PickupEffect; public float distanceFromCenter = 0; RocketLevel rocketStats; [Header("AudioFX")] public AudioSource engineAudio; public float normalPitch,boostedPitch; 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})"); } bool isBoostAvailable {get{ return trailTime > minTrailTime;}} void OnTrailTimeChanged(float oldValue, float newValue) { trailMgr.trail.time = newValue; if (isLocalPlayer) { // SceneData.holder.boostBtn.gameObject.SetActive(isBoostAvailable); SceneData.holder.boostBtn.transform.GetChild(0).gameObject.SetActive(isBoostAvailable); SceneData.holder.boostBtn.transform.GetChild(0).GetChild(0).gameObject.SetActive(trailTime > maxTrailTime/2f); SceneData.holder.boostBtn.transform.GetChild(0).GetChild(1).gameObject.SetActive(trailTime > maxTrailTime* 0.8f); SceneData.holder.boostBtn.transform.GetChild(0).GetChild(2).gameObject.SetActive(trailTime >= maxTrailTime); SceneData.holder.boostBtn.transform.GetChild(0).Find("Slider").GetComponent().fillAmount = Mathf.Clamp(0.73f - ((trailTime /maxTrailTime)*0.73f),0f,0.73f); if (!isBoostAvailable && boosting) { CmdSetBoosting(false); } } } void OnBoostDown() { if(!isBoostAvailable){return; } if (isLocalPlayer) { AudioManager.instnace.Boost(); if (isServer) { boosting = true; } else { CmdSetBoosting(true); } CameraFollower.instance.ShakeBoost(); // GameObject fx = EffectPool.Spawn(boostStartEffect, transform.position); foreach(ParticleSystem particle in boostStartEffect){ particle.Play(); } } } 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() { clientInputBuffer = new InputState[BUFFER_SIZE]; serverInputBuffer = new InputState[BUFFER_SIZE]; clientStateBuffer = new PlayerState[BUFFER_SIZE]; serverStateBuffer = new PlayerState[BUFFER_SIZE]; NetworkTime.PingFrequency = SERVER_TICK_RATE; minTimeBetweenTicks = 1f / SERVER_TICK_RATE; string[] args = System.Environment.GetCommandLineArgs(); for (int i = 0; i < args.Length; i++) { // Debug.Log("ARG " + i + ": " + args[i]); if (args[i] == "-et") { ERROR_THRESHOLD =float.Parse(args[i+1]); } if(args[i] == "-grace"){ MIN_ERROR_COUNT = int.Parse(args[i+1]); } } 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); }else{ if(AudioManager.SFX_enabled){ engineAudio.volume = 1; }else{ engineAudio.volume= 0; } } } // Update is called once per frame int timeInMillis => (int)(NetworkTime.time * 1000); int roundedTime => Mathf.FloorToInt((float)timeInMillis / 100f) * 100; int lastClientUpdateTime = 0; Vector3 lineCorrection; float scale => Mathf.Clamp(1 + (trailTime * _scaleMultiplier), 1, 10); void Update() { 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){ input = joystick.input; if (Input.GetKeyDown(KeyCode.F)) { CmdCheatKills(); } if(Input.GetKeyDown(KeyCode.Space)){ OnBoostDown(); } if(Input.GetKeyUp(KeyCode.Space)){ OnBoostUp(); } // 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); } timer += Time.deltaTime; while(timer >= minTimeBetweenTicks){ timer -= minTimeBetweenTicks; HandleTick(); currentTick++; } } Vector3 serverPosition => serverStateBuffer[bufferIndex].Position; Vector3 clientPosition => clientStateBuffer[bufferIndex].Position; int curBufferIndex; int errorCounter = 0; int bufferIndex => currentTick % BUFFER_SIZE; int ticksGap => (int)(clientNetworkRTT * SERVER_TICK_RATE); int latencyBufferIndex { get{ int val =(int)(bufferIndex - (clientNetworkRTT * SERVER_TICK_RATE)); if(val < 0){ val = BUFFER_SIZE - val; } if(val >= BUFFER_SIZE || val < 0){ Debug.LogError(val + " is bigger than buffer size, latency: " + clientNetworkRTT); val = BUFFER_SIZE-1; } return val; } } public float MovementSmoothnessFactor = 0.1f; void HandleTick(){ curBufferIndex = bufferIndex; if(isLocalPlayer){ // HandleInput(input); InputState curInputState = new InputState(){Tick=currentTick, Input = input}; PlayerState curState = new PlayerState(){Tick=currentTick, Position= transform.position, Rotation = transform.rotation}; clientInputBuffer[bufferIndex] = curInputState; clientStateBuffer[bufferIndex] = curState; // transform.Translate(transform.forward * input.y); // transform.Rotate(transform.up * input.x); UpdateStates(currentTick, NetworkTime.rtt, input, curState.Position, curState.Rotation); body.position = targetState.Position; body.rotation = targetState.Rotation; // body.position = Vector3.Lerp(body.position,targetState.Position,MovementSmoothnessFactor * movingSpeed); // body.rotation = Quaternion.Lerp(body.rotation, targetState.Rotation,MovementSmoothnessFactor*movingSpeed); }else if (isServer){ HandleInput(m_Input); RpcUpdateOnClient(body.position, body.rotation, m_Input); }else{ //client but not localPlayer body.position = targetState.Position; body.rotation = targetState.Rotation; // for(int i = 0; i < ticksGap; i++){ // HandleInput(m_Input); // } } if(isLocalPlayer){CameraFollower.UpdateFrame();} float stateError = clientStateBuffer[bufferIndex].Difference(serverStateBuffer[latencyBufferIndex]); foreach(ParticleSystem boostEffect in boostEffects){ if(boostEffect.isEmitting && !boosting){ boostEffect.Stop(); }else if(!boostEffect.isEmitting && boosting){ boostEffect.Play(); } } if(isServer){ InputState curServerInputState = new InputState(){Tick=currentTick, Input = m_Input}; PlayerState curServerState = new PlayerState(){Tick=currentTick, Position = transform.position, Rotation = transform.rotation}; serverInputBuffer[bufferIndex] = curServerInputState; serverStateBuffer[bufferIndex] = curServerState; if(!clientInputBuffer[bufferIndex].CloseEnough(serverInputBuffer[latencyBufferIndex],ERROR_THRESHOLD)){ ErrorInputCount++; } // if(!clientStateBuffer[bufferIndex].CloseEnough(serverStateBuffer[latencyBufferIndex],ERROR_THRESHOLD)){ if(stateError > ERROR_THRESHOLD){ ErrorStateCount++; errorCounter++; if(errorCounter> MIN_ERROR_COUNT){RpcRubberband(transform.position, transform.rotation); errorCounter=0;} }else{ errorCounter = 0; } //boost check if (boosting && scale > 1) { speed = movingSpeed * 2 * speedMultiplier; DecreaseTrail(Time.deltaTime * boostConsumption * 0.5f); // scaleMultiplier -= Time.deltaTime; // if (scaleMultiplier < 1) { scaleMultiplier = 1; } //Clamp in case gets lower } else { speed = movingSpeed * speedMultiplier; } }else{ // not server engineAudio.pitch = Mathf.Lerp(engineAudio.pitch, (boosting) ? boostedPitch : normalPitch, 0.1f); } } void HandleInput(Vector2 _input){ // transform.Translate(transform.forward * _input.y); // transform.Rotate(transform.up * _input.x); body.Translate(new Vector3(0, speed), body); Turn(_input); } void UpdateStates(int tick,double rtt,Vector2 m_input, Vector3 m_position, Quaternion m_rotation){ if(isServer){ m_updateInput(tick,rtt,m_input, m_position, m_rotation); }else{ CmdUpdateInput(tick,rtt,m_input, m_position, m_rotation); } } [Command] void CmdUpdateInput(int tick,double rtt, Vector2 m_input, Vector3 m_position, Quaternion m_rotation){ m_updateInput(tick,rtt,m_input, m_position, m_rotation); } private void m_updateInput(int tick,double rtt, Vector2 m_input, Vector3 m_position, Quaternion m_rotation){ clientNetworkRTT = rtt; m_Input = m_input; clientInputBuffer[bufferIndex] = new InputState(){Tick=tick, Input=m_input}; clientStateBuffer[bufferIndex] = new PlayerState(){Tick=tick, Position=m_position, Rotation=m_rotation}; } [ClientRpc] void RpcRubberband(Vector3 m_position, Quaternion m_rotation){ PlayerState serverState = new PlayerState(){Tick=0, Position = m_position, Rotation = m_rotation}; PlayerState clientState = new PlayerState(){Tick=0, Position = transform.position, Rotation = transform.rotation}; float diff = serverState.Difference(clientState); Debug.Log("Rubber banded, Strength -> " + diff); if(diff < 0.3f){ transform.position = Vector3.Lerp(transform.position, m_position, rubberbandSmoothness); // transform.rotation = Quaternion.Lerp(transform.rotation, m_rotation,rubberbandSmoothness); transform.rotation = m_rotation; return; } transform.position = m_position; transform.rotation = m_rotation; RubberBandsCount++; } PlayerState targetState; [ClientRpc] void RpcUpdateOnClient(Vector3 m_position, Quaternion m_rotation, Vector2 _input){ if(isLocalPlayer){ // return; } targetState = new PlayerState(){Position = m_position, Rotation = m_rotation}; } 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); } [ClientRpc] void RpcUpdateTrail(Vector3[] positions) { //trailMgr.trail.SetPositions(positions); } void OnGUI() { if (!isLocalPlayer) { return; } if (showDebugHUD) { Vector3 bodyOnScreen = Camera.main.WorldToScreenPoint(body.position); // Vector3 DetourOnScreen = Camera.main.WorldToScreenPoint(body.position + Detour); Vector3 bodyUpOnScreen = Camera.main.WorldToScreenPoint(body.position + body.up); Vector3 bodyRightOnScreen = Camera.main.WorldToScreenPoint(body.position + body.right); // GUI.Label(new Rect(DetourOnScreen.x,DetourOnScreen.y, 100, 20), Detour.ToString()); // GUI.Label(new Rect(bodyUpOnScreen.x,bodyUpOnScreen.y, 100, 20), body.up.ToString()); // GUI.Label(new Rect(bodyRightOnScreen.x, bodyRightOnScreen.y, 100, 20), body.right.ToString()); 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.RpcShowDeathEffect(deadPlayer.transform.position); deadPlayer.Die(pname); Debug.Log($"{pname} killed {deadPlayer.pname}"); OnKill(); } } public void ShowDeathEffect(Vector2 position){ if(isServer){ // MinigameManager.instance.ShowDeathEffect(position); EffectPool.Spawn(DeathEffect, position); RpcShowDeathEffect(position); }else{ CmdShowDeathEffect(position); } } [Command] void CmdShowDeathEffect(Vector2 position){ EffectPool.Spawn(DeathEffect, position); RpcShowDeathEffect(position); } [ClientRpc] void RpcShowDeathEffect(Vector2 position){ EffectPool.Spawn(DeathEffect, position); EffectPool.Spawn(debrisEffect, position).GetComponent().Play(); } 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 if(type == PickupItem.PickupType.Moon1 || type == PickupItem.PickupType.Moon2|| type == PickupItem.PickupType.Moon3|| type == PickupItem.PickupType.Moon4|| type == PickupItem.PickupType.Moon5){ //Moon! IncreaseTrail(trailIncrementRate); // scaleMultiplier += 0.05f; moonsCollected++; Scores += 2; }else { IncreaseTrail(trailIncrementRate / 2f); Scores += 1; } RpcCollectPickup(type); } [ClientRpc] void RpcCollectPickup(PickupItem.PickupType type) { if (isLocalPlayer) { if (type == PickupItem.PickupType.Twep) { DBmanager.SetTweps(DBmanager.Tweps + 1); } else { int gainedEnergy = (int)(energyGain * type.GetEnergy()); MinigameManager.instance.GainMetals(gainedEnergy); } AudioManager.instnace.CollectMoon(trailTime,maxTrailTime); } } public void Die(string killer) { MinigameManager.instance.SpawnLeftoverPickups(transform.position, (int)((float)Scores / 4f)); RpcShowDeathEffect(transform.position); 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; } }