Network Timer

I am attempting to add two timers:

  • The first is a countdown timer, where people can join the server before the match starts. The controls should be disabled at this point
  • The second is the game timer, which dictates how long the match should last, as well as send out other signals (sound effects, trigger power up drops, etc…).

My code is below. It has two problems:

  1. If a player joins after the host’s countdown timer is zero, the countdown timer UI always stays up and their controls are always disabled.
  2. The first time I run this code, the

Any advice is appreciated :slight_smile:

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using TMPro;

public class NetworkTimer : NetworkBehaviour
{
    [Header("Game Timer")]
    [SerializeField] private float timer = 45f;
    [SerializeField] private TMP_Text timerText;

    [Header("Countdown Timer")]
    [SerializeField] private float countdownToGameTimer = 15f;
    [SerializeField] private GameObject countdownTimerScreen;
    [SerializeField] private TMP_Text countdownTimerText;
    //[SerializeField] private ControlSignalSender controlSignalSender;
    [SerializeField] private GameObject skipCountdownTimerButton;

    [Header("Game Over Information")]
    [SerializeField] private GameObject gameOverScreen;

    //Signals for other scrips

    public static event Action<NetworkTimer> OnTimerStart;
    public static event Action<bool> OnGameStart;
    public static event Action<NetworkTimer> OnFiveMinuteWarning;
    public static event Action<NetworkTimer> OnOneMinuteWarning;
    public static event Action<bool> OnGameOver;

    //bools for if statements
    private bool disableCountdownTimer = false;
    private bool fiveMinuteWarning = false;
    private bool oneMinuteWarning = false;


    public override void OnNetworkSpawn()
    {
        if (!IsOwner) { return; }

        //This is the signal to change the music to the correct track
        OnTimerStart?.Invoke(this);
        //This is the UI for the countdown screen
        countdownTimerScreen.SetActive(true);

        if (countdownToGameTimer <= 0)
        {
            //begin the game
            countdownTimerScreen.SetActive(false);

            //Send the signal to enable controls
            OnGameStart?.Invoke(true);
            Debug.Log("The timer is sending the signal from the Awake command to enable controls");
            return;
        }
    }

    private void Update()
    {
        if (disableCountdownTimer == false)
        {
            CountdownTimerToStart();
            return;
        }

        //Update the time
        GameTimer();
        //Check the time, to see what events should happen
        GameTimerOptions();
    }

    private void CountdownTimerToStart()
    {
        if (countdownToGameTimer <= 0)
        {
            return;
        }
        if (IsServer)
        {
            countdownToGameTimer -= Time.deltaTime;
            UpdateCountdownTimerClientRpc(countdownToGameTimer);
        }
    }

    [ClientRpc]
    private void UpdateCountdownTimerClientRpc(float newTime)
    {

        //the client now has the new time
        countdownToGameTimer = newTime;
        //...and we update the display
        UpdateCountdownTimerDisplay();
        //Now, if the time is zero
        if (countdownToGameTimer <= 0)
        {
            //begin the game
            countdownTimerScreen.SetActive(false);

            //Now, we make sure this script never runs again
            disableCountdownTimer = true;

            //This signal enables the controls for each player
            OnGameStart?.Invoke(true);
            Debug.Log("The timer is sending the signal to enable controls");
        }
    }

    private void UpdateCountdownTimerDisplay()
    {
        int countdownSeconds = Mathf.FloorToInt(countdownToGameTimer % 60);
        countdownTimerText.text = countdownSeconds.ToString("00");
    }


    public void StartNowButton()
    {
        countdownToGameTimer = 0f;
        UpdateCountdownTimerClientRpc(0);
    }

    private void GameTimer()
    {
        //we don't need to run anything if the timer is done
        if (timer <= 0)
        {
            return;
        }
        //If we are the server, we need to run the following code:
        if (IsServer)
        {
            //decrease the timer, and...
            timer -= Time.deltaTime;
            //..update the timer on the client side
            UpdateTimerClientRpc(timer);
            return;
        }
    }

    [ClientRpc]
    private void UpdateTimerClientRpc(float newTime)
    {
        //the client now has the new time
        timer = newTime;
        //...and we update the display
        UpdateTimerDisplay();
        //Now, if the time is zero
        if (timer <= 0)
        {
            //set the text to always say no time left
            timerText.text = "Time Left: 00:00";

            //...and set the game over screen
            gameOverScreen.SetActive(true);

            Time.timeScale = 0.0f;

            //We are using this script to announce that the player has spawned
            OnGameOver?.Invoke(this);
        }
    }

    private void UpdateTimerDisplay()
    {
        float minutes = Mathf.FloorToInt(timer / 60);
        float seconds = Mathf.FloorToInt(timer % 60);
        timerText.text = string.Format("Time Left: {0:00}:{1:00}", minutes, seconds);
    }

    private void GameTimerOptions()
    {
        if (timer <= 300f && timer > 299f)
        {
            if (fiveMinuteWarning == false)
            {
                fiveMinuteWarning = true;
                OnFiveMinuteWarning?.Invoke(this);
            }
        }
        else if (timer >= 59f && timer < 60f)
        {
            if (oneMinuteWarning == false)
            {
                oneMinuteWarning = true;
                OnOneMinuteWarning?.Invoke(this);
            }
        }
    }
}

Could you sync the timer as a NetworkVariable, and new clients can check it themselves?
Else the server could it for all clients and send a clientRPC to start the game when a new client spawns.

That is exactly what I did! Thanks!

I’ve attached the Network Timer as a game object in the Game Scene. My new problem, however is the timing. Since the HostGameManager script loads the player before it loads the scene, the first player in has a 50-50 chance of finding the timer (and if it fails, a whole bunch of problems occur.

Any advice?

P.S. Here is my Network Timer script, along with my modified Player Movement script, if this helps anyone in the future :slight_smile:

Network Timer:

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using TMPro;

public class NetworkTimer : NetworkBehaviour
{
    [Header("Game Timer")]
    [SerializeField] private float timer = 45f;
    [SerializeField] private TMP_Text timerText;

    [Header("Countdown Timer")]
    [SerializeField] private float countdownToGameTimer = 15f;
    [SerializeField] private GameObject countdownTimerScreen;
    [SerializeField] private TMP_Text countdownTimerText;
    [SerializeField] private GameObject skipCountdownTimerButton;

    [Header("Game Over Information")]
    [SerializeField] private GameObject gameOverScreen;

    //Signals for other scrips

    public static event Action<NetworkTimer> OnTimerStart;
    public static event Action<bool> OnGameStartControls;
    public static event Action<NetworkTimer> OnGameStartMusic;
    public static event Action<NetworkTimer> OnFiveMinuteWarning;
    public static event Action<NetworkTimer> OnFourMinuteWarning;
    public static event Action<NetworkTimer> OnThreeMinuteWarning;
    public static event Action<NetworkTimer> OnTwoMinuteWarning;
    public static event Action<NetworkTimer> OnOneMinuteWarning;
    public static event Action<NetworkTimer> OnGameOver;
    public static event Action<bool> OnGameEndControls;


    //bools for if statements
    public NetworkVariable<bool> DisableCountdownTimerNetwork = new NetworkVariable<bool>();
    private bool disableCountdownTimer = false;
    private bool fiveMinuteWarning = false;
    private bool fourMinuteWarning = false;
    private bool threeMinuteWarning = false;
    private bool twoMinuteWarning = false;
    private bool oneMinuteWarning = false;


    public override void OnNetworkSpawn()
    {
        if (!IsOwner) { return; }

        DisableCountdownTimerNetwork.Value = false;

        //This is the signal to change the music to the correct track
        OnTimerStart?.Invoke(this);
        //This is the UI for the countdown screen
        countdownTimerScreen.SetActive(true);
        if (skipCountdownTimerButton != null)
        {
            if (NetworkObject.IsOwnedByServer)
            {
                skipCountdownTimerButton.SetActive(true);
            }
            else
            {
                skipCountdownTimerButton.SetActive(false);
            }
        }

    }

    private void Update()
    {
        //if(hostManagerSignal == false) { return; }
        if (disableCountdownTimer == false)
        {
            CountdownTimerToStart();
            return;
        }

        //Update the time
        GameTimer();
        //Check the time, to see what events should happen
        GameTimerOptions();
    }

    private void CountdownTimerToStart()
    {
        if (DisableCountdownTimerNetwork.Value == true)
        {
            return;
        }
        if (IsServer)
        {
            countdownToGameTimer -= Time.deltaTime;
            UpdateCountdownTimerClientRpc(countdownToGameTimer);
        }
    }

    [ClientRpc]
    private void UpdateCountdownTimerClientRpc(float newTime)
    {

        //the client now has the new time
        countdownToGameTimer = newTime;
        //...and we update the display
        UpdateCountdownTimerDisplay();
        //Now, if the time is zero
        if (countdownToGameTimer <= 0f)
        {
            //begin the game
            countdownTimerScreen.SetActive(false);

            //Now, we make sure this script never runs again
            disableCountdownTimer = true;
            if(IsServer)
            {
                DisableCountdownTimerNetwork.Value = true;
            }

            //This signal enables the controls for each player
            OnGameStartControls?.Invoke(true);

            //This signal enables the new song to play
            OnGameStartMusic?.Invoke(this);
        }
    }

    public void TurnOffCountdownTimerScreen()
    {
        //begin the game
        countdownTimerScreen.SetActive(false);
    }

    private void UpdateCountdownTimerDisplay()
    {
        int countdownSeconds = Mathf.FloorToInt(countdownToGameTimer % 60);
        countdownTimerText.text = countdownSeconds.ToString("00");
    }


    public void StartNowButton()
    {
        countdownToGameTimer = 0f;
        UpdateCountdownTimerClientRpc(countdownToGameTimer);
    }

    private void GameTimer()
    {
        //we don't need to run anything if the timer is done
        if (timer <= 0)
        {
            if (IsServer)
            {
                DisableCountdownTimerNetwork.Value = true;
                Debug.Log("My network bool value is: " + DisableCountdownTimerNetwork.Value);
            }
            return;
        }
        //If we are the server, we need to run the following code:
        if (IsServer)
        {
            //decrease the timer, and...
            timer -= Time.deltaTime;
            //..update the timer on the client side
            UpdateTimerClientRpc(timer);
            return;
        }
    }

    [ClientRpc]
    private void UpdateTimerClientRpc(float newTime)
    {
        //the client now has the new time
        timer = newTime;
        //...and we update the display
        UpdateTimerDisplay();
        //Now, if the time is zero
        if (timer <= 0)
        {
            //set the text to always say no time left
            timerText.text = "Time Left: 00:00";

            //...and set the game over screen
            gameOverScreen.SetActive(true);

            //We are using this script to announce that the player has spawned
            OnGameOver?.Invoke(this);

            //We are using this script to announce that the player controls should be disabled
            OnGameEndControls?.Invoke(this);
        }
    }

    private void UpdateTimerDisplay()
    {
        float minutes = Mathf.FloorToInt(timer / 60);
        float seconds = Mathf.FloorToInt(timer % 60);
        timerText.text = string.Format("Time Left: {0:00}:{1:00}", minutes, seconds);
    }

    private void GameTimerOptions()
    {
        if (timer <= 300f && timer > 299f)
        {
            if (fiveMinuteWarning == false)
            {
                fiveMinuteWarning = true;
                OnFiveMinuteWarning?.Invoke(this);
            }
        }
        else if (timer >= 239f && timer < 240f)
        {
            if (fourMinuteWarning == false)
            {
                fourMinuteWarning = true;
                OnFourMinuteWarning?.Invoke(this);
            }
        }
        else if (timer >= 179f && timer < 180f)
        {
            if (threeMinuteWarning == false)
            {
                threeMinuteWarning = true;
                OnThreeMinuteWarning?.Invoke(this);
            }
        }
        else if (timer >= 119f && timer < 120f)
        {
            if (twoMinuteWarning == false)
            {
                twoMinuteWarning = true;
                OnTwoMinuteWarning?.Invoke(this);
            }
        }
        else if (timer >= 59f && timer < 60f)
        {
            if (oneMinuteWarning == false)
            {
                oneMinuteWarning = true;
                OnOneMinuteWarning?.Invoke(this);
            }
        }
    }

    public override void OnNetworkDespawn()
    {
        if (!IsOwner) { return; }

        DisableCountdownTimerNetwork.Value = false;
    }
}

Player Movement

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;

public class PlayerMovement : NetworkBehaviour
{
    [Header("References")]
    [SerializeField] private InputReader inputReader;
    [SerializeField] private Transform bodyTransform;
    [SerializeField] private Rigidbody2D rb;
    [SerializeField] private ParticleSystem dustCloud;

    [Header("Settings")]
    [SerializeField] private float movementSpeed = 4f;
    [SerializeField] private float turningRate = 30f;
    [SerializeField] private float particleEmmisionValue = 10f;

    private ParticleSystem.EmissionModule emissionModule;
    private Vector2 previousMovementInput;

    //This bool is to prevent movement until I say "go" as the server
    private bool movementAllowed = false;
    private bool movementAllowedNetwork;
        
    //This variable is for determining whether we should produce a dust cloud
    private Vector3 previousPos;
    private const float ParticleStopThreshold = 0.005f;

    private NetworkTimer myNetworkTimer;

    private void Awake()
    {
        //We need this in memory
        emissionModule = dustCloud.emission;
    }

    //If this was a single player game, we would use the "Start" method here
    //However, with multiplayer games, "Start" happens too early.

    //Therefore, we use this function, but it lets you know that it is not called
    //until everything on the network has been spawned and set up (variables have been synced)

    public override void OnNetworkSpawn()
    {
        if(!IsOwner) { return; }
        //Subscribe to the event from the InputReader
        inputReader.MoveEvent += HandleMove;
        NetworkTimer.OnGameStartControls += NetworkTimer_OnGameStartControls;
        NetworkTimer.OnGameEndControls += NetworkTimer_OnGameEndControls;

        movementAllowed = false;
    }

    private void Start()
    {
        myNetworkTimer = FindObjectOfType<NetworkTimer>();
        if (myNetworkTimer != null)
        {
            Debug.Log("We DID find the network timer");
            movementAllowedNetwork = myNetworkTimer.DisableCountdownTimerNetwork.Value;
            //Debug.Log("Network bool = " + myNetworkTimer.DisableCountdownTimerNetwork.Value);
            movementAllowed = movementAllowedNetwork;
            //Debug.Log("MovementAllowed bool = " + movementAllowed);
        } else
        {
            Debug.Log("We DID NOT find the network timer");
        }
        if (movementAllowed == true)
        {
            myNetworkTimer.TurnOffCountdownTimerScreen();
        }
    }

    private void NetworkTimer_OnGameStartControls(bool obj)
    {
        movementAllowedNetwork = myNetworkTimer.DisableCountdownTimerNetwork.Value;
        movementAllowed = movementAllowedNetwork;
    }

    private void Update()
    {
        //turning rate has to be negative, in order to be accurate with rotation direction
        float zRotation = previousMovementInput.x * -turningRate * Time.deltaTime;
        bodyTransform.Rotate(0f, 0f, zRotation);
    }

    private void FixedUpdate()
    {
        //If we are moving....
        if ((transform.position - previousPos).sqrMagnitude > ParticleStopThreshold) 
        {
            //...turn on the emission particle systemm
            emissionModule.rateOverTime = particleEmmisionValue;
        } else
        {
            //Otherwise, turn it off
            emissionModule.rateOverTime = 0;
        }
        previousPos = transform.position;

        if (!IsOwner) { return; }
        //This bool is to prevent movement until I say "go" as the server
        if (movementAllowed == false) { return; }
        //We need to use FixedUpdate when dealing with Physics (Rigidbody2D)
        //DeltaTime included in when using velocity
        rb.velocity = (Vector2)bodyTransform.up * movementSpeed * previousMovementInput.y;
    }

    private void HandleMove(Vector2 movementInput)
    {
        previousMovementInput = movementInput;
    }

    public float ReturnMovementSpeed()
    {
        return movementSpeed;
    }

    public void AlterMovementSpeed(float alteredValue)
    {
        Debug.Log("We have changed the movement speed");
        movementSpeed += alteredValue;
    }

    public bool ReturnBoolStatus()
    {
        return movementAllowed;
    }

    private void NetworkTimer_OnGameEndControls(bool obj)
    {
        movementAllowedNetwork = myNetworkTimer.DisableCountdownTimerNetwork.Value;
        movementAllowed = movementAllowedNetwork;
    }

    //We run this JUST before everything has been despawned
    public override void OnNetworkDespawn()
    {
        if (!IsOwner) { return; }

        inputReader.MoveEvent -= HandleMove;
        NetworkTimer.OnGameStartControls -= NetworkTimer_OnGameStartControls;
        NetworkTimer.OnGameEndControls -= NetworkTimer_OnGameEndControls;
    }
}

You could move your check for the myNetworkTimer into the update loop. Check every frame until you get a hold of it. In the rest of your class, just check if it is null before you use it.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms