На этой неделе не делал новые фичи, зато наконец сделал более удобную архитектуру. Это упростит разработку в дальнейшем.
Вводная инфа
Делаю коллекционную карточную 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.
