AbilityManager

AbilityManager은 몬스터의 능력을 발동시킨다. Update 문에서 지속적으로 확인한다. 몬스터의 능력은 여러가지 상황에서 발동되는데, 턴 시작 시, 소환 시, 공격 시, 공격을 당할 시, 턴 종료 시, 몬스터가 죽을 시 등 다양한 상황에 발동된다. 위의 상황들을 enum을 통해 상태를 관리한다. 능력을 구현하는 코드는 방대하여, partial로 나누어 따로 작성하였다. 능력 중 일부만 구현하였고, 그 중 한 개씩만 posting하였다.

1.turnstart

내 턴이 시작하면 능력이 발동된다. 그 중에 감염이라는 능력을 구현하는 것을 올린다. 감염의 효과는 우선 “내 턴 시작 시에 무작위 몬스터를 지정하여 공격력 1 감소한다”는 능력이다. 이 능력은 턴이 시작하면 바로 발동되며, 상대방 몬스터가 없을 시 능력은 발동하지 않는다.

먼저 턴이 시작하는 것을 확인을 해야 한다. 내 턴이 시작되는 것은 TurnManager에서 StartTurnCo가 호출될 때 TurnStart 상태가 된다.

    IEnumerator StartTurnCo()
    {
        isLoading =true;
        SetGauge(myTurn);
        if(myTurn)
        {
            GameManager.Inst.Notification("나의 턴!");
            AbilityManager.Inst.eTurnState=AbilityManager.ETurnState.turnStart;
            AbilityManager.Inst.stateTrigger =true; // enum 상태가 바뀌고, Trigger를 true로 바꾼다. 
        }
        yield return delay07;
        if(myTurn)
            OnAddCard?.Invoke(myTurn);
        yield return delay07;
        isLoading =false;
        OnTurnStarted?.Invoke(myTurn);
    }

Trigger가 동작되면, AbilityManager의 Update에서 함수를 호출한다.

    void Update() 
    {
        GetEntities(); // entity list를 가져온다. 
        if(stateTrigger)
        {
            stateTrigger = false; //trigger를 원 상태로 돌린다.
            Debug.Log(eTurnState.ToString());
            UseAbility(); //능력 발동
            eTurnState = ETurnState.normal; //상태를 다시 기본 상태로 돌린다. 
        }
    }

UseAbility는 Switch문을 바탕으로 구현되어 있다.

case ETurnState.turnStart:
    isThereEntities = CheckAbl(true,eTurnState); //CheckAbl은 소환된 내 entity 중에서 능력이 turnstart일 때 발동하는 entity list들을 불러온다.
    if(isThereEntities != null)
    {
        StartCoroutine(DoAbility0(myEntities,otherEntities,isThereEntities)); //코루틴을 통해 능력 구현
    }
    isThereEntities = null;
    break;
IEnumerator DoAbility0(List<Entity> myEntities, List<Entity> otherEntities, List<Entity> ablEntities)
    {
        int[] abilityNum = { 9, 15, 20, 23, 24, 26, 27, 34, 37, 47 , 5};
        int entityCnt = ablEntities.Count;

        foreach (Entity entity in ablEntities)
        {
            switch (entity.ablcode1)
            {
                case 9:
                    //감염
                    yield return StartCoroutine(Ability9(myEntities, otherEntities, entity));
                    yield return new WaitForSeconds(6.0f);
                    break;
            }
        }
    }

일부의 코드만 긁어 왔다.

위의 코루틴은 다시 코루틴을 호출하게 된다.

    //감염
    IEnumerator Ability9(List<Entity> myEntities, List<Entity> otherEntities, Entity entity)
    {
        yield return new WaitForSeconds(3.0f);
        List<Entity> targetList = new List<Entity>();
        foreach (Entity target in otherEntities)
        {
            if (target.attack > 0)
                targetList.Add(target);
        }
        if (targetList != null)
        {
            entity.GetComponent<Order>().SetOrder(0, "Ability");
            GameManager.Inst.Notification("감염 발동!");
            yield return new WaitForSeconds(2.0f);
            entity.transform.DOScale(1.5f, 0.5f);
            yield return new WaitForSeconds(1.0f);
            int index = Random.Range(0, targetList.Count);
            byte[] send = new byte[3] {0x09, Convert.ToByte(entity.gameObject.name),Convert.ToByte(targetList[index].gameObject.name)};
            BackEndMatch.SendMessage(send);
            targetList[index].attack--;
            targetList[index].gameObject.transform.Find("TMP_ATT").transform.GetComponent<TextMeshPro>().text = targetList[index].attack.ToString();
            targetList[index].gameObject.transform.Find("TMP_ATT").transform.DOShakeScale(1.0f, 2, 10, 90, true);
            targetList[index].gameObject.transform.Find("TMP_ATT").transform.GetComponent<TextMeshPro>().fontMaterial = blueColor;
            entity.transform.DOScale(0.8f, 0.5f);
            entity.GetComponent<Order>().SetOriginOrder(0);
        }

    }

2.summonsMon

몬스터를 소환했을 때 발동한다. 능력 중 소환이라는 능력의 코드를 일부 긁어 왔다. 소환이라는 능력은 “몬스터 소환 시 m을 소환한다” 이다. 이 때 m은 item 코드에 link라는 항목이 있는데, link된 몬스터 중 무작위 소환된다. summonsMon은 CardManager에서 TryPutCard가 동작할 때 trigger가 동작한다.

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))
                {
                    targetCards.Remove(card);
                    card.transform.DOKill();
                    DestroyImmediate(card.gameObject);
                    if (isMine)
                    {
                        selectCard = null;
                        myPutCount++;
                        TurnManger.Inst.ConsumeCost(isMine, card.item.cost_num);
                    }
                    CardAlignment(isMine);
                    AbilityManager.Inst.eTurnState = AbilityManager.ETurnState.summonsMon;
                    AbilityManager.Inst.stateTrigger = true; // trigger 동작
                    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;
                }
        }
    }

trigger가 동작하면 turnstart와 동일하게 함수가 2개의 코루틴이 동작한다.

case ETurnState.summonsMon:
    entity = CheckAblEntity1(true,eTurnState);
    if(entity != null)
    {
        StartCoroutine(DoAbility1(myEntities,otherEntities,entity));
    }
    entity = null;
    break;
    IEnumerator DoAbility1(List<Entity> myEntities, List<Entity> otherEntities, Entity entity)
    {
        switch (entity.ablcode1)
        {
            case 2:
                //소환
                yield return StartCoroutine(Ability2(myEntities, entity));
                break;
        }
    } // 코드 일부 발췌
    //소환
    IEnumerator Ability2(List<Entity> myEntities, Entity entity)
    {
        yield return new WaitForSeconds(3.0f);
        int code = int.Parse(entity.link[Random.Range(0, entity.link.Count)]);
        SpawnEntity(SetEntity(code)); //AbilityManager에서 따로 SpawnEntity를 작성
        entity.ablcode1 = 0;
        byte[] send = new byte[4]{0x02,BackEndMatch.ConvertByte(code)[0],BackEndMatch.ConvertByte(code)[1],BackEndMatch.ConvertByte(code)[2]};
        BackEndMatch.SendMessage(send);
    }

3.attacking

공격 시 동작하는 부분이다. 이 부분은 데이터 전송하는 단계에서 문제가 생길 것으로 생각되어, 공격 받는 부분도 같이 구현하였다. 공격을 받았을 때 동작은 내가 공격을 한 상대의 능력이 공격 당했을 시 나타나는 능력일 경우 동작한다. 이 때는 내 능력이 아닌 상대의 능력 code를 확인한다. 공격 시에서 불기둥 code와 공격 당했을 시에서는 회피의 code를 리뷰하겠다.

case ETurnState.attacking:
    entity = EntityManeger.Inst.castEntity;
    if(entity != null)
    {
        if(abilityNum[entity.ablcode1].state == (int)ETurnState.attacking)
        {
            StartCoroutine(DoAbility2(myEntities,otherEntities,entity,EntityManeger.Inst.targetEntity));
        }
        if(abilityNum[EntityManeger.Inst.targetEntity.ablcode1].state == (int)ETurnState.beAttacked)
        {
            StartCoroutine(DoAbility7(myEntities,otherEntities,entity,EntityManeger.Inst.targetEntity));
        }
    }
    entity = null;
    break;

둘 다 한 번에 동작할 수 있게 if는 따로 써주었다.

불기둥 : 공격 시 필드에 있는 모든 상대 몬스터와 플레이어에게 1의 데미지를 준다.

//불기둥
    IEnumerator Ability25(List<Entity> otherEntities, Entity entity)
    {
        List<GameObject> fires = new List<GameObject>(); //이펙트 (진짜 메이플스토리 게임 속의 "아니"의 스킬 구현)
        List<Entity> dieEntity = new List<Entity>(); //공격을 당해 죽는 entity
        yield return null;
        byte[] send =new byte[2] {Convert.ToByte(25),Convert.ToByte(entity.gameObject.name)};
        BackEndMatch.SendMessage(send);//데이터 전송
        foreach(var target in otherEntities)
        {
            var fire = Instantiate(firePrefab,target.transform.position,target.transform.rotation);
            fires.Add(fire); // 타겟 위에 fire 이펙트 설정
            target.Damaged(1);// 타겟들에게 1 데미지
            if(target.health <=0) //entity가 방어력이 0이 되면, 죽는 entity list에 추가
            {
                dieEntity.Add(target);
            }
        }
        var fire2 = Instantiate(firePrefab,otherBoss.transform.position,otherBoss.transform.rotation); // 플레이어에게도 데미지
        fires.Add(fire2);
        otherBoss.Damaged(1);
        yield return new WaitForSeconds(1.0f);
        foreach(var fire in fires)
        {
            Destroy(fire.gameObject);
        } // 이펙트는 1초 진행되고 파괴
        foreach(var target in otherEntities)
        {
            EntityManeger.Inst.SpawnDamage(1,target.transform); //데미지 오브젝트 spawn
        }
        EntityManeger.Inst.SpawnDamage(1,otherBoss.transform);
        foreach(var row in dieEntity)
        {
            EntityManeger.Inst.AttackCallback(row); //entity가 죽을 경우 attackcallback을 통해 entity 파괴
        }
        yield return StartCoroutine(EntityManeger.Inst.CheckBossDie()); //플레이어에게 데미지를 1 주어서 플레이어가 죽었는 지 확인
    }

회피 : 50% 확률로 공격을 회피한다.

IEnumerator Ability4(Entity attacker,Entity defender)
    {
        yield return null;
        if(Random.Range(0,2) ==0) // 1 또는 0
        {
            byte[] send = new byte[3] {Convert.ToByte(4),Convert.ToByte(attacker.gameObject.name),Convert.ToByte(defender.gameObject.name)};
            BackEndMatch.SendMessage(send);
            attacker.attackable = false;
            Sequence sequence = DOTween.Sequence()
                .Append(attacker.transform.DOMove(defender.transform.position, 0.4f)).SetEase(Ease.InSine)
                .AppendCallback(() =>
                { //attack과 동일하지만 움직임만 있고 공격 데미지가 없다
                })
                .Append(attacker.transform.DOMove(attacker.transform.position, 0.4f)).SetEase(Ease.OutSine)
                .OnComplete(() => EntityManeger.Inst.AttackCallback(attacker, defender));
        }
        else
        {
            if(abilityNum[attacker.ablcode1].state != (int)ETurnState.attacking)
            {
                byte[] send = new byte[3] { 0x00, Convert.ToByte(attacker.gameObject.name), Convert.ToByte(defender.gameObject.name) };
                BackEndMatch.SendMessage(send);
                Attack(attacker,defender); // 회피 실패 시 기본 공격
            }
            
        }
    }

4.drag

entity를 드래그 했을 때 발동한다. 여기서는 발동하는 능력이 1개 밖에 없다. 저주라는 능력인데, 저주는 “상대 플레이어를 직접 공격할 수 없다”이다. 그래서 저주 능력을 가진 entity를 드래그할 경우 상대 플레이어를 공격 대상으로 삼을 수 없다.

case ETurnState.drag:
    entity = CheckAblEntity3(true,eTurnState);
    if(entity != null)
    {
        StartCoroutine(DoAbility3(myEntities,otherEntities,entity));
    }
    entity = null;
    break;
    //저주
    IEnumerator Ability29(Entity entity)
    {
        otherBoss.canBeAttacked = false;
        while(true)
        {
            yield return null;
            if(entity != EntityManeger.Inst.selectEntity)
            {
                otherBoss.canBeAttacked = true;
                break;
            }
        }
    }

5.turnEnd

턴 종료 시에 발동 된다. 턴 종료 버튼을 누를 때 호출된다. 치유 능력 code를 리뷰하겠다. 치유는 “내 턴이 종료할 때마다 내 필드 위의 모든 몬스터 방어력 1 회복한다.”이다.

case ETurnState.turnEnd:
    isThereEntities = CheckAbl(true,eTurnState);
    if(isThereEntities != null)
    {
        StartCoroutine(DoAbility5(myEntities,otherEntities,isThereEntities));
    }
    isThereEntities = null;
    break;
    //치유
    IEnumerator Ability11(List<Entity> myEntities, Entity entitiy)
    {
        foreach(var row in myEntities)
        {
            row.health+=1;
            yield return UpdateATTDEF(row,true); //공격력이나 방어력이 바뀔 때 숫자의 색깔이 바뀐다. true일땐 빨간색, false일 때는 파란색
        }
        byte[] send = new byte[2] {0x0B, Convert.ToByte(entitiy.gameObject.name)};
        BackEndMatch.SendMessage(send);
    }

6.myMonDie

내 entity가 죽었을 때 호출된다. 이 중에 제물 code를 리뷰하겠다. 제물은 “이 몬스터가 죽을 시 m을 소환한다.”이다.

case ETurnState.myMonDie:
    entity = CheckAblEntity7(true,ETurnState.beAttacked);
    if(entity != null)
    {
        Debug.Log(entity);
        StartCoroutine(DoAbility9(myEntities,otherEntities,entity));
    }
    isThereEntities = null; 
    break;
    //제물
    IEnumerator Ability3(Entity entity)
    {
        yield return null;
        if(myEntities.Count<10)
        {
            int index = Random.Range(0,entity.link.Count);
            yield return new WaitForSeconds(1.0f);
            SpawnEntity(SetEntity(int.Parse(entity.link[index])));
            byte[] send = new byte[4]{0x02,BackEndMatch.ConvertByte(index)[0],BackEndMatch.ConvertByte(index)[1],BackEndMatch.ConvertByte(index)[2]};
            BackEndMatch.SendMessage(send);
        }
    }

모든 상황 별 능력을 1개씩만 리뷰하였다. 이와 비슷한 코루틴이 약 50개 정도이다. 몬스터의 능력을 모두 구현하였고, 마법카드와 장비카드의 능력은 구현하지 않았다.