你好,我目前正在使用 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 项目,我会留下一个链接。 链接