MindBlaster

Опубликовано 08 марта 2010 г. 12:00 | Coding4Fun

В этой статье Брайен Пик (Brian Peek) описывает свой проект на PDC 2009 – игру MindBlaster на платформе XNA, которая использует Nintendo Wii Remote в сочетании с гарнитурой MindSet от Neurosky, позволяющей считывать электромагнитные колебания мозга по принципу электроэнцефалограммы.

Автор: Брайен Пик (Brian Peek)

Исходный код: загрузить

Запустить программу: прямо сейчас

Сложность: повышенная

Необходимое время: много часов

Затраты: около $250 на необходимое оборудование

ПО: Visual Basic или Visual C# Express, XNA Game Studio, WiimoteLib, ThinkGearNET

Оборудование: контроллер Wiimote, гарнитура MindSet от Neurosky, инфракрасный светодиод или Wireless Sensor Bar

Введение

В июле 2009 года через сайт Engadget.com я узнал об устройстве MindSet, производимом Neurosky. MindSet – это гарнитура, позволяющая считывать электромагнитные колебания мозга по принципу электроэнцефалограммы стоимостью всего $200. Я связался с компанией, и меня включили в список избранных на получение этого оборудования, как только оно станет доступным, для создания демонстрации на конференции PDC 2009. Вот результат того проекта.

MindBlaster – довольно простая игра, написанная с применением XNA и способная работать на ПК под управлением Windows в сочетании с гарнитурой MindSet и контроллером Nintendo Wiimote. Суть игры такова: вы поворачиваете голову, чтобы перемещать прицел на экране и наводить его на вражеские корабли пришельцев, затем сосредотачиваете свое внимание на выбранном корабле, чтобы накалить его и в конечном счете взорвать. Чем сильнее вы концентрируетесь, тем быстрее взрывается вражеский корабль. Но чем больше врагов вы уничтожаете, тем интенсивнее они ведут ответный огонь по вам, что обеспечивает весьма бурный геймплей до тех пор, пока атака не усиливается настолько, что вы не выдерживаете натиска и вас уничтожают.

Давайте обсудим некоторые компоненты, которые были использованы при создании MindBlaster, и то, как они были интегрированы в финальную версию игры.

Гарнитура MindSet

Как я уже говорил, Mindset – это гарнитура от Neurosky, способная считывать альфа-, бета-, тета- и гамма-волны, излучаемые мозгом, через четыре сухих датчика, три из которых размещаются на левой части гарнитуры, прикрывающей ухо, и один – на креплении, касающемся лба. Кроме того, это устройство содержит микрофон и наушники, что делает его полноценной аудиогарнитурой.

Устройство подключается к ПК по Bluetooth и появляется в системе как последовательный порт Bluetooth. А это означает, что мы можем крайне легко взаимодействовать с этим устройством. Однако Neurosky также предоставляет собственный API с простой оболочкой на C#, которая еще больше облегчает разработку под .NET. Поверх него я написал несложный API, отчасти имитирующий мою Managed Wiimote Library, которую можно загрузить отдельно от данного приложения; этот API еще чуточку упрощает разработку с применением Mindset в среде .NET.

Одна из интересных особенностей MindSet – от вас не требуется знать, что такое волны, излучаемые мозгом, или как все это работает. Я понятия не имею, чем отличается альфа-волна от тета-волны, но все равно смог создать более-менее приличную игру, в которой они используются. Neurosky разработала свой (закрытый) алгоритм, который принимает значения мозговых волн и преобразует их в единый показатель «внимания» и «созерцания» от 1 до 100. Это позволяет очень легко определять, насколько сконцентрирован или расслаблен человек.

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

 // Получаем идентификатор соединения
 int connectionId = ThinkGear.TG_GetNewConnectionId();
 if(connectionId < 0)
     return false;
  
 // Подключаемся к COM-порту
 int connect = ThinkGear.TG_Connect(_connectionId, @"\\.\COM1", baud, ThinkGear.STREAM_PACKETS);
 if(connect < 0)
     return false;
  
 while(true)
 {
     // Считываем пакеты данных
     int packetsRead = ThinkGear.TG_ReadPackets(_connectionId, -1);
  
     if(packetsRead > 0)
     {
         // Получаем значение внимания
         if(ThinkGear.TG_GetValueStatus(_connectionId, ThinkGear.DATA_ATTENTION) != 0)
             int attention = ThinkGear.TG_GetValue(connectionId, ThinkGear.DATA_ATTENTION);
  
         // Получаем значение созерцания (релаксации)
         if(ThinkGear.TG_GetValueStatus(_connectionId, ThinkGear.DATA_MEDITATION) != 0)
             int attention = ThinkGear.TG_GetValue(connectionId, ThinkGear.DATA_MEDITATION);
     }
 }
  

Этот код, используя API, получает идентификатор соединения, открывает соединение с гарнитурой по предоставленному COM-порту, а затем в цикле считывает все пакеты данных, поступающие от гарнитуры, и получает текущее состояние запрошенных значений.

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

Wiimote

Nintendo Wiimote крепится вверх тормашками к верхней части гарнитуры, его инфракрасная камера используется для отслеживания поворота головы в двухмерной плоскости. Для конференции PDC я извлек начинку Wiimote из корпуса и смонтировал прямо на гарнитуре, добавив закрепляемый аккумуляторный отсек от Radio Shack. Реальной необходимости в этом не было, и вы можете закрепить свой Wiimote в корпусе.

clip_image002

Wiimote содержит камеру с разрешением 1024×768 пикселей, которая способна распознавать до четырех источников инфракрасного излучения (ИК). В этом приложении мне нужен был лишь один ИК-источник, так как наклон меня не интересовал. В качестве источника я использовал Nyko Wireless Sensor Bar.

С помощью своей Managed Wiimote Library я могу опрашивать текущее состояние ИК-камеры, чтобы получать координаты X и Y ИК-источника и транслировать их в позицию прицела на экране.

 WiimoteState wmState = MindBlasterGame.Wiimote.WiimoteState;
  
 if(wmState.IRState.IRSensors[0].Found)
 {
     _reticule.Position.X = (wmState.IRState.IRSensors[0].Position.X) * MindBlasterGame.ScreenSize.X;
     _reticule.Position.Y = MindBlasterGame.ScreenSize.Y - (wmState.IRState.IRSensors[0].Position.Y) * MindBlasterGame.ScreenSize.Y;
 }

Архитектура игры

В общей архитектуре игры и ее вывода на экран используются части примера Game State Management от XNA Creators Club. Каждый экран помещается в собственный объект со своими методами Update, Draw, LoadContent и др. Это позволяет сохранять титульный экран, экран настроек, игровой экран и экран финальных результатов как отдельные «сущности» для упрощения разработки.

Давайте немного поговорим о каждом экране и выделим в них самое главное.

Экран настроек

Ведя разработку дома и демонстрируя результат на PDC, мне нужен был экран настроек, сделанный «дешево и сердито», который позволял бы по-быстрому включать и отключать различные функции игры. Например, при разработке на самом деле не было нужды все время носить гарнитуру, поэтому я написал кое-какой код, чтобы пользоваться мышью вместо реального устройства. Экран настроек как раз и давал мне возможность легко переключать подобные вещи. С этой целью я создал объект, наследующий от класса MenuScreen из примера Game State Management. Элементы меню в этом случае создаются так:

 private readonly MenuEntry _menuEntry;
 _menuEntry = new MenuEntry("My menu entry");
 _menuEntry.Selected += _menuEntry_Selected;
 MenuEntries.Add(_menuEntry);

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

Титульный экран

На титульном экране просто показывается графическая заставка и текст «Press Start» или любой дополнительный текст в том случае, если отключена гарнитура, отсоединен Wiimote либо обнаружена какая-то другая ошибка.

Игровой экран

Вот тут и творится все волшебство. Возможно, вас это удивит, но в самом файле GamePlayScreen.cs вовсе нет тонны кода. Логика игры весьма проста: перемещаем прицел на основе позиции, сообщаемой ИК-камерой Wiimote, проверяем, попадает ли он на врага; если да, уменьшаем уровень жизни (hit points) врага, и, если этот уровень падает ниже 0, взрываем вражеский корабль. Кроме того, враги летают по экрану и обстреливают игрока ракетами. Давайте подробнее рассмотрим некоторые элементы игры.

Враги

Каждый рисуемый объект в игре наследует от базового класса Drawable2D или Drawable3D. Эти классы определяют некоторые простые свойства, например позицию, начало координат, масштаб и др., и предоставляют методы, которые должны быть реализованы в производных классах, скажем, LoadContent, Update и Draw, соответствующих архитектуре стандартного XNA-приложения. Вот как выглядит класс Drawable2D:

 public abstract class Drawable2D
 {
     public Vector2 Position;
     public Vector2 Origin;
     public float Rotation;
     public float Scale;
  
     public Drawable2D()
     {
         Scale = 1.0f;
         Visible = true;
     }
  
     public Drawable2D(Vector2 position)
     {
         Position = position;
     }
  
     // Update/Draw/Load должны быть реализованы всеми типами Drawable
     public virtual void LoadContent(ContentManager contentManager) {}
     public virtual void Update(GameTime gameTime) {}
     public virtual void Draw(GameTime gameTime) {}
  
     public bool Visible { get; set; }
 }

Класс Drawable3D почти идентичен предыдущему, но переносит все в трехмерное пространство, используя для обозначения позиций Vector3. Он также включает свойство Model для хранения геометрии объекта и свойство, содержащее текущую матрицу преобразования World.

 public class Drawable3D
 {
     public Vector3 Position;
     public Vector3 Rotation;
     public float Scale;
  
     protected Model Model;
     protected Matrix World;
  
     private Vector2 _position2D;
  
     protected Drawable3D()
     {
         Scale = 1.0f;
         Visible = true;
     }
  
     public Drawable3D(Vector3 position)
     {
         Position = position;
     }
  
     public Vector2 GetPosition2D(Camera camera)
     {
         Vector3 position = MindBlasterGame.ScreenManager.GraphicsDevice.Viewport.Project(this.Position, camera.Projection, camera.View, Matrix.Identity);
         _position2D.X = position.X;
         _position2D.Y = position.Y;
         return _position2D;
     }
  
     // Update/Draw/Load должны быть реализованы всеми типами Drawable
     public virtual void LoadContent(ContentManager contentManager) {}
     public virtual void Update(GameTime gameTime, Camera camera) {}
     public virtual void Draw(GameTime gameTime, Camera camera) {}
  
     public bool Visible { get; set; }

Вражеские корабли и ракеты были смоделированы в 3D Studio Max и экспортированы в файловом формате .X для использования конвейером XNA Content Pipeline с помощью kW X-port – бесплатного плагина для 3D Studio Max, который умеет экспортировать модели и анимации в формат .X с самыми разнообразными пользовательскими настройками.

Поворот и полет

Если вы сыграли в игру или посмотрели видеоролик в этой статье, то заметили, как вражеские корабли поворачивают в какую-то позицию, летят в нее, а затем поворачивают обратно в направлении камеры. Чтобы добиться такого поведения кораблей, программа вычисляет случайную позицию в трехмерном пространстве. Затем выполняется следующий код для поворота корабля в полученную позицию:

 protected Matrix RotateToTarget(Vector3 position, Vector3 target)
 {
     // Получаем направление от позиции к цели
     Vector3 vecToTarget = Vector3.Normalize(target - position);
  
     // Получаем угол поворота
     double angle = Math.Acos(Vector3.Dot(vecToTarget, Vector3.Backward));
  
     // Получаем ось поворота
     Vector3 axisToRotate = Vector3.Cross(Vector3.Backward, vecToTarget);
     axisToRotate.Normalize();
  
     // Создаем матрицу вращения на основе вычисленных оси и угла
     return Matrix.CreateFromAxisAngle(axisToRotate, (float)angle);
 }

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

Эта матрица используется для поворота корабля в течение короткого периода с применением метода Vector3.SmoothStep, который интерполирует два значения, используя кубическое уравнение (уравнение третьей степени). Как только поворот к цели выполнен, корабль «плавно шагает» в свою конечную позицию, а потом поворачивается в сторону камеры с помощью уже показанного выше метода RotateToTarget – на этот раз целевой является позиция камеры.

Использование примеров XNA

В игре MindBlaster я использовал несколько примеров из XNA Creators’ Club. На мой взгляд, на сайте есть просто великолепные примеры, которые облегчают жизнь всем разработчикам на платформе XNA. Не буду говорить за всех, но лично я предпочел позаимствовать работающий, хорошо протестированный движок частиц (particle engine), а не тратить время на его создание с нуля, особенно при столь сжатых сроках, в которые я должен был уложиться с этим проектом. Ранее в этой статье я пояснил, как я использовал пример Game State Management, чтобы получить базовую архитектуру экранов для своего приложения. Я также использовал его класс MenuScreen для описанного выше экрана настроек.

Движок частиц

Движок двухмерных частиц я взял из примера NetRumble. Хотя моя игра на самом деле трехмерная, я смог вывернуться так, чтобы эффекты частиц на экране в двухмерном пространстве поверх координат корабля в трехмерном пространстве производили вполне приличное впечатление.

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

Получить позицию в двухмерном пространстве трехмерного объекта в пространстве мировых координат позволяет следующий метод, содержащийся в классе Drawable3D:

 public Vector2 GetPosition2D(Camera camera)
 {
     Vector3 position = MindBlasterGame.ScreenManager.GraphicsDevice.Viewport.Project(this.Position, camera.Projection, camera.View, Matrix.Identity);
     _position2D.X = position.X;
     _position2D.Y = position.Y;
     return _position2D;
 }

Он использует XNA-метод Viewport.Project для возврата двухмерной позиции трехмерного объекта с учетом его текущей позиции в трехмерном пространстве, текущей матрицы проекции камеры и матрицы представления камеры.

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

Эффект расплывания

Пример Bloom Postprocessor на сайте XNA Creators Club предоставляет DrawableGameComponent, позволяющий очень легко использовать расплывание (bloom). DrawableGameComponent – это класс, реализующий несколько методов, таких как Update и Draw; он добавляется к набору Component объекта Game. При создании каждого кадра вызываются соответствующие методы, в результате чего объект обновляется, а потом рисуется.

Расплывание – красивый эффект, который вызывает искажения от источников яркого света. Примерно так же реагируют ваши глаза, когда вы выходите из темной комнаты на улицу в яркий солнечный день. Я активно применял этот эффект в игре, чтобы при накаливании изменялся блеск кораблей. Кроме того, когда вражеская ракета попадает в игрока, я на несколько миллисекунд заливаю экран белым цветом; в сочетании с расплыванием это производит отличный эффект, аналогичный тому, как если бы вы долго смотрели на яркий свет, а затем быстро перевели взгляд на что-то темное.

Использовать BloomComponent удивительно легко. В методе Initialize игры укажите:

 Bloom = new BloomComponent(this);
 Components.Add(Bloom);
 Bloom.Settings = BloomSettings.PresetSettings[3];

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

Как установить и играть

Убедитесь, что у вас установлена XNA Game Studio 3.1 или редистрибутивный пакет XNA 3.1.

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

Стрелки вверх/вниз/влево/вправо – навигация по меню настроек

1 – включение отладочного вывода в игре

F1 – назад

F2 – запуск

Page Up – сброс игры

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

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

Заключение

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

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

Благодарности

  • Джоуи Бучеку (Joey Buczek) за все трехмерные модели и дизайн игры.
  • Дэвиду Уоллиманну (David Wallimann) за все звуковые эффекты и музыку в игре.
  • Рику Барразе (Rick Barraza) за идею переноса игры в космическое пространство. Это лучше, чем дорога, о которой я подумывал поначалу… :)
  • Грегу Хайверу (Greg Hyver) и Джонни Лью (Johnny Liu) из компании Neurosky за предоставленную мне гарнитуру и за оборудование, выданное на время проведения конференции PDC.
  • Мишель Ливитт (Michelle Leavitt) за бесконечное тестирование.
  • Разработчиков XNA, написавших разнообразные учебные пособия и наборы для начинающих, код из которых сэкономил мне уйму времени при создании игры. Без них я скорее всего и сейчас бы еще писал движки частиц, шейдеры расплывания изображений и прочие части…. Вы очень помогли мне!

Об авторе

Брайен является Microsoft C# MVP. Занимается активной разработкой для .NET со времен первых бета-версий, выпускавшихся аж в 2000 году, а создает решения с применением технологий и платформ Microsoft еще дольше. Помимо .NET, Брайен особенно хорошо знает языки C, C++ и ассемблеры для процессоров различных архитектур. Он также хорошо разбирается в широком спектре технологий, включая веб-разработку, сканирование документов, ГИС, графику, разработку игр и программирование с применением аппаратных интерфейсов. У него имеется большой опыт в создании приложений для здравоохранения, а также в разработке решений для портативных устройств, таких как планшетные ПК и КПК. Кроме того, Брайен является соавтором книги «Coding4Fun: 10 .NET Programming Projects for Wiimote, YouTube, World of Warcraft, and More», опубликованной издательством O'Reilly. Ранее был соавтором книги «Debugging ASP.NET», выпущенной New Riders. Автор множества статей на MSDN-сайте Coding4Fun.