CardManager

CardManager를 통해 카드를 관리한다.

1.덱 구성, shuffle, pop

item class의 구성 요소이다.

public class Item
{
    public string name;
    public int attack;
    public int health;
    public Sprite main;
    public Sprite cardbase;
    public Sprite background;
    public string abl;
    public Sprite cost;
    public int cost_num;
    public int grade;
    public int code;
    public string ablcode;
    public string link;
}

아래의 코드는 밑의 post에 들어가면 확인할 수 있다.
post

public void LoadMydeck()
    {
        itemBuffer = new List<Item>(50);
        string[] type = new string[3] {"monster.xml","magic.xml","equip.xml"};
        var bro = Backend.GameData.GetMyData("character",new Where());
        var keys = bro.Rows()[0]["deck"][0].Keys;
        foreach(var key in keys)
        {
            if(key.Substring(0,1)== "1")
            {
                XDocument document = XDocument.Load(@"C:\nexon_maplestory_battle_monsters_league_s\Assets\Resources\CARD\xml\" + type[0]);
                var books = from row in document.Descendants("row")
                            where(row.Element("CODE").Value == key)
                            select new
                            {
                                name = row.Element("NAME").Value,
                                grade = row.Element("GRADE").Value,
                                cost = row.Element("COST").Value,
                                att = row.Element("ATT").Value,
                                def = row.Element("DEF").Value,
                                abl = row.Element("ABL").Value,
                                code = row.Element("CODE").Value,
                                area = row.Element("AREA").Value,
                                ablcode = row.Element("ABLCODE").Value,
                                link = row.Element("LINK").Value
                            };
                for(int i=0;i<int.Parse(bro.Rows()[0]["deck"][0][key][0].ToString());i++)
                {
                    foreach(var row in books)
                    {
                        Item item = new Item();
                        item.name = row.name;
                        item.grade = int.Parse(row.grade);
                        item.cost = Resources.Load<Sprite>("CARD/num/"+row.cost);
                        item.cost_num = int.Parse(row.cost);
                        item.attack = int.Parse(row.att);
                        item.health=int.Parse(row.def);
                        item.abl = row.abl;
                        item.code = int.Parse(row.code);
                        item.main = Resources.Load<Sprite>("CARD/monster/"+row.code);
                        item.background = Resources.Load<Sprite>("CARD/background/"+row.area);
                        item.cardbase = card_base_mon[item.grade];
                        item.ablcode = row.ablcode;
                        item.link = row.link;
                        itemBuffer.Add(item);
                    }
                }
            }
            if(key.Substring(0,1)== "2")
            {
                XDocument document = XDocument.Load(@"C:\nexon_maplestory_battle_monsters_league_s\Assets\Resources\CARD\xml\" + type[1]);
                var books = from row in document.Descendants("row")
                            where(row.Element("CODE").Value == key)
                            select new
                            {
                                name = row.Element("NAME").Value,
                                grade = row.Element("GRADE").Value,
                                cost = row.Element("COST").Value,
                                abl = row.Element("ABL").Value,
                                code = row.Element("CODE").Value
                            };
                for(int i=0;i<int.Parse(bro.Rows()[0]["deck"][0][key][0].ToString());i++)
                {
                    foreach(var row in books)
                    {
                        Item item = new Item();
                        item.name = row.name;
                        item.grade = int.Parse(row.grade);
                        item.cost = Resources.Load<Sprite>("CARD/num/"+row.cost);
                        item.cost_num = int.Parse(row.cost);
                        item.attack = -1;
                        item.health=-1;
                        item.abl = row.abl;
                        item.code = int.Parse(row.code);
                        if(int.Parse(row.code.Substring(3,2))<=27)
                        {
                            item.background = Resources.Load<Sprite>("CARD/magic/"+row.code);
                            item.cardbase =card_base[item.grade];
                            item.main = null;
                        }
                        itemBuffer.Add(item);
                    }
                }
            }
            if(key.Substring(0,1)== "3")
            {
                XDocument document = XDocument.Load(@"C:\nexon_maplestory_battle_monsters_league_s\Assets\Resources\CARD\xml\" + type[2]);
                var books = from row in document.Descendants("row")
                            where(row.Element("CODE").Value == key)
                            select new
                            {
                                name = row.Element("NAME").Value,
                                grade = row.Element("GRADE").Value,
                                cost = row.Element("COST").Value,
                                att = row.Element("ATT").Value,
                                def = row.Element("DEF").Value,
                                abl = row.Element("ABL").Value,
                                code =row.Element("CODE").Value
                            };
                for(int i=0;i<int.Parse(bro.Rows()[0]["deck"][0][key][0].ToString());i++)
                {
                    foreach(var row in books)
                    {
                        Item item = new Item();
                        item.name = row.name;
                        item.grade = int.Parse(row.grade);
                        item.cost = Resources.Load<Sprite>("CARD/num/"+row.cost);
                        item.attack = int.Parse(row.att);
                        item.health=int.Parse(row.def);
                        item.abl = row.abl;
                        item.code = int.Parse(row.code);
                        item.cardbase = card_base_mon[item.grade];
                        item.cost_num = int.Parse(row.cost);
                        item.background = back[item.grade];
                        item.main = Resources.Load<Sprite>("CARD/equip/"+row.code);
                        itemBuffer.Add(item);
                    }
                }
            }
        }
    }

위의 코드로 itembuffer에 item을 넣는다. 그 뒤에 덱을 섞는다.

    void ShuffleItemBuffer()
    {
        for(int i=0;i<itemBuffer.Count;i++)
        {
            int rand = UnityEngine.Random.Range(i,itemBuffer.Count);
            Item temp = itemBuffer[i];
            itemBuffer[i]=itemBuffer[rand];
            itemBuffer[rand]=temp;
        }
    }

게임 중에 카드를 뽑을 때는 가장 앞의 요소를 뽑고 삭제한다.

    public Item PopItem(bool isMine)
    {
        List<Item> items = isMine ? itemBuffer : otherItemBuffer;
        if(items.Count ==0)
        {
            Debug.Log("끝");
            return null;
        }
        Item item = items[0];
        items.RemoveAt(0);
        return item;
    }

2.AddCard

덱에서 카드를 뽑고 카드를 손에 위치하게 한다.

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

[System.Serializable]
public class PRS 
{
    public Vector3 pos;
    public Quaternion rot;
    public Vector3 scale;
    public PRS(Vector3 pos,Quaternion rot, Vector3 scale)
    {
        this.pos =pos;
        this.rot=rot;
        this.scale =scale;
    }
}
public class Utils
{
    public static Quaternion QI => Quaternion.identity;
    public static Vector3 MousePos
    {
        get
        {
            Vector3 result =Camera.main.ScreenToWorldPoint(Input.mousePosition);
            result.z =-10;
            return result;
        }
    }
}

Utils.cs라는 스크립트를 만들어 오브젝트의 위치를 좀 더 쉽게 찾을 수 있게 하였다.

    public void AddCard(bool isMine)
    {
        if(isMine) //내 차례일 때
        {
            var cardObject = Instantiate(cardPrefab,cardSpwanPoint.position,Utils.QI);
            var card =cardObject.GetComponent<Card>();
            card.Setup(PopItem(isMine), isMine);
            myCards.Add(card); // mycards -> 내 손의 카드 (list<card>)
            cardObject.transform.parent = CardList.transform;
            SetOriginOrder(isMine); //카드가 겹쳐있을 떄 제일 뒤 쪽의 카드가 앞 쪽으로 오게 해주는 함수 
            CardAlignment(isMine); //카드 정렬
            byte[] send = new byte[1] {Convert.ToByte(107)};
            Backend.Match.SendDataToInGameRoom(send);
        }
        else
        {
            var cardObject = Instantiate(cardPrefab,otherCardSpawnPoint.position,Utils.QI);
            var card =cardObject.GetComponent<Card>();
            card.Setup(PopItem(isMine), isMine);
            otherCards.Add(card);
            SetOriginOrder(isMine);
            CardAlignment(isMine);
        }
    }

3.SetOriginOrder

카드를 뽑았을 때 호출되며, 카드를 뽑으면 카드가 둥근 모습으로 겹쳐 보이는데 이 때 나중에 뽑은 카드일수록 앞쪽에 있어야 하므로 사용

    void SetOriginOrder(bool isMine)
    {
        int count = isMine ? myCards.Count : otherCards.Count;
        for(int i=0;i<count;i++)
        {
            var targetCard = isMine ? myCards[i] : otherCards[i];
            targetCard?.GetComponent<Order>().SetOriginOrder(i); //Order.cs
        }
    }
    //order.cs
    public void SetOriginOrder(int originOrder)
    {
        this.originOrder=originOrder;
        SetOrder(originOrder,sortingLayerName);
    }
    //order.cs
    public void SetOrder(int order,string sortingLayerName) //카드 prefebs에도 sprite와 text 들의 순위를 정하여 정렬(text는 무조건 제일 앞으로)
    {
        int mulOrder = order * 10;

        foreach(var renderer in backRenderers)
        {
            renderer.sortingLayerName = sortingLayerName;
            renderer.sortingOrder = mulOrder;
        }
        foreach(var renderer in middleRenderers)
        {
            renderer.sortingLayerName = sortingLayerName;
            renderer.sortingOrder = mulOrder+1;
        }
        foreach(var renderer in frontRenderers)
        {
            renderer.sortingLayerName = sortingLayerName;
            renderer.sortingOrder = mulOrder+2;
        }
    }

3.CardAlignment

카드를 뽑아서 손에 놓았을 때 카드가 자연스럽게 둥근 형태를 띄게 바꾼다.(1~3장까지는 일반 형태)

< CardAlignment >

아래의 code는 고라니님의 영상을 보면 자세히 확인할 수 있다.
YouTube

    public void CardAlignment(bool isMine)
    {
        List<PRS> originCardPRSs = new List<PRS>();
        if(isMine)
        {
            originCardPRSs =RoundAlignment(myCardLeft,myCardRight,myCards.Count,0.5f,UnityEngine.Vector3.one*0.8f); // 내카드 
        }
        else
        {
            originCardPRSs =RoundAlignment(otherCardLeft,otherCardRight,otherCards.Count,-0.5f,UnityEngine.Vector3.one*0.8f); //상대 카드는 뒤집어서 진행
        }
        var targetCards = isMine ? myCards : otherCards;
        for(int i =0;i<targetCards.Count;i++)
        {
            var targetCard =targetCards[i];

            targetCard.originPRS = originCardPRSs[i];
            targetCard.MoveTransform(targetCard.originPRS,true,0.7f);
        }
    }
    List<PRS> RoundAlignment(Transform leftTr, Transform rightTr, int objCount, float height,UnityEngine.Vector3 scale)
    {
        float[] objLerps =new float[objCount];
        List<PRS> results =new List<PRS>(objCount);

        switch(objCount)
        {
            case 1: objLerps =new float[] {0.5f}; break;
            case 2: objLerps =new float[] {0.27f, 0.73f}; break;
            case 3: objLerps =new float[] {0.1f,0.5f,0.9f};break; // 카드 3개까지는 그대로
            default:
                float interval = 1f / (objCount-1);
                for (int i=0;i<objCount;i++)
                    objLerps[i]=interval*i;
                break;
        }
        for(int i=0;i<objCount;i++)
        {
            var targetPos = UnityEngine.Vector3.Lerp(leftTr.position,rightTr.position,objLerps[i]);
            var targetRot = Utils.QI;
            if(objCount>=4)
            {
                float curve = Mathf.Sqrt(Mathf.Pow(height,2)- Mathf.Pow(objLerps[i]-0.5f,2));
                curve = height >= 0 ? curve : -curve;
                targetPos.y +=curve;
                targetRot =UnityEngine.Quaternion.Slerp(leftTr.rotation,rightTr.rotation,objLerps[i]);
            }
            results.Add(new PRS(targetPos,targetRot,scale));
        }
        return results;
    }

4.TryPutCard

카드를 필드에 소환한다.

    public bool TryPutCard(bool isMine, int code)
    {
        if (isMine)
        {
            Card card = selectCard; //내가 선택한 카드 
            var spawnPos = Utils.MousePos;
            var targetCards = myCards;

            if (card.item.cost_num <= TurnManger.Inst.myNowGaugeCnt) // 카드 코스트 < 현재의 코스트
            {

                if (EntityManeger.Inst.SpawnEntity(isMine, card.item, spawnPos)) // entity 소환 
                {
                    targetCards.Remove(card); // 내 카드 목록에서 카드 삭제
                    card.transform.DOKill(); 
                    DestroyImmediate(card.gameObject); //카드 바로 삭제 (사용시 주의)
                    if (isMine)
                    {
                        selectCard = null;
                        myPutCount++;
                        TurnManger.Inst.ConsumeCost(isMine, card.item.cost_num); //카드의 코스트만큼 코스트 사용
                    }
                    CardAlignment(isMine); // 카드의 갯수가 1개 줄어 들어 다시 카드 정렬
                    AbilityManager.Inst.eTurnState = AbilityManager.ETurnState.summonsMon;
                    AbilityManager.Inst.stateTrigger = true;
                    BackEndMatch.SendSpawn(card.item.code);
                    return true;
                }
                else
                {
                    targetCards.ForEach(x => x.GetComponent<Order>().SetMostFrontOrder(false));
                    CardAlignment(isMine);
                    return false;
                }
            }
            else
            {
                return false;
            }
        }
        else // 상대가 카드를 냈을 때 (위와 동일)
        {
            Card card = new Card();
            for(int i =0;i<otherCards.Count;i++)
            {
                if(otherCards[i].item.code==code)
                {
                    card = otherCards[i];
                }
            }
            var spawnPos = otherCardSpawnPoint.position;
            var targetCards = otherCards;
             if (EntityManeger.Inst.SpawnEntity(isMine, card.item, spawnPos))
                {
                    targetCards.Remove(card);
                    card.transform.DOKill();
                    DestroyImmediate(card.gameObject);
                    if (isMine)
                    {
                        selectCard = null;
                        myPutCount++;
                        TurnManger.Inst.ConsumeCost(isMine, card.item.cost_num);
                    }
                    CardAlignment(isMine);
                    return true;
                }
                else
                {
                    targetCards.ForEach(x => x.GetComponent<Order>().SetMostFrontOrder(false));
                    CardAlignment(isMine);
                    return false;
                }
        }
    }

위에보면 DestroyImmediate()라는 함수가 있다. Destroy()와의 차이점은 파괴되는 시점이다. Destroy()는 한 프레임이 지난 시점에 파괴되고, DestroyImmediate()는 함수가 호출된 시점에 바로 파괴가 된다. 두 번째 차이점은 Asset의 파괴 가능 유무이다. 자세한 내용은 아래 링크를 달아둔다.
Link

5.카드의 움직임

카드를 클릭하거나, 마우스를 올릴 때 마우스에 따라 같이 움직여야 카드를 소환하거나 할 수 있다. 카드 prefebs에서 onmouse함수를 통해 진행되고 호출되는 함수는 CardManager에서 관리한다.

카드에 마우스가 올라갔을 때 내 카드일 경우, 카드를 확대시켜 준다.

    public void CardMouseOver(Card card)
    {
        if(eCardState==ECardState.Nothing)
            return;

        selectCard = card;
        EnlargeCard(true,card);
    }

확대

void EnlargeCard(bool isEnlarge, Card card)
    {
        if(isEnlarge)
        {
            UnityEngine.Vector3 enlargePos = new UnityEngine.Vector3(card.originPRS.pos.x,-10f,-10f); // 카드 확대
            card.MoveTransform(new PRS(enlargePos,Utils.QI,Vector3.one*1.0f),false);
        }
        else
            card.MoveTransform(card.originPRS,false); //원래 자리로 이동 

        card.GetComponent<Order>().SetMostFrontOrder(isEnlarge);//카드 확인을 위해 제일 앞으로 이동
    }

카드에서 마우스 커서가 나가 확대를 멈춤

    public void CardMouseExit(Card card) 
    {
        EnlargeCard(false,card);
    }

카드를 클릭했을 때

    public void CardMouseDown() 
    {
        if(eCardState !=ECardState.CanMouseDrag)
            return;
        isMyCardDrag =true;
    }

카드의 클릭을 취소했을 때

public void CardMouseUp()
    {
        isMyCardDrag =false;
        if(eCardState !=ECardState.CanMouseDrag)
            return;

        if(onMyCardArea)
            EntityManeger.Inst.RemoveMyEmptyEntity();  //다시 카드로 이동
        else
            TryPutCard(true,selectCard.item.code); // 카드를 소환
    }
void CardDrag()
    {
        if(eCardState !=ECardState.CanMouseDrag)
            return;
        if(!onMyCardArea)
        {
            selectCard.MoveTransform(new PRS(Utils.MousePos,Utils.QI,selectCard.originPRS.scale*0.8f),false); //카드를 0.8배로해서 작게 마우스를 따라다님
            EntityManeger.Inst.InsertMyEmptyEntity(Utils.MousePos.x);
        }
    }

6.마우스 상태

Update 함수에서 지속적인 호출을 통해 마우스의 상태를 지속적으로 업데이트한다.

    enum ECardState {Nothing,CanMouseOver,CanMouseDrag}

    void SetECardState()
    {
        if(TurnManger.Inst.isLoading) //로딩 중에는 마우스로 아무것도 할 수 없다.
            eCardState = ECardState.Nothing;
        else if(!TurnManger.Inst.myTurn || TurnManger.Inst.myNowGaugeCnt <=0) // 내 코스트가 0이고, 내 턴이 아닐 때는 클릭을 할 수 없다.
            eCardState =ECardState.CanMouseOver;
        else if(TurnManger.Inst.myTurn) // 내턴일 때는 드래그를 할 수 있다.
            eCardState = ECardState.CanMouseDrag;
    }