На этой неделе не делал новые фичи, зато наконец сделал более удобную архитектуру. Это упростит разработку в дальнейшем.

Вводная инфа

Делаю коллекционную карточную PvP-игру на Unity и C#. Гитхаб.

Что сделал

Раньше у меня анимации были намертво прибиты к логике. Я получал комментарии, что это вообще-то не очень хорошо, но сам минусов не находил. Пока не столкнулся с задачей, где требовалось моментальная работа логической части, а она у меня приостанавливалась, подстраиваясь под выполнение анимаций.

Раньше было вот так. Чтобы у меня карты выскакивали по одной из колоды, я приостанавливал выполнение логических операций:

   public IEnumerator DrawingCard(Side side, int amountOfCards = 5, float pauseTime = 0)
    {
        yield return new WaitForSeconds(pauseTime);
        int i = side.TableCards.Count;
        while (side.TableCards.Count < amountOfCards + i)
        {
            if (side.Cards.Count == 0)
            {
                shuffledComplete = false;
                StartCoroutine(ShufflingDeck(side));
                yield return new WaitUntil(() => shuffledComplete == true);
            }
            GameObject card = side.Cards[0];
            //card.transform.localPosition = side.StartPosition;
            side.TableCards.Add(card);
            side.Cards.RemoveAt(0);
            yield return new WaitForSecondsRealtime(.3f);
        }
    }

Теперь у меня DrawCard - это метод в логической части кода и там только происходит удаление карты из одного списка и добавление в другой. Ну, и отправка соответствующего ивента:

    public void DrawCard(GameObject card)
    {
        TableCards.Add(card);
        Cards.Remove(card);
        CardAction?.Invoke(card, this, CardActionType.Draw);
    }


    public void DrawCards()
    {
        DrawCards(StartDrawCards);
    }

    public void DrawCards(int numberOfCards)
    {
        int i = TableCards.Count;
        while (TableCards.Count < numberOfCards + i)
        {
            if (Cards.Count == 0)
            {
                ShufflingDrawDeck();
                CardAction?.Invoke(null, this, CardActionType.Shuffle);
            }
            GameObject card = Cards[0];
            DrawCard(card);
        }
    }

Все остальное в скрипте, который отвечает за анимации. Там создается очередь из анимаций, которые проигрываются друг за другом. Да, знаю, else if тут выглядит по-колхозному, но пока не знаю решения, как это сократить, а разбираться было лень:

    void Update()
    {
        if (!isProcessingQueue && eventQueue.Count > 0)
        {
            StartCoroutine(ProcessEventQueue());
        }
    }

    IEnumerator ProcessEventQueue()
    {
        isProcessingQueue = true;
        CardEvent cardEvent = eventQueue.Dequeue();
        
        if (cardEvent.ActionType == CardActionType.Draw)
        {
            PlayDrawAnimation(cardEvent.Card, cardEvent.TriggeringSide);
        }
        else if (cardEvent.ActionType == CardActionType.Discard)
        {
            PlayDiscardAnimation(cardEvent.Card, cardEvent.TriggeringSide);
        }
        else if (cardEvent.ActionType == CardActionType.Shuffle)
        {
            PlayShuffleAnimation(cardEvent.TriggeringSide);
        }
        else if (cardEvent.ActionType == CardActionType.Burn)
        {
            PlayBurnAnimation(cardEvent.Card, cardEvent.TriggeringSide);
        }
        yield return new WaitForSeconds(0.3f);
        isProcessingQueue = false;
    }

    void PlayDrawAnimation(GameObject card, Side side)
    {
        side.DoubleTableCards.Add(card);
        card.transform.localPosition = side.StartPosition;
        CalculateTableCardsPosition(side);
        side.DrawCounter--;
    }

    void OnCardAction(GameObject card, Side side, CardActionType cardActionType)
    {
        eventQueue.Enqueue(new CardEvent(card, cardActionType, side));
    }

Долго боролся с этим багом: при перетасовке колоды карты вылетают из стопки сброса, хотя должны как бы оказаться сначала в стопке выдачи:

Решение нашел супер простое. Каждый раз, когда вызывается анимация вытаскивания карты, я сначала карту перемещаю в нужное место:

    void PlayDrawAnimation(GameObject card, Side side)
    {
        side.DoubleTableCards.Add(card);
        card.transform.localPosition = side.StartPosition; // вот эта строчка
        CalculateTableCardsPosition(side);
        side.DrawCounter--;
    }

Как можете видеть, все равно у меня логика и визуал еще смешаны. В классе Side помимо логических методов я храню еще позиции карт (для стопки выдачи и стопки сброса) и дубль списка TableCards. От DoubleTableCards пока не знаю, как избавиться: он нужен, чтобы собирать в него карты с временным промежутком и красиво выстраивать на экране. Раньше у меня этот метод работал с TableCards, но при текущей логике появление всех карт на экране при выдаче будет моментальным:

    public void CalculateTableCardsPosition(Side side)
    {
        int offset = 105;
        int width = (side.DoubleTableCards.Count - 1) * offset;
        int halfWidth = width / 2;
        for (int cardIndex = 0; cardIndex < side.DoubleTableCards.Count; cardIndex++)
        {
            GameObject Card = side.DoubleTableCards[cardIndex];
            CardScript grid = Card.GetComponent<CardScript>();
            sprite = Card.GetComponent<SpriteRenderer>();
            sprite.sortingOrder = cardIndex;
            float desiredX = -halfWidth + (offset * cardIndex);
            grid.desiredPosition = new Vector2(desiredX, side.HandPosition);
            grid.timestamp = Time.time + grid.timeBetweenMoves;
            grid.startPosition = grid.desiredPosition;
        }
    }

В любом случае, эти штуки пока сильно не мешают. А в дальнейшем решение вижу в добавлении еще одной прослойки, которая соединяет чисто логические операции с расположением объектов на экране.

Что сделаю к следующей субботе

От прежнего стандарта планирования одной фичи на каждую неделю я отошел. Сейчас хочу продолжить менять архитектуру, чтобы дальше добавить кучу новых карт легко и быстро. Вы только посмотрите, какой треш в методе, который определяет эффекты для каждой карты. Займусь в первую очередь этой частью:

    public void DescriptionTranscription()
    {
        if (cardSide.Strength != lastStrength)
        {
            lastStrength = cardSide.Strength;
            finalDamage = 0;
            cardDescriptionDynamic = LocalizationSettings.StringDatabase.GetLocalizedString(cardId + "_Description");
            //string cardDescriptionDynamicWithoutTags;
            if (cardDescriptionDynamic.Contains("["))
            {
                int firstSym = cardDescriptionDynamic.IndexOf('[');
                int secondSym = cardDescriptionDynamic.IndexOf(']');
                string damage = "";
                for (int i = firstSym + 1; i < secondSym; i++)
                {
                    damage += cardDescriptionDynamic[i];
                }
                cardDamage = Int32.Parse(damage);
                cardDescriptionDynamic = cardDescriptionDynamic.Replace("[", "");
                cardDescriptionDynamic = cardDescriptionDynamic.Replace("]", "");
                finalDamage = cardDamage + cardSide.Strength;
                cardDescriptionDynamic = cardDescriptionDynamic.Replace(damage, finalDamage.ToString());


                if (cardSide.Strength != 0)
                {
                    string coloredDamage = "<color=" + (cardSide.Strength > 0 ? "green" : "red") + ">" + finalDamage.ToString() + "</color>";
                    cardDescriptionDynamic = cardDescriptionDynamic.Replace(finalDamage.ToString(), coloredDamage);
                }

            }
            if (cardDescriptionDynamic.Contains(";"))
            {
                int firstSym = cardDescriptionDynamic.IndexOf(';');
                int secondSym = cardDescriptionDynamic.IndexOf('?');
                string block = "";
                for (int i = firstSym + 1; i < secondSym; i++)
                {
                    block += cardDescriptionDynamic[i];
                }
                cardBlock = Int32.Parse(block);
                cardDescriptionDynamic = cardDescriptionDynamic.Replace(";", "");
                cardDescriptionDynamic = cardDescriptionDynamic.Replace("?", "");
                cardDescriptionDynamic = cardDescriptionDynamic.Replace(block, cardBlock.ToString());
            }
            if (cardDescriptionDynamic.Contains("{"))
            {
                int firstSym = cardDescriptionDynamic.IndexOf('{');
                int secondSym = cardDescriptionDynamic.IndexOf('}');
                string draw = "";
                for (int i = firstSym + 1; i < secondSym; i++)
                {
                    draw += cardDescriptionDynamic[i];
                }
                cardDraw = Int32.Parse(draw);
                cardDescriptionDynamic = cardDescriptionDynamic.Replace("{", "");
                cardDescriptionDynamic = cardDescriptionDynamic.Replace("}", "");
                cardDescriptionDynamic = cardDescriptionDynamic.Replace(draw, cardDraw.ToString());
            }
            if (cardDescriptionDynamic.Contains("("))
            {
                int firstSym = cardDescriptionDynamic.IndexOf('(');
                int secondSym = cardDescriptionDynamic.IndexOf(')');
                string strength = "";
                for (int i = firstSym + 1; i < secondSym; i++)
                {
                    strength += cardDescriptionDynamic[i];
                }
                cardStrength = Int32.Parse(strength);
                cardDescriptionDynamic = cardDescriptionDynamic.Replace("(", "");
                cardDescriptionDynamic = cardDescriptionDynamic.Replace(")", "");
                cardDescriptionDynamic = cardDescriptionDynamic.Replace(strength, cardStrength.ToString());
            }
        }

    }

Чем я делился в прошлых девлогах:


Этот пост входит в цикл постов про игру, которую я потихоньку делаю уже несколько месяцев. Я делюсь всем производственным процессом: какие решения я принимаю в разработке, геймдизайне, интерфейсе, арте и других сферах. Подписывайтесь тут или в телеграм-канале: @nigylam_blog.

Комментарии (0)