Search Unity

Games Competitive Online Card Game

Discussion in 'Projects In Progress' started by jabensen, Feb 18, 2024.

  1. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    Hello! I'm a university software engineering student who was introduced to Unity through an elective game development class. I'm currently a senior, and for whatever reason, the university decided to assign us our capstone projects. I was fortunate enough to be assigned to work on a VR project using Unity, but this thread is going to devoted to something else.

    A mutual friend of my older brother and I introduced us to a friend of his who has recently left his IT job because he was spending all his time working on a competitive card game instead of asking people if they had tried turning it on and off again. He had already created a few proto-type decks, so we spent some hours hanging out and playing cards. The game is insanely fun. It’s akin to games like MTG, but with a unique twist that holds a lot of depth while being very easy to pick up and play.

    Our mutual friend and this new acquaintance had decided that they were going to start a company with this card game as their flagship product. They already had a few artists on board, but what they really needed were software developers. My brother majored in organic chemistry, but after working for about a year, he realized that he hated his job and started looking for a way out. He signed up for one of those web-development boot camps that pay you to train in software development for 9 months with the caveat that you are then contractually obligated to work at a fixed salary for a couple years. He did his time, and then got hired as a back-end developer. We talked it over, and we agreed to join this start-up venture. Since I'm currently obligated to work on a Unity project for the university, we decided that our minimum viable product (MVP) would include a client application built with the Unity game engine (my responsibility) and a server-side web application built with spring boot (my brother's responsibility).

    For my part, I need to create a menu system that supports multiple pages. The Start page will allow players to login and/or create an account. Once they’ve successfully logged in, they will be transported to a Multiplayer page that allows them to create a new game lobby or select and join an existing game lobby from a list of created game lobbies. Once a player has created a new game or selected an existing game, the Multiplayer page will begin displaying a new menu with the name of the game lobby and the names of the players who are currently in the lobby. When two players are in the same game lobby, they will be able to press the start game button, at which point they will be transported to the Game page.

    The current plan is to keep things simple with a 2D game, so I intend to build it into the menu system like any other page. To ensure that players can read the card text and appreciate the kick-ass artwork, hovering your mouse over a card on the game board will result in a magnified copy of the card appearing on screen, and the magnified card needs to appear in different positions relative to the smaller card depending on which zone the card belongs to. One of our goals for the minimum viable product is to simulate the feeling of playing a card game in real life. To that end, every card will be created when the game starts, and the Unity application will maintain some sort of game state context that is continually updated as cards are moved from zone to zone. Players will be able to move cards by clicking, dragging, and dropping them. If they were dropped within the boundaries of a valid zone, the card should automatically snap into position with the other cards in that zone. The game-state context will also need to be updated to reflect those changes.

    When the game client makes a get game-state request to the server, it will receive a json string that represents a game-state object with data such as gameId, playerOne, playerTwo, and a list of “card-states”. Each card state will include the cardId, cardName, the zone that the card is in, the position that the card occupies in that zone, and the player who is currently in control of the card. When the unity application receives that string from the server, it needs to automatically rearrange the game board to accurately reflect the new gamestate and simultaneously update the context. When a player moves a card from one zone to another, the Unity application needs to construct and post its own game state json string to the server, but it should only include the card that moved and any cards that changed positions due to the move. The Game page will also need to include separate menus for cards that are in zones which are not visible on the game board. For example, cards in the deck need to exist in a separate deck menu that players can access by pressing a button.

    I will do my best to continually update this thread throughout development of the project. I mostly intend to use this as a development journal of sorts, so UML diagrams, source code, and discussion of problems encountered and how they were (hopefully) solved are likely inclusions.
     
    Last edited: Feb 22, 2024
    CodeRonnie likes this.
  2. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    Since most of the non-game board UI is going to be directly dependent on the web-application, I decided to start with the gameboard itself as it will provide me with a great deal of development that can occur while the server is being setup. The gameboard will exist under a canvas in the hierarchy, so my hope is that it can slot into a larger menu system as one of multiple pages later on.

    There's a lot to consider when it comes to the game board, but the most important design decisions relate to how we setup the cards, the zones, and their relationship to each-other. The obvious path forward would be a GameObject for each card with a Card MonoBehaviour that implements the various interfaces needed to allow the player to interact with the cards. That said, I spent a bit of time searching for examples of playing card implementations in Unity, and my take-away was that it would be better to create a distinction between the data of the card and the display of the card. We still have a Card class, but its not a MonoBehaviour. The idea is to setup a sort of database class to construct a static list that holds objects of type Card. We'll then create another class that I'm calling "CardSlot" to display the card and implement the UI interfaces.

    //
    using UnityEngine;
    [System.Serializable]
    public class Card
    {
    public int CardID { get; set; }
    public string CardName { get; set; }
    public string CardPower { get; set; }
    public string CardEffect { get; set; }
    public Sprite CardSprite { get; set; }

    public Card()
    { //Empty constructor that we may use down the line if we want to set card data from server

    }

    public Card(int cardID, string cardName, string cardPower, string cardEffect, Sprite sprite)
    { //Constructor we will use to instantiate each card when the game starts.
    this.CardID = cardID;
    this.CardPower = cardPower;
    this.CardName = cardName;
    this.CardEffect = cardEffect;
    this.CardSprite = cardSprite;
    }
    }
    //

    //Note that this class doesn't need to be implemented as a MonoBehaviour, but we don't have //another Monobehaviour to construct it yet.
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class CardDatabase : MonoBehaviour
    {
    private static readonly List<Card> cardBaseList = new List<Card>();

    private void Awake()
    { //Test Data for the cards. We have a folder named Resources with appropriately named images
    cardBaseList.Add(new Card(0, "Sum Nam", "14", "None", Resources.Load<Sprite>("A")));
    cardBaseList.Add(new Card(1, "Sum Otto Nam", "30", "None", Resources.Load<Sprite>("B")));
    cardBaseList.Add(new Card(2, "Hello", "7", "None", Resources.Load<Sprite>("C")));
    cardBaseList.Add(new Card(3, "World", "5", "None", Resources.Load<Sprite>("D")));
    cardBaseList.Add(new Card(4, "Goodbye", "5", "None", Resources.Load<Sprite>("E")));
    cardBaseList.Add(new Card(5, "Moon", "15", "None", Resources.Load<Sprite>("F")));
    cardBaseList.Add(new Card(6, "This", "12", "None", Resources.Load<Sprite>("G")));
    cardBaseList.Add(new Card(7, "Is", "29", "None", Resources.Load<Sprite>("H")));
    cardBaseList.Add(new Card(8, "A", "3", "None", Resources.Load<Sprite>("I")));
    cardBaseList.Add(new Card(9, "Name", "6", "None", Resources.Load<Sprite>("J")));
    }

    public Card GetCardFromId(int Id)
    {
    return cardBaseList[Id];
    }

    public List<Card> GetCardBaseList()
    {
    return cardBaseList;
    }
    }
    //

    //
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using TMPro;
    using UnityEngine.EventSystems;

    public class CardSlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IPointerDownHandler, IDragHandler, IBeginDragHandler, IEndDragHandler
    {
    [field: SerializeField] public bool IsDisplaying { get; private set; }
    [field: SerializeField] public Card CardInSlot { get; private set; }
    private Image image;
    private Sprite slotSprite;
    private TextMeshProUGUI textPower;
    private static readonly int childIndexTextPower = 0;
    private RectTransform cardSlotRT;
    private Canvas canvas;


    private void Awake()
    {
    CardInSlot = null;
    IsDisplaying = false;
    image = GetComponent<Image>();
    slotSprite = image.sprite;
    textPower = this.gameObject.transform.GetChild(childIndexTextPower).gameObject.GetComponent<TextMeshProUGUI>();
    cardSlotRT = GetComponent<RectTransform>();
    canvas = GetComponent<Canvas>();

    }

    public void OnPointerEnter(PointerEventData eventData)
    {
    //We're going to use this to trigger the magnification effect.
    //An easy way to do this would be to create a copy of the cardslot,
    //scale it up, and make it a child of the card slot.
    //We will need fields for the various references and additions to DisplayCard() and DisplaySlot()
    }

    public void OnPointerExit(PointerEventData eventData)
    {
    //if (IsDisplaying)
    //{
    //childToMagnify.SetActive(false);
    //}
    }

    public void OnPointerDown(PointerEventData eventData)
    {
    //if (IsDisplaying)
    //{
    //childToMagnify.SetActive(false);
    //}
    }
    public void OnDrag(PointerEventData eventData)
    {
    if(IsDisplaying)
    {
    cardSlotRT.anchoredPosition += eventData.delta / canvas.scaleFactor;
    }
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
    //To Do: We will need to handle things like forcing other cardslots to stop blocking raycasts
    }

    public void OnEndDrag(PointerEventData eventData)
    {
    //To Do: We will need to handle clean-up that occurs at the end of a drag.
    //Eventually this will need to check if the drag ended via a drop on a valid zone.
    }

    public void DisplayCard(Card card)
    {
    if (!IsDisplaying)
    {
    image.sprite = card.CardSprite;
    textPower.text = card.CardPower.ToString();
    IsDisplaying = true;
    CardInSlot = card;
    //magnifyImage.sprite = card.CardSprite;
    }
    else
    {
    Debug.LogError("The slot already contains a card.");
    }
    }

    public void DisplaySlot()
    {
    if(IsDisplaying)
    {
    image.sprite = slotSprite;
    textPower.text = "";
    IsDisplaying = false;
    CardInSlot = null;
    //magnifyImage.sprite = slotSprite;
    }
    else
    {
    Debug.LogError("The slot does not contain a card.");
    }
    }
    }
     
    Last edited: Feb 18, 2024
  3. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    Now that we have a way to generate, display, and interact with cards, we need to define the various zones that the cards will inhabit throughout the game. Combining the concept of the card slot with Unity's Horizontal Layout Group resulted in an implementation that solves the problem with a lot less work than I had anticipated.

    At the moment, card slots are setup with a default sprite that lets the player know its an empty card slot. In this state, players can't interact with the card slot. Once CardSlot.DisplayCard(Card card) is invoked, the card slot begins displaying the card, and user interaction is enabled. Showing empty card slots would cue the player into the number of cards that are allowed to be in each zone, and we can build the AddCardSlot and AddCard functions in a way that allows player actions to invoke AddCard on a zone without any available card slots, but doing so results in the card reverting to its previous position.

    I chose to designate Zone as an abstract class such that each zone is its own subclass. I'm envisioning a future where the zones override certain functions to implement unique behavior (ex: different effect triggers), and I'd like to be able to add new functionality to classes such as deck to initiate a single player game for testing.

    //
    //
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using CardGame;

    namespace CardGame
    {
    public enum Zones
    {
    Deck = 0,
    HandPlayerOne = 1,
    HandPlayerTwo = 2,
    Loop = 3,
    FieldPlayerOne = 4,
    FieldPlayerTwo = 5,
    Grave = 6,
    Exile = 7,
    }
    }

    public abstract class Zone : MonoBehaviour //An instance of this class is never instantiated. It is a base class that //other classes derive from.
    {
    protected List<CardSlot> slots = new List<CardSlot>();
    protected List<Card> cards = new List<Card>();
    [field: SerializeField] public int NumSlots { get; protected set; }
    [field: SerializeField] public int NumCards { get; protected set; }
    [field: SerializeField] public Zones ZoneEnum { get; set; }
    [SerializeField] private GameObject cardSlotPrefab;

    protected void SetInitialSlots(int num)
    {
    NumSlots = 0;
    NumCards = 0;
    for(int i = 0; i < num; i++)
    {
    GameObject prefabInstantiated = Instantiate(cardSlotPrefab, transform.position, Quaternion.identity);
    prefabInstantiated.transform.SetParent(transform);
    }
    }

    public void AddCardSlot() //This requires card slots to be direct children of the zone GameObject.
    {
    foreach (Transform child in transform)
    {
    GameObject childGameObject = child.gameObject;
    if(!childGameObject.activeSelf)
    {
    childGameObject.SetActive(true);
    CardSlot cardSlot = childGameObject.GetComponent<CardSlot>();
    slots.Add(cardSlot);
    cardSlot.SetAsLastChild();
    numSlots++;
    return;
    }
    }
    }

    public void RemoveCardSlot() //This requires card slots to be direct children of the zone GameObject.
    { //This function only removes a card slot if an active card slot is not displaying a card.
    foreach(Transform child in transform)
    {
    GameObject childGameObject = child.gameObject;
    CardSlot cardSlot = childGameObject.GetComponent<CardSlot>();
    if(!cardSlot.IsDisplaying && slots.Contains(cardSlot))
    {
    slots.Remove(cardSlot);
    childGameObject.SetActive(false);
    numSlots--;
    return;
    }
    }
    }

    public void AddCard(Card card) //AddCard(Card card) invokes CardSlot.DisplayCard(Card card) and then adds the card to the cards list.
    {
    foreach (CardSlot cardSlot in slots)
    {
    if (!cardSlot.IsDisplaying)
    {
    cardSlot.DisplayCard(card);
    cards.Add(card);
    numCards++;
    return;
    }
    }
    }

    public void RemoveCard(Card card) //RemoveCard(Card card) invokes //CardSlot.DisplaySlot() and then removes the card from the cards list.
    {
    foreach (CardSlot cardSlot in slots)
    {
    if(cardSlot.CardInSlot == card)
    {
    cardSlot.DisplaySlot();
    cards.Remove(card);
    numCards--;
    return;
    }
    }
    }

    public void MakeLastCard(Card targetCard)
    {
    if(!cards.Contains(targetCard))
    {
    Debug.LogError("Can't make last card if the card is not in the list.");
    }
    else
    {
    cards.RemoveAt(cards.IndexOf(targetCard));
    cards.Add(targetCard);
    }
    }
    }
     
    Last edited: Feb 19, 2024
  4. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
  5. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    The class diagram above is a work in progress for the direction I'm taking the game board. There are some extraneous bits and things that I probably missed, but its a pretty good representation of how the software is and/or will be structured.

    It was fairly straight forward to setup the add/remove functions for cards and cardslots, but implementing the ability for players to click on cards and move them to new zones was a huge pain in the ass. This was mostly because I didn't really understand how Unity's UI Interfaces work until I started working with them.

    Rather than having each zone implement an IDropHandler interface, I decided to move that responsibility into a new class that I'm calling "DropReceiver". For every object of type Zone, there is another object of type DropReceiver. Here's how it works:

    //
    //
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.EventSystems;
    using Dragon;
    public class DropReceiver : MonoBehaviour, IDropHandler //This class is attached to the GameObject that displays the image of its associated Zone.
    { //This class detects when a card slot is dropped on a zone image so that the zone manager can be told to move a card from one zone to another.
    private Zone zoneReceiver; //Assigned at Start() and then remains static
    private Zone zoneSender; //Assigned null at Start() and then is reassigned based on drops received.
    private Card card; //Assigned null at Start() and then is reassigned based on drops received.
    private ZoneManager zoneManager; //Assigned on Awake() by finding game object of same type.

    void Awake()
    {
    zoneManager = FindObjectOfType<ZoneManager>().GetComponent<ZoneManager>();
    }
    void Start()
    {
    zoneReceiver = zoneManager.GetZoneReference(gameObject.tag);
    zoneSender = null;
    card = null;
    }
    public void MoveCardCallBack() //This should only be invoked by CardSlots
    {
    if (zoneReceiver == null || zoneSender == null || card == null)
    {
    Debug.LogError("MoveCardCallBack() Invoked inappropriately.");
    }
    else
    {
    zoneManager.MoveCard(zoneReceiver, zoneSender, card);
    zoneSender = null;
    card = null;
    }
    }
    public void OnDrop(PointerEventData eventData) //Note: This class is currently assuming that the only thing that could possibly be dropped is a card slot with a card.
    { //If we end up adding additional drag and drop GameObjects, we will need to add checks to ensure that the dropped object was a card slot.
    if (eventData.pointerDrag != null) //Note: Dragging is disabled when a card slot isn't displaying a card.
    {
    CardSlot slot = eventData.pointerDrag.gameObject.GetComponent<CardSlot>();
    zoneSender = slot.zone;
    card = slot.cardInSlot;
    slot.dropReceiver = this;
    } //After the callback, slot will set slot.dropReceiver back to null.
    }
    }
    //
    //

    There were also significant additions made to CardSlot, some of which was expected, but there was also a lot of required micromanagement that I didn't consider. The following screen shot should serve as a suitable example to illustrate the issues I encountered.

    upload_2024-2-20_19-53-39.png

    As mentioned in the original post, one of the requirements for this project is to display a game board small enough that all active cards are visible on the screen. In order to make this work, the plan was always to create a separate magnified version of the card that would appear when the user hovers their mouse over the smaller card on the board.

    To accomplish this, I went with the first solution that popped into my head. The CardSlot GameObjects have another scaled up version of themselves nested within the hierarchy, but its only set active when the CardSlot is displaying a card and the user has hovered their mouse over the card.

    The problems I didn't consider mostly revolve around render order. With Unity, UI components under the same canvas have a render order based around their position in the hierarchy. As you might have guessed, my magnified cards were frequently rendering below the images from other zones and UI elements on the board. To fix this, I had to ensure that all of the base GameObjects for each zone share the same parent in the hierarchy. Then, anytime the users mouse pointer hovers over an active CardSlot, I have the CardSlot access the base GameObject for the zone it belongs to and move that GameObject below the other zones in the hierarchy. I also had to do the same thing for the CardSlot's own gameObject to ensure that it would render over other CardSlots in the same zone.

    The expected micromanagement deals with raycasts. When the user begins dragging a CardSlot, the CardSlot calls the zone it belongs to, which in turn calls the ZoneManager (A new class with references to all zones in the game). The ZoneManager then calls every zone so that the zones can force all of their active CardSlots to stop blocking Raycasts. This ensures that other CardSlots don't interfere with the player's ability to drop a card into a new zone.

    Its worth noting that in my current implementation, Each zone instantiates a set number of CardSlots from a prefab when the game begins. These CardSlots initialize as inactive GameObjects. In order to add a card to a zone, a call is made to Zone.AddCardSlot() which searches for an inactive CardSlot in the hierarchy, sets it active at the GameObject level, and then adds the CardSlot component of the GameObject to the Zone.slots list. A call can then be made to Zone.AddCard(Card card) which searches through the Zone.slots list to find a CardSlot that isn't displaying a card. If one is found, a call is made to CardSlot.DisplayCard(Card card), and then that Card is also added to the Zone.cards list.

    The nice thing about this setup is that the list of CardSlots in each zone only includes the CardSlots that are currently active. This makes it very easy to apply changes to every active CardSlot without worrying about null references to CardSlots that are inactive.
     
    Last edited: Feb 21, 2024
  6. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    There was one other issue that came up when implementing the ability for players to drag and drop cards into different zones. To truly explain the problem, I'll have to provide an overview of the sequence of events that occur when the user is dragging and dropping cards.

    User hovers mouse over card:
    The Zone moves to bottom of hierarchy,
    The magnified card becomes active.

    User clicks on card:
    The magnified card becomes inactive.

    User begins moving their mouse pointer while holding the click:
    The game object displaying the card moves to bottom of hierarchy.
    A call is made that stops all active card slots from blocking raycasts.
    The game object displaying the card begins ignoring the layout group it's in.

    Every frame while user is still dragging the card:
    The card's position changes relative to the users mouse pointer.

    The user ends dragging of the card by releasing the mouse button:
    A call is made that causes all active card slots to block raycasts again.
    the game object displaying the card stops ignoring its layout group.

    The game object displaying the card being dragged always needs to return back to its previous position when the user stops dragging it, even if it was successfully dropped on another zone. We don't actually move the game object from one zone to another, we just make it look like we did. On a successful zone change, the card slot will snap back into position in the zone it was originally in, stop displaying a card, and become inactive at the game object level. Then, an inactive card slot that is not displaying a card will become active in the zone the card is being sent to. That card slot then starts displaying the card.

    To the player, it appears as though they just dragged and dropped the card, and then it snapped into position with the other cards in that zone. All that other stuff executes so fast that the player doesn't perceive it at all.

    Here's the problem: The OnDrop(PointerEventData eventData) function implemented by the DropReceiver class is executed by the game engine before the OnEndDrag(PointerEventData eventData) function implemented by the CardSlot class. It results in game breaking behavior with visible bugs and broken card slots.

    When I finally realized what was happening, I knew that I had to setup a callback from DropReceiver to the CardSlot that was dropped. I am by no means an expert with C#, so after reading through some documentation and watching some quick tutorials, I had convinced myself that I needed to learn about Delegates to solve this problem elegantly. It turns out that there are a lot of very in depth tutorials on C# delegates that all teach you completely useless ways to use delegates, but very little in depth tutorials on useful ways to use delegates. Since this is the minimum viable product I'm working on, I ultimately decided that the juice wasn't worth the squeeze in this particular instance, so this is how I solved the problem:

    CardSlot now has a reference to an object of type DropReceiver as a class attribute, which is set to null by default. When a player successfully drops a card on a valid DropReceiver, that instance of DropReceiver is able to acquire a reference through the PointerEventData passed to the OnDrop(PointerEventData eventData) function.

    CardSlot slot = eventData.pointerDrag.gameObject.GetComponent<CardSlot>();

    Once DropReceiver has the slot, it can be used to figure out which zone the cardslot belongs to and which card its displaying. These are both assigned to references which are class attributes, so they persist outside of this function.

    zoneSender = slot.zone;
    card = slot.cardInSlot;

    Since every DropReceiver corresponds to a zone, they all have a reference to their corresponding zone.

    DropReceiver now has the receiver, sender, and the card being sent, but we can't initiate MoveCard() until the CardSlot's OnEndDrag cleanup has finished. So, instead of calling ZoneManager.MoveCard(Zone, Zone, Card),
    we do this:

    slot.dropReceiver = this;

    What did that do? It changed the CardSlot's DropReceiver reference from null to the DropReceiver that it was dropped on.

    Now, the engine is going to invoke the CardSlot's OnEndDrag function:

    public void OnEndDrag(PointerEventData eventData)
    {
    canvasGroup.blocksRaycasts = true;
    layoutElement.ignoreLayout = false;
    zoneManager.BlockRaycasts();
    if(dropReceiver != null) //dropReceiver should be null until a drop is received.
    {
    dropReceiver.MoveCardCallBack();
    dropReceiver = null;
    }
    else //If no drop is received, the cardslot will return to the original layout group
    {
    zone.MakeLastCard(cardInSlot); //This will change the position of the card in zone.cards.
    }
    }

    After the CardSlot has done its OnEndDrag business, it checks if its DropReceiver reference is not null, which will only be the case if it was just dropped on a valid DropReceiver. If it isn't null, CardSlot uses that reference to call dropReceiver.MoveCardCallBack(), and then sets its dropReceiver reference back to null.

    public void MoveCardCallBack() //This should only be invoked by CardSlots
    {
    if (zoneReceiver == null || zoneSender == null || card == null)
    {
    Debug.LogError("MoveCardCallBack() Invoked inappropriately.");
    }
    else
    {
    zoneManager.MoveCard(zoneReceiver, zoneSender, card);
    zoneSender = null;
    card = null;
    }
    }

    I still plan to learn more about Delegates at some point, but this was a hell of a lot simpler for me.
     
  7. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    As I moved away from the game board and started thinking more about how the application would function as a whole, it started to make sense to me to view the entire gameboard and all of the associated classes as part of a "GamePage" within a larger menu system.

    With this in mind, I started work on a MenuPageManager which would maintain a list of MenuPage classes. This was the result:

    upload_2024-3-4_2-18-55.png
     
  8. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
  9. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
  10. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    And here is the base MenuPage class:

    upload_2024-3-4_2-22-14.png
     
  11. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    So far, I've implemented new classes that derive from MenuPage to handle account creation, user login, creating a new game, joining an existing game, initializing the game, and then both getting and posting gamestate data to and from the server.

    I had never seen an example of a game that doesn't use some sort of high-level networking library for multiplayer, and I had very little prior networking experience. The initial difficulty was that I built the skeleton of the Menu system without really considering how each page would make a connection to the server. I could've included post and get request coroutines in the MenuPage base class, but I had originally planned on using a single class to handle all of the network requests, and I still believe that this was the right call.

    I started by creating a very explicit series of functions from the page class to the page manager to the network manager and then to a coroutine in the network manager that would end by setting off another chain of function calls back to the page, which would then use the server's response to alter data and make visual changes to the UI.

    This was a terrible approach, but it was enough to reveal the pattern that I used to improve the situation. Now, MenuPageManager's reference to the NetworkManager is a public property that all pages can access, which means that MenuPage classes can directly invoke functions in the NetworkManager.

    Next, I figured out my previous issues with delegates and used it to further simplify the situation. Now, when a page invokes a function in the NetworkManager, it passes a delegate which has been pre-assigned to a "return function" in the page class. This means that the NetworkManager only needs a single coroutine for post, and a single coroutine for get. These coroutines accept string url, string json (post request only), and RequestReturnDelegate callback. After the request is made to the server, the delegate is invoked and the response from the server is passed back to the page in the process.

    There's currently three page classes each handling multiple requests, but I'll use the StartPage's login functionality to demonstrate the process with code:
     
    Last edited: Mar 4, 2024
  12. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
  13. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    LoginPressed() is invoked when the user presses the login button. It does some basic client side verification, and if the conditions are met, it defines a delegate and assigns the ReturnLogin(bool, string) function to the delegate. It passes the loginUsername and loginPassword strings to the NetworkManager, along with the delegate.

    Note that the signature for this delegate is defined within the Dragon namespace in the file containing the NetworkManager class:

    upload_2024-3-4_2-44-50.png

    Both NetworkManager and all of the page classes are "using Dragon;"

    So, the start page invoked NetworkManager.Login(string, string, RequestReturnDelegate). That function will then start the coroutine that actually makes the http request.

    upload_2024-3-4_2-46-40.png

    upload_2024-3-4_2-47-17.png
     
    Last edited: Mar 4, 2024
  14. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    It took a situation like this for me to really understand why Delegates are useful. NetworkManager doesn't know that these page classes even exist, yet it can invoke functions in all of them.
     
  15. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    These page designs are by no means final, but here's how the UI currently looks:

    upload_2024-3-4_2-54-18.png
     
  16. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    upload_2024-3-4_2-54-37.png

    The start page contains a few more functions to handle account creation and toggling back and forth between the two prompts.
     
  17. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    When you create an account successfully, the game client will automatically trigger a login request using the same account info, so in either case, you will be logged in and immediately transported to the multiplayer page, which is still using default Unity UI elements and a basic version of my own custom coded UI element for the lobby records. I built the multiplayer page to instantiate 10 of these LobbyRecord game objects and then set them inactive until the user presses the refresh button which currently provides the most recent 10 games. Moving forward, the plan is to implement a paging system for these lobby records within the multiplayer page of the Menu. As the user cycles through these "pages" of lobby records, those ten gameObjects will be updated to represent the next 10 games/lobbies in the list.

    Here is the page before the user presses refresh:
    upload_2024-3-4_3-2-46.png

    and then this is after refresh has been pressed:

    upload_2024-3-4_3-5-32.png

    When the user has clicked on a lobby record:

    upload_2024-3-4_3-6-19.png
     
  18. jabensen

    jabensen

    Joined:
    Nov 1, 2023
    Posts:
    18
    Updated Class Diagram (heavily abstracted):

    upload_2024-3-5_5-3-14.png