Анимация вращения колеса рулетки

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

Давайте разберем задачу, которая часто встречается: анимация движущегося колеса лотереи.

Немного физики: движение должно постепенно замедляться из-за трения, поэтому нам нужно не просто вращение, а остановка с плавным снижением скорости.

Немного игры: каждый раз колесо раскручивается с разной начальной скоростью — именно это создаёт ощущение случайности и делает результат непредсказуемым.

И немного реальности: эта скорость не может быть абсолютно любой.

  • Слишком медленно — скучно: если колесо прокрутится меньше, чем несколько оборотов, игра будет неинтересной.
  • Слишком быстро — нереалистично: человек физически не способен раскрутить его до бесконечности.

Итак, начнём.

.NET MAUI предлагает целый набор метод анимации графики: RotateTo, ScaleTo, FadeTo, TranslateTo. Они применимы к любому классу, порожденному от VisualElement. VisualElement – это главный класс элемента:


А с этим классом уже работает статический класс ViewExtensions, который и обеспчивает анимационные движения (все методы асинхронные в последних версиях), например:

RotateToAsync(VisualElement element, Double rotation, UInt32 length, Easing easing) 

— поворот на угол rotation, за число милисекнд length, с кривой плавности easing (Easing.Linear, Easing.SinInOut, Easing.CubicOut или своя функция).

ScaleToAsync масштабирует элемент относительно его центра,

FadeToAsync плавно меняет прозрачность элмента от текущей до заданной (например, FadeTo(0, 500) значит плавное, за 500 мс, исчезновение, а label.FadeTo(1, 500) — такое же плавное появление),

TranslateToAsync cмещает элемент относительно исходного положения.

Все методы возвращают Task, чтобы их можно было комбинировать с await. И их можно вызывать всех вместе, одновременно, например:

await Task.WhenAll(
    image.RotateTo(360, 1000),
    image.ScaleTo(1.2, 1000),
    image.FadeTo(0.5, 1000)
); 

обеспечит плавное изменение сразу трех параметров элемента.

Как легко догадаться, в нашем случае подойдёт RotateTo.

Вот такой вызов:

await image.RotateTo(360, 1000, Easing.CubicOut);

приведет к быстрому старту и постепенному замедлению колеса.

На заметку:

Есть такие параметры Easing:
Easing.Linear – равномерное вращение,
Easing.CubicIn — ускорение,
Easing.CubicOut – замедление,
Easing.CubicInOut — медленный старт, быстрое вращение, медленное замедление.

(если честно, я как-то реализовал собственный механизм вычисления углов, отрисовки картинки с поворотом на этот угол, и постенного замедления — это работало, колесо красиво крутилось, ну и я был рад успешности собственного алгоритма).

Что же сделает нашу лотерею естественной?

Параметр 1. Случайное число оборотов вращения. Естественными были бы несколько полных оборотов колеса. Скажем, от 3 до 8.

Параметр 2. Случайный угол остановки (достаточно в целых градусах, то есть от 0 до 359).

Вот как-то так:

double finalRotation = 360 * _random.Next(3, 9) + _random.Next(0, 360);
// ^ 9 – max value, не достигается
await WheelImage.RotateToAsync(finalRotation, 4000, Easing.CubicOut);

?

Неправильно. Есть константа — число миллисекунд. А оно разное, зависит, естественно, от начальной скорости. Поэтому:

Параметр 3. Как-то вычисленное время. Оно зависит от числа оборотов. То есть:

uint duration = (uint)(Math.Abs(finalRotation) / degreesPerMillisecond);
await wheel.RotateToAsync(finalRotation, duration, Easing.CubicOut);

Параметр 4. Ну и мы не всегда начинаем вращение с нулевого угла. Если в прошлый раз колесо остановилось на каком-то углу, то мы начнем с него:

// Полный угол, на который нужно повернуть:
double finalRotation = 360 * _random.Next(3, 9) + _random.Next(0, 360) — startAngle;
// Продожительность:
uint duration = (uint)(Math.Abs(finalRotation) / degreesPerMillisecond);
// Вращаем колесо:
await wheel.RotateToAsync(startAngle + finalRotation, duration, Easing.CubicOut);

Ну и теперь полный пример:

public class WheelGame
{
    private readonly Image _wheel;
    private readonly string[] _prizes; // Список секторов/призов
    private readonly Random _random = new();

    public WheelGame(Image wheel, string[] prizes)
    {
        _wheel = wheel;
        _prizes = prizes;
    }

    /// <summary>
    /// Запускает игру: крутит колесо и возвращает выигрыш
    /// </summary>
    public async Task<string> PlayAsync()
    {
        // Текущий угол колеса
        double startAngle = _wheel.Rotation;

        // Случайное число полных оборотов (3–8)
        int turns = _random.Next(3, 9);

        // Случайный угол остановки (0–359)
        int randomAngle = _random.Next(0, 360);

        // Полный угол, на который нужно повернуть относительно текущего положения
        double finalRotation = 360 * turns + randomAngle - startAngle;

        // Длительность анимации рассчитываем через постоянную скорость
        double degreesPerMillisecond = 0.1; // Можно менять для ускорения/замедления
        uint duration = (uint)(Math.Abs(finalRotation) / degreesPerMillisecond);

        // Вращаем колесо
        await _wheel.RotateToAsync(startAngle + finalRotation, duration, Easing.CubicOut);

        // Рассчитываем фактический угол остановки (0–359°)
        double landedAngle = (startAngle + finalRotation) % 360;
 
        // Определяем сектор
        int sectorCount = _prizes.Length;
        double sectorAngle = 360.0 / sectorCount;
        int prizeIndex = (int)(landedAngle / sectorAngle);

        return _prizes[prizeIndex];
    }
}

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *