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

Акселерометр, микрофон, панель приложения

Показывать лекцию целиком

Цель работы: Научиться работать с акселерометром, микрофоном, панелью приложения в Silverlight-приложениях

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

В этом примере мы хотели бы использовать данные, получаемые с акселерометра, для управления экранным объектом. Объект должен перемещаться в том направлении, в котором наклонён телефон, если телефон лежит экраном вверх на горизонтальной поверхности, объект должен быть неподвижным.

Создадим новый проект, P27_1, он будет состоять из одной страницы, оформление которой, в целом, аналогично тому, которое было представлено в проекте P26_1. На рис. 34.1 вы можете видеть экран приложения, в листинге 34.1 – код страницы.

(рис 34.1) Экран проекта P27_1
<phone:PhoneApplicationPage 
    x:Class="P27_1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="480"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Landscape" Orientation="LandscapeLeft"
    shell:SystemTray.IsVisible="False">

    <!--LayoutRoot представляет корневую сетку, где размещается все содержимое страницы-->
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>


        <!--ContentPanel — поместите здесь дополнительное содержимое-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="0,0,0,0">
            <Image Height="480" HorizontalAlignment="Left" Name="image1" Stretch="Fill" 
            VerticalAlignment="Top" Width="800" Source="/P27_1;component/Res/Background.jpg" />
            <Canvas>
                <Ellipse Height="100" 
                     Name="ellipse1" 
                     Stroke="Black" 
                     StrokeThickness="1" 
                     Width="100" 
                     Fill="#FF00CB00" 
                     Canvas.Top="190"
                     Canvas.Left="350"/>
            </Canvas>
        </Grid>
    </Grid>

</phone:PhoneApplicationPage> 

Обратите внимание на то, что здесь мы размещаем элемент Ellipse внутри элемента Canvas (холст). В результате, у элемента Ellipse появляются присоединенные свойства (Canvas.Top и Canvas.Left). Они задают позиционирование объекта внутри Canvas, модифицируя их, мы сможем перемещать объект по экрану, ориентируясь на них сможем предотвратить пересечение объектом границ экрана.

Для успешной работы с приведенным примером нам понадобится подключить библиотеки Microsoft.Devices.Sensors и Microsoft.Xna.Framework. Первая нужна для работы с акселерометром. Вторая – для обработки значений типа Vector3, с которыми мы столкнёмся при получении данных от акселерометра.

В листинге 34.2 приведен программный код, обеспечивающий работу нашего примера.

using System;
using System.Windows.Controls;
using Microsoft.Phone.Controls;
using Microsoft.Devices.Sensors;
namespace P27_1
{
    public partial class MainPage : PhoneApplicationPage
    {
        //Для акселерометра
        Accelerometer accelerometer;
        // Конструктор
        public MainPage()
        {
            InitializeComponent();
            //Новый экземпляр объекта Accelerometer
            accelerometer = new Accelerometer();
            //Время между обновлениями показаний акселерометра
            accelerometer.TimeBetweenUpdates = TimeSpan.FromMilliseconds(20);
            //Подключаем обработчик события, возникающего при получении новых данных
            accelerometer.CurrentValueChanged += new EventHandler<SensorReadingEventArgs
            <AccelerometerReading>>(accelerometer_CurrentValueChanged);
           //Запуск акселерометра
            accelerometer.Start();
        }
        //Обработчик
        void accelerometer_CurrentValueChanged(object sender, 
        SensorReadingEventArgs<AccelerometerReading> e)
        {
            //Вызов метода из потока пользовательского интерфейса
            Dispatcher.BeginInvoke(() => MoveObject(e.SensorReading));
        }
        //Метод для перемещения объекта
        void MoveObject(AccelerometerReading accData)
        {
            //Переместим объект по оси Y
            Canvas.SetLeft(ellipse1, Canvas.GetLeft(ellipse1) - accData.Acceleration.Y*5);
            //Переместим объект по оси X
            Canvas.SetTop(ellipse1, Canvas.GetTop(ellipse1) - accData.Acceleration.X*5);
            //Проверим на столкновение с границами экрана
            CheckBounds();
        }
        void CheckBounds()
        {
            //При выходе объекта за границы экрана
            //Установим позицию так, чтобы он касался
            //границ экрана
            if (Canvas.GetLeft(ellipse1) < 0 )          
            {
                Canvas.SetLeft(ellipse1, 0);
            }
            if (Canvas.GetLeft(ellipse1) > 700)
            {
                Canvas.SetLeft(ellipse1, 700);
            }
            if (Canvas.GetTop(ellipse1) < 0)
            {
                Canvas.SetTop(ellipse1, 0);
            }
            if (Canvas.GetTop(ellipse1) > 380)
            {
                Canvas.SetTop(ellipse1, 380);
            }
        }

    }
}

В конструкторе мы инициализируем объект типа Accelerometer, задаём интервал обновления данных, назначаем обработчик события, возникающего при получении новых данных и запускаем акселерометр.

Обратите внимание на обработчик accelerometer_CurrentValueChanged. Этот обработчик выполняется в собственном потоке, а пользовательский интерфейс – в собственном, так называемом UI-потоке. В итоге, для того, чтобы обратиться к элементу пользовательского интерфейса, нам понадобится воспользоваться диспетчером (Dispatcher), с помощью которого можно направить задание из одного потока в другой. В данном случае мы используем лямбда-выражение.

Перемещая объект, мы модифицируем его свойства Canvas.Top и Canvas.Left. Для работы с ними существуют соответствующие методы класса Canvas, например Canvas.GetTop(object) получает значение Top для объекта, SetTop – устанавливает.

После перемещения мы проверяем, не пересек ли объект границы экрана (мы работаем в ландшафтной ориентации), если пересек – возвращаем его к границам экрана.

34.2. Работа с микрофоном, панель приложения

В основу этого примера лёг образец от Microsoft (http://msdn.microsoft.com/en-us/library/gg442302%28v=vs.92%29.aspx). Здесь создан пользовательский интерфейс на основе панели приложения. С помощью кнопок на панели приложения мы управляем записью звуков посредством микрофона и воспроизведением этих звуков.

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

(рис 34.2) Окно программы

Кнопки отображены пустыми значками, так как для них использованы пустые заготовки изображений, которые можно нарисовать самостоятельно, учитывая особенности оформления Windows Phone-приложений.

В листинге 34.3 приведен XAML-код страницы MainPage

<phone:PhoneApplicationPage 
    x:Class="P27_2.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="696"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True">

    <!--LayoutRoot представляет корневую сетку, где размещается все содержимое страницы-->
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--TitlePanel содержит имя приложения и заголовок страницы-->
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock x:Name="ApplicationTitle" Text="МИКРОФОН" 
            Style="{StaticResource PhoneTextNormalStyle}"/>
            <TextBlock x:Name="PageTitle" Text="микрофон" Margin="9,-7,0,0" 
            Style="{StaticResource PhoneTextTitle1Style}"/>
        </StackPanel>

        <!--ContentPanel — поместите здесь дополнительное содержимое-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <Image x:Name="StatusImage" Height="334" Width="366" 
               HorizontalAlignment="Center" VerticalAlignment="Center"
               Source="Images/blank.png" Margin="45,79,45,18" />
            <TextBlock Height="30" HorizontalAlignment="Left" Margin="12,6,0,0" 
            Name="UserHelp" Text="Нажмите кнопку Запись" VerticalAlignment="Top" 
            Width="414" />
        </Grid>
    </Grid>

    <!--Пример кода, иллюстрирующий использование ApplicationBar-->
    <phone:PhoneApplicationPage.ApplicationBar>
        <shell:ApplicationBar IsVisible="True" IsMenuEnabled="False">
            <shell:ApplicationBar.Buttons>
                <shell:ApplicationBarIconButton x:Name="recordButton" Text="запись" 
                IconUri="/Images/record.png" Click="recordButton_Click" IsEnabled="True"/>
                <shell:ApplicationBarIconButton x:Name="playButton" Text="проиграть" 
                IconUri="/Images/play.png" Click="playButton_Click" IsEnabled="False"/>
                <shell:ApplicationBarIconButton x:Name="stopButton" Text="стоп" 
                IconUri="/Images/stop.png" Click="stopButton_Click" IsEnabled="False"/>
            </shell:ApplicationBar.Buttons>
        </shell:ApplicationBar>
    </phone:PhoneApplicationPage.ApplicationBar>

</phone:PhoneApplicationPage>

Обратите внимание на описание ApplicationBar. Здесь задано три кнопки на панели приложения. Одна из них, кнопка записи, активирована по умолчанию, доступ к двум другим закрыт, так как сразу после запуска приложения единственное, что может сделать пользователь – это сделать новую запись с микрофона.

Для данного проекта мы должны подключить библиотеку Microsoft.Xna.Framework, объектное представление аппаратного микрофона представлено именно в ней. В листинге 34.4 вы можете видеть программный код, обеспечивающий работу примера. Он подробно прокомментирован.

using System;
using System.IO;
using System.Threading;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;

namespace P27_2
{
    public partial class MainPage : PhoneApplicationPage
    {
// Объект, представляющий физический микрофон на устройстве
        private Microphone microphone = Microphone.Default;  
        //Динамический буфер для получения аудиоданных с микрофона
        private byte[] buffer;               
        //Хранение аудиоданных для последующего воспроизведения           
        private MemoryStream stream = new MemoryStream();   
        //Используется для воспроизведения звука
        private SoundEffectInstance soundInstance;          
        //Флаг для отслеживания состояния проигрывания звука
        private bool soundIsPlaying = false;

        // Изображения, сигнализирующие о состоянии программы
        private BitmapImage blankImage;
        private BitmapImage microphoneImage;
        private BitmapImage speakerImage;

        /// <summary>
        /// Конструктор
        /// </summary>
        public MainPage()
        {
            InitializeComponent();

            // Таймер для организации игрового цикла XNA (обект Microphone взят 
            // из XNA Framework). Мы так же используем этот таймер для отслеживания
            // состояния воспроизведения аудио, что позволяем нам обновлять
            // состояние пользовательского интерфейса
            DispatcherTimer dt = new DispatcherTimer();
            dt.Interval = TimeSpan.FromMilliseconds(33);
            dt.Tick += new EventHandler(dt_Tick);
            dt.Start();

            //Обработчик событя для получения аудиоданных, когда буфер заполнится 
            microphone.BufferReady += new EventHandler<EventArgs>(microphone_BufferReady);
            //Подготовка ссылок на изображения
            blankImage = new BitmapImage(new Uri("Images/blank.png", UriKind.RelativeOrAbsolute));
            microphoneImage = new BitmapImage(new Uri("Images/microphone.png", UriKind.RelativeOrAbsolute));
            speakerImage = new BitmapImage(new Uri("Images/speaker.png", UriKind.RelativeOrAbsolute));
        }

        /// <summary>
        /// Вызвает XNA FrameworkDispatcher и проверяет, проигрывается ли звук.
        /// Если проигрывание завершено, обновляет состояние пользовательского интерфейса.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void dt_Tick(object sender, EventArgs e)
        {
            //Организуем игровой цикл XNA
            try { FrameworkDispatcher.Update(); }
            catch { }
            //Если звук проигрывается
            if (true == soundIsPlaying)
            {
                //Если проигрывание завершено
                if (soundInstance.State != SoundState.Playing)
                {
                    // Проигрывание завершено
                    soundIsPlaying = false;

                    // Обновляет пользовательский интерфейс
                    // того, чтобы отразить, что прекращено проигрывание звука
                    SetButtonStates(true, true, false);
                    UserHelp.Text = "Нажмите кнопку Проиграть или Запись";
                    StatusImage.Source = blankImage;
                }
            }
        }

        /// <summary>
        /// Обработчик события Microphone.BufferReady.
        /// Получает аудиоданные с микрофона, сохраняет их в буфер,
        /// затем записывает содержимое буфера в поток для последующего воспроизведения.
        /// Любые действия в этом обработчике должны быть как можно более быстрыми!
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void microphone_BufferReady(object sender, EventArgs e)
        {
            // Получаем аудиоданные
            microphone.GetData(buffer);

            // Сохраняем данные в поток
            stream.Write(buffer, 0, buffer.Length);
        }

        /// <summary>
        /// Обрабатывает событие Click для кнопки record (запись).
        /// Устанавливает микрофон и буферы данных для получения аудиоданных,
        /// затем запускает микрофон. Кроме того, обновляет пользовательский интерфейс.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void recordButton_Click(object sender, EventArgs e)
        {
            // Получение аудиоданных участками по 1/2 секунды
            microphone.BufferDuration = TimeSpan.FromMilliseconds(500);

            // Выделяет память для хранения аудиоданных
            buffer = new byte[microphone.GetSampleSizeInBytes(microphone.BufferDuration)];

            // Сбрасывает поток в 0 для того случая, если он уже содержит
            //какие-либо данные
            stream.SetLength(0);

            // Начало записи
            microphone.Start();
            
            // Обновление пользовательского интерфейса
            SetButtonStates(false, false, true);
            UserHelp.Text = "Идёт запись...";
            StatusImage.Source = microphoneImage;
        }

        /// <summary>
        /// Обрабатывает событие Click для кнопки stop (стоп).
        /// Останавливает запись звука с микрофона и обновляет пользовательский интерфейс.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void stopButton_Click(object sender, EventArgs e)
        {
            //Если запись идёт
            if (microphone.State == MicrophoneState.Started)
            {
                // В режиме записи пользователь нажимает на кнопку  
                // stop для того, чтобы прекратить запись
                microphone.Stop();
            }
            // Если звук воспроизводится
            else if (soundInstance.State == SoundState.Playing)
            {
                // В режиме воспроизведения пользоатель нажимает на кнопку 
                // stop для того, чтобы остановить воспроизведение
                soundInstance.Stop();
            }
            // Обновляет пользовательский интерфейс
            SetButtonStates(true, true, false);
            UserHelp.Text = "Готово";
            StatusImage.Source = blankImage;
        }

        /// <summary>
        /// Обрабатывает событие Click для кнопки play (воспроизведение).
        /// Проигрывает звук, записанный в прошлом сеансе записи с микрофона
        /// и обновляет пользовательский интерфейс.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void playButton_Click(object sender, EventArgs e)
        {
            if (stream.Length > 0)
            {
                // Обновление пользовательского интефейса,
                // указывающее на то, что звук проигрывается
                SetButtonStates(false, false, true);
                UserHelp.Text = "Воспроизведение записи";
                StatusImage.Source = speakerImage;

                // Проигрывает звук в новом потоке, что позволяет
                //пользовательскому интерфейсу обновляться.
                Thread soundThread = new Thread(new ThreadStart(playSound));
                soundThread.Start();
            }
        }

        /// <summary>
        /// Воспроизводит звук, используя SoundEffectInstance 
        /// это позволяет нам отслеживать состояние проигрывания.
        /// </summary>
        private void playSound()
        {
            // Воспроизводит звук, используя SoundEffectInstance 
            // что позволяет нам отслеживать состояние воспроизведения 
            // и обновлять пользовательский интерфейс в обработчике dt_Tick, когда воспроизведение завершитсяg.
            SoundEffect sound = new SoundEffect(stream.ToArray(), microphone.SampleRate, AudioChannels.Mono);
            soundInstance = sound.CreateInstance();
            soundIsPlaying = true;
            soundInstance.Play();
        }

        /// <summary>
        /// Вспомогательный метод для изменения свойства IsEnabled 
        /// для объектов ApplicationBarIconButtons (кнопок на панели приложения).
        /// </summary>
        /// <param name="recordEnabled">Новое состояние для кнопки record.</param>
        /// <param name="playEnabled">Новое состояние для  play .</param>
        /// <param name="stopEnabled">Новое состояние для stop .</param>
        private void SetButtonStates(bool recordEnabled, bool playEnabled, bool stopEnabled)
        {
            (ApplicationBar.Buttons[0] as ApplicationBarIconButton).IsEnabled = recordEnabled;
            (ApplicationBar.Buttons[1] as ApplicationBarIconButton).IsEnabled = playEnabled;
            (ApplicationBar.Buttons[2] as ApplicationBarIconButton).IsEnabled = stopEnabled;
        }
    }
}

Так как в работе мы пользуемся механизмами XNA, нам нужно организовать таймер, имитирующий игровой цикл XNA. Этот таймер мы запускаем в конструкторе, с его помощью мы, кроме того, отслеживаем состояние проигрывания записи и соответствующим образом обновляем пользовательский интерфейс.

В конструкторе же мы добавляем обработчик события BufferReady, которое происходит, когда буфер микрофона (то есть – записанные данные) готов к обработке.

По кнопке старта записи мы подготавливаем всё, что нужно для работы с данными микрофона и запускаем микрофон. По кнопке остановки, которая останавливает запись, если запись идёт, или останавливает воспроизведение при проигрывании записи, мы, в первом случае, останавливаем микрофон (после чего происходит событие BufferReady, данные с микрофона сохраняются), во втором – останавливаем воспроизведение.

При нажатии на кнопку воспроизведения мы проверяем, есть ли данные в потоке, хранящем данные последней записи, если они есть – запускаем проигрывание в отдельном потоке, пользуясь методом PlaySound().

Большинство операций, имеющих отношение к работе со звуком, сопровождаются настройками пользовательского интерфейса. Как правило, меняется сообщение для пользователя, которое выводится в текстовый блок UserHelp, кроме того, с помощью вспомогательного метода SetButtonStates настраивается доступность кнопок панели приложения.

34.3. Выводы

В данной лабораторной работе мы ознакомились с использованием показаний акселерометра для управления экранными объектами в Silverlight-приложениях, рассмотрели элемент управления Canvas и концепцию присоединенных свойств, научились работать с микрофоном и оснащать программу панелью приложения

34.4. Задание

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

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

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

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

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