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