GameDev Blog: Goblin Rules Football #11: Pause Game and Timeouts

In my last post, I mentioned wanting to add a way to pause the game and to make certain phases “timeout” if the player doesn’t take an action without a set time. And, well, I did that!

First, I had to make an “escape menu” that is brought up whenever the player presses the Esc key or the start button on their gamepad.

To make sure that when the menu is open the player doesn’t take any game actions, I made sure that opening the Escape Menu disables all gameplay controls. The controls then need to be re-enabled when the escape menu is closed. One issue is if a player brings up the menu while in one gamephase and the game changes phases before they close the menu. So, when a player opened the menu, their gameplay phase controls were disabled. But, when the closed the menu, it was now the kick-after-attempt phase. So, when the escape menu closes, I needed a way to know what controls to re-enable. Also, I didn’t what game phase controls to be enabled while the escape menu was open. So, I had the server keep an authoritative record for each player of what controls they should have enabled at any given point in time. This is done with SyncVar boolean variables. When the escape menu is closed, the player’s client checks their control values set by the server, and then enables those controls the server says they should have during that current phase.

With the escape menu done I wanted to be able to pause the game. To pause the game, I’m just setting the Time.timeScale value to 0. In single player, this is very easy and straightforward. For multiplayer, it’s a little bit more involved. If you set the timeScale to 0 on the server, all time on the server stops. However, it doesn’t on the clients, so things like animation still runs on the clients and movement was allowed.

So, to make sure the clients are also stopped, I just used a TargetRpc to all players to set their timeScale

[TargetRpc]
public void RpcGamePausedByServer(NetworkConnection target, bool wasPaused)
{
	Debug.Log("RpcGamePausedByServer: for player: " + this.PlayerName + ":" + this.ConnectionId.ToString() + ". Will pause game? " + wasPaused.ToString());
	if (hasAuthority && this.isLocalPlayer)
	{
		if (wasPaused)
			Time.timeScale = 0f;
		else
			Time.timeScale = 1.0f;
		this.isGamePaused = wasPaused;
		GameplayManager.instance.ActivateGamePausedText(wasPaused);
		if (!GameplayManager.instance.isSinglePlayer)
		{
			try
			{
				EscMenuManager.instance.UpdatePauseGameButtonText(wasPaused);
			}
			catch (Exception e)
			{
				Debug.Log("RpcGamePausedByServer: could not access the EscMenuManager. Error: " + e);
			}
		}
		
	}
}

After the game is paused, the “paused” text appears to let players know about the paused game.

The server tracks who paused the game as well. So, only the player who paused can unpause the game.

[ServerCallback]
public void ResumeGameOnServer(GamePlayer pausingPlayer, uint pausingPlayerNetId)
{
	if (!this.isGamePaused)
		return;
	Debug.Log("ResumeGameOnServer: Player with a netid of: " + pausingPlayerNetId.ToString() + " wants to RESUME the game.");
	if (pausingPlayerNetId == this.playerWhoPausedNetId)
	{
		Debug.Log("ResumeGameOnServer: Player with a netid of: " + pausingPlayerNetId.ToString() + " matches netid of player who paused: " + this.playerWhoPausedNetId.ToString() + " will resume game.");
		playerWhoPaused = null;
		playerWhoPausedNetId = 0;
		PauseGameForAllPlayers(false);
		this.HandleIsGamePaused(this.isGamePaused, false);
		if (isGamePausedTimeoutRoutineRunning)
		{
			StopCoroutine(GamePausedTimeout);
			isGamePausedTimeoutRoutineRunning = false;
		} 
		//Time.timeScale = 1.0f;
	}
}

There is a timeout for the pausing as well. When a game is paused, a 1 minute timer is started using “unscaled time.” If the game is still paused after a minute, the server automatically resumes the game.

[Server]
    IEnumerator PauseGameTimeoutRoutine()
    {
        isGamePausedTimeoutRoutineRunning = true;
        yield return new WaitForSecondsRealtime(60f);
        if (this.isGamePaused)
        {
            Debug.Log("PauseGameTimeoutRoutine: Reached paused game timeout");
            playerWhoPaused = null;
            playerWhoPausedNetId = 0;
            PauseGameForAllPlayers(false);
            this.HandleIsGamePaused(this.isGamePaused, false);
            isGamePausedTimeoutRoutineRunning = false;
        }
    }

I also want to add pause cooldowns so a team can only pause the game every 30 seconds of game time or something to prevent pause spamming but haven’t done that yet.

I imagine the multiplayer pausing is going to break easily with players not syncing their timescale correctly and 1 player being paused while everyone else is not. So, I may potentially get rid of it. Took a lot of testing for me to get it working at all, so throwing it all away would be a shame, but oh well. It may prove to be far more trouble than what it’s worth.

The game phase timeouts were fairly straightforward. It was just starting a co-routine to countdown, and when the countdown got to the end, do some “automatic” action to advance the phase.

The phases that will have timeouts:

  • Cointoss
    • Separate timeouts for Heads/Tails decision and Kick/Receive
  • Kick off
  • Kick After Attempt

For the cointoss timeout, the automatic action will be to select whatever the player already has selected and submit that as their choice. If the player hasn’t made a selection yet, it will force heads/receive for the choices.

For the kick off, when the timeout kicks in, the player submits a kick for whatever direction they are pointing in and whatever power they have (if they started the kick yet). So if a player doesn’t kick for >30 seconds on the kick off, the kick off automatically happens.

For the kick after attempt, the kicker has 30 total seconds to choose their kick location, submit the location, and then to submit their kick power. If they don’t submit a kick before 30 seconds, the kicking goblin is automatically knocked out and the kick after attempt is “blocked.”

The phase timeouts are displayed with a timer in the top right corner.

That’s basically everything I got done in the past two weeks. Not much, but it’s something!

Next Steps…

I still need to work on adding a settings menu for the player to adjust game settings (resolution, volume, maybe keybinding, etc.) as well as just redoing the whole title screen UI to look better. That will surely be fun!

Smell ya later nerds!