The Abilities Selector is a complex window that displays up to three abilities for the player to choose from.
Abilities Selector Prefab Structure
Background Image - A dark, full-screen image that dims the background and blocks raycasts to all elements behind the Ability Selector.
Slots Holder - The parent object that contains all Ability Selector Slots.
Level Up Header - The visual header of the Ability Selector. It includes the Level Up text, decorative images, and an animated shine line that plays when the Ability Selector is shown.
Rarity - A visual element representing the rarity tier of the abilities currently shown to the player. It contains Rarity Text and a Gradient Image.
Skip Button - A full-screen invisible button that allows the player to skip the Ability Selector’s animation.
Roll Again Button - A button that lets the player reroll the available abilities for a small gold cost.
Gold - Displays the player’s current gold amount.
Abilities Selector Animation flow
The Ability Selector window features a complex multi step animation. It's controlled by ShowCoroutine() method. Let's break it down
The Ability Selector Component contains many variables that control the animation flow, durations, and delays of different UI elements.
If the player clicks Skip, the animation immediately jumps to the CompleteShowCoroutine() method.
Ability Selector Slot Animation Flow
Each slot scrolls cards vertically in a continuous loop, controlled by ScrollingCoroutine().
Here’s a breakdown:
Ability Card Animation Flow
The animation for revealing an ability card is handled by the Init() method in AbilityCardBehavior:
// Method is public virtual for easier modification
public virtual IEnumerator ShowCoroutine(List<AbilityData> abilities, AbilityRarity rarity)
{
IsOpen = true;
Rarity = rarity;
// Tell the Game Screen to hide the Pause button and temporary gold indicator
StageController.GameScreen.HideSideUI();
// Initialize audio (plays popup and ambient scroll sounds)
InitAudio();
// Reset all UI parameters and components to default values
ResetUI();
// Gradually slow down time to 0
timeScaleEasingCoroutine = EasingManager.DoTimeScale(0, timescaleStopDuration);
// Fade in background image
backgroundAlphaEasingCoroutine =
backgroundImage.DoAlpha(1, backgroundFadeInDuration)
.SetUnscaledTime(true);
// Show Abilities Selector header
title.Show();
// Wait briefly before initializing slots
yield return new WaitForSecondsRealtime(slotInitialDelay);
// Fetch slots from the pool and initialize them with ability data and IDs
InitSlots(abilities);
// Allow player to skip the remaining animation
skipButton.gameObject.SetActive(true);
// Wait before showing rarity flash
yield return new WaitForSecondsRealtime(flashInitialDelay);
// Fade in rarity header (initially gray/undefined)
rarityAlphaEasingCoroutine =
rarityCanvasGroup.DoAlpha(1, 0.5f)
.SetUnscaledTime(true);
// Flash rarity until desired level is reached
// Order: Common -> Rare -> Mystical -> Legendary
var rarityIndex = (int)rarity;
for (int i = 0; i <= rarityIndex; i++)
{
var data = rarityData[i];
DoRarityFlash(data); // Trigger rarity flash animation
if (i != rarityIndex)
yield return new WaitForSecondsRealtime(flashStepDelay);
}
// Wait before stopping the slot scroll
yield return new WaitForSecondsRealtime(stopScrollingDelay);
// Stop scrolling slots, show abilities, reroll button, and gold indicator
yield return CompleteShowCoroutine();
}
protected virtual IEnumerator ScrollingCoroutine( AbilityData data, int id)
{
isStopped = false;
// Reset gradient image to gray (undefined) and make it transparent
gradientImage.color = initialGradientColor;
gradientImage.SetAlpha(0);
// Fade in the gradient background
gradientImage.DoAlpha(initialGradientColor.a, 0.3f).SetUnscaledTime(true);
// Get an Ability Card from the pool and show its back side
highestCard = cardsPool.GetEntity();
// Telling card to initialize showing it's back.
// hiddenCardMargin is uset to calculate card's size
highestCard.ShowHidden(hiddenCardMargin);
var scrollContentSize = scrollContentRect.sizeDelta;
var cardSize = highestCard.RectTransform.sizeDelta;
// Calculating first card's position.
// Adjusted card's position adds ofsett from slot's id.
cardPosition = new Vector2(0, scrollContentSize.y / 2f + cardSize.y / 2f - hiddenCardMargin);
var adjustedCardPosition = new Vector2(0, scrollContentSize.y / 2f + cardSize.y / 2f + cardSize.y / 4f * id - hiddenCardMargin);
highestCard.RectTransform.anchoredPosition = adjustedCardPosition;
var cardHeightOffset = new Vector2(0, cardSize.y);
// Card image is faded by a custom gradient, not by a single alpha value.
// We use custom shader for that, and this are the parameters that are provided
// To the material
highestCard.SetAlphaParameters(200, 200 - fadeSize, true);
highestCard.CanvasGroup.alpha = 1;
// Claculating the position the highest card needs to be in order to spawn next card
// and the position card will be completely invisible and can be returned to the pool.
nextCardSpawnTriggerY = cardPosition.y - cardSize.y + hiddenCardMargin * 2 - scrollingSpacing;
var cardDisappearTriggerY = -cardPosition.y;
var flipCardTriggerY = nextCardSpawnTriggerY - fadeSize;
// Creating and adding to a list Card data - a simple wrapper for Ability card
// with an additional flipped value
// Card is "Flipped" when it passes a middle point of a slot
var highestCardData = new CardData
{
Card = highestCard,
flipped = false
};
cards.Add(highestCardData);
var scrollSpeed = this.scrollSpeed;
MainCard = null;
LastCardSpawnTime = Time.unscaledTime;
// Scroll is coutinuous until Abilities Selector doesn't stop it.
while (true)
{
// We run this loop every frame
yield return null;
bool shouldBreak = false;
// If we stopping scrolling and showing main card
if (isStopped && MainCard != null)
{
var time = Time.unscaledTime - mainCardSpawnTime;
var t = time / mainCardMovementDuration;
if(t > 1)
{
t = 1;
shouldBreak = true;
}
// We moving top card using a animationCurve you can adjust in the inspector
var y = Mathf.LerpUnclamped(cardPosition.y, 0, mainCardMovementCurve.Evaluate(t));
var difference = MainCard.RectTransform.anchoredPosition.y - y;
// At this point scrolling speed is determined by the curve
scrollSpeed = difference / Time.unscaledDeltaTime;
// Hiding other card by another adjustable curve aswell
var otherCardsAlpha = otherCardsFadeCurve.Evaluate(t);
for (int i = 0; i < cards.Count; i++)
{
var card = cards[i];
if(card.Card != MainCard)
{
card.Card.CanvasGroup.alpha = otherCardsAlpha;
}
}
}
for (int i = 0; i < cards.Count; i++)
{
var cardData = cards[i];
var card = cardData.Card;
// Moving all card down
card.RectTransform.anchoredPosition += Vector2.down * scrollSpeed * Time.unscaledDeltaTime;
if(card != MainCard)
{
// reseting the scale of all cards but the main one
card.transform.localScale = Vector3.one * cardsScale;
}
// Hiding card if it is bellow cardDisappearTriggerY
if (card.RectTransform.anchoredPosition.y < cardDisappearTriggerY)
{
card.gameObject.SetActive(false);
cards.RemoveAt(i);
i--;
}
// The card reached the point where it's completely visible
if(card.RectTransform.anchoredPosition.y < flipCardTriggerY && !cardData.flipped)
{
cardData.flipped = true;
card.SetAlphaParameters(-650 + fadeSize, -650, false);
}
}
// Spawning next card
if (highestCard.RectTransform.anchoredPosition.y < nextCardSpawnTriggerY)
{
var difference = highestCard.RectTransform.anchoredPosition.y - nextCardSpawnTriggerY;
highestCard = cardsPool.GetEntity();
highestCard.ShowHidden(hiddenCardMargin);
highestCard.RectTransform.anchoredPosition = cardPosition + new Vector2(0, difference);
highestCard.SetAlphaParameters(200, 200 - fadeSize, true);
highestCard.CanvasGroup.alpha = 1;
LastCardSpawnTime = Time.unscaledTime;
// This will be the card we will show to the player
if (isStopped && MainCard == null)
{
MainCard = highestCard;
MainCard.Init(data, mainCardMovementDuration, id * 0.25f);
mainCardSpawnTime = Time.unscaledTime;
var halfDuration = mainCardMovementDuration / 2;
gradientImage.DoAlpha(0, halfDuration).SetDelay(halfDuration).SetUnscaledTime(true);
}
cards.Add(new CardData() { Card = highestCard });
}
if (shouldBreak) break;
}
}
public virtual void Init(AbilityData data, float duration, float delay = 0)
{
IsAnimationActive = true;
button.enabled = false;
// Initializing Ability Card UI, such as Icon, name and description
SetData(data);
// Setting position of card's front rect
cardFrontRect.SetStretchedOffset(Vector2.zero, Vector2.zero);
// By default cardBackSideImage.sprite is set to blurry version of a sprite to
// better show movement.
// Here we switch it to a clear version
cardBackSideImage.sprite = cardBackSprite;
var offsetMin = cardBackRect.offsetMin;
var offsetMax = cardBackRect.offsetMax;
var scaleX = (RectTransform.sizeDelta.x - offsetMin.x + offsetMax.x) / RectTransform.sizeDelta.x;
var scaleY = (RectTransform.sizeDelta.y - offsetMin.y + offsetMax.y) / RectTransform.sizeDelta.y;
CardBackRect.SetStretchedOffset(Vector2.zero, Vector2.zero);
transform.localScale = new Vector3(scaleX, scaleY, 1);
// Scaling card to a final size
scaleEasingCoroutine =
transform.DoLocalScale(Vector3.one, scaleDuration)
.SetDelay(duration - scaleDelayReduction + delay)
.SetEasingCurve(scaleCurve)
.SetUnscaledTime(true);
// Rotating card's back
backRotationEasingCoroutine =
cardBackRect.DoLocalEulerAngles(new Vector3(0, 90, 0), backRotationDuration)
.SetDelay(duration - rotationDelayReduction + delay)
.SetUnscaledTime(true)
.SetEasingCurve(backScaleCurve)
.SetOnFinish(OnCardBackScaledToZero);
// Rotating card's front
frontRotationEasingCoroutine =
cardFrontRect.DoLocalEulerAngles(Vector3.zero, frontRotationDuration)
.SetDelay(duration - rotationDelayReduction + delay + backRotationDuration)
.SetEasingCurve(frontScaleCurve)
.SetUnscaledTime(true);
// Scale the gradient image to its original size
gradientStretchEasingCoroutine =
gradientImage.rectTransform.DoStretchedOffset(gradientOffsetMin, gradientOffsetMax, gradientStretchDuration)
.SetDelay(duration - gradientStretchDelayReduction + delay)
.SetEasing(EasingType.CubicOut)
.SetUnscaledTime(true)
.SetOnFinish(OnGradientStretched);
// If ability is already acquired than we activate upgrade indicator
if (shouldShowUpgrade)
{
upgradeIndicator.gameObject.SetActive(true);
upgradeIndicator.DoLocalScale(Vector3.one, 0.4f)
.SetUnscaledTime(true)
.SetEasing(EasingType.CubicOut);
}
// Playing reveal sound
EasingManager.DoAfter(cardRevealSoundDelay + delay, () => GameController.AudioManager.PlayAudio(cardRevealedSound)).SetUnscaledTime(true);
}