Разработка компьютерных игр для Windows Phone 7 с использованием технологий Silverlight и XNA

Акселерометр

Разбить на страницы
Показывать лекцию целиком

Цель работы: Научиться обрабатывать данные акселерометра для использования их в игровых приложениях

12.1. Получение данных акселерометра

Устройства на Windows Phone имеют встроенный акселерометр, который позволяет организовывать управление программами путем перемещения телефона в пространстве. Это открывает богатые возможности для интерактивного управления играми. Если речь идёт о двумерных играх, использование акселерометра в игровом процессе позволяет смоделировать что-то вроде шарика, который катается по горизонтальной плоскости. Если речь идет об играх трехмерных, то здесь мы можем получить интерфейс управления неким объектом, например, автомобилем, напоминающий руль.

Рассмотрим работу с акселерометром на примере, который получает данные с устройства и выводит сведения о них на экран.

Создадим новый стандартный игровой проект, подготовим его для вывода текста в игровое окно XNA, добавим новый ресурс .spritefont с именем MyFont. Для доступа к акселерометру нам понадобится, во-первых, подключить библиотеку Microsoft.Devices.Sensors, во-вторых – пространство имен Microsoft.Devices.Sensor.

Для подключения библиотеки нужно сделать щелчок правой кнопкой мыши в Обозревателе решений по группе Ссылки в проекте P6_1, выбрать команду Добавить ссылку и в появившемся окне, на закладке .NET выбрать Microsoft.Devices.Sensor, рис. 12.1.

(рис 12.1) Подключение библиотеки

В итоге окно Обозреватель решений должно принять вид, показанный на рис. 12.2

(рис 12.2) Окно Обозреватель решений

При работе с акселерометром нужно учитывать, что данные, которые можно от него получить, представляют собой трехмерный вектор, содержащий данные об ускорении устройства в гравитационных единицах. Например, (1,0,0). Если сопоставить эти данные с телефоном, то, если расположить телефон кнопками, расположенными под экраном, вниз, лицевой стороной к наблюдателю, окажется, что ось Y проходит вдоль длинной стороны экрана (положительное направление оси – вверх), X – вдоль короткой (положительное направление оси – вправо), ось Z располагается перпендикулярно экрану (положительное направление – в сторону наблюдателя). Это остаётся справедливым и при поворотах телефона.

Когда телефон неподвижен, акселерометр, фактически, регистрирует силу земного притяжения, что отражается в его показаниях (они, по соответствующей оси, близки к 1), перемещения телефона в пространстве приводят к изменению показаний.

Таким образом, оказывается, например, если телефон лежит на горизонтальной поверхности неподвижно, экраном вверх, показания акселерометра выглядят как (0, 0, -1). Если расположить телефон вертикально, кнопками вниз, мы имеем в показаниях (0, -1, 0), перевернув телефон "вверх ногами" – (0, 1, 0). Если повернуть телефон на 90° против часовой стрелки – мы получим (-1,0,0). Акселерометр очень чувствителен, даже при неподвижном устройстве показания не бывают в точности равными идеальным, к тому же, они колеблются в небольших пределах.

Мы реализовали процедуру получения данных с акселерометра в коде основной программы. Вы можете видеть этот код в листинге 12.1

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;
using Microsoft.Devices.Sensors;
namespace P6_1
{
    /// <summary>
    /// Это главный тип игры
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        //Строка для вывода
        string text;
        //Для хранения позиции вывода текста 
        Vector2 textPosition=new Vector2(100,100);
        //Для хранения шрифта
        SpriteFont MyFont;
        //Показатели акселерометра
        Vector3 accVector;
        //Акселерометр
        Accelerometer accelerometer;
        //Объект для блокировки
        object accLock = new object();
       
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            //Отображение игрового окна на полный экран, без строки состояния
            graphics.IsFullScreen = true;
            // Частота кадра на Windows Phone по умолчанию — 30 кадров в секунду.
            TargetElapsedTime = TimeSpan.FromTicks(333333);
           // Дополнительный заряд аккумулятора заблокирован.
            InactiveSleepTime = TimeSpan.FromSeconds(1);
        }

        /// <summary>
        /// Позволяет игре выполнить инициализацию, необходимую перед запуском.
        /// Здесь можно запросить нужные службы и загрузить неграфический
        /// контент.  Вызов base.Initialize приведет к перебору всех компонентов и
        /// их инициализации.
        /// </summary>
        protected override void Initialize()
        {
            //Новый объект для работы с акселерометром
            accelerometer = new Accelerometer();
            //Задаем время обновления
            accelerometer.TimeBetweenUpdates = TimeSpan.FromMilliseconds(1000);
            //Подключаем обработчик события
            accelerometer.CurrentValueChanged += new EventHandler<SensorReadingEventArgs
            <AccelerometerReading>>(accelerometer_CurrentValueChanged);
            //Пытаемся запустить акселерометр
            try
            {
                accelerometer.Start();
            }
            catch
            {
            }
            base.Initialize();
        }
        //Обработчик события акселерометра
        void accelerometer_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e)
        {
            //Блокировка обеспечивает получение данных акселерометра в одном потоке
            lock (accLock)
            {
                //Получаем данные
                accVector = e.SensorReading.Acceleration;
            }
        }
        ///// <summary>
        /// LoadContent будет вызываться в игре один раз; здесь загружается
        /// весь контент.
        /// </summary>
        protected override void LoadContent()
        {
            // Создайте новый SpriteBatch, который можно использовать для отрисовки текстур.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            //Загружаем шрифт
            MyFont = Content.Load<SpriteFont>("MyFont");
           
        }

        /// <summary>
        /// UnloadContent будет вызываться в игре один раз; здесь выгружается
        /// весь контент.
        /// </summary>
        protected override void UnloadContent()
        {
            // ЗАДАЧА: выгрузите здесь весь контент, не относящийся к ContentManager
        }

        /// <summary>
        /// Позволяет игре запускать логику обновления мира,
        /// проверки столкновений, получения ввода и воспроизведения звуков.
        /// </summary>
        /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Update(GameTime gameTime)
        {
            // Позволяет выйти из игры
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
            //Блокировка обеспечивает чтение данных в одном потоке
            lock (accLock)
            {
                //Записываем в строку для вывода строковое представление 
                //данных, полученных с акселерометра
                text = accVector.ToString();
            }
            base.Update(gameTime);
        }

        /// <summary>
        /// Вызывается, когда игра отрисовывается.
        /// </summary>
        /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();
            //Выводим строку
            spriteBatch.DrawString(MyFont, text, textPosition, Color.Red,
                0, new Vector2(0,0), 1.0f, SpriteEffects.None, 0.5f);
            spriteBatch.End();
            base.Draw(gameTime);
        }

        
    }
}

Рассмотрим ключевые моменты этого кода. Здесь мы собираемся вывести значение показаний акселерометра на экран. Эти показания будут храниться в переменной accVector типа Vector3. Получение данных акселерометра и игровой цикл работают в разных потоках, для того, чтобы обеспечить правильность работы программы, то есть – исключить считывание в методе Update данных из переменной accVector, которые принадлежат различным результатам измерений показаний акселерометра, мы блокируем, используя конструкцию lock, критически важные операции, в нашем случае это – установка переменной accVector в обработчике события accelerometer_CurrentValueChanged и в методе Update.

В методе Initialize мы создаем новый объект типа Accelerometer, устанавливаем его параметр TimeBetweenUpdates, равный 1000 миллисекунд, то есть показатели акселерометра будут сниматься 1 раз в секунду. Это время может быть и другим – всё зависит от задач, которые стоят перед приложением.

Для получения данных с акселерометра мы подключаем обработчик события CurrentValueChanged, которое происходит при поступлении новых данных с устройства. Этот обработчик реализован процедурой accelerometer_CurrentValueChanged, здесь мы получаем данные (они представлены в виде трёхмерного вектора) и записываем их в переменную accVector.

Прежде чем использовать акселерометр, нужно выполнить метод Start() объекта Accelerometer. Попытка исполнить этот метод связана с работой с физическим устройством, здесь возможны ошибки, поэтому мы заключаем эту попытку в конструкцию try {} catch {}. Здесь можно настроить поведение программы в том случае, если данная попытка не удастся, то есть, акселерометр окажется не инициализированным.

В методе Update мы приводим содержимое переменной accVector к текстовому виду и записываем полученную строку в переменную text, текст, содержащийся в которой, выводим на экран в методе Draw.

Запуск программы на реальном устройстве, рис. 12.3. позволит лучше понять, как именно работает акселерометр и какие данные с него можно получить. Уменьшите время снятия показаний акселерометра, изменив параметр TimeBetweenUpdates при запуске приложения на устройстве для того, чтобы увидеть изменения показателей при быстрых перемещениях телефона.

(рис 12.3) Показания акселерометра, устройство лежит на горизонтальной поверхности

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

12.2. Отладка приложения, использующих акселерометр, в эмуляторе

Запустим программу в эмуляторе, в панели инструментов эмулятора нажмем на кнопку >>, появится окно Дополнительные инструменты, в котором на вкладке Акселерометр можно, перемещая трехмерную модель телефона, моделировать поведение акселерометра. Так, на рис. 12.4 можно видеть эмулятор, моделирующий альбомную ориентацию устройства, расположенного вертикально. Как видите, данные акселерометра представляют собой вектор (-1, 0, 0).

(рис 12.4) Эмуляция показаний акселерометра

Перемещать трехмерную модель телефона можно с помощью изменения положения красной точки с помощью мыши. Вот, например (рис. 12.5), как в эмуляторе выглядит положение телефона, похожее на то, в котором было сфотографировано устройство на рис. 12.3.

(рис 12.5) Эмуляция показаний акселерометра

В поле Ориентация в окне Дополнительные инструменты можно выбирать из нескольких предустановленных ориентаций устройства в пространстве.

В поле Записанные данные можно выбрать набор записанных данных Shake, эмулирующих тряску телефона, и, нажав на Воспроизведение, понаблюдать в окне эмулятора за реакцией программы на эти данные. Изображение телефона при воспроизведении данных не перемещается.

Рассмотрим пример, в котором показания акселерометра используются для управления игровым объектом.

12.3. Управление игровым объектом с помощью акселерометра

Сущность данного примера заключается в следующем. Мы получаем показания акселерометра и используем данные по осям X и Y для управления объектом в двумерной системе координат. Мы используем в этом примере уже известный вам по прошлым практическим занятиям игровой компонент с некоторыми изменениями. Рассмотрим основные особенности этого компонента, листинг 12.2.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace P6_2
{
    /// <summary>
    /// Это игровой компонент, реализующий интерфейс IUpdateable.
    /// </summary>
    public class spriteComp : Microsoft.Xna.Framework.DrawableGameComponent
    {
        //Изображение
        private Texture2D sprTexture;
        //Прямоугольник, ограничивающий спрайт
        private Rectangle sprRectangle;
        //Координата спрайта
        private Vector2 sprPosition;
        //Границы экрана
        private Rectangle scrBounds;
        //Направление движения спрайта
        private Vector2 sprMove = new Vector2(0, 0);
        public spriteComp(Game game, ref Texture2D newTexture,
            Rectangle newRectangle, Vector2 newPosition
)
            : base(game)
        {
            sprTexture = newTexture;
            sprRectangle = newRectangle;
            sprPosition = newPosition;
            //Работаем в портретном режиме, высота - 480, ширина - 800
            scrBounds = new Rectangle(0, 0,
                game.Window.ClientBounds.Height,
                game.Window.ClientBounds.Width
                );
        }

        /// <summary>
        /// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.  
Здесь можно запросить нужные службы и загрузить контент.
        /// 
        /// </summary>
        public override void Initialize()
        {
            // ЗАДАЧА: добавьте здесь код инициализации

            base.Initialize();
        }

        /// <summary>
        /// Позволяет игровому компоненту обновиться.
        /// </summary>
        /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        public override void Update(GameTime gameTime)
        {
            //Изменение координат в соответствии с данными, имеющимися в sprMove
            sprPosition.Y = sprPosition.Y + sprMove.Y;
            sprPosition.X = sprPosition.X + sprMove.X;
            //Если вышли за пределы экрана - исправляем 
            //и сбрасываем скорость в направлении, в котором
            //пересекли границу
            if (sprPosition.X < scrBounds.Left)
            {
                sprPosition.X = scrBounds.Left;
                sprMove.X = 0;
            }
            if (sprPosition.X > scrBounds.Width - sprRectangle.Width)
            {
                sprPosition.X = scrBounds.Width - sprRectangle.Width;
                sprMove.X = 0;
            }
            if (sprPosition.Y < scrBounds.Top)
            {
                sprPosition.Y = scrBounds.Top;
                sprMove.Y = 0;
            }
            if (sprPosition.Y > scrBounds.Height - sprRectangle.Height)
            {
                sprPosition.Y = scrBounds.Height - sprRectangle.Height;
                sprMove.Y = 0;
            }

            base.Update(gameTime);
        }
        public override void Draw(GameTime gameTime)
        {
            SpriteBatch sprBatch =
                (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));

            sprBatch.Draw(sprTexture, sprPosition, sprRectangle, Color.White);

            base.Draw(gameTime);
        }
        //Метод для установки направления движения спрайта
        public void Move(Vector2 move)
        {   
            //Изменяем вектор, задающий направление и скорость движения спрайта
            sprMove += move;
            //Ограничиваем скорость 5-ю пикселями
            if (sprMove.X > 5) { sprMove.X = 5; }
            if (sprMove.X < -5) { sprMove.X = -5; }
            if (sprMove.Y > 5) { sprMove.Y = 5; }
            if (sprMove.Y < -5) { sprMove.Y = -5; }
        }
    }
}

Здесь мы используем для установки двумерного вектора, задающего направление и скорость перемещения спрайта, метод Move. В этот метод передаются данные, полученные с акселерометра. При поступлении новых данных они прибавляются к двумерному вектору sprMove. Положительные значения увеличивают скорость спрайта, отрицательные – уменьшают. После получения новых значений мы проверяем, не превышают ли они 5 (или не меньше ли они -5) и, если это так – сбрасываем до 5 (или до -5) для того, чтобы ограничить скорость движения спрайта 5-ю пикселями за один шаг.

В процедуре Update мы прибавляем к позиции спрайта данные, хранящиеся в sprMove. Кроме того, мы контролируем пересечение объектом границ экрана. Если объект пересек границу, мы возвращаем его к границе и сбрасываем соответствующий показатель вектора sprMove, устанавливая скорость движения спрайта по соответствующей координате равной 0.

Рассмотрим основной игры, листинг 12.3.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;
using Microsoft.Devices.Sensors;
namespace P6_2
{
    /// <summary>
    /// Это главный тип игры
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        spriteComp gameObject;
        Texture2D texture;
        //Показатели акселерометра
        Vector3 accVector;
        //Акселерометр
        Accelerometer accelerometer;
        //Блокировка
        object accLock = new object();

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            //Задаем параметры экрана                        
            graphics.PreferredBackBufferWidth = 800;
            graphics.PreferredBackBufferHeight = 480;
            //Фиксируем положение экрана
            graphics.SupportedOrientations = DisplayOrientation.LandscapeLeft;
            //Отображение игрового окна на полный экран, без строки состояния
            graphics.IsFullScreen = true;
            // Частота кадра на Windows Phone по умолчанию — 30 кадров в секунду.
            TargetElapsedTime = TimeSpan.FromTicks(333333);

            // Дополнительный заряд аккумулятора заблокирован.
            InactiveSleepTime = TimeSpan.FromSeconds(1);
        }

        /// <summary>
        /// Позволяет игре выполнить инициализацию, необходимую перед запуском.
        /// Здесь можно запросить нужные службы и загрузить неграфический
        /// контент.  Вызов base.Initialize приведет к перебору всех компонентов и
        /// их инициализации.
        /// </summary>
        protected override void Initialize()
        {
            accelerometer = new Accelerometer();
            accelerometer.TimeBetweenUpdates = TimeSpan.FromMilliseconds(20);
            accelerometer.CurrentValueChanged += new EventHandler<SensorReadingEventArgs
            <AccelerometerReading>>(accelerometer_CurrentValueChanged);
            try
            {
                accelerometer.Start();
            }
            catch
            {
            }
            base.Initialize();
        }

        void accelerometer_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e)
        {
            lock (accLock)
            {
                accVector = e.SensorReading.Acceleration;
            }
        }
        ///// <summary>
        /// LoadContent будет вызываться в игре один раз; здесь загружается
        /// весь контент.
        /// </summary>
        protected override void LoadContent()
        {
            // Создайте новый SpriteBatch, который можно использовать для отрисовки текстур.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            Services.AddService(typeof(SpriteBatch), spriteBatch);
            texture = Content.Load<Texture2D>("BallandBats");
            CreateNewObject();
        }

        protected void CreateNewObject()
        {
            gameObject = new spriteComp(this, ref texture,
                new Rectangle(18, 9, 17, 88), new Vector2(100, 150));
            Components.Add(gameObject);
        }

        /// <summary>
        /// UnloadContent будет вызываться в игре один раз; здесь выгружается
        /// весь контент.
        /// </summary>
        protected override void UnloadContent()
        {
            // ЗАДАЧА: выгрузите здесь весь контент, не относящийся к ContentManager
        }

        /// <summary>
        /// Позволяет игре запускать логику обновления мира,
        /// проверки столкновений, получения ввода и воспроизведения звуков.
        /// </summary>
        /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Update(GameTime gameTime)
        {
            // Позволяет выйти из игры
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
            lock (accLock)
            {
                gameObject.Move(new Vector2(-accVector.Y, -accVector.X)); 
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// Вызывается, когда игра отрисовывается.
        /// </summary>
        /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // ЗАДАЧА: добавьте здесь код отрисовки
            spriteBatch.Begin();
            base.Draw(gameTime);
            spriteBatch.End();
            
        }


    }
}

Этот код похож на тот, который мы рассматривали в предыдущем примере. Здесь мы, однако, внесли некоторые изменения. Так, в конструкторе класса мы, используя команды graphics.PreferredBackBufferWidth = 800 и graphics.PreferredBackBufferHeight = 480 в явном виде задали ширину и высоту игрового экрана, кроме того, мы зафиксировали положение экрана в левосторонней ландшафтной ориентации командой graphics.SupportedOrientations = DisplayOrientation.LandscapeLeft. Результат этой команды заключается в том, что при повороте экрана не меняется автоматически его ориентация.

Код работы с акселерометром в данном примере тот же, что и в предыдущем. Главная разница заключается в том, что в методе Update мы вызываем метод Move() игрового объекта и передаем ему последние измеренные значения по осям X и Y, полученные с акселерометра.

На рис. 12.6 вы можете видеть результат запуска программы в эмуляторе.

(рис 12.6) Результаты запуска программы в эмуляторе

12.4. Выводы

В этой лабораторной работе была рассмотрена методика получения и обработки показаний акселерометра, который является одним из обязательных сенсоров, присутствующих в устройствах, работающих под управлением Windows Phone.

12.5. Задание

Реализуйте управление экранным объектом с использованием акселерометра. Если у вас есть устройство под управлением Windows Phone, изучите на практике поведение показаний акселерометра при различных положениях устройства в пространстве и перемещениях.

12.6. Дополнительные материалы

К данной лабораторной работе прилагается видеоурок

и демонстрационный проект.

Страницы:

Цель работы: Научиться обрабатывать данные акселерометра для использования их в игровых приложениях

12.1. Получение данных акселерометра

Устройства на Windows Phone имеют встроенный акселерометр, который позволяет организовывать управление программами путем перемещения телефона в пространстве. Это открывает богатые возможности для интерактивного управления играми. Если речь идёт о двумерных играх, использование акселерометра в игровом процессе позволяет смоделировать что-то вроде шарика, который катается по горизонтальной плоскости. Если речь идет об играх трехмерных, то здесь мы можем получить интерфейс управления неким объектом, например, автомобилем, напоминающий руль.

Рассмотрим работу с акселерометром на примере, который получает данные с устройства и выводит сведения о них на экран.

Создадим новый стандартный игровой проект, подготовим его для вывода текста в игровое окно XNA, добавим новый ресурс .spritefont с именем MyFont. Для доступа к акселерометру нам понадобится, во-первых, подключить библиотеку Microsoft.Devices.Sensors, во-вторых – пространство имен Microsoft.Devices.Sensor.

Для подключения библиотеки нужно сделать щелчок правой кнопкой мыши в Обозревателе решений по группе Ссылки в проекте P6_1, выбрать команду Добавить ссылку и в появившемся окне, на закладке .NET выбрать Microsoft.Devices.Sensor, рис. 12.1.

(рис 12.1) Подключение библиотеки

В итоге окно Обозреватель решений должно принять вид, показанный на рис. 12.2

(рис 12.2) Окно Обозреватель решений

При работе с акселерометром нужно учитывать, что данные, которые можно от него получить, представляют собой трехмерный вектор, содержащий данные об ускорении устройства в гравитационных единицах. Например, (1,0,0). Если сопоставить эти данные с телефоном, то, если расположить телефон кнопками, расположенными под экраном, вниз, лицевой стороной к наблюдателю, окажется, что ось Y проходит вдоль длинной стороны экрана (положительное направление оси – вверх), X – вдоль короткой (положительное направление оси – вправо), ось Z располагается перпендикулярно экрану (положительное направление – в сторону наблюдателя). Это остаётся справедливым и при поворотах телефона.

Когда телефон неподвижен, акселерометр, фактически, регистрирует силу земного притяжения, что отражается в его показаниях (они, по соответствующей оси, близки к 1), перемещения телефона в пространстве приводят к изменению показаний.

Таким образом, оказывается, например, если телефон лежит на горизонтальной поверхности неподвижно, экраном вверх, показания акселерометра выглядят как (0, 0, -1). Если расположить телефон вертикально, кнопками вниз, мы имеем в показаниях (0, -1, 0), перевернув телефон "вверх ногами" – (0, 1, 0). Если повернуть телефон на 90° против часовой стрелки – мы получим (-1,0,0). Акселерометр очень чувствителен, даже при неподвижном устройстве показания не бывают в точности равными идеальным, к тому же, они колеблются в небольших пределах.

Мы реализовали процедуру получения данных с акселерометра в коде основной программы. Вы можете видеть этот код в листинге 12.1

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;
using Microsoft.Devices.Sensors;
namespace P6_1
{
    /// <summary>
    /// Это главный тип игры
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        //Строка для вывода
        string text;
        //Для хранения позиции вывода текста 
        Vector2 textPosition=new Vector2(100,100);
        //Для хранения шрифта
        SpriteFont MyFont;
        //Показатели акселерометра
        Vector3 accVector;
        //Акселерометр
        Accelerometer accelerometer;
        //Объект для блокировки
        object accLock = new object();
       
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            //Отображение игрового окна на полный экран, без строки состояния
            graphics.IsFullScreen = true;
            // Частота кадра на Windows Phone по умолчанию — 30 кадров в секунду.
            TargetElapsedTime = TimeSpan.FromTicks(333333);
           // Дополнительный заряд аккумулятора заблокирован.
            InactiveSleepTime = TimeSpan.FromSeconds(1);
        }

        /// <summary>
        /// Позволяет игре выполнить инициализацию, необходимую перед запуском.
        /// Здесь можно запросить нужные службы и загрузить неграфический
        /// контент.  Вызов base.Initialize приведет к перебору всех компонентов и
        /// их инициализации.
        /// </summary>
        protected override void Initialize()
        {
            //Новый объект для работы с акселерометром
            accelerometer = new Accelerometer();
            //Задаем время обновления
            accelerometer.TimeBetweenUpdates = TimeSpan.FromMilliseconds(1000);
            //Подключаем обработчик события
            accelerometer.CurrentValueChanged += new EventHandler<SensorReadingEventArgs
            <AccelerometerReading>>(accelerometer_CurrentValueChanged);
            //Пытаемся запустить акселерометр
            try
            {
                accelerometer.Start();
            }
            catch
            {
            }
            base.Initialize();
        }
        //Обработчик события акселерометра
        void accelerometer_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e)
        {
            //Блокировка обеспечивает получение данных акселерометра в одном потоке
            lock (accLock)
            {
                //Получаем данные
                accVector = e.SensorReading.Acceleration;
            }
        }
        ///// <summary>
        /// LoadContent будет вызываться в игре один раз; здесь загружается
        /// весь контент.
        /// </summary>
        protected override void LoadContent()
        {
            // Создайте новый SpriteBatch, который можно использовать для отрисовки текстур.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            //Загружаем шрифт
            MyFont = Content.Load<SpriteFont>("MyFont");
           
        }

        /// <summary>
        /// UnloadContent будет вызываться в игре один раз; здесь выгружается
        /// весь контент.
        /// </summary>
        protected override void UnloadContent()
        {
            // ЗАДАЧА: выгрузите здесь весь контент, не относящийся к ContentManager
        }

        /// <summary>
        /// Позволяет игре запускать логику обновления мира,
        /// проверки столкновений, получения ввода и воспроизведения звуков.
        /// </summary>
        /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Update(GameTime gameTime)
        {
            // Позволяет выйти из игры
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
            //Блокировка обеспечивает чтение данных в одном потоке
            lock (accLock)
            {
                //Записываем в строку для вывода строковое представление 
                //данных, полученных с акселерометра
                text = accVector.ToString();
            }
            base.Update(gameTime);
        }

        /// <summary>
        /// Вызывается, когда игра отрисовывается.
        /// </summary>
        /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();
            //Выводим строку
            spriteBatch.DrawString(MyFont, text, textPosition, Color.Red,
                0, new Vector2(0,0), 1.0f, SpriteEffects.None, 0.5f);
            spriteBatch.End();
            base.Draw(gameTime);
        }

        
    }
}

Рассмотрим ключевые моменты этого кода. Здесь мы собираемся вывести значение показаний акселерометра на экран. Эти показания будут храниться в переменной accVector типа Vector3. Получение данных акселерометра и игровой цикл работают в разных потоках, для того, чтобы обеспечить правильность работы программы, то есть – исключить считывание в методе Update данных из переменной accVector, которые принадлежат различным результатам измерений показаний акселерометра, мы блокируем, используя конструкцию lock, критически важные операции, в нашем случае это – установка переменной accVector в обработчике события accelerometer_CurrentValueChanged и в методе Update.

В методе Initialize мы создаем новый объект типа Accelerometer, устанавливаем его параметр TimeBetweenUpdates, равный 1000 миллисекунд, то есть показатели акселерометра будут сниматься 1 раз в секунду. Это время может быть и другим – всё зависит от задач, которые стоят перед приложением.

Для получения данных с акселерометра мы подключаем обработчик события CurrentValueChanged, которое происходит при поступлении новых данных с устройства. Этот обработчик реализован процедурой accelerometer_CurrentValueChanged, здесь мы получаем данные (они представлены в виде трёхмерного вектора) и записываем их в переменную accVector.

Прежде чем использовать акселерометр, нужно выполнить метод Start() объекта Accelerometer. Попытка исполнить этот метод связана с работой с физическим устройством, здесь возможны ошибки, поэтому мы заключаем эту попытку в конструкцию try {} catch {}. Здесь можно настроить поведение программы в том случае, если данная попытка не удастся, то есть, акселерометр окажется не инициализированным.

В методе Update мы приводим содержимое переменной accVector к текстовому виду и записываем полученную строку в переменную text, текст, содержащийся в которой, выводим на экран в методе Draw.

Запуск программы на реальном устройстве, рис. 12.3. позволит лучше понять, как именно работает акселерометр и какие данные с него можно получить. Уменьшите время снятия показаний акселерометра, изменив параметр TimeBetweenUpdates при запуске приложения на устройстве для того, чтобы увидеть изменения показателей при быстрых перемещениях телефона.

(рис 12.3) Показания акселерометра, устройство лежит на горизонтальной поверхности

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

12.2. Отладка приложения, использующих акселерометр, в эмуляторе

Запустим программу в эмуляторе, в панели инструментов эмулятора нажмем на кнопку >>, появится окно Дополнительные инструменты, в котором на вкладке Акселерометр можно, перемещая трехмерную модель телефона, моделировать поведение акселерометра. Так, на рис. 12.4 можно видеть эмулятор, моделирующий альбомную ориентацию устройства, расположенного вертикально. Как видите, данные акселерометра представляют собой вектор (-1, 0, 0).

(рис 12.4) Эмуляция показаний акселерометра

Перемещать трехмерную модель телефона можно с помощью изменения положения красной точки с помощью мыши. Вот, например (рис. 12.5), как в эмуляторе выглядит положение телефона, похожее на то, в котором было сфотографировано устройство на рис. 12.3.

(рис 12.5) Эмуляция показаний акселерометра

В поле Ориентация в окне Дополнительные инструменты можно выбирать из нескольких предустановленных ориентаций устройства в пространстве.

В поле Записанные данные можно выбрать набор записанных данных Shake, эмулирующих тряску телефона, и, нажав на Воспроизведение, понаблюдать в окне эмулятора за реакцией программы на эти данные. Изображение телефона при воспроизведении данных не перемещается.

Рассмотрим пример, в котором показания акселерометра используются для управления игровым объектом.

12.3. Управление игровым объектом с помощью акселерометра

Сущность данного примера заключается в следующем. Мы получаем показания акселерометра и используем данные по осям X и Y для управления объектом в двумерной системе координат. Мы используем в этом примере уже известный вам по прошлым практическим занятиям игровой компонент с некоторыми изменениями. Рассмотрим основные особенности этого компонента, листинг 12.2.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace P6_2
{
    /// <summary>
    /// Это игровой компонент, реализующий интерфейс IUpdateable.
    /// </summary>
    public class spriteComp : Microsoft.Xna.Framework.DrawableGameComponent
    {
        //Изображение
        private Texture2D sprTexture;
        //Прямоугольник, ограничивающий спрайт
        private Rectangle sprRectangle;
        //Координата спрайта
        private Vector2 sprPosition;
        //Границы экрана
        private Rectangle scrBounds;
        //Направление движения спрайта
        private Vector2 sprMove = new Vector2(0, 0);
        public spriteComp(Game game, ref Texture2D newTexture,
            Rectangle newRectangle, Vector2 newPosition
)
            : base(game)
        {
            sprTexture = newTexture;
            sprRectangle = newRectangle;
            sprPosition = newPosition;
            //Работаем в портретном режиме, высота - 480, ширина - 800
            scrBounds = new Rectangle(0, 0,
                game.Window.ClientBounds.Height,
                game.Window.ClientBounds.Width
                );
        }

        /// <summary>
        /// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.  
Здесь можно запросить нужные службы и загрузить контент.
        /// 
        /// </summary>
        public override void Initialize()
        {
            // ЗАДАЧА: добавьте здесь код инициализации

            base.Initialize();
        }

        /// <summary>
        /// Позволяет игровому компоненту обновиться.
        /// </summary>
        /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        public override void Update(GameTime gameTime)
        {
            //Изменение координат в соответствии с данными, имеющимися в sprMove
            sprPosition.Y = sprPosition.Y + sprMove.Y;
            sprPosition.X = sprPosition.X + sprMove.X;
            //Если вышли за пределы экрана - исправляем 
            //и сбрасываем скорость в направлении, в котором
            //пересекли границу
            if (sprPosition.X < scrBounds.Left)
            {
                sprPosition.X = scrBounds.Left;
                sprMove.X = 0;
            }
            if (sprPosition.X > scrBounds.Width - sprRectangle.Width)
            {
                sprPosition.X = scrBounds.Width - sprRectangle.Width;
                sprMove.X = 0;
            }
            if (sprPosition.Y < scrBounds.Top)
            {
                sprPosition.Y = scrBounds.Top;
                sprMove.Y = 0;
            }
            if (sprPosition.Y > scrBounds.Height - sprRectangle.Height)
            {
                sprPosition.Y = scrBounds.Height - sprRectangle.Height;
                sprMove.Y = 0;
            }

            base.Update(gameTime);
        }
        public override void Draw(GameTime gameTime)
        {
            SpriteBatch sprBatch =
                (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));

            sprBatch.Draw(sprTexture, sprPosition, sprRectangle, Color.White);

            base.Draw(gameTime);
        }
        //Метод для установки направления движения спрайта
        public void Move(Vector2 move)
        {   
            //Изменяем вектор, задающий направление и скорость движения спрайта
            sprMove += move;
            //Ограничиваем скорость 5-ю пикселями
            if (sprMove.X > 5) { sprMove.X = 5; }
            if (sprMove.X < -5) { sprMove.X = -5; }
            if (sprMove.Y > 5) { sprMove.Y = 5; }
            if (sprMove.Y < -5) { sprMove.Y = -5; }
        }
    }
}

Здесь мы используем для установки двумерного вектора, задающего направление и скорость перемещения спрайта, метод Move. В этот метод передаются данные, полученные с акселерометра. При поступлении новых данных они прибавляются к двумерному вектору sprMove. Положительные значения увеличивают скорость спрайта, отрицательные – уменьшают. После получения новых значений мы проверяем, не превышают ли они 5 (или не меньше ли они -5) и, если это так – сбрасываем до 5 (или до -5) для того, чтобы ограничить скорость движения спрайта 5-ю пикселями за один шаг.

В процедуре Update мы прибавляем к позиции спрайта данные, хранящиеся в sprMove. Кроме того, мы контролируем пересечение объектом границ экрана. Если объект пересек границу, мы возвращаем его к границе и сбрасываем соответствующий показатель вектора sprMove, устанавливая скорость движения спрайта по соответствующей координате равной 0.

Рассмотрим основной игры, листинг 12.3.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;
using Microsoft.Devices.Sensors;
namespace P6_2
{
    /// <summary>
    /// Это главный тип игры
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        spriteComp gameObject;
        Texture2D texture;
        //Показатели акселерометра
        Vector3 accVector;
        //Акселерометр
        Accelerometer accelerometer;
        //Блокировка
        object accLock = new object();

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            //Задаем параметры экрана                        
            graphics.PreferredBackBufferWidth = 800;
            graphics.PreferredBackBufferHeight = 480;
            //Фиксируем положение экрана
            graphics.SupportedOrientations = DisplayOrientation.LandscapeLeft;
            //Отображение игрового окна на полный экран, без строки состояния
            graphics.IsFullScreen = true;
            // Частота кадра на Windows Phone по умолчанию — 30 кадров в секунду.
            TargetElapsedTime = TimeSpan.FromTicks(333333);

            // Дополнительный заряд аккумулятора заблокирован.
            InactiveSleepTime = TimeSpan.FromSeconds(1);
        }

        /// <summary>
        /// Позволяет игре выполнить инициализацию, необходимую перед запуском.
        /// Здесь можно запросить нужные службы и загрузить неграфический
        /// контент.  Вызов base.Initialize приведет к перебору всех компонентов и
        /// их инициализации.
        /// </summary>
        protected override void Initialize()
        {
            accelerometer = new Accelerometer();
            accelerometer.TimeBetweenUpdates = TimeSpan.FromMilliseconds(20);
            accelerometer.CurrentValueChanged += new EventHandler<SensorReadingEventArgs
            <AccelerometerReading>>(accelerometer_CurrentValueChanged);
            try
            {
                accelerometer.Start();
            }
            catch
            {
            }
            base.Initialize();
        }

        void accelerometer_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e)
        {
            lock (accLock)
            {
                accVector = e.SensorReading.Acceleration;
            }
        }
        ///// <summary>
        /// LoadContent будет вызываться в игре один раз; здесь загружается
        /// весь контент.
        /// </summary>
        protected override void LoadContent()
        {
            // Создайте новый SpriteBatch, который можно использовать для отрисовки текстур.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            Services.AddService(typeof(SpriteBatch), spriteBatch);
            texture = Content.Load<Texture2D>("BallandBats");
            CreateNewObject();
        }

        protected void CreateNewObject()
        {
            gameObject = new spriteComp(this, ref texture,
                new Rectangle(18, 9, 17, 88), new Vector2(100, 150));
            Components.Add(gameObject);
        }

        /// <summary>
        /// UnloadContent будет вызываться в игре один раз; здесь выгружается
        /// весь контент.
        /// </summary>
        protected override void UnloadContent()
        {
            // ЗАДАЧА: выгрузите здесь весь контент, не относящийся к ContentManager
        }

        /// <summary>
        /// Позволяет игре запускать логику обновления мира,
        /// проверки столкновений, получения ввода и воспроизведения звуков.
        /// </summary>
        /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Update(GameTime gameTime)
        {
            // Позволяет выйти из игры
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
            lock (accLock)
            {
                gameObject.Move(new Vector2(-accVector.Y, -accVector.X)); 
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// Вызывается, когда игра отрисовывается.
        /// </summary>
        /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // ЗАДАЧА: добавьте здесь код отрисовки
            spriteBatch.Begin();
            base.Draw(gameTime);
            spriteBatch.End();
            
        }


    }
}

Этот код похож на тот, который мы рассматривали в предыдущем примере. Здесь мы, однако, внесли некоторые изменения. Так, в конструкторе класса мы, используя команды graphics.PreferredBackBufferWidth = 800 и graphics.PreferredBackBufferHeight = 480 в явном виде задали ширину и высоту игрового экрана, кроме того, мы зафиксировали положение экрана в левосторонней ландшафтной ориентации командой graphics.SupportedOrientations = DisplayOrientation.LandscapeLeft. Результат этой команды заключается в том, что при повороте экрана не меняется автоматически его ориентация.

Код работы с акселерометром в данном примере тот же, что и в предыдущем. Главная разница заключается в том, что в методе Update мы вызываем метод Move() игрового объекта и передаем ему последние измеренные значения по осям X и Y, полученные с акселерометра.

На рис. 12.6 вы можете видеть результат запуска программы в эмуляторе.

(рис 12.6) Результаты запуска программы в эмуляторе

12.4. Выводы

В этой лабораторной работе была рассмотрена методика получения и обработки показаний акселерометра, который является одним из обязательных сенсоров, присутствующих в устройствах, работающих под управлением Windows Phone.

12.5. Задание

Реализуйте управление экранным объектом с использованием акселерометра. Если у вас есть устройство под управлением Windows Phone, изучите на практике поведение показаний акселерометра при различных положениях устройства в пространстве и перемещениях.

12.6. Дополнительные материалы

К данной лабораторной работе прилагается видеоурок

и демонстрационный проект.

Вернуться к учебному плану