Unity卡牌游戏设计:从基础到实战

    本章介绍了卡牌游戏设计的部分基础,国内关于卡牌游戏的设计教程都很少,而且都涉及的很浅显,基本只教你大概的框架,我学了一段时间的unity2d卡牌游戏设计,也看过很多大佬的讲解,今天讲讲我遇到的部分问题和学到的一些基础。我的unity版本是2022版的,可能一些内容会因版本问题有些差错。

一、基本流程:

卡牌的显示流程如下图:

先读取卡牌数据再存储到CardStore并生成对应的卡组,然后playerdata读取CardStore里的卡组信息加载对应的卡组,再讲加载的卡牌通过存有CardDisplay组件的卡牌显示出来。

二、设计内容:

1.card设计

    首先是界面和卡牌这两部分的设计,代码的调用首先得创造一个具体的游戏项目作为调用对象,那卡牌的设计怎么设计呢,卡牌的设计一般通过创建ui里的image图像和text文本,一张卡牌就好像一个盒子(即空物体),里面分别存着image和text的方块,image方块代表着显示出来的卡牌图像,关系到后面的美术设计,而其中的text方块存储着卡牌的数值,这是卡牌游戏的重点。我们可以通过ui创建我们自己的卡牌模版,往里面加入image和text,譬如下图:

    这张卡CarplayerCharacter由七个方块组成,两个image和五个text,Backgroundimage是青色的背景,image是白色的边框,剩下五个name(角色名)、text(技能描述)、attack(攻击力)、mana(法力值)、armor(护甲)则是text。

2.脚本介绍

    那么说了方块,接下来我们来聊聊scripts也就是脚本或者说组件,通常在检查器里的添加组件里,我们可以直接往往卡牌套上组件,也就是盒子,也可以通过在项目assets文件里开一个新文件储存我们的脚本,方法是右键鼠标创建脚本,更加建议用这种方法,方便储存我们我们写过的脚本。

    写代码的部分我个人习惯用Visual Studio,我们可以再左上角的编辑-->首选项-->外部工具,外部脚本编辑器选择Visual Studio 2022(我的版本是2022),然后我们创建脚本就会默认使用Visual Studio进行编写,没有Visual Studio可以自行下载。

    接下来进入到脚本的编写,我们可以在脚本文件夹里右键创建C#脚本,首先需要编写card脚本存储我们已经设计好了的属性。

public class Card
{
    public int id;
    public string cardName;
    public int mana; // 将 mana 添加到基类

    // 构造函数
    public Card(int id, string cardName, int mana)
    {
        this.id = id;
        this.cardName = cardName;
        this.mana = mana;
    }
}
public class AttackCard : Card
{
    public int attack;
    public int attackTime;

    public AttackCard(int _id, string _cardName, int _attack, int _mana)
        : base(_id, _cardName, _mana) // 调用基类构造函数
    {
        this.attack = _attack;
        attackTime = 2;
    }
}
public class SpellCard : Card
{
    public int effectvalue;
    public string effect;

    public SpellCard(int _id, string _cardName, string _effect, int _effectvalue, int _mana) : base(_id, _cardName, _mana)
    {
        this.effect = _effect;
        this.effectvalue = _effectvalue;

    }
}
public class ShieldCard : Card
{
    public string effect;
    public int Shield;
    public ShieldCard(int _id, string _cardName, int _Shield, int _mana) : base(_id, _cardName, _mana)
    {
        this.Shield = _Shield;
    }
}
public class CharacterCard : Card
{
    public int healthPoint; // 生命值
    public string skill;   // 技能
    public int shield;
    // 构造函数
    public CharacterCard(int _id, string _cardName, int _healthPoint, int _mana, string _skill, int _shield) : base(_id, _cardName, _mana)
    {
        this.healthPoint = _healthPoint;
        this.skill = _skill;
        this.shield = _shield;
    }
}

    这里涉及到建立基类这一操作,就好比所以得卡牌都会涉及到卡牌的id(便于后面的读取和运用),卡牌的name(无论是本身的角色卡,还是需要打出的战斗卡、护盾卡、魔法卡),以及魔力的大小(持有魔力和消耗魔力的大小)而基类的使用就是让后续设计的角色卡,战斗卡以及其他卡牌可以基于card基类进行编写,默认存在基类中涉及的数据,在我的代码就是id、cardName和mana。后续的战斗卡只需要加上 “:card”在建立的类名后面就可以使用基类,然后定义设置数值。

    “this.变量 = _变量”的写法确保了每当一个新的卡牌对象被创建时,它的属性都会被赋予一个从外部传递进来的具体值,而这个外部传递的数值就涉及到另一部分存储的问题,我们通常使用一个csv文件或者excel、json文件来储存我们的各类数值,我代码中使用的是csv文件,csv文件的创建也很简单,只需要建立一个文本编辑器,加入数值,然后保存为csv文件即可,或者建立excel文件填入数值在转化为csv文件。

    譬如下面我的csv文件,值得一提的是设置了Visual Studio作为外部脚本编辑器,可以直接将我们设置的csv文件拉入Visual Studio中进行编写。

    接下来就是如何将csv文件中填写的数据通过代码组件填入到我们的卡牌的text中(填入text后会换掉原本卡牌text写过的内容,这也是为什么在上文CarplayerCharacter的text只写了简单的内容),这种方法更有利于后期的数据更新,总不能没有一张卡都重新设置一张卡牌在慢慢填入相应的内容吧。这个时候就得介绍一下prefab(预制件)了,在我们设计好一张卡牌的ui界面(指text和image的位置设置以及美术设计)时,我们可以将它整体拉到我们的assets项目文件(建议新建一个文件夹存储预制体)中并点击选择原始预制件即可储存为预制体,这样随时想要使用的时候就可以调用它。之后的很多项目文件都可以存储成预制体。

三、代码编写:

1.CardDisplay编写

    而不同的卡牌需要显示的text不同,有些可能需要显示,有些不需要,下面CardDisplay代码通过定义text属性和image属性,以及最重要Card型变量card,创建一个新类来判断卡牌类型并显示我们想要的属性,用if函数进行卡牌类型判断,然后将变量 card 转换为 AttackCard 类型,并将结果赋值给 attackcard,以战斗卡为例攻击卡只需要显示卡牌名字,攻击造成的伤害值,以及消耗的魔力值,这个时候将需要的数值填入text1、text2、text3并且在检查器中将对应的text拉入即可显示,而其中的“Text3.gameObject.SetActive(false);”是让对应的文本不显示。之后的卡牌也是同样的意思。

using UnityEngine;
using UnityEngine.UI;

public class CardDisplay : MonoBehaviour
{
    public Text nameText;
    public Text Text1;
    public Text Text2;
    public Text effectText;
    public Text Text3;

    public Image backgroundImage;

    public Card card;
    // Start is called before the first frame update
    void Start()
    {
        ShowCard();
    }

    // Update is called once per frame
    void Update()
    {

    }
    public void ShowCard()
    {
        if (card is AttackCard)
        {
            var attackcard = card as AttackCard;
            //将变量 card 转换为 AttackCard 类型,并将结果赋值给 attackcard
            nameText.text = card.cardName;
            //将文本nameText设置为card的cardName
            Text1.text = attackcard.attack.ToString();
            //将文本Text1设置为attackcard的attack
            Text2.text = attackcard.mana.ToString();
            effectText.gameObject.SetActive(false);
            Text3.gameObject.SetActive(false);
            //隐藏文字描述
        }
        else if (card is SpellCard)
        {
            var spell = card as SpellCard;
            effectText.text = spell.effect;
            nameText.text = card.cardName;
            Text1.text = spell.effectvalue.ToString();
            Text2.text = spell.mana.ToString();
            Text3.gameObject.SetActive(false);
        }
        else if (card is CharacterCard)
        {
            var character = card as CharacterCard;
            nameText.text = card.cardName;
            effectText.text = character.skill;
            Text1.text = character.mana.ToString();
            Text2.text = character.healthPoint.ToString();
            Text3.text = character.shield.ToString();
        }
        else if (card is ShieldCard)
        {
            var shieldCard = card as ShieldCard;
            nameText.text = card.cardName;
            effectText.gameObject.SetActive(false);
            Text1.gameObject.SetActive(false);
            Text.text2 = shieldCard.mana.ToString();
            Text.text3 = shieldCard.Shield.ToString();
        }
    }
}

2.CardStore编写

在完成了卡牌显示的脚本后,接下来需要来编写另一个脚本也就是CardStore,其中包含了读取卡牌数据的功能,通过读取的第一列内容进行分类,譬如第一列是“#”的就跳过,是“attack”生成攻击卡,以此类推。以战斗卡为例,读取第二列的卡牌id(相同的卡牌可以有不同的id,以此来进行相同卡牌的不同调用),第三列则是卡牌的名字,第四列攻击力,第五列消耗的蓝量,然后新建一个AttackCard变量(参考card脚本)来存储这张卡并将其加入卡组中(这个卡组也是之后playerdata存储需要用到的卡组),在完成魔法卡,护甲卡,角色卡(游戏卡需要单独一个卡组,你也不想抽卡抽出角色卡吧)后,便是随机抽卡也是之后游戏抽卡的逻辑,还有复制卡牌的逻辑(现在可以先不管)。

using System.Collections.Generic;
using UnityEngine;

public class CardStore : MonoBehaviour
{
    public TextAsset cardData;
    public List<Card> cardList = new List<Card>();
    public List<CharacterCard> characterCardList = new List<CharacterCard>();
    // Start is called before the first frame update
    void Start()
    {
        LoadCardData();
        //TestLoad();
    }

    // Update is called once per frame
    void Update()
    {

    }
    public void LoadCardData()
    {
        string[] dataRow = cardData.text.Split('\n');
        foreach (var row in dataRow)
        {
            string[] rowArray = row.Split(',');
            if (rowArray[0] == "#")
            {
                continue;
            }
            else if (rowArray[0] == "attack")
            {
                //新建攻击卡
                int id = int.Parse(rowArray[1]);
                string name = rowArray[2];
                int atk = int.Parse(rowArray[3]);
                int mana = int.Parse(rowArray[4]);
                AttackCard attackCard = new AttackCard(id, name, atk, mana);
                cardList.Add(attackCard);

                //Debug.Log("读取到攻击卡:" + monsterCard.cardName);
            }
            else if (rowArray[0] == "spell")
            {
                //新建魔法卡
                int id = int.Parse(rowArray[1]);
                string name = rowArray[2];
                string effect = rowArray[3];
                int effectvalue = int.Parse(rowArray[4]);
                int mana = int.Parse(rowArray[5]);
                SpellCard spellCard = new SpellCard(id, name, effect, effectvalue, mana);
                cardList.Add(spellCard);
            }
            else if (rowArray[0] == "character")
            {
                // 加载角色卡
                int id = int.Parse(rowArray[1]);
                string name = rowArray[2];
                int health = int.Parse(rowArray[3]);
                int mana = int.Parse(rowArray[4]);
                string skill = rowArray[5];
                int shield = int.Parse(rowArray[6]);
                CharacterCard characterCard = new CharacterCard(id, name, health, mana, skill, shield);
                characterCardList.Add(characterCard);
            }
            else if (rowArray[0] == "shield")
            {
                // 加载护盾卡
                int id = int.Parse(rowArray[1]);
                string name = rowArray[2];
                int shield = int.Parse(rowArray[3]);
                int mana = int.Parse(rowArray[4]);
                ShieldCard shieldCard = new ShieldCard(id, name, shield, mana);
                cardList.Add(shieldCard);
            }
        }
    }
    //测试卡牌是否进去卡包
    public void TestLoad()
    {
        foreach (var card in cardList)
        {
            Debug.Log("卡牌:" + card.id.ToString() + card.cardName);
        }
        foreach (var CharacterCard in characterCardList)
        {
            Debug.Log("卡牌:" + CharacterCard.id.ToString() + CharacterCard.cardName);
        }
    }

    public Card RandomCard()
    {
        //随机从cardList取一张卡
        Card card = cardList[Random.Range(0, cardList.Count)];
        return card;
    }
    public CharacterCard RandomCharacterCard()
    {
        if (characterCardList.Count > 0)
        {
            CharacterCard randomCard = characterCardList[Random.Range(0, characterCardList.Count)];
            return randomCard;
        }

        Debug.LogWarning("没有可用的角色卡!");
        return null;
    }

    public Card CopyCard(int _id)
    {
        if (_id < cardList.Count)
        {
            // 普通卡牌
            Card originalCard = cardList[_id];
            if (originalCard is AttackCard)
            {
                var attackCard = originalCard as AttackCard;
                return new AttackCard(attackCard.id, attackCard.cardName, attackCard.attack, attackCard.mana);
            }
            else if (originalCard is SpellCard)
            {
                var spellCard = originalCard as SpellCard;
                return new SpellCard(spellCard.id, spellCard.cardName, spellCard.effect, spellCard.effectvalue, spellCard.mana);
            }
            else if (originalCard is ShieldCard)
            {
                var shieldCard = originalCard as ShieldCard;
                return new ShieldCard(shieldCard.id, shieldCard.cardName, shieldCard.Shield, shieldCard.mana);
            }

        }
        // 角色卡
        if (_id < characterCardList.Count)
        {
            var characterCard = characterCardList[_id];
            return new CharacterCard(characterCard.id, characterCard.cardName, characterCard.healthPoint, characterCard.mana, characterCard.skill, characterCard.shield);
        }

        Debug.LogError("无效的卡牌 ID: " + _id);
        return null;
    }
}

3.playerdata编写

接下来就是playerdata脚本的编写,首先编写加载数据的方法,加载CardStore里的卡组和角色卡卡组,还有玩家的金币(后续做商店会用到),然后将卡牌按id数量存储到playerdata.csv文件

using System.Collections.Generic;
using UnityEngine;
using System.IO;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class PlayerData : MonoBehaviour
{
    public CardStore CardStore;
    public int playerCoins;
    public int[] playerCards;
    public int[] playerDeck;
    public CharacterCard CharacterCard;

    public TextAsset playerData;
    // Start is called before the first frame update
    void Start()
    {
        //先加载卡牌在加载数据
        CardStore.LoadCardData();
        LoadPlayerData();
        InitializeManaFromCharacterCard();
    }

    // Update is called once per frame
    void Update()
    {

    }
    public void LoadPlayerData()
    {
        playerCards = new int[CardStore.cardList.Count];
        playerDeck = new int[CardStore.cardList.Count];
        string[] dataRow = playerData.text.Split('\n');
        foreach (var row in dataRow)
        {
            string[] rowArray = row.Split(',');
            if (rowArray[0] == "#")
            {
                continue;
            }
            else if (rowArray[0] == "coins")
            {
                playerCoins = int.Parse(rowArray[1]);
            }
            else if (rowArray[0] == "card")
            {
                int id = int.Parse(rowArray[1]);
                int num = int.Parse(rowArray[2]);
                //载入玩家数据
                //确保id在有效范围内,避免数组越界或无效赋值
                playerCards[id] = num;
            }
            else if (rowArray[0] == "deck")
            {
                int id = int.Parse(rowArray[1]);
                int num = int.Parse(rowArray[2]);
                //载入玩家数据
                //确保id在有效范围内,避免数组越界或无效赋值
                playerDeck[id] = num;
            }
            else if (rowArray[0] == "character")
            {
                int characterId = int.Parse(rowArray[1]);
                // 根据 ID 加载角色卡
                if (characterId >= 0 && characterId < CardStore.characterCardList.Count)
                {
                    CharacterCard = CardStore.characterCardList[characterId];
                }
            }

        }
    }
    public void SavePlayerData()
    {
        //待完善
        //csv文件无法实时更新,已解决在编辑器实时更新,但最后构筑的时候是无法更新的
        //需之后修改为json文件进行读取
        string path = Application.dataPath + "/datas/playerdata.csv";

        List<string> datas = new List<string>();
        datas.Add("coins," + playerCoins.ToString());
        for (int i = 0; i < playerCards.Length; i++)
        {
            if (playerCards[i] != 0)
            {
                datas.Add("card," + i.ToString() + "," + playerCards[i].ToString());
            }
        }
        //保存卡组
        for (int i = 0; i < playerDeck.Length; i++)
        {
            if (playerDeck[i] != 0)
            {
                datas.Add("deck," + i.ToString() + "," + playerDeck[i].ToString());
            }
        }
        if (CharacterCard != null)
        {
            int characterId = CardStore.characterCardList.IndexOf(CharacterCard);
            if (characterId != -1)
            {
                datas.Add("character," + characterId.ToString());
            }
        }

        //保存数据
        File.WriteAllLines(path, datas);
#if UNITY_EDITOR
        AssetDatabase.Refresh();
#endif
    }
}

4.OpenPackage编写

最后是开卡包的功能,我们需要编写一个代码,将想要显示的卡牌显示到特定的位置,下面是它的代码:

首先是生成卡牌的逻辑,调用到newCard.GetComponent<CardDisplay>().card = CardStore.RandomCard();,,即上文CardStore里的抽卡逻辑在cardPool的位置生成卡牌,并加入销毁卡牌和存储数据的操作,这样可以把以出现的卡牌进行存储并更新显示。

using System.Collections.Generic;
using UnityEngine;

public class OpenPackage : MonoBehaviour
{
    public GameObject cardPrefab;
    public GameObject cardPool;

    CardStore CardStore;
    List<GameObject> Cards = new List<GameObject>();

    public PlayerData PlayerData;

    private int maxCardLimit = 4;
    // Start is called before the first frame update
    void Start()
    {
        CardStore = GetComponent<CardStore>();
    }

    // Update is called once per frame
    void Update()
    {

    }

    public void OnClinkOpen()
    {

        // 计算还能生成多少张卡牌
        int remainingSlots = maxCardLimit - Cards.Count;

        // 如果没有剩余槽位,则直接返回
        if (remainingSlots <= 0)
        {
            Debug.Log("卡牌数量已达到上限,无法继续生成!");
            return;
        }
        //每次点击扣除2金币
        if (PlayerData.playerCoins < 2)
        {
            return;
        }
        else
        {
            PlayerData.playerCoins -= 2;
        }
        // 只生成不超过剩余槽位数量的卡牌
        int cardsToGenerate = Mathf.Min(3, remainingSlots);

        for (int i = 0; i < cardsToGenerate; i++)
        {
            GameObject newCard = GameObject.Instantiate(cardPrefab, cardPool.transform);

            newCard.GetComponent<CardDisplay>().card = CardStore.RandomCard();

            Cards.Add(newCard);
        }
        SaveCardData();
        PlayerData.SavePlayerData();
    }
    //销毁全部卡牌
    public void ClearPool()
    {
        foreach (var card in Cards)
        {
            Destroy(card);
        }
        if (Cards.Count >= maxCardLimit)
        {
            Cards.Clear();
        }
    }
    public void SaveCardData()
    {
        foreach (var card in Cards)
        {
            int id = card.GetComponent<CardDisplay>().card.id;
            PlayerData.playerCards[id] += 1;
        }
    }
}

四、挂件的挂载和项目实现

然后将四个组件依次挂载上去就可以完成简单的卡牌显示啦

把playerdata绑定到一个空物体

还有CardStore绑定到另一个空物体,并加入Open Package的代码挂载上去

并新建一个CardPool,挂载上Grid Layout Group挂件(同于填充卡牌组,并设计其间隔)

最后添加一个按钮open在鼠标点击中选择OpenPackage里的OnClickOpen方法,即鼠标点击时会调用此代码

这个时候一个简易的卡牌显示功能就完成了。

本人只是一位初学者,还差错的话,请多多理解。

本文参考了部分大佬的设计,适合初学者看看,大佬可绕道。

以上只是卡牌游戏中比较小的一部分,后面战斗编辑器最简单的都写到了一千行左右,之后再结合介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值