Context
On my last post, I mentioned my inspiration for PRGDA's game jam called What the Jam?!. I also spoke about my game idea of re-creating an old-school game called "Estop!", after seeing that the game jam theme was tradition
!
πNow I'll finish this happy fail story by showing how far I got in the gameplay aspect of this small project!π
Gameplay UI
The Gameplay UI View
In the Assets/Scenes/
folder, let's create a new scene called Gameplay
:
Note: Again, remember to add any new scenes that will be part of your final project in the project's
File > Build Settings...
.
Let's repeat the same process as with the main menu, and create a UI Document
for the main gameplay phase:
Just to have something working, let's quickly prototype a quick UI that handles all of what we currently need:
- Player info
- Connected players
- Game Status
- Current Letter used
- Total score(each player gets their own)
- Results of each round
Here's my CURRENT implementation.
β WARNING: This is going to be very ugly! π
The preview of Gameplay.uxml
:
Yep, sorry about the nightmares! π»
And I'm not even going to show the UXML contents jaja.
Setting up the Gameplay UI View Logic
Open the Gameplay
scene. Let's create a new GameObject
called GameplayUI
:
Now let's create a new script called SetupGameplayUIDocument
, and add it to the GameplayUI
game object:
using Unity.Netcode;
using UnityEngine;
using UnityEngine.UIElements;
public class SetupGameplayUIDocument : NetworkBehaviour
{
private UIDocument m_UIDocument;
private VisualElement uiDocRoot;
private Label m_gameStatusLabel;
private void Start()
{
m_UIDocument = this.gameObject.GetComponent<UIDocument>();
if (m_UIDocument == null)
return;
uiDocRoot = m_UIDocument.rootVisualElement;
PrepareStartRoundButton();
PrepareReadyButton();
PrepareGameStatusLabel();
}
private void PrepareStartRoundButton()
{
var startRoundButton = uiDocRoot.Q<Button>("startRoundButton");
if (startRoundButton == null)
return;
if(!NetworkManager.Singleton.IsServer)
startRoundButton.SetEnabled(false);
else
{
startRoundButton.clicked += () =>
{
if (ConnectedPlayersManager.Instance.confirmedPlayers.Value > 1)
{
GameStatusManager.Instance.gameStatus.Value = GameStatus.pickingLetter;
}
};
}
}
private void PrepareReadyButton()
{
var readyButton = uiDocRoot.Q<Button>("readyButton");
if (readyButton != null)
{
readyButton.clicked += OnReadyButtonClicked;
}
}
private void OnReadyButtonClicked()
{
if (IsServer)
{
IncrementConfirmedPlayers();
}
else
{
//TODO: FIX, does not work from the client.
//I'm a client, so please tell the server to count me up
HelpClientIncrementConfirmedPlayerCountServerRPC();
}
}
private void IncrementConfirmedPlayers()
{
ConnectedPlayersManager.Instance.confirmedPlayers.Value += 1;
Debug.Log("Confirmed Players:" +
ConnectedPlayersManager.Instance.confirmedPlayers.Value);
}
[ServerRpc]
private void HelpClientIncrementConfirmedPlayerCountServerRPC()
{
IncrementConfirmedPlayers();
}
private void PrepareGameStatusLabel()
{
m_gameStatusLabel = uiDocRoot.Q("gameStatusLabel").Q<Label>();
GameStatusManager.Instance.gameStatus.OnValueChanged += OnGameStatusChanged;
}
private void OnGameStatusChanged(GameStatus oldValue, GameStatus newValue)
{
m_gameStatusLabel.text = newValue.ToString();
}
}
Note: β There are some classes that have not been shown yet, they'll be explained later in this post.
Firing up the Network Session!
Let's open the MainMenu
scene again.
Now to properly start the network session as the host or client, you need to call the necessary functions to the NetworkManager
. Let's create a new script called SetupMainMenuUIDocument
, and add it to the MainMenu
GameObject.
Insert this code into the file:
using Unity.Netcode;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
public class SetupMainMenuUIDocument : MonoBehaviour
{
private UIDocument m_mainMenuUIdocument;
private void Start()
{
m_mainMenuUIdocument = gameObject.GetComponent<UIDocument>();
if (m_mainMenuUIdocument == null)
return;
var uiDocRoot = m_mainMenuUIdocument.rootVisualElement;
PrepareButtons(uiDocRoot);
}
private void PrepareButtons(VisualElement uiDocRoot)
{
var hostButton = uiDocRoot.Q<Button>("HostButton");
hostButton.clicked += () =>
{
NetworkManager.Singleton.StartHost();
NetworkManager.Singleton.SceneManager.LoadScene("Gameplay", LoadSceneMode.Single);
};
var joinButton = uiDocRoot.Q<Button>("JoinButton");
joinButton.clicked += () =>
{
NetworkManager.Singleton.StartClient();
};
var quitButton = uiDocRoot.Q<Button>("QuitButton");
quitButton.clicked += () =>
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.ExitPlaymode();
#else
Application.Quit(0);
#endif
};
}
}
As you can see, it assumes that the owner of this component holds the proper UIDocument
to connect the buttons to the right callback functions. Here's what I get when I play it now:
We're now successfully starting the network session!
More Gameplay!
Now we're gonna move a bit faster here. By this point I kept working on the project, without the small stops to update this blog entry. But I'll give a good summary of what happened.
Note: β Remember, this is my participation in a
game jam
. What you're seeing in these posts does NOT represent top quality work or project structure. This is for fun, more than anything π
Let's create some new objects in our Gameplay
scene:
Note: All 3 of these GOs have the
NetworkObject
component tied to it, which is strictly required to access NGO's network capabilities.
Let's go through these 3 new GO's...
GameStatusManager
The sole purpose of this object is to be a singleton that holds the current state of the game. Other classes can subscribe to the event of a NetworkVariable
changing:
using Unity.Netcode;
public enum GameStatus
{
waitingToStart,
pickingLetter,
playersAnswering,
postRound
}
public class GameStatusManager : SingletonNetwork<GameStatusManager>
{
public NetworkVariable<GameStatus> gameStatus { get; private set; } =
new NetworkVariable<GameStatus>(
GameStatus.waitingToStart,
NetworkVariableReadPermission.Everyone);
}
ConnectedPlayersManager
The purpose of this object is to be a singleton that holds the current amount of players connected. Let's also add this script to it:
using Unity.Netcode;
using UnityEngine;
public class ConnectedPlayersManager : SingletonNetwork<ConnectedPlayersManager>
{
public NetworkVariable<int> connectedPlayers { get; private set; } =
new NetworkVariable<int>(1, NetworkVariableReadPermission.Everyone);
public NetworkVariable<int> confirmedPlayers { get; private set; } =
new NetworkVariable<int>(0, NetworkVariableReadPermission.Everyone);
void Start()
{
NetworkManager.Singleton.OnClientConnectedCallback += OnNewPlayerConnected;
NetworkManager.Singleton.OnClientDisconnectCallback += OnPlayerDisconnected;
}
private void OnNewPlayerConnected(ulong clientId)
{
connectedPlayers.Value += 1;
Debug.Log("Connected players: " + connectedPlayers.Value);
}
private void OnPlayerDisconnected(ulong clientId)
{
connectedPlayers.Value -= 1;
Debug.Log("Connected players: " + connectedPlayers.Value);
}
}
GameRoundBehavior
The purpose of this object is to hold the behavior (host or client) of a new game round. It also holds the following script, which adds a new row of values reacts to
using UnityEngine.UIElements;
using Unity.Netcode;
using UnityEngine;
public class GameRoundBehavior : NetworkBehaviour
{
public UIDocument gameplayUIDocument;
private VisualElement m_uiDocRoot;
void Start()
{
GameStatusManager.Instance.gameStatus.OnValueChanged += OnGameStatusChanged;
if (gameplayUIDocument != null)
{
m_uiDocRoot = gameplayUIDocument.rootVisualElement;
}
}
private void OnGameStatusChanged(GameStatus oldValue, GameStatus newValue)
{
Debug.Log("New value is " + newValue);
if (newValue == GameStatus.pickingLetter)
{
var gameRoundScrollView = m_uiDocRoot.Q<ScrollView>("gameRoundScrollView");
if (gameRoundScrollView != null)
{
gameRoundScrollView.Add(new GameRoundRowItem());
}
}
}
}
And this is the custom VisualElement
that gets created when a new round starts. It contains all of the necessary fields to insert the answers:
using UnityEngine.UIElements;
public class GameRoundRowItem : VisualElement
{
public GameRoundRowItem()
{
var nameText = new TextField(string.Empty)
{
name = "name",
value = "nombre..."
};
this.Add(nameText);
var lastNameText = new TextField(string.Empty)
{
name = "lastName",
value = "apellido..."
};
this.Add(lastNameText);
var countryOrCounty = new TextField(string.Empty)
{
name = "countryOrCounty",
value = "pueblo o paΓs..."
};
this.Add(countryOrCounty);
var showOrMovie = new TextField(string.Empty)
{
name = "showOrMovie",
value = "pelΓcula o serie..."
};
this.Add(showOrMovie);
var animalOrThing = new TextField(string.Empty)
{
value = "animal o cosa..."
};
this.Add(animalOrThing);
var scoreLabel = new Label("...");
scoreLabel.style.color = new StyleColor(UnityEngine.Color.black);
this.Add(scoreLabel);
this.style.flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row);
}
}
Conclusion!
Here's as far as I got! π
- Project setup with UGS (did too early IMO). Not needed with just NGO.
- Host starts & clients connect
- At least the game status changes is recognized, and a new round "starts"
Completed Goals
- Joined yet another game jam, even if just partiallyπͺπΎ
- Practiced with Unity multiplayer development πΆ
- Practiced with Unity's UI toolkit! π
- Practiced documenting my progress as my game jam project grows! π
Lessons Learned from this Fail
- I basically know the setup process for NGO + UGS by heart! π
- NGO, and similar technologies save developers a lot of time! β
- Multiplayer game dev is STILL very tough, DO NOT go for it in a very short time! π£
- UXML panel settings - study them! π«
- It's completely possible to start a network session without registered prefabs! π€
- When first starting to test a multiplayer game, LOG EVERYTHING! π
Please let me know what you think!
- In that short time, what multiplayer game would you have built?
- What's the next game jam you're joining?
- What multiplayer technology would you use for your next project?
πππ Happy coding! πππ