Использование акселерометра в WinRT
172Разработка под Windows 10 --- Использование акселерометра
Акселерометр часто используется в играх для мобильных устройств. Например, если ваша игра имитирует управление машиной, пользователь может «рулить», наклоняя компьютер влево или вправо.
В следующих двух программах по экрану катается шарик. Если расположить экран планшета параллельно земле и положить сверху шарик, можно заставить его кататься по экрану, наклоняя планшет. Чем больше угол наклона, тем больше ускорение шарика. Виртуальный шарик в двух следующих программах ведет себя аналогичным образом. Как и в программе с ватерпасом из предыдущей статьи, эти программы игнорируют компонент 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". В этой программе отражение шарика от одного края может привести к его выходу за другой край, поэтому позицию шарика необходимо многократно проверять в цикле.