Unity问题中使用镜像同步(CCG卡牌游戏)

问题描述 投票:0回答:0

你好,我目前正在使用 Unity 和 Mirror 制作一款类似于《炉石传说》的同人游戏。(P2P 主机)

目前有两个场景。 一种是没有网络通讯的DeckBuilding场景, 其中一个是战斗场景,您与通过镜子连接的玩家进行战斗。

从 DeckBuilding 场景移动到 Battle 场景时,在 DeckBuilding 场景中选择的牌组(scriptabaleObject)将保存为 PlayerPrefs,然后在 Battle 场景中创建玩家时导入。

每个客户都安全地从前一个场景导入了他们的套牌,但随后出现了问题。

客户端,即主机(服务器),已通过CmdLoadDeck()函数成功同步牌组。

但是非服务器参与者客户端无法同步他们的牌组,导致 0 张牌。 断点不会击中 CmdLoadDeck() 函数。(但服务器客户端会击中)

我不知道为什么会出现这种情况(我对服务器的了解很少。我只了解一点Mirror的Command、SyncVar和ClientRPC属性的概念)

玩家.cs

using System;
using UnityEngine;
using Mirror;

//Useful for UI. Whether the player is, well, a player or an enemy.
public enum PlayerType { PLAYER, ENEMY };

[RequireComponent(typeof(Deck))]
[Serializable]
public class Player : Entity // Entity inherits NetworkBehaviour
{
    [Header("Player Info")]
    [SyncVar(hook = nameof(UpdatePlayerName))] public string username; // SyncVar hook to call a command whenever a username changes (like when players load in initially).

    [Header("Portrait")]
    public Sprite portrait; // For the player's icon at the top left of the screen & in the PartyHUD.

    [Header("Deck")]
    public Deck deck;
    public Sprite cardback;
    [SyncVar, HideInInspector] public int tauntCount = 0; // Amount of taunt creatures on your side of the board.

    [Header("Stats")]
    [SyncVar] public int maxMana = 10;
    [SyncVar] public int currentMax = 0;
    [SyncVar] public int _mana = 0;
    [SyncVar] public int mana;

    // Quicker access for UI scripts
    [HideInInspector] public static Player localPlayer;
    [HideInInspector] public bool hasEnemy = false; // If we have set an enemy.
    [HideInInspector] public PlayerInfo enemyInfo; // We can't pass a Player class through the Network, but we can pass structs. 
    //We store all our enemy's info in a PlayerInfo struct so we can pass it through the network when needed.
    [HideInInspector] public static GameManager gameManager;
    [SyncVar] public bool firstPlayer = false; // Is it player 1, player 2, etc.

    public override void OnStartLocalPlayer()
    {
        localPlayer = this;
        firstPlayer = isServer;//if Server == firstPlayer

        //Get and update the player's username and stats
        CmdLoadPlayer(PlayerPrefs.GetString("Name"));
        LoadBuildingDeck();//Load Deck Json and Command
    }

    public void LoadBuildingDeck()
    {

        string jsonData = PlayerPrefs.GetString("DeckData");


        CardAndAmountListWrapper wrapper = JsonUtility.FromJson<CardAndAmountListWrapper>(jsonData);


        deck.startingDeck = wrapper.deckList.ToArray();

        CmdLoadDeck(); //<======this is problem

    }

    public override void OnStartClient()
    {
        base.OnStartClient();

        deck.deckList.Callback += deck.OnDeckListChange;
        //deck.hand.Callback += deck.OnHandChange;
        deck.graveyard.Callback += deck.OnGraveyardChange;
    }

    [Command]
    public void CmdLoadPlayer(string user)
    {
        //Update the player's username, which calls a SyncVar hook.
        //Learn more here: https://mirror-networking.com/docs/Guides/Sync/SyncVarHook.html
        username = user;
    }

    // Update the player's username, as well as the box above the player's head where their name is displayed.
    void UpdatePlayerName(string oldUser, string newUser)
    {
        //Update username
        username = newUser;

        //Update game object's name in editor (only useful for debugging).
        gameObject.name = newUser;
    }

    [Command]
    public void CmdLoadDeck()
    {
        //Fill deck from startingDeck array
        for (int i = 0; i < deck.startingDeck.Length; ++i)
        {
            CardAndAmount card = deck.startingDeck[i];
            for (int v = 0; v < card.amount; ++v)
            {
                deck.deckList.Add(card.amount > 0 ? new CardInfo(card.card, 1) : new CardInfo());
                if (deck.hand.Count < 7) deck.hand.Add(new CardInfo(card.card, 1));
            }
        }
        if (deck.hand.Count == 7)
        {
            deck.hand.Shuffle();
        }
    }

    private void Start()
    {
        //memoryChecker = GameObject.Find("MemoryChecker").GetComponent<RectTransform>();
        gameManager = FindObjectOfType<GameManager>();
        health = gameManager.maxHealth;
        maxMana = gameManager.maxMana;
        deck.deckSize = gameManager.deckSize;
        deck.handSize = gameManager.handSize;
    }

    // Update is called once per frame
    public override void Update()
    {
        base.Update();

        //Get EnemyInfo as soon as another player connects. Only start updating once our Player has been loaded in properly(username will be set if loaded in).
        if (!hasEnemy && username != "")
        {
            UpdateEnemyInfo();
        }

        if (hasEnemy && isLocalPlayer && gameManager.isGameStart == false)
        {
            Debug.Log(enemyInfo.data.username);
            //CmdLoadEnemyDeck();            
            gameManager.StartGame();
        }
    }

    public void UpdateEnemyInfo()
    {
        //Find all Players and add them to the list.
        Player[] onlinePlayers = FindObjectsOfType<Player>();

        //Loop through all online Players(should just be one other Player)
        foreach (Player players in onlinePlayers)
        {
            //Make sure the players are loaded properly(we load the usernames first)
            if (players.username != "")
            {
                //There should only be one other Player online, so if it's not us then it's the enemy.
                if (players != this)
                {
                    //Get & Set PlayerInfo from our Enemy's gameObject
                    PlayerInfo currentPlayer = new PlayerInfo(players.gameObject);
                    enemyInfo = currentPlayer;
                    hasEnemy = true;
                    enemyInfo.data.casterType = Target.OPPONENT;
                    //Debug.LogError("Player " + username + " Enemy " + enemy.username + " / " + enemyInfo.username); // Used for Debugging
                }
            }
        }
    }
    
    public bool IsOurTurn() => gameManager.isOurTurn;
}

Deck.cs

using UnityEngine;
using Mirror;

public class Deck : NetworkBehaviour
{
    [Header("Player")]
    public Player player;
    [HideInInspector] public int deckSize = 50;
    [HideInInspector] public int handSize = 7;

    [Header("Decks")]
    public SyncListCard deckList = new SyncListCard(); // DeckList used during the match. Contains all cards in the deck. This is where we'll be drawing card froms.
    public SyncListCard graveyard = new SyncListCard(); // Cards in player graveyard.
    public SyncListCard hand = new SyncListCard(); // Cards in player's hand during the match.

    [Header("Battlefield")]
    public SyncListCard playerField = new SyncListCard(); // Field where we summon creatures.

    [Header("Starting Deck")]
    public CardAndAmount[] startingDeck;

    [HideInInspector] public bool spawnInitialCards = true;

    public void OnDeckListChange(SyncListCard.Operation op, int index, CardInfo oldCard, CardInfo newCard)
    {
        UpdateDeck(index, 1, newCard);
    }

    public void OnHandChange(SyncListCard.Operation op, int index, CardInfo oldCard, CardInfo newCard)
    {
        UpdateDeck(index, 2, newCard);
    }

    public void OnGraveyardChange(SyncListCard.Operation op, int index, CardInfo oldCard, CardInfo newCard)
    {
        UpdateDeck(index, 3, newCard);
    }

    public void UpdateDeck(int index, int type, CardInfo newCard)
    {
        // Deck List
        if (type == 1) deckList[index] = newCard;

        // Hand
        if (type == 2) hand[index] = newCard;

        // Gaveyard
        if (type == 3) graveyard[index] = newCard;

    }


    ///////////////
    public bool CanPlayCard(int manaCost)
    {
        if (player.mana - manaCost > -10 && player.health > 0)
        { return true; }// player.mana >= manaCost && player.health > 0;
        else
        { return false; }
    }

    public void DrawCard(int amount)
    {
        PlayerHand playerHand = Player.gameManager.playerHand;
        for (int i = 0; i < amount; ++i)
        {
            int index = i;
            playerHand.AddCard(index);
        }
        spawnInitialCards = false;
    }

    [Command]
    public void CmdPlayCard(CardInfo card, int index, Player owner)
    {
        CreatureCard creature = (CreatureCard)card.data;
        GameObject boardCard = Instantiate(creature.cardPrefab.gameObject);
        FieldCard newCard = boardCard.GetComponent<FieldCard>();
        newCard.card = new CardInfo(card.data); // Save Card Info so we can re-access it later if we need to.
        //newCard.cardName.text = card.name;
        newCard.health = creature.health;
        newCard.strength = creature.strength;
        newCard.image.sprite = card.image;
        newCard.image.color = Color.white;
        newCard.player = owner;

        // If creature has charge, reduce waitTurn to 0 so they can attack right away.
        if (creature.hasCharge) newCard.waitTurn = 0;

        // Update the Card Info that appears when hovering
        newCard.cardHover.UpdateFieldCardInfo(card);

        // Spawn it
        NetworkServer.Spawn(boardCard);

        // Remove card from hand
        hand.RemoveAt(index);

        if (isServer) RpcPlayCard(boardCard, index);
    }

    [Command]
    public void CmdStartNewTurn()
    {
        
    }

    [ClientRpc]
    public void RpcPlayCard(GameObject boardCard, int index)
    {
        if (Player.gameManager.isSpawning)
        {
            // Set our FieldCard as a FRIENDLY creature for our local player, and ENEMY for our opponent.
            boardCard.GetComponent<FieldCard>().casterType = Target.FRIENDLIES;
            boardCard.transform.SetParent(Player.gameManager.playerField.content, false);
            Player.gameManager.playerHand.RemoveCard(index); // Update player's hand
            Player.gameManager.isSpawning = false;
        }
        else if (player.hasEnemy)
        {
            boardCard.GetComponent<FieldCard>().casterType = Target.ENEMIES;
            boardCard.transform.SetParent(Player.gameManager.enemyField.content, false);
            Player.gameManager.enemyHand.RemoveCard(index);
        }
    }
}

SyncListCard继承自SyncList 卡信息 它具有卡的唯一 ID 和金额。

CardInfo.cs

//Learn more : https://mirror-networking.com/docs/Guides/DataTypes.html#scriptable-objects
using System;
using UnityEngine;
using Mirror;
using System.Collections.Generic;
using static UnityEngine.GraphicsBuffer;

[Serializable]
public partial struct CardInfo
{
    // A uniqueID (unique identifier) used to help identify which ScriptableCard is which when we acess ScriptableCard data.
    // If any ScriptableCards share the same uniqueID, Unity will return a bunch of errors.
    public string cardID;
    public int amount; // Used for deck building only. Serves no purpose once the card is in the game / on the board.

    public CardInfo(ScriptableCard data, int amount = 1)
    {
        cardID = data.CardID;
        this.amount = amount;
    }

    public ScriptableCard data
    {
        get
        {
            // Return ScriptableCard from our cached list, based on the card's uniqueID.
            return ScriptableCard.Cache[cardID];
        }
    }

    public Sprite image => data.image;
    public string name => data.name; // Scriptable Card name (name of the file)
    public int cost => data.cost;
    public string description => data.description;

    public List<Target> acceptableTargets => ((CreatureCard)data).acceptableTargets;

    #region Equals
    //=========== "=="연산자 용 추가 함수 ===========//
    public bool Equals(CardInfo other)
    {
        return name == other.name && image == other.image;
    }

    public override bool Equals(object obj)
    {
        return obj is CardInfo other && Equals(other);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hashCode = name != null ? name.GetHashCode() : 0;
            hashCode = (hashCode * 397) ^ (image != null ? image.GetHashCode() : 0);
            return hashCode;
        }
    }
    #endregion
}

// Card List
public class SyncListCard : SyncList<CardInfo> { }

ScriptableCard.cs

// Put all our cards in the Resources folder
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

[Serializable]
public struct CardAndAmount
{
    public ScriptableCard card;
    public int amount;
}

[System.Serializable]
public class CardAndAmountListWrapper
{
    public List<CardAndAmount> deckList;
}

public enum CardColor1
{ Red, Green, Blue, Yellow, Purple, Black, }
public enum CardColor2
{ None, Red, Green, Blue, Yellow, Purple, Black, }

// Struct for cards in your deck. Card + amount (ex : Sinister Strike x3). Used for Deck Building. Probably won't use it, just add amount to Card struct instead.
public partial class ScriptableCard : ScriptableObject
{
    [SerializeField] string id = "";
    public string CardID { get { return id; } }

    [Header("Image")]
    public Sprite image; // Card image

    [Header("Card Color")]
    public CardColor1 color1;
    public CardColor2 color2;

    [Header("Properties")]
    public string cardName;
    public int cost;
    public string category;

    [Header("Initiative Abilities")]
    public List<CardAbility> intiatives = new List<CardAbility>();

    [HideInInspector] public bool hasInitiative = false; // If our card has an INITIATIVE ability

    [Header("Description")]
    [SerializeField, TextArea(1, 30)] public string description;

    // We can't pass ScriptableCards over the Network, but we can pass uniqueIDs.
    // Throughout this project, you'll find that I've passed uniqueIDs through the Server,
    static Dictionary<string, ScriptableCard> _cache;
    public static Dictionary<string, ScriptableCard> Cache
    {
        get
        {
            if (_cache == null)
            {
                // Load all ScriptableCards from our Resources folder
                ScriptableCard[] cards = Resources.LoadAll<ScriptableCard>("");

                _cache = cards.ToDictionary(card => card.CardID, card => card);
            }
            return _cache;
        }
    }

    // Called when casting abilities or spells
    public virtual void Cast(Entity caster, Entity target)
    {

    }

    private void OnValidate()
    {
        // Get a unique identifier from the asset's unique 'Asset Path' (ex : Resources/Weapons/Sword.asset)
        // You're free to set your own uniqueIDs instead of using this current system, but unless
        // you know what you're doing, I wouldn't recommend changing this in the inspector.
        // If you do change it and want to change back, just erase the uniqueID in the inspector and it will refill itself.
        if (CardID == "")
        {
#if UNITY_EDITOR
            string path = AssetDatabase.GetAssetPath(this);
            id = AssetDatabase.AssetPathToGUID(path);
#endif
        }

        if (intiatives.Count > 0) hasInitiative = true;
    }
}

对于每个玩家,我检查了startingDeck(CardAndAmount[]变量)是否带有我从DeckBuildingScene中选择的卡(无论是否是服务器)。

但是如上所述 CmdLoadDeck() 函数由 NonServer Client 传递。

所以我想不出一种方法将参与者客户端的牌组传递到服务器客户端。

我知道 [Command] 属性负责每个客户端向服务器传递信息,我是否误解了?

还是我在其他地方做错了什么?

如果你想尝试运行我的 Unity 项目,我会留下一个链接。 链接

c# unity-game-engine synchronization multiplayer unity3d-mirror
© www.soinside.com 2019 - 2024. All rights reserved.