2
\$\begingroup\$

I'm looking for some guidance on how to develop a card combat system similar to Legends of RuneTerra auto combat. I'm a bit lost on how to get the proper card info from each drop zone (5 each player) and how to compare them.

I'm trying to run a combat function when both players have pressed their End Turn button again during the Execute phase. Currently, pressing the button just disconnects a player (there was a Red jester card across from the Blue one).

enter image description here enter image description here

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Mirror;

public class CombatManager : NetworkBehaviour
{
    // Reference to the GameManager
    public GameManager gameManager;

    // Reference to the PlayerManager
    public PlayerManager playerManager;

    // Start is called before the first frame update
    void Start()
    {
        // Find references to GameManager and PlayerManager
        gameManager = GameObject.Find("GameManager").GetComponent<GameManager>();
        playerManager = GameObject.Find("PlayerManager").GetComponent<PlayerManager>();
    }

    // Method to compare power and health values of cards in the first drop zones of both players
    public void CompareCards()
    {
        // Get the card GameObjects from the first drop zones of both players
        GameObject allyCardObject = playerManager.AllyDropZone1?.transform?.GetChild(0)?.gameObject;
        GameObject enemyCardObject = playerManager.EnemyDropZone1?.transform?.GetChild(0)?.gameObject;

        // Check for null references
        if (allyCardObject == null || enemyCardObject == null)
        {
            Debug.LogError("One or both card GameObjects are null.");
            return; // Exit the method to prevent further execution
        }

        // Get the Card components attached to the card GameObjects
        Card allyCard = allyCardObject.GetComponent<Card>();
        Card enemyCard = enemyCardObject.GetComponent<Card>();

        // Check for null Card components
        if (allyCard == null || enemyCard == null)
        {
            Debug.LogError("One or both Card components are null.");
            return; // Exit the method to prevent further execution
        }

        // Display the initial health values before combat
        Debug.Log("Ally Card Initial Health: " + allyCard.health);
        Debug.Log("Enemy Card Initial Health: " + enemyCard.health);

        // Subtract the power of the enemy card from the health of the ally card
        allyCard.health -= enemyCard.power;

        // Display the updated health values after combat
        Debug.Log("Ally Card Updated Health: " + allyCard.health);
        Debug.Log("Enemy Card Health: " + enemyCard.health);

        // Check if the ally card's health is less than or equal to 0
        if (allyCard.health <= 0)
        {
            // Destroy the ally card GameObject if its health is <= 0
            NetworkServer.Destroy(allyCardObject);
        }

        // Notify clients to update the displayed cards after combat
        RpcUpdateCombatResult(allyCardObject, allyCard.health);
    }

    // Method to notify clients to update the displayed cards after combat
    [ClientRpc]
    private void RpcUpdateCombatResult(GameObject cardObject, int newHealth)
    {
        // Update the health of the card on the client side
        Card card = cardObject.GetComponent<Card>();
        card.health = newHealth;

        // Implement logic to update the UI or perform other actions based on the combat result
    }
}
public class PlayerManager : NetworkBehaviour
{
    //Start of all network shared objects & variables//
    public GameManager GameManager;
    public GameObject PlayerCard;
    public CombatManager CombatManager;


    //All ally objects
    public int AllyHealth;
    public GameObject AllyDropZone1;
    public GameObject AllyDropZone2;
    public GameObject AllyDropZone3;
    public GameObject AllyDropZone4;
    public GameObject AllyDropZone5;
    public GameObject AllyHand;
    public GameObject AllyDeck;
    
    //All enemy object
    public int EnemyHealth;
    public GameObject EnemyDropZone1;
    public GameObject EnemyDropZone2;
    public GameObject EnemyDropZone3;
    public GameObject EnemyDropZone4;
    public GameObject EnemyDropZone5;
    public GameObject EnemyHand;
    public GameObject EnemyDeck;

    //Lists that will hold ally and enemy drop zones resepctively
    public List<GameObject> AllyDropZones = new List<GameObject>();
    public List<GameObject> EnemyDropZones = new List<GameObject>();

    //Card list we are pulling from
    public static List<Card> cardList = new List<Card>();

    //Variable tracking current player
    public int CardsPlayed = 0;
    [SyncVar]
    public int TurnsEnded = 0;


    public bool IsMyTurn = false;

    //Possible alternative Card List; same purpose but better way of getting it
    private List<GameObject> cards = new List<GameObject>();

    //Runs the code below when Host+Client/Client button is selected
    public override void OnStartClient()
    {
        base.OnStartClient();

        //Connect to GameManager
        GameManager = GameObject.Find("GameManager").GetComponent<GameManager>();

        //Connects hand and deck of both players to a variable by looking for objects with listed name 
        AllyHand = GameObject.Find("AllyHand");
        AllyDeck = GameObject.Find("AllyDeck");
        EnemyHand = GameObject.Find("EnemyHand");
        EnemyDeck = GameObject.Find("EnemyDeck");

        //Connects each ally drop zones to each variable by looking for objects with listed name 
        AllyDropZone1 = GameObject.Find("AllyDropZone");
        AllyDropZone2 = GameObject.Find("AllyDropZone (1)");
        AllyDropZone3 = GameObject.Find("AllyDropZone (2)");
        AllyDropZone4 = GameObject.Find("AllyDropZone (3)");
        AllyDropZone5 = GameObject.Find("AllyDropZone (4)");

        //Connects each enemy drop zones to each variable by looking for objects with listed name 
        EnemyDropZone1 = GameObject.Find("EnemyDropZone");
        EnemyDropZone2 = GameObject.Find("EnemyDropZone (1)");
        EnemyDropZone3 = GameObject.Find("EnemyDropZone (2)");
        EnemyDropZone4 = GameObject.Find("EnemyDropZone (3)");
        EnemyDropZone5 = GameObject.Find("EnemyDropZone (4)");

        //Fills Ally list with ally drop zones
        AllyDropZones.Add(AllyDropZone1);
        AllyDropZones.Add(AllyDropZone2);
        AllyDropZones.Add(AllyDropZone3);
        AllyDropZones.Add(AllyDropZone4);
        AllyDropZones.Add(AllyDropZone5);

        //Fills Enemy list will enemy drop zones
        EnemyDropZones.Add(EnemyDropZone1);
        EnemyDropZones.Add(EnemyDropZone2);
        EnemyDropZones.Add(EnemyDropZone3);
        EnemyDropZones.Add(EnemyDropZone4);
        EnemyDropZones.Add(EnemyDropZone5);


        //Determines which player goes first; Whoever presses "Client" button goes first
        if (isClientOnly)
        {
            IsMyTurn = true;
        }

        //Message to show that client is running; appears in console tab
        Debug.Log("OnStartClient was activated");
    }

    //Starts server (Host + Client)
    [Server]
    public override void OnStartServer()
    {
        base.OnStartServer();
        
        //Tells us that the server is running
        Debug.Log("OnStartServer was activated");
    }

    //Command that adds cards to players hands and grants authority
    [Command]
    public void CmdDealCards()
    {
        //Fills player's hand with a card and repeats 3 times
        for (int i = 0; i < 3; i++)
        {
            GameObject card = Instantiate(PlayerCard, new Vector2(0, 0), Quaternion.identity);
            NetworkServer.Spawn(card, connectionToClient);

            //Sets current status of the card to "Dealt" which makes it appear in hand for the Server
            RpcShowCard(card, "Dealt");
        }

        //Changes game manager current state
        RpcGMChangeState("Compile {}");
    }

    //Calls upon CmdPlayCard
    public void PlayCard(GameObject card)
    {
        CmdPlayCard(card);
        Debug.Log("Cards Played counter: " + CardsPlayed);
    }

    //Calls upon RocShowCard
    [Command]
    void CmdPlayCard(GameObject card)
    {
        RpcShowCard(card, "Played"); //Sets current status of the card to "Played" which makes it appear in a drop zone for the Server
    }

    //Command to call function that checks if player ends their turn
    [Command]
    public void CmdEndTurn()
    {
        Debug.Log("TurnsEnded = " + TurnsEnded);
        RpcEndTurn();

        if (TurnsEnded == 1)
        {
            //Changes game manager current state
            RpcGMChangeState("Execute {}");
        }
        Debug.Log("TurnsEnded = " + TurnsEnded);
    }

    [ClientRpc]
    public void RpcEndTurn()
    {
        PlayerManager pm = NetworkClient.connection.identity.GetComponent<PlayerManager>();
        pm.IsMyTurn = !pm.IsMyTurn;
        Debug.Log("Ran RpcEndTurn");
    }

    //Command that initializes combat system
    [Command]
    public void CmdCombatSystem()
    {
        CombatManager.CompareCards();
    }


    //Actual function that moves the recently added card to hand and/or drop zone
    [ClientRpc]
    void RpcShowCard(GameObject card, string type)
    {
        if(type == "Dealt") // "Dealt" cards are placed into player's hand and checking ownership allows for it mirror
        {
            if (isOwned)
            {
                card.transform.SetParent(AllyHand.transform, false);
            }
            else
            {
                card.transform.SetParent(EnemyHand.transform, false);
            }
        }
        else if(type == "Played") // "Played" cards are placed into a player's drop zone and check mirror condition
        {
            if (CardsPlayed == 5)
            {
                CardsPlayed = 5;
            }

            if (isOwned)
            {
                //card.transform.SetParent(AllyDropZones[CardsPlayed].transform, false);
                card.transform.SetParent(AllyDropZones[CardsPlayed].transform, false);
            }
            else if (!isOwned)
            {
                card.transform.SetParent(EnemyDropZones[CardsPlayed].transform, false);
            }

            //Increases card counter
            CardsPlayed++;
            Debug.Log("Card ID: " + card.name);
        }
    }

    //Changes GameState by passing Game State name and calling function
    //Check Gamemanger to see function def. (TLDR: ReadyClicks var +1)
    [ClientRpc]
    void RpcGMChangeState(string stateRequest)
    {
        GameManager.ChangeGameState(stateRequest);
        if (stateRequest == "Compile {}")
        {
            GameManager.ChangeReadyClicks();
        }

        if(stateRequest == "Execute {}")
        {
            GameManager.ChangeReadyClicks();
        }
    }

}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ I didn't catch it in my first editing pass, but I noticed that you had a number of error messages in your screenshots. It's not necessarily an issue if they're unrelated to selecting your networking model, but in the future please post relevant error messages as text. Some of the many reasons to post errors/code as text include making your question more searchable, and more accessible for folks using screen readers or translation software, so you get access to more experts who can help you. \$\endgroup\$
    – Pikalek
    Commented Mar 21, 2024 at 16:39

1 Answer 1

0
\$\begingroup\$

If you are a beginner in networking, forget about Host mode. Host mode allows you to make easy mistakes that are hard to figure out if you do not fully understand what information the server and client has and how the communication between them is. It can introduce bugs that are working on one client but fail on the second one.

Instead of Host mode + client I would advise to start one Server + 2x client. It makes the setup harder but your coding easier in the long run.

Server code/ logic
A server does not care who is Ally or Enemy. It does not care about the visual representation or physical location for cards, draw stacks, graveyard, drop zones or whatever your game uses (this is different for different types of games). From the servers point of view, everyone is a player. It holds the logic for your game, knows the state of the game, how cards are interacting, how a round works. Your combatmanager looks like it should be server code. But you should have no need for code like AllyDropZone1?.transform?.GetChild(0)?.gameObject on the server. It knows which card is first in hand or first on board. You could write the whole game in the server logic first without a single visual representation. There is no secret information on the server. Your draw cards are probably stored in a list. Hand cards in an array or another list. Board? Array. Graveyard? Array or list.

Client
One error that you will notice if (isClientOnly) will be true for every player. Never trust the client with any game important decision making. They can choose to do an action, but the server in the end performs that action. In this case the server decides who goes first and informs that client. Or all clients which the name of which client goes first. If that name is your client, you can play your turn. All other clients can display the currents player turn or handle however you choose to do if it is offturn. Hitting end turn button? Just sending that flag to the server. You would evaluate there if it is 1) the current players turn who hit it 2) what happens next.

Communication
The server would not care, where the card is located (transform) of it. Nor should it care that you said: I played the first card in my hand. The client would sent the identification of the card to the server along with the action (which is usually only one possible option). Hand cards can usually be played. If it is important to be able to place them on specific locations, you would include the location index, not coordinates (different players usually have different resolutions, maybe even custom layouts - server does not care). Playing the energy card from hand that goes to the land stack on the board? Telling server I played energy. Playing monster X and it should go in the middle of the board? Telling the server played monster X at 5. Now the servers job is to check that you have that card in hand and that the location is valid. Maybe you played a monster already. Maybe you can't pay the cost. Maybe the location is blocked. When the move is valid, the server informs all players about the new state of the game and each client handles that information accordingly. If player 1 played monster X, that will look visually different location vise for player 2.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ @SmileyTheMimic A card game is luckily quite easy (compared for example to a RTS game). A lot of the game critical information can be sent with SyncVars to the clients without the need of cmd mirror-networking.gitbook.io/docs/manual/guides/synchronization/… \$\endgroup\$
    – Zibelas
    Commented Mar 20, 2024 at 15:21
  • \$\begingroup\$ And keep in mind, your first screenshot is still throwing an error, likely due that the combat manager is never assigned to your script or loaded. \$\endgroup\$
    – Zibelas
    Commented Mar 20, 2024 at 16:18
  • \$\begingroup\$ Thank you again for the advice. I'll move forward with what you have suggested, even if a lot of the already existing code has to be readjusted/remade. As it stands, players are able to play cards from their hand only on their turn and are able these are all controlled by the 'End Turn' button' which changes its function based on the gameState. \$\endgroup\$ Commented Mar 20, 2024 at 16:26
  • 1
    \$\begingroup\$ A lot will depend as well on how your project is going to get graded. For example if the host disconnects during the game, how do you handle that? Is the data seperated from the players? Will you need to add later more functionality? Do you have to write test cases against the code? Is there going to be manipulated input data? Etc \$\endgroup\$
    – Zibelas
    Commented Mar 20, 2024 at 17:09
  • \$\begingroup\$ @Ziblelas As there isn't such a focus on stress testing the game, I'm just focusing on it working when played as intended. I'm starting on creating a ServerManager script and Player Script to replace existing code. I'll be trying to carry over somethings like the DragDrop logic I used in another script and etc. \$\endgroup\$ Commented Mar 20, 2024 at 19:04

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .