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:
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.
The first time I run this code, the
Any advice is appreciated
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.
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
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.