What the Jam?! - 2022 - pt. 2

A happy fail, multiplayer style!

What the Jam?! - 2022 - pt. 2

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: image.png

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: image.png

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 : image.png

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: image.png

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: Screen Recording 2022-07-31 at 12.28.54 AM.gif

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: image.png

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!

devenv_IdjC2CC8lg.gif 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! πŸŽ‰πŸŽ‰πŸŽ‰

Β