The Invaders Sample Project to understand the game flow and modes with Netcode for GameObjects (Netcode) using Scene Management, Unconventional Movement Networked, and a Shared Timer between clients updated client-side with server side seeding.
Most Multiplayer games have multiple Networked Scenes, where players can join, communicate, and progress through scenes and maps together. This is a game flow: players join and establish communication together, the server determines the next scene or map, and transitions all clients to the new scene and loads the required map. This ensures players can play together.
The logic and transitions are a specified game flow. To transition in a smooth manner, you need to create game flows and back-end systems that support those flows.
Invaders implements this game-flow by creating different controller classes that handle each Game Mode such as MainMenu, a simple Networked Lobby and InGame modes, and other support classes that help with the transition from one Game Mode to another.
The backbones of the flow/system mentioned above is consisting of two main components:
SceneTransitionHandler with a lightweight state machine allows you to track clients' progress in regards to Scene Loading. It notifies the server when clients finish loading so that the other listeners are informed, by subscribing to the
NetworkedSceneManager load events and creating a wrapper around it that others can subscribe to.
At the same time, we have implemented a light State Machine to keep track of the current
SceneState. For example, the
SceneState could indicate if players are in the Init or Bootstrap scene, Start or Lobby, or InGame. You can run a different Behavior or in this case a different
UpdateLoop function, for each state.
One example of how to update the current
SceneState is in InvadersGame.cs, in the
This class has the same role as the Lobby Controller, it acts as a Manager, for a specific part of the game.
A Lobby Controller is a Manager for the lobby. This is where we applied a simple Mediator Design Pattern that restricts direct communications between the objects and forces them to collaborate only using a moderator object. In this case, the LobbyControl.cs handles Lobby interactions and state. This works hand-in-hand with the
SceneTransitionHandler, by subscribing to the
OnClientLoadedScene of that class in
OnClientLoadedScene callback is called, the custom
ClientLoadedScene function is also called. And that is the location where you add the new Player to a container that just loaded the Lobby Scene, generates user stats for it (which is just a random name), and then later sends an update to the rest of the users notifying them that someone new has joined the Lobby.
When the players join this Lobby, they all need to click Ready before the game can progress to the next scene (before the Host can start the game). After players click Ready, you send a
OnClientIsReadyServerRpc (inside the
PlayerIsReady function). When it arrives server-side, it marks the client state as ready based on its
ClientId. You keep track of if a client is ready in the
At the same time, in order to sync up with the rest of the clients and update their UI, we send a ClientRpc. The update is handled by the ClientRpc called
When all the players have joined the lobby and are ready,
CheckForAllPlayersReady to transition to the next scene.
Invaders has an easy movement type - moving only on one (horizontal) axis - which allows you to only modify the transform client-side without waiting for server-side validation. You can find where we perform the move logic in PlayerControl.cs in the
InGameUpdate function . With the help of a
NetworkTransform that is attached directly to the Player Game Object, it will automatically sync up the Transform with the other clients. At the same time, it will smooth out the movement by interpolating or extrapolating for all of them.
Games commonly have timers to display in the UI such as Start Timer, Round Timer, and Cooldowns. The Invaders sample also has a shared timer to ensure all players start the game at the same time. Otherwise, players with higher-end devices and better network access may have an unfair advantage by loading scenes and maps faster.
When you implement this kind of timer, usually you would use a
NetworkVariable<float> to replicate and display the exact time value across all clients. To improve performance, you do not need to replicate that float every Network Tick to the Clients, which would only waste network bandwidth and some minimal CPU resources.
An alternative solution is to sync only the start of the timer to the clients, and afterwards only sync the remaining value of the timer when a new client joins. For the remaining time, clients can update the timer locally. This method ensures the server does not need to send the value of that timer every Network Update tick since you know what the approximated value will be.
In Invaders we chose the second solution instead, and simply start the timer for clients via an RPC.
ShouldStartCountDown to start the timer and send the time remaining value to the client-side. This initiates and sends the value only once, indicating the game has started and how much time remains. The game then counts down locally using these values. See Update game timer Client-Side.
Example code to start the countdown:
In the case of a late-joining client, if the timer is already started, we send them an RPC to tell them the amount of time remaining.
On the client-side, use the
UpdateGameTimer to locally calculate and update the
gameTimer. The server only needs to be contacted once to check if the game has started (
m_HasGameStared is true) and the
m_TimeRemaining amount, recieved by
ShouldStartCountDown. When met, it locally calculates and updates the
gameTimer reducing the remaining time on the client-side for all players. When
m_TimeRemaining reaches 0.0, the timer is up.
Example code to update the game timer: