Использование акселерометра в WinRT

172

Акселерометр часто используется в играх для мобильных устройств. Например, если ваша игра имитирует управление машиной, пользователь может «рулить», наклоняя компьютер влево или вправо.

В следующих двух программах по экрану катается шарик. Если расположить экран планшета параллельно земле и положить сверху шарик, можно заставить его кататься по экрану, наклоняя планшет. Чем больше угол наклона, тем больше ускорение шарика. Виртуальный шарик в двух следующих программах ведет себя аналогичным образом. Как и в программе с ватерпасом из предыдущей статьи, эти программы игнорируют компонент Z вектора акселерометра и используют только компоненты X и Y для управления ускорением на двумерной поверхности экрана.

В программе TiltAndRoll шарик при столкновении с одним из краев экрана теряет всю свою скорость в направлении, перпендикулярном краю, и продолжает катиться вдоль края так, словно он сохранил скорость в этом направлении. Шарик определяется в файле XAML. Элемент EllipseGeometry позволяет позиционировать шарик в конкретных координатах заданием свойства Center:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <Path Fill="Red">
            <Path.Data>
                <EllipseGeometry x:Name="ball" />
            </Path.Data>            
        </Path>
    </Grid>
</Page>

Файл фонового кода начинается с определения константы GRAVITY в пикселах в секунду в квадрате. Теоретически на шарик, катящийся без трения, действует полная сила тяжести, но ускорение катящегося шарика составляет 2/3 от гравитационного ускорения.

Двумерные векторы очень удобны в вычислениях с двумерным ускорением, скоростью и позицией, поэтому я включил в программу структуру Vector2 из статьи "Сглаживание мазков кисти и сила нажатия в WinRT". Поскольку шарик должен катиться независимо от выдачи событий ReadingChanged классом Accelerometer, программа не назначает обработчик этого события; вместо этого она использует CompositionTarget.Rendering для получения текущего значения и применения его к шарику. Обратите внимание на использование компонентов X и Y показаний Accelerometer для создания значения Vector2, которое затем усредняется с предыдущим значением, которое также получено в результате усреднения и т.д. Так реализуется чрезвычайно простая форма сглаживания:

using System;
using Windows.Devices.Sensors;
using Windows.Foundation;
using Windows.Graphics.Display;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

using WinRTTestApp.VectorDrawing;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        const double GRAVITY = 5000;    // пикселы в секунду в квадрате
        const double BALL_RADIUS = 32;

        Accelerometer accelerometer = Accelerometer.GetDefault();
        TimeSpan timeSpan;
        Vector2 acceleration;
        Vector2 ballPosition;
        Vector2 ballVelocity;

        public MainPage()
        {
            this.InitializeComponent();
            DisplayProperties.AutoRotationPreferences = DisplayProperties.NativeOrientation;

            ball.RadiusX = BALL_RADIUS;
            ball.RadiusY = BALL_RADIUS;

            Loaded += OnMainPageLoaded;
        }

        private async void OnMainPageLoaded(object sender, RoutedEventArgs e)
        {
            if (accelerometer == null)
            {
                await new MessageDialog("Accelerometer is not available").ShowAsync();
            }
            else
            {
                CompositionTarget.Rendering += OnCompositionTargetRendering;
            }
        }

        private void OnCompositionTargetRendering(object sender, object e)
        {
            AccelerometerReading reading = accelerometer.GetCurrentReading();

            if (reading == null)
                return;

            // Получение времени, прошедшего с момента последнего события
            TimeSpan timeSpan = (e as RenderingEventArgs).RenderingTime;
            double elapsedSeconds = (timeSpan - this.timeSpan).TotalSeconds;
            this.timeSpan = timeSpan;

            // Преобразование показаний акселерометра в экранные координаты
            double x = reading.AccelerationX;
            double y = -reading.AccelerationY;

            // Получение текущего ускорения X-Y и его сглаживание
            acceleration = 0.5 * (acceleration + new Vector2(x, y));

            // Вычисление новой скорости и позиции
            ballVelocity += GRAVITY * acceleration * elapsedSeconds;
            ballPosition += ballVelocity * elapsedSeconds;

            // Проверка соприкосновений с краями
            if (ballPosition.X - BALL_RADIUS < 0)
            {
                ballPosition = new Vector2(BALL_RADIUS, ballPosition.Y);
                ballVelocity = new Vector2(0, ballVelocity.Y);
            }

            if (ballPosition.X + BALL_RADIUS > this.ActualWidth)
            {
                ballPosition = new Vector2(this.ActualWidth - BALL_RADIUS, ballPosition.Y);
                ballVelocity = new Vector2(0, ballVelocity.Y);
            }

            if (ballPosition.Y - BALL_RADIUS < 0)
            {
                ballPosition = new Vector2(ballPosition.X, BALL_RADIUS);
                ballVelocity = new Vector2(ballVelocity.X, 0);
            }
            if (ballPosition.Y + BALL_RADIUS > this.ActualHeight)
            {
                ballPosition = new Vector2(ballPosition.X, this.ActualHeight - BALL_RADIUS);
                ballVelocity = new Vector2(ballVelocity.X, 0);
            }

            ball.Center = new Point(ballPosition.X, ballPosition.Y);
        }
    }
}

Важнейшие вычисления выполняются следующими командами:

ballVelocity += GRAVITY * acceleration * elapsedSeconds;
ballPosition += ballVelocity * elapsedSeconds;

Помните, что acceleration, ballVelocity и ballPosition являются значениями Vector2, поэтому все они имеют компоненты X и Y. Скорость увеличивается на ускорение, умноженное на время, а позиция увеличивается на скорость, умноженную на время. Далее остается только проверить, не вышла ли новая позиция на пределы страницы. Если шарик достиг края экрана, он перемещается внутри страницы, а одна из составляющих скорости обнуляется.

Физическая модель получается довольно реалистичной. С увеличением и уменьшением наклона увеличивается и уменьшается ускорение шарика. Более того, поскольку в программе скорость и позиция вычисляются по формулам, можно без труда реализовать эффект отражения от краев. Проще всего не обнулять составляющую скорости при соприкосновении с краем, а изменить ее знак на противоположный. Однако в этом случае модуль скорости шарика после столкновения не изменится, а это нереалистично. Разумнее включить коэффициент ослабления, который я назвал BOUNCE.

Программа TiltAndBounce почти не отличается от TiltAndRoll, если не считать введения константы BOUNCE и измененной логики движения шарика в обработчике CompositionTarget.Rendering:

using System;
using Windows.Devices.Sensors;
using Windows.Foundation;
using Windows.Graphics.Display;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

using WinRTTestApp.VectorDrawing;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        const double BOUNCE = -2.0 / 3; // коэффициент потери скорости
        
        // ...

        private void OnCompositionTargetRendering(object sender, object e)
        {
            AccelerometerReading reading = accelerometer.GetCurrentReading();

            if (reading == null)
                return;

            // Получение времени, прошедшего с момента последнего события
            TimeSpan timeSpan = (e as RenderingEventArgs).RenderingTime;
            double elapsedSeconds = (timeSpan - this.timeSpan).TotalSeconds;
            this.timeSpan = timeSpan;

            // Преобразование показаний акселерометра в экранные координаты
            double x = reading.AccelerationX;
            double y = -reading.AccelerationY;

            // Получение текущего ускорения X-Y и его сглаживание
            acceleration = 0.5 * (acceleration + new Vector2(x, y));

            // Вычисление новой скорости и позиции
            ballVelocity += GRAVITY * acceleration * elapsedSeconds;
            ballPosition += ballVelocity * elapsedSeconds;
            
            // Проверка отражения от краев
            bool needAnotherLoop = true;

            while (needAnotherLoop)
            {
                needAnotherLoop = false;

                if (ballPosition.X - BALL_RADIUS < 0)
                {
                    ballPosition = new Vector2(-ballPosition.X + 2 * BALL_RADIUS, ballPosition.Y);
                    ballVelocity = new Vector2(BOUNCE * ballVelocity.X, ballVelocity.Y);
                    needAnotherLoop = true;
                }
                else if (ballPosition.X + BALL_RADIUS > this.ActualWidth)
                {
                    ballPosition = new Vector2(-ballPosition.X + 2 * (this.ActualWidth - BALL_RADIUS), 
                                               ballPosition.Y);
                    ballVelocity = new Vector2(BOUNCE * ballVelocity.X, ballVelocity.Y);
                    needAnotherLoop = true;
                }
                else if (ballPosition.Y - BALL_RADIUS < 0)
                {
                    ballPosition = new Vector2(ballPosition.X, -ballPosition.Y + 2 * BALL_RADIUS);
                    ballVelocity = new Vector2(ballVelocity.X, BOUNCE * ballVelocity.Y);
                    needAnotherLoop = true;
                }
                else if (ballPosition.Y + BALL_RADIUS > this.ActualHeight)
                {
                    ballPosition = new Vector2(ballPosition.X, 
                                               -ballPosition.Y + 2 * (this.ActualHeight - BALL_RADIUS));
                    ballVelocity = new Vector2(ballVelocity.X, BOUNCE * ballVelocity.Y);
                    needAnotherLoop = true;
                }
            }
            
            ball.Center = new Point(ballPosition.X, ballPosition.Y);
        }
    }
}

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

Пройди тесты
Лучший чат для C# программистов