Программирование компьютерных игр под Windows в XNA Game Studio Express Горнаков С. Г.
Москва, 2008
УДК 004.4 ББК 32.973.26018.2 ***
Содержание ***
Горнаков С. Г. Программирование компьютерных игр под Windows в XNA Game Studio Express. – М.: ДМК Пресс, 2008. – 384 с.: ил. ISBN *** Это серия книг настольной библиотеки начинающего программиста игр. На данный момент серия состоит из двух книг и поможет программистам изучить технику разработки игр для системы Windows и Xbox 360. В этом издании с помощью студии XNA Game Studio Express рассматривается пол( ный цикл создания компьютерных игр для операционной системы Windows. Изучая эту книгу, вы освоите основы работ с инструментариями Visual C# Express и XNA Game Studio Express. Научитесь работать с двухмерной и трехмерной графикой, анимацией, познакомитесь с техникой создания иг( ровых классов и формированием механизма игровых состояний. Овладеете секретами создания интерактивных заставок и меню, работой с устройства( ми ввода и звуком. Итогом книги станет создание двухмерной и трехмер( ной игры с формированием инсталляционного пакета. На базе полученных знаний вы сможет создавать свои собственные компьютерные игры и про( давать или распространять их бесплатно через Интернет. В дополнение на диске имеется потрясающая подборка материала по технике разработки компьютерных игр для операционной системы Windows! УДК 004.4 ББК 32.973.26018.2 Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения вла( дельцев авторских прав. Материал, изложенный в данной книге, многократно проверен. Но поскольку вероятность технических ошибок все равно существует, издательство не может гарантировать абсолютную точность и правильность приводимых сведений. В связи с этим издательство не несет ответ( ственности за возможные ошибки, связанные с использованием книги.
ISBN ***
© Горнаков С. Г., 2007 © Оформление, ДМК Пресс, 2008
Предисловие ........................................................................... 11 О чем эта книга ............................................................................ 12 Структура книги ........................................................................... 12 Часть первая ............................................................................... 12 Часть вторая ................................................................................ 12 Часть третья ................................................................................ 12 Что вы должны знать .................................................................... 12 Что вы должны иметь ................................................................... 13 Диск ............................................................................................. 13 Благодарности ............................................................................. 13 Об авторе ..................................................................................... 13
Часть 1 Введение в программирование игр Глава 1 В качестве вступительного слова .................................. 14 1.1. Этап проектирования игры .................................................... 16 1.2. Двухмерная игра ................................................................... 16 1.3. Трехмерная игра ................................................................... 17 1.4. Исходные коды проектов ....................................................... 18
Глава 2 Платформа XNA .................................................................... 19 2.1. И был сначала DirectX ............................................................ 19 2.1.1. Уровни абстракции .............................................................. 20 2.2. Новые реалии ........................................................................ 20 2.3. Managed DirectX ..................................................................... 21 2.4. Платформа XNA Framework .................................................... 22
4
Программирование компьютерных игр под Windows в XNA Game Studio Express 2.4.1. Уровни абстракции XNA ............................................................. 2.4.2. Application Model ........................................................................ 2.4.3. Компонент Content Pipeline ........................................................ 2.4.4. Компонент Graphics ................................................................... 2.4.5. Компонент Math ......................................................................... 2.4.6. Компонент Input ......................................................................... 2.4.7. Компонент Audio ........................................................................ 2.4.8. Компонент Storage .....................................................................
Содержание 23 24 27 28 29 30 30 30
Глава 3 Инструментарий Visual C# Express Edition ............... 31 3.1. Инструментарий Visual Studio 2005........................................ 31 3.2. Инструментарий Visual C# Express ........................................ 32 3.3. Установка Visual C# Express вместе с Visual Studio 2005 ........ 34 3.3.1. Механизм установки ............................................................ 34 3.4. Регистрация Visual C# Express ............................................... 41 3.5. Основы работы с Visual C# Express ........................................ 43 3.5.1. Создаем простой проект ...................................................... 45 3.5.2. Компиляция и запуск проекта ............................................... 47 3.5.3. Сборка проекта ................................................................... 48
Глава 4 Студия разработки игр XNA Game Studio Express ........................................................................................ 50 4.1. Студии разработки игр .......................................................... 50 4.1.1. XNA Game Studio Express ...................................................... 50 4.1.2. XNA Game Studio Professional ................................................ 52 4.1.3. XNA Studio .......................................................................... 52 4.2. Установка XNA Game Studio Express ...................................... 53 4.3. Знакомство с XNA Game Studio Express ................................. 54 4.3.1. Настройка получения новостей в XNA Game Studio Express ....... 56 4.3.2. Шаблоны XNA Game Studio Express ............................................ 58
Часть 2 Создаем двухмерную игру Глава 5 Формируем каркас игровых классов ......................... 60 5.1. Формируем проект в Visual C# Express .................................. 61
5
5.2. Структура проекта ................................................................. 62 5.3. Класс Program ....................................................................... 63 5.4. Класс Game1 ......................................................................... 66 5.5. Механизм работы программы ............................................... 72 5.6. Переходим в полноэкранный режим ..................................... 73
Глава 6 Работа с двухмерной графикой .................................... 78 6.1. Система координат ............................................................... 78 6.2. Проект DrawSprite .................................................................. 79 6.3. Проект DrawSpriteClass.......................................................... 86 6.3.1. Класс Sprite проекта DrawSpriteClass ..................................... 87 6.3.2. Класс Game1 проекта DrawSpriteClass .................................... 90
Глава 7 Спрайтовая анимация ........................................................ 93 7.1. Проект Animation .................................................................... 94 7.1.1. Анимационная последовательность ....................................... 94 7.1.2. Класс Sprite проекта Animation .............................................. 95 7.1.3. Класс Game1 проекта Animation ............................................. 99 7.2. Проект Background .............................................................. 103
Глава 8 Движение спрайтов в пространстве .......................... 109 8.1. Проект MoveSprite ............................................................... 112 8.2. Проект MoveSpriteArray ....................................................... 116
Глава 9 Устройства ввода ................................................................ 125 9.1. Проект Platform .................................................................... 125 9.2. Проект PauseGame .............................................................. 131
Глава 10 Игровые столкновения ..................................................... 136 10.1. Структура BoundingBox ..................................................... 136 10.2. Проект Collision .................................................................. 138
6
Программирование компьютерных игр под Windows в XNA Game Studio Express
Глава 11 Подсчет очков и вывод текста на экран ................. 149 11.1. Подсчет очков .................................................................... 150 11.2. Работа с текстом ............................................................... 151 11.2.1. Готовое решение XNAExtras ............................................... 153 11.2.2. Как это работает? ............................................................ 153 11.2.3. Как создать шрифт? ......................................................... 154 11.2.4. Добавляем в проект необходимые компоненты ................... 154 11.2.5. Работа с текстом ............................................................. 157
Глава 12 Создаем игровое меню .................................................... 165 12.1. Планируем запуск меню .................................................... 165 12.2. Проект Menu ...................................................................... 166 12.2.1. Класс Menu ..................................................................... 168 12.2.2. Загружаем в игру меню ..................................................... 172 12.3. Проект MenuCursor ............................................................ 182
Глава 13 Звуковые эффекты ............................................................. 195 13.1. Создаем проект XACT ........................................................ 195 13.1.1. Пошаговая инструкция ..................................................... 196 13.2. Класс Sound....................................................................... 201 13.3. Воспроизведение звука в игре .......................................... 206 13.4. Цикличное воспроизведение музыки ................................ 213
Глава 14 Добавляем в игру новые уровни ................................ 214 14.1. Переход с уровня на уровень ............................................. 215 14.2. Набранные очки ................................................................. 216 14.3. Проект NewLevels .............................................................. 217 14.3.1. Изменения в классе Game1 ............................................... 218
Глава 15 Формируем инсталляционный пакет ....................... 235 15.1. Программа Smart Install Maker ........................................... 236 15.1.1. Информация ................................................................... 236 15.1.2. Файлы ............................................................................ 238
Содержание
7
15.1.3. Требования ..................................................................... 241 15.1.4. Интерфейс ...................................................................... 242 15.1.5. Диалоги .......................................................................... 243 15.1.6. Ярлыки ........................................................................... 245 15.1.7. Деинсталлятор ................................................................ 246
15.2. Инсталляция созданной программы .................................. 247 15.2.1. Окно приветствия ............................................................ 249 15.2.2. Лицензия ........................................................................ 249 15.2.3. Выбор директории ........................................................... 250 15.2.4. Выбор места установки ярлыков ........................................ 250 15.2.5. Начало установки программы ............................................ 251 15.2.6. Установка программы ...................................................... 251 15.2.7. Окончание установки программы ....................................... 252
Часть 3 Создаем трехмерную игру Глава 16 Основы программирования трехмерной графики ................................................................................... 253 16.1. Система трехмерных координат ........................................ 253 16.2. Точки и вершины ............................................................... 255 16.3. Модель .............................................................................. 256 16.4. Матрицы ............................................................................ 257 16.4.1. Сложение и вычитание матриц .......................................... 257 16.4.2. Умножение матриц ........................................................... 258 16.5. Матричные преобразования .............................................. 258 16.5.1. Мировая матрица ............................................................ 259 16.5.2. Матрица вида .................................................................. 261 16.5.3. Матрица проекции ........................................................... 261 16.6. Свет ................................................................................... 262 16.7. Шейдеры ........................................................................... 263 16.7.1. Шейдерная модель .......................................................... 263 16.7.2. Механизм работы шейдерных программ ............................ 265 16.7.3. Вершинные и пиксельные шейдеры ................................... 265 16.7.4. Подводя итоги шейдерной технологии ............................... 266 16.8. Графический конвейер ...................................................... 267
Глава 17 Смена игровых состояний ............................................... 269 17.1. Автоматический подбор разрешения экрана .................... 269 17.2. Улучшаем смену игровых состояний ................................. 271
8
Программирование компьютерных игр под Windows в XNA Game Studio Express
Глава 18 Загружаем в игру модель .............................................. 277 18.1. Рисуем модель на экране монитора .................................. 277 18.1.1. Механизм загрузки модели в игру ...................................... 279 18.1.2. Метод DrawModel() .......................................................... 283 18.2. Класс ModelClass ............................................................... 290 18.3. Создаем объект класса LoadingModelClass ....................... 291
Содержание
9
22.2.1. Как сделана титульная заставка ............................................. 333 22.2.2. Разрабатываем класс SplashScreen ....................................... 334
22.3. Заставки Помощь, Об игре и Книги ................................... 338 22.4. Создаем меню игры .......................................................... 341 22.5. Смена игровых состояний в классе Game1 ........................ 347 22.5.1. Создаем объекты для меню и заставок .................................. 348 22.5.2. Обновляем состояние игры в методе Update() ....................... 348 22.5.3. Обновляем графику в методе Draw() ...................................... 353
22.6. Добавим в игру логику ....................................................... 355
Глава 19 Движение моделей в пространстве ........................... 295 19.1. Задаем скорость движения модели ................................... 295 19.2. Создаем массив данных .................................................... 296 19.3. Инициализация и установка моделей на позиции ............. 296 19.4. Установка матриц .............................................................. 298 19.5. Формируем метод для перемещения моделей .................. 298 19.6. Случайный выбор позиции на экране ................................ 299
Глава 20 Стреляем по целям ............................................................ 305 20.1. Класс для работы с двухмерными изображениями ............ 305 20.2. Задаем радиус для мячей .................................................. 305 20.3. Рисуем на экране прицел .................................................. 306 20.4. Получаем координаты прицела ......................................... 307 20.5. Целимся и стреляем .......................................................... 308
Глава 21 Формируем трехмерную сцену ................................... 317 21.1. Изменяем позицию камеры ............................................... 317 21.2. Загружаем в игру стадион ................................................. 318 21.3. Новые игровые позиции для мячей ................................... 319 21.4. Падение мячей на поле стадиона ...................................... 320 21.5. Небо и тучи ........................................................................ 321
Глава 22 Последние штрихи ............................................................. 330 22.1. Схема работы меню и показ заставок ................................ 330 22.2. Титульная заставка ............................................................ 332
Приложение 1 Обзор компакт-диска ....................................................... 358 Приложение 2 Интернет-ресурсы ............................................................... 359 2.1. Русскоязычные ресурсы ...................................................... 359 2.1.1. http://www.xnadev.ru ................................................................ 2.1.2. http://www.gamedev.ru ............................................................. 2.1.3. http://dft.ru .............................................................................. 2.1.4. http://www.kriconf.ru ................................................................ 2.1.5. http://www.codeplex.com .......................................................... 2.1.6. http://www.gamedev.kz ............................................................. 2.1.7. http://www.render.ru ................................................................. 2.1.8. http://www.xboxrussia.ru ........................................................... 2.1.9. http://www.xboxland.net ............................................................ 2.1.10. http://www.dmk]press.ru ......................................................... 2.1.11. http://www.gornakov.ru ...........................................................
359 359 359 361 361 361 363 363 363 365 365
2.2. Англоязычные ресурсы........................................................ 365 2.2.1. http://www.xbox360.com ........................................................... 2.2.2. http://creators.xna.com ............................................................. 2.2.3. http://www.xna.com ................................................................... 2.2.4. http://www.microsoft.com/xna ................................................... 2.2.5. http://www.msdn.com/xna ........................................................ 2.2.6. http://blogs.msdn.com/xna ....................................................... 2.2.7. http://forum.microsoft.com ....................................................... 2.2.8. http://www.ziggyware.com ........................................................ 2.2.9. http://www.xna101.net .............................................................. 2.2.10. http://xbox360homebrew.com ................................................. 2.2.11. http://www.garagegames.com ................................................. 2.2.12. http://learnxna.com ................................................................. 2.2.13. http://www.turbosquid.com/xna ............................................... 2.2.14. http://www.xnaresources.com .................................................
367 367 367 369 369 369 369 371 371 371 373 373 373 373
10
Программирование компьютерных игр под Windows в XNA Game Studio Express 2.2.15. http://www.xnadevelopment.com ............................................. 2.2.16. http://xnamagic.com ............................................................... 2.2.17. http://blog.tehone.com ........................................................... 2.2.18. http://www.student.dtu.dk ....................................................... 2.2.19. http://www.dhpoware.com ....................................................... 2.2.20. http://www.nuclex.org ............................................................. 2.2.21. http://www.xnaportal.com ....................................................... 2.2.22. http://www.xnatutorial.com ......................................................
376 376 376 376 378 378 378 378
Предметный указатель .................................................... 381
Предисловие Прежде всего хочу поблагодарить вас за то, что вы приобрели эту книгу или обратили на нее свое внимание! В какой(то момент времени рынок компьютерных игр словно «завис». Создалось такое впечатление, что интерес со стороны простых пользователей несколько угас и та былая игровая активность просто пропала. Что называется, сели батарейки. И вдруг компания Microsoft «выкидывает» на рынок студию разработки игр XNA Game Studio Express, с помощью которой практически любой пользователь, зна( комый с элементарными основами языка программирования C#, может создать свою игру. В этот самый момент батарейки моментально сменились на долгоиграющие аккумуляторы, кнопка Reset была нажата, а миллионы людей бросились к компьюте( рам создавать свои маленькие, но греющие душу аркады, стрелялки, леталки, пи( щалки, лягалки, пускалки! Инновация Microsoft в этом плане заключается в том, что она предоставляет шанс огромному количеству программистов реализовать себя и создать игру лю( бого уровня. До этого момента, например, разработка игр для Xbox 360 была уде( лом отдельно взятых компаний с проверенной временем репутацией и хорошими финансовыми возможностями. Комплект разработчика для приставки Xbox 360 стоил порядка 20–30 тыс. долл., для простого программиста(одиночки эта сумма практически не подъемная. Ко всему прочему комплект разработчика, даже при наличии нужной суммы денег, просто так и кому попало не продадут. Ситуация с XNA Game Studio Express прямо противоположна. Студия разра( ботки игр бесплатна и доступна для свободного скачивания. На базе этой студии вы можете создавать свои игры для операционной системы Windows без каких( либо ограничений и лицензий. Эти игры можно продавать, распространять бес( платно и т. д. Ограничений в этом плане нет никаких: сделайте любую игру (2D или 3D) – и продавайте ее через Интернет или любым другим удобным для вас способом! Для приставки Xbox 360 в дальнейшем, по всей видимости, будет предусмот( рена система отбора игр. В настоящее время XNA Game Studio Express находится на этапе становления, и вы можете запускать игры, сделанные в XNA Game Studio Express, на своей приставке, только если вступите в клуб разработчиков игр. Но уже сейчас на сайте http://creators.xna.com для любого человека доступно участие в конкурсе разработки игр, где предусмотрено 20 ценных денежных призов. Все говорит о том, что XNA Game Studio Express – это серьезный инструмент, и Microsoft связывает с этим программным продуктом большие надежды.
12
Программирование компьютерных игр под Windows в XNA Game Studio Express
Предисловие
13
О чем эта книга
Что вы должны иметь
Издание имеет две разные версии книг. В этих книгах рассматривается процесс создания игр для операционной системы Windows и приставки Xbox 360. Цель именно этой книги заключается в том, чтобы научить читателя создавать двух( мерные и трехмерные компьютерные игры. В книге шаг за шагом и от простого материала к сложному вы пройдете этапы формирования двух игр для операци( онной системы Windows с применением инструментария Visual C# Express и сту( дии разработки игр XNA Game Studio Express.
Компьютерную систему с установленной на ней операционной системой от вы( пуска Windows XP и выше, а также диск к этой книге, который поставляется в комплекте с изданием.
Структура книги Часть первая Первая часть книги – это вводный курс в разработку и программирование игр на языке C# с использованием инструментария Visual C# Express и студии разработ( ки игр XNA Game Studio Express. Первые четыре главы этой книги познакомят вас с архитектурой платформы XNA Framework и научат работать с программны( ми продуктами Visual C# Express и XNA Game Studio Express.
Часть вторая Эта часть книги направлена на создание двухмерной игры для операционной сис( темы Windows. Здесь читатель пройдет все этапы создания простой двухмерной игры, сформировав в конечном счете полноценный инсталляционный пакет, гото( вый к установке на любой компьютерной системе.
Часть третья В третьей части книги читатель на конкретном примере познакомится с создани( ем небольшой трехмерной игры.
Что вы должны знать Книга рассчитана на широкий круг читателей и рекомендована к изучению как начинающим программистам, так и более опытным. В книге предполагается, что читатель знаком с основами языка программирования C# и может создать и от( компилировать простейший проект. Тем не менее в первой части книги предлага( ются к изучению основные приемы работы с инструментарием Visual C# Express и XNA Game Studio Express, что дает возможность работать с этой книгой челове( ку, незнакомому с данными программными продуктами.
Диск На диске находятся примеры и исходные коды, созданные за время изучения кни( ги, а также инсталляционные пакеты студии разработки игр XNA Game Studio Express и инструментария Visual C# Express. Сторонние библиотеки, программы, примеры и Starter Kits с сайта XNA Creators Club, а также методические материа( лы с Gamefest 2006 и 2007 по разработке игр для Windows!
Благодарности Хочу поблагодарить всех тех людей, которые так или иначе были связаны с выхо( дом этой книги в свет! Персональные благодарности издательству ДМК Пресс и генеральному директору Дмитрию Алексеевичу Мовчану. Особые слова благодарности моей жене Светлане за все рисунки, сделанные к обеим книгам и играм. Спасибо тебе, дорогая, за помощь!
Об авторе Горнаков Станислав Геннадьевич – профессиональный программист в области создания мобильных и компьютерных игр. Автор книг: «Программирование игр для приставки Xbox 360 в XNA Game Studio Express», «Инструментальные сред( ства программирования и отладки шейдеров в DirectX и OpenGL», «DirectX 9. Уроки программирования на С++», «Symbian OS. Программирование мобильных телефонов на С++», «Программирование мобильных телефонов на Java 2 ME», «Самоучитель работы на смартфонах и коммуникаторах под управлением Sym( bian OS» и «Самоучитель работы на КПК, смартфонах и коммуникаторах под уп( равлением Windows Mobile». Больше информации о книгах автора вы можете найти в Интернете по адресу: http://www.gornakov.ru.
В качестве вступительного слова
Часть 1 Введение в программирование игр
15
В свою очередь, казуальные игры – это огромная индустрия развлечений с боль( шим рынком сбыта по всему миру. Для начала рекомендую вам посетить сайт Ев( ропейской конференции казуальных игр (Casuality Europe: East 2007). На этом сайте вы найдете массу ссылок на компании, которые занимаются распростране( нием казуальных игр и которые с большим удовольствием помогут продвинуть на рынок предлагаемую вами игру. Например, всем известная компания Alawar (ад( рес в Интернете www.alawar.ru) занимается тем, что помогает разработчикам про( двигать свой товар на рынке (рис. 1.1).
Глава 1 В качестве вступительного слова Важно понимать, что процесс создания игры – это очень трудоемкое дело, отни( мающее огромное количество времени из жизни тех людей, которые делают эту работу. Самому, в одиночку, создать полноценную игру фактически нереально, и тем более при всех нынешних технологиях и требованиях к графической кар( тинке. Никогда не пытайтесь в своем первом проекте делать игры плана Gears of War или Need for Speed! Нельзя откусить больше, чем можно прожевать, не говоря уже о том, что все откушенное нужно проглотить, да еще и не поперхнуться. Создание таких боль( ших и мощных игр – это удел игровых студий с огромным штатом программис( тов, художников, дизайнеров, аниматоров, модельеров… Если вы в одиночку или небольшим коллективом решите создавать нечто подобное, то скорее всего ваш проект растянется на долгие, долгие годы работы, а за это время все ваши графические инновации станут просто неактуальными. Эта аксиома проверена временем, а ее несоблюдение сгубило уже множество коллективов и программи( стов(одиночек. Тогда возникает вопрос: а что тогда можно программировать? Чаще всего программисты(одиночки или коллективы единомышленников создают неболь( шие, так называемые казуальные игры, или игры, распространяемые через Интер( нет, стоимость которых составляет не более пары десятков долларов. Набив руку на этом поприще и небольших проектах, эти самые коллективы либо перерастают в более мощные игровые студии, либо сотрудники фирмы находят себе хорошую работу в стане сильных компаний.
Рис. 1.1. Сайт компании Alawar
В контексте этой книги мы постараемся создать нечто подобное и похожее на казуальную игру. Цель книги заключается в том, чтобы научить вас создавать двухмерные и трехмерные игры для операционной системы Windows. Самый луч( ший способ научиться программировать игры – это сделать игру своими соб( ственными руками, поэтому на протяжении всей этой книги мы будем создавать две небольшие, пусть и простенькие, но зато свои игры. В дальнейшем на базе полученных знаний, применив все свое желание и не( реализованные идеи, вы вольны сделать нечто большее и лучшее, чем описано в этой книге (я на это искренне надеюсь). Свою задачу в этом плане я вижу в том, чтобы дать вам необходимый запас знаний для старта и толчок в реализации сво( их идей, все остальное зависит исключительно от вас…
16
Трехмерная игра
В качестве вступительного слова
1.1. Этап проектирования игры Это очень важный и главный этап работы над игрой. На этой стадии необходимо решить массу различных организационных вопросов и выработать общую кон( цепцию игры, создать документацию к игре, проработать дизайн уровней, персо( нажей, спроектировать игровую модель, сформировать набор игровых классов, разделить проектные задания среди персонала и многое, многое другое. По этой теме написано немало книг, но у нас есть одна небольшая проблема. Дело в том, что вы не умеете программировать игры и до сих пор не сделали ни одной игры. Вы не имеете представления о том, как устроен каркас игровых классов, что нуж( но сделать для того, чтобы загрузить в игру графику, и т. д. Поэтому для книги была придумана своя модель реализации игр. Вместо большого этапа проектирования игры мы с вами в каждой последую( щей главе, строка за строкой, будем писать исходный код игры. Наш каждый сле( дующий проект будет модернизировать предыдущий. Например, в двухмерной игре мы сначала создадим пустой проект, затем выберем разрешение для экрана. Загрузим в игру простое графическое изображение, нарисуем его на экране. Со( здадим анимацию, увеличим на экране количество изображений, добавим фон, рассмотрим работу с клавиатурой и мышью. Изучим механизм игровых столк( новений, добавим в игру меню и заставки – и в итоге создадим своими руками простую двухмерную игру, а весь процесс работы над игрой станет для вас про( стым учебным пособием, изучив которое вы в дальнейшем сможете создавать свои проекты. Что касается серьезного подхода в этапе проектирования игры, а также со( ставления необходимой документации к игре, то могу вам порекомендовать инте( ресные материалы на эту тему. В первую очередь это, конечно, книги по этой те( матике, которые можно найти в магазинах, но, пожалуй, самое главное, что можно посоветовать по этой теме, – это документация от ведущих отечественных компа( ний разработчиков игр. Не поленитесь и зайдите на сайт компании 1C в раздел Разработчикам. На этой странице сайта для свободного скачивания доступен потрясающий набор документации, рассказывающий о том, как правильно можно создать дизайн(до( кумент для игры, с чего лучше начать и как лучше всего представить свою игру издателю и т. д. Это просто потрясающая подборка материала, которая проверена жизнью. Аналогичная документация имеется на сайтах компании Бука и Alawar. Это лучшее, что можно порекомендовать вам в этом плане.
1.2. Двухмерная игра Теперь несколько поясняющих слов об идее двухмерной игры. Основной задачей игры является разностороннее изучение способов работы с двухмерной графи( кой. В этом контексте базовой идеей для игры послужили игры, где пользователь должен ловить некоторые предметы, падающие с неба. За каждый такой пойман(
17
ный объект пользователю начисляются очки. Дополнительно с неба падают объ( екты, которые ловить не нужно, за столкновение с такими объектами очки соот( ветственно отнимаются. По достижении определенного порога очков пользова( тель переходит на следующий уровень. Это простой показательный пример, с помощью которого мы сможем просле( дить за сменой игровых состояний, переходом на новые уровни игры, движением объектов на экране, анимацией, столкновением, работой с клавиатурой и т. д. Главное – разобраться с техникой программирования игр и внутренним меха( низмом работы платформы XNA. Все остальное, в том числе графика, – дело вто( ричное. Оформление и антураж игры – также дело личных и коммерческих предпоч( тений. Предлагаемая идея игры, чтобы вам было не скучно, «одета» в обвертку индейских прерий. Соответственно игра носит название «Летящие в прерии»… Предположительно все действия в игре разворачиваются в прериях, над которы( ми терпит крушение самолет, а люди и предметы с самолета падают с неба. У вас как у игрока имеется специальная платформа (с матрасом…), с помощью которой вы должны спасти как можно больше падающих людей. Платформа перемещает( ся исключительно в нижней части экрана с помощью клавиш с командами Влево и Вправо, и вам нужно подставить ее под падающего человека. Спасли человека – получите очки, набрали потолок очков для текущего уровня – переходите на сле( дующий уровень и т. д.
1.3. Трехмерная игра В трехмерной игре, так же как и в двухмерной, главной задачей является возмож( ность всестороннего рассмотрения методов работы с 3D(моделями и графикой. По большому счету, работать с трехмерной графикой на порядок сложнее, чем с двухмерной, поэтому уровень изучаемой игры невысок, но этого уровня вполне достаточно, чтобы понять, как работать с 3D(графикой и в каком направлении необходимо двигаться дальше. В качестве примера в книге была сделана игра «Футбольный стрелок». Суть игрового процесса этой игры состоит в следующем. Сверху на футбольное поле падают три мячика. Задача игрока заключается в том, чтобы стрелять по мячам (курсор мыши – это мушка), подбивая мячи тем самым вверх, не давая в конечном счете одному из мячей коснуться футбольного поля. Как только один из мячей касается футбольного поля, наступает окончание текущего уровня, и игроку предлагается переиграть этот уровень заново. Для того чтобы перейти на новый уровень, необходимо подбить каждый из мячей по двадцать раз. Самое интересное заключается в том, что даже если игрок подбил один из мячей 20 раз, то мяч все равно будет падать на землю, но его прио( ритетность в подбитии значительно уменьшается. Как только все три мяча будут подбиты по двадцать раз, то считается, что текущий уровень пройден. На новом уровне скорость падения мячей увеличивается. Подробнее о способах реализации
18
В качестве вступительного слова
игры и используемой графике вы узнаете из третьей части книги, но изучать кни( гу необходимо строго в хронологическом порядке.
1.4. Исходные коды проектов Я по(прежнему остаюсь сторонником книг, в которых дается полный исходный код рассматриваемого примера. Не всегда удобно, а порой и невозможно иметь под рукой компьютер для просмотра всего примера в целом. Поэтому в книге в каждой главе приводится полный листинг изучаемого в данный момент класса, но поскольку в каждой последующей главе мы модернизируем код, то часть кода методов может не изменяться. В связи с этим в некоторых главах код этих методов может быть вырезан за ненадобностью. Плюс все нововведения в коде выделяют( ся жирным шрифтом, чтобы вам было проще следить за изменениями. На этом небольшая вступительная часть этой главы подошла к концу, пора переходить от слов к делу!
Глава 2 Платформа XNA Разработка игр для компьютерных систем, консольных приставок и мобильных устройств – занятие трудоемкое. Если говорить только о специфике создания игр для любой из перечисленных платформ, то можно отметить, что подход в созда( нии игр у каждой платформы будет свой (по крайней мере, так было до недавнего времени). Более того, огромное количество устройств может иметь различное ап( паратное обеспечение. Если взять, к примеру, несколько компьютеров, то все они с большой долей вероятности будут компоноваться различными комплектующи( ми. И как быть в этом случае бедному программисту, которому нужно попытаться создать игру для всех этих разных устройств? Ответ прост: необходимы общие стандарты и спецификации. И они есть, за что им большой и огромный респект от всех нас. Конечно, не все в этом мире иде( ально и гармонично, те же стандарты и спецификации бывают разными, но! Для компьютерных систем есть свой стандарт – это DirectX и теперь XNA Framework, для мобильных устройств Java 2 ME, Symbian OS & C++ и Windows Mobile & C++ и C#, для Xbox 360 тоже появился свой стандарт в виде платформы XNA. В целом все постепенно унифицируется и сводится к одному стандарту, по край( ней мере, в семействе продуктов Microsoft. Кстати, я больше чем уверен, что вы не очень сильно любите Microsoft и дядюшку Билла, но при этом все равно пользуе( тесь Windows на своем компьютере. Но это не главное, главное другое. Представьте, если бы у нас сейчас было 10 разных и мощных операционных систем от разных компаний. Думаю, что все эти компании ни за что и никогда не договорились бы об общем стандарте в разработке игр для своих операционных систем. И тогда у нас на рынке сейчас имелся бы не DirectX 10, а десять разных DirectX’ов (или как бы они там назывались)… Со стороны пользователя конку( ренция – это просто замечательно, а вот со стороны программиста куча разных стандартов в одной сфере – это ой как тяжело.
2.1. И был сначала DirectX Осознав факт необходимости целостной спецификации, корпорация Microsoft много лет назад попыталась создать стандартный набор игровых библиотек для операционной системы Windows. В 1995 году на свет появилась первая версия DirectX 1.0 (изначально, правда, была еще одна ранняя версия с названием WinG, но сейчас это не столь важно). Эта версия игровой библиотеки (API) была построе( на на базе библиотеки Reality Lab компании RenderMorfics, которую Microsoft прикупила по этому случаю.
20
Managed DirectX
Платформа XNA
Первый блин, как всегда, комом, поэтому и у DirectX не все сложилось сразу. Выходили новые версии библиотек, которые сменяли одна другую, а главное – постоянно изменялось содержимое самих библиотек. И только в 1999 году после выхода DirectX 7.1 (2D) и в 2001 году после выхода DirectX 8.1 (с полноценной 3D(поддержкой и первой версией шейдеров) появилась относительная стабиль( ность. Выход новой версии DirectX 9 в 2002 году привносит ряд изменений в API, но это все плановая модернизация, а не коренное изменение всей целостности биб( лиотеки. В период с 2004 по 2007 год DirectX 9 подвергался мощной перестройке, причем обновления происходили (особенно в 2006 году) буквально через каждые два(три месяца. И уже в 2007 году появилась новая библиотека DirectX 10. Все изменения в DirectX в основном связаны с перестройкой работы с 3D(гра( фикой и, главное, с некоторыми упрощениями в подходе создания конечного продукта. Нельзя сказать, что перестройка DirectX прошла безболезненно, в том числе и для нас с вами. Вы просто не представляете, какое количество писем я получил за это время! Люди, купившие мою книгу «DirectX 9. Уроки программи( рования на С++» (дата выхода – 2004 год), абсолютно не понимали, почему но( вый DirectX SDK не хочет работать с примерами, рассматриваемыми в книге! Но со временем все утряслось, и сейчас на рынке появились стабильные и мощные инструменты, направленные на создание хороших игр.
2.1.1. Уровни абстракции Схема взаимодействия DirectX и компьютерной системы на уровне абстракции может «вылиться» примерно в следующий рисунок (рис. 2.1). Как видно из этой схемы, любая игра базируется на классах DirectX, которые, в свою очередь, взаи( модействуют с операционной системой или специальными сервисами, включаю( щими в себя драйвера устройства и другие API. Такой подход в реализации многоуровневых прослоек позволяет программис( ту не обращать внимания на аппаратную часть устройства и спокойно работать с программным кодом. В итоге для программиста абсолютно не важно (в идеале), какое аппаратное обеспечение имеет это устройство, он работает непосредственно с кодом игры и библиотечными классами DirectX. Останавливаться подробно на устройстве и работе библиотеки DirectX в этой главе и всей книге мы не будем, поскольку к нашей теме DirectX не имеет прямого отношения и служит просто звеном одной цепочки, о котором обязательно нужно было упомянуть.
2.2. Новые реалии Время вносит в жизнь свои коррективы. В какой(то момент стали очень бурно развиваться мобильные устройства, а также у Microsoft появился определенный интерес к рынку консольных приставок. Этот интерес материализовался кон(
21
Рис. 2.1. Схема взаимодействия DirectX и аппаратной части устройства
солью Xbox, а затем появилась и вторая версия приставки с несколько видоизмененным названием Xbox 360. После этого корпорация Microsoft имела на руках три разные платформы – компьютерную Windows, мобильную Win( dows Mobile и консольную приставку. Для всех трех плат( форм игровая спецификация была абсолютно разной. Разработчики, создавая игру для PC, могли перенести ее на Xbox, но для этого приходилось тратить очень много времени. Порой проекты для обеих платформ делали две разные команды, а точнее одна команда создавала, а другая адаптировала. С этим нужно было что(то делать. Создание общей спецификации для всех платформ се( мейства Microsoft стало большой головной болью для про( граммистов, работающих в корпорации. Чтобы создать та( кую спецификацию, необходимо было создать общую инструментальную базу, один общий язык программирования, а также общую библиотеку классов. Но задача была решена. Сейчас в качестве инструментальной базы выступает инструментарий Vi( sual C# Express или Visual Studio. Общим языком программирования стал язык C#, который безусловно проще и лучше своих прародителей С/C++/Java. Общей библиотекой классов стала платформа XNA Framework, которая включает огром( ный набор системных инструментов для решения большинства серьезных задач. В итоге мы имеем одну спецификацию, меньше головных болей и безболезненное портирование игр с одной платформы на другую. Правда, перед XNA Framework у Microsoft был еще один промежуточный этап понимания того, как должна быть устроена подобная библиотека, и этот этап был ознаменован выходом Managed DirectX.
2.3. Managed DirectX Основная проблема переноса DirectX на все семейство продуктов Microsoft связа( на с тем, что библиотека DirectX построена на независимой спецификации моде( ли составных компонентов (COM – Component Object Model), которая, в свою очередь, очень сильно привязана к Windows и языку программирования С++. По( этому перенос DirectX на все продукты был просто невозможен. Необходимо было выдумать и реализовать нечто новое, простое, да так чтобы еще и работало на всех платформах сразу и одинаково. Вот так и родилась библиотека Managed DirectX для языка С# и .NET(плат( формы, которая по своей сути представляла системную надстройку классов над стандартным DirectX. В конечном счете на свет появились всего две версии Mana(
22
Платформа XNA Framework
Платформа XNA
ged DirectX. В данный момент эта технология пребывает в летаргическом сне и, по всей видимости, там и будет пребывать еще долгое время, поскольку от разви( тия этой платформы на неопределенное время пришлось отказаться. В основном проблема Managed DirectX и всего проекта в частности заключа( лась в том, что Managed DirectX – это всего лишь надстройка над DirectX. В мо( мент создания и развития Managed DirectX никто не принимал во внимание кон( соль Xbox 360, а также возможность портирования полноценных трехмерных игр на мобильные устройства под управлением Windows Mobile. Когда встал вопрос о том, чтобы абстрагироваться от привязанности к DirectX и Windows, выясни( лось, что с Managed DirectX сделать это будет очень(очень трудно. Вот тогда и начались разработки новой платформы, которая впоследствии получила назва( ние XNA Framework.
23
даже больше создадите за пару строк исходного кода, при этом эти строки сфор( мирует за вас сам компилятор и XNA Framework! Работать стало проще, быстрее, и главное – нет борьбы и драк с DirectX… Роль DirectX в этом деле – это низко( уровневая прослойка API, к которой вы не касаетесь никоим образом. Все мани( пуляции производятся только через библиотеку XNA Framework.
2.4.1. Уровни абстракции XNA Так же как и в случае с DirectX, мы можем графически представить взаимодей( ствие приложений через XNA Framework с аппаратной частью устройства. По( смотрите на рис. 2.2, где представлены уровни абстракции платформы XNA Framework.
На момент выхода книги работать с мобильными устройствами на базе Windows Mobile 5.0 можно в основном с языком программирования С++. В Visual Studio 2005 есть встроенные инструменты для работы с С# и .NET Compact Framework 1.0. В новой операционной системе Windows Mobile 6.0 используется уже вторая версия платформы .NET Compact Framework 2.0, которая значительно мощнее и лучше.
2.4. Платформа XNA Framework Платформа XNA Framework – это большой набор системных библиотек, построен( ных на базе .NET(библиотек и направленных на улучшение и упрощение созда( ния игр для всех продуктов семейства Microsoft. Создавая игру для PC на базе платформы XNA Framework, вы можете быть уверены, что эта игра будет работать на приставке Xbox 360 и в скором времени на новой системе Windows Mobile. Ис( ключения в этом случае могут составлять некоторые системные классы, которые необходимы только для работы с той или иной платформой. Например, приставка Xbox 360 не имеет мыши, поэтому в программах для консоли нельзя использовать методы и классы, направленные на работу с мышью. В свою очередь, для компьютерных игр можно задавать различные разрешения экрана, тогда как Xbox 360 использует в качестве монитора телевизор, который имеет свою специфику вывода изображения на экран. Эти и другие исключения составляют мышиную долю (примерно 3–5%) от всей библиотеки XNA Frame( work. При правильном подходе в использовании библиотечных классов можно написать программу, которая на 100% будет работать как на консоли, так и на компьютере и, надеемся, на Windows Mobile тоже. Еще одной важной особенностью платформы XNA Framework является зна( чительное упрощение в подходе реализации игр для всех платформ. Прежде чем начать писать именно исходный код игр с использованием DirectX SDK, необхо( димо создать большую кучу различных объектов, создать окно, настроить видео( адаптер и т. д. Применяя платформу XNA Framework, вы все перечисленное и
Рис. 2.2. Взаимодействия XNA и аппаратной части устройства
Как видно из рис. 2.2, схема работы XNA Framework напоминает схему работы с DirectX, но в основе библиотеки XNA лежат другие компоненты и механизмы взаимодействия с аппаратной частью устройства. На диаграмме также видно, что вся платформа XNA Framework состоит из трех уровней абстракции, через кото( рые программа обращается к системе.
24
Платформа XNA Framework
Платформа XNA Игра – это самый высокий уровень абстракции, включающий в себя исход( ный код игры, различные игровые данные и компоненты. Это все то, что вы пишите и создаете лично сами. Расширенный каркас (Extended Framework) – расширенный каркас сис( темных высокоуровневых классов содержит простые механизмы для рабо( ты с загрузкой моделей, текстур и других графических элементов. В состав этого каркаса входят Application Model и Content Pipeline. Со временем эта часть библиотеки может пополняться новыми компонентами, что по( зволит еще больше упростить создание игр. Методы, классы, структуры этого уровня абстракции можно смело использовать в своих программах. Основной каркас (Core Framework) – основной каркас библиотеки за( ключает в себе ядро платформы XNA Framework и обеспечивает базовые механизмы работы всей библиотеки в целом. Этот уровень абстракции со( держит несколько основных классов, таких как Graphics, Math, Input, Audio и Storage. Все классы упрощают работу соответственно с графикой, звуком, устройствами ввода информации, математическими операциями и работой с данными для записи или чтения их с файловой системы. Классы этого уровня вам также будут доступны в полном объеме. Со временем эта часть библиотеки может пополняться новыми компонентами. Платформа (Platform) – это самый нижний и независимый уровень абст( ракции или набор классов, осуществляющий обращение непосредственно к аппаратной части устройства. Доступ к компонентам этого уровня у про( граммиста есть, но если их использовать, то это значительно сузит пор( тирование созданного приложения на другие платформы. Более того, не рекомендуется использовать прямое обращение исходного кода игры к прослойке этих компонентов, и по большому счету ваша игра даже не должна подозревать об этом уровне абстракции. В состав этой прослойки входят DirectX, XACT, XInput и XContent.
Подводя промежуточный итог обзора платформы XNA Framework, можно констатировать, что механизмы работы как DirectX, так XNA Framework с аппа( ратной частью устройства почти одинаковы, но внутренние особенности обеих библиотек значительно отличаются друг от друга, и сейчас мы поговорим об этом более подробно.
2.4.2. Application Model В расширенном каркасе высокоуровневых классов и, в частности, в Application Model (Модель приложения) реализована общая структура работы модели при( ложения. Этот набор компонентов позволяет автоматизировать процесс создания игры и создает за вас механизм реализации оконного или полноэкранного прило( жения, организует необходимый в игре таймер, обрабатывает различные сообще( ния, управляет процессом рисования сцены на экране монитора (рендиринг), за( грузкой ресурсов игры и т. д.
25
Все перечисленные элементы преподносятся вам в виде готового шаблона игры с небольшим количеством строк исходного кода, в которые вам необходимо вставлять свой код игры. То есть за вас создается полнофункциональный каркас игрового приложения, готовый к «употреблению». Например, давайте посмотрим на исходный код каркаса игры, созданный шаблоном XNA Game Studio Express и Visual C# Express, представленный в листинге 2.1. // ============================================================================= /// <summary> /// Ëèñòèíã 2.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 2 /// Êëàññ: Game1 /// Ïðîñòîé øàáëîííûé ïðîåêò /// <summary> // ============================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace WindowsGame1 { /// <summary> /// Ýòî øàáëîí âàøåé èãðû /// public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); } /// /// /// /// ///
<summary> Ýòîò ìåòîä ïðåäíàçíà÷åí äëÿ èíèöèàëèçàöèè ðàçëè÷íûõ èãðîâûõ äàííûõ Çäåñü ìîæíî çàäàâàòü ðàçëè÷íûå ïåðâè÷íûå ñîñòîÿíèÿ èãðîâûì êîìïîíåíòàì Íî çäåñü íåëüçÿ çàãðóæàòü ãðàôèêó, ýòîò ìåòîä ïðåäíàçíà÷åí òîëüêî äëÿ èíèöèàëèçàöèè ïåðâè÷íûõ èãðîâûõ ñîñòîÿíèé
26
Платформа XNA Framework
Платформа XNA /// protected override void Initialize() { // Äîáàâüòå çäåñü êîä èíèöèàëèçàöèè äàííûõ base.Initialize(); } /// <summary> /// Ýòîò ìåòîä ïðåäíàçíà÷åí äëÿ çàãðóçêè ãðàôè÷åñêîé ñîñòàâëÿþùåé â èãðó /// protected override void LoadGraphicsContent(bool loadAllContent) { if(loadAllContent) { // Çäåñü ïðîèñõîäèò çàãðóçêà êîìïîíåíòîâ â èãðó } } /// <summary> /// Ìåòîä äëÿ îñâîáîæäåíèÿ çàõâà÷åííûõ ñèñòåìîé ðåñóðñîâ /// Ýòîò ìåòîä àâòîìàòè÷åñêè âûãðóæàåò çàãðóæåííûå êîìïîíåíòû /// ïðè âûõîäå èëè çàêðûòèè èãðû /// protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Çäåñü ïðîèñõîäèò îáíîâëåíèå ñîñòîÿíèÿ èãðîâûõ îáúåêòîâ, ëîãèêè, çâóêà, /// ïîëó÷åíèå ñîáûòèé ñ óñòðîéñòâ ââîäà è òàê äàëåå /// Ìåòîä ðåàëèçóåò ïðîñòîé òàéìåð /// protected override void Update(GameTime gameTime) { // Îáðàáîòêà ñîáûòèé, ïîëó÷àåìûõ ñ äæîéñòèêà if(GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); // Äîáàâüòå ñâîé êîä äëÿ îáíîâëåíèÿ ñîñòîÿíèÿ èãðîâûõ îáúåêòîâ // Ýòî òàéìåð base.Update(gameTime); } /// <summary> /// Ðåíäèðèíã ñöåíû èëè âûâîä íà ýêðàí ãðàôèêè /// protected override void Draw(GameTime gameTime) {
27
graphics.GraphicsDevice.Clear(Color.CornflowerBlue); // Äîáàâüòå çäåñü ñâîé êîä base.Draw(gameTime); } } }
Класс Game1 описан в файле с расширением *.cs (Game1.cs). Расширение в данном случае указывает на то, что используется язык программирования С#. Весь исходный код создан шаблоном, а перевод комментариев на русский язык осуществлен в вольной форме. Класс Game1.cs создал за нас полное игровое оконное приложение с метода( ми для загрузки графической составляющей игры, инициализации данных, систе( му вывода сцены на экран, систему обновления состояния игры (таймер) и даже обработку событий, получаемых с джойстика. Неплохо, не правда ли? Более под( робно с классом Game1 мы начнем знакомиться в следующих главах, когда при( ступим к работе над своей игрой.
2.4.3. Компонент Content Pipeline Одной из важнейших составляющих любой программы является графический и игровой контент, который включает в себя различные графические изображения (текстуры, спрайты…), звуковые файлы, модели и материалы. Если вспомнить методику загрузки графики в DirectX, то станет понятно, почему в XNA уделено этому большое внимание. В DirectX, чтобы загрузить модель, приходилось писать десяток(другой строк исходного кода! Более того, загружать можно было только определенные графи( ческие файлы, которые DirectX понимал. Для остальных моделей и изображений приходилось писать свои экспортеры или искать инструменты для конвертирова( ния контента в необходимый формат данных. Используя XNA Framework и Content Pipeline (Конвейер контента), который входит в пространство имен Microsoft.Xna.Framework.Content, вам не нужно беспокоиться о том, в каком формате представлено изображение или мо( дель. Вы просто пишете пару строк исходного кода, и ваш графический контент будет успешно загружен в игру внутренними средствами конвейера. В главе 6 мы подробно остановимся на этом вопросе. Конвейер контента понимает следующие форматы. Трехмерные модели – формат X и формат FBX. Формат Х – это всем изве( стный Х(файл, или мэш. Формат FBX – относительно новый формат в иг( ровой индустрии, продвигаемый компанией Autodesk, и поскольку эта же компания является создателем всем известного инструментария 3DMAX Studio, то сомнений в дальнейшем распространении этого формата нет. На сайте компании Autodesk можно бесплатно скачать плагин и конвертер для
28
Платформа XNA Framework
Платформа XNA
29
преобразования сделанной модели в 3DMAX Studio до версии 8 в формат FBX. В 3DMAX Studio 9 все перечисленные средства встроены по умол( чанию. Графические изображения – поддержаны наиболее распространенные графические форматы DDS, BMP, JPG, PNG и TGA. Материал – имеется поддержка формата FX, или формата шейдеров. Аудиоформат – осуществлена поддержка работы с проектами, созданны( ми при помощи XACT. Подробно о самом формате и загрузке в игру звуко( вых данных вы узнаете в главе 13. Как видите, список поддерживаемых форматов достаточно большой и внуши( тельный, но главное – это простота загрузки, например текстуры или модели в игру. Посмотрите на пример двух строк исходного кода, загружающих в про( грамму графическое изображение и модель. private Texture2D mySprite; private Model myBoll; … mySprite = content.Load
(“sprite”) myBoll = content.Load<Model>(“boll”);
В последних двух строках этого блока кода мы загрузили в программу из корне( вого каталога проекта графическое изображение и модель (для дополнительной или вложенной папки используется следующая запись «Èìÿ_ïàïêè\\boll»). Вот и все, теперь вы можете использовать загруженные элементы для вывода на экран монитора (о том, как это сделать, в следующих главах). Если представить себе абстрактно внутренний принцип работы Content Pipeline и то, как происходит загрузка контента в игру и его преобразование, то можно перейти к следующей схеме (рис. 2.3). Как видно из этого рисунка, весь игровой контент проходит через конвейер импорта и преобразования данных, после чего на выходе в проекте получаются файлы со специфическим расширени( ем XNB и XACT для звуковых данных. На этапе запуска игры в игровом процессе принимают участие именно преобразованные файлы с расширением XNB и XACT. Все преобразования контента происходят внутренне, без вашего участия и на эта( пе компиляции проекта в Visual C#.
2.4.4. Компонент Graphics Графическая часть библиотеки Microsoft.Xna.Framework.Graphics осно( вана на принципе работы библиотеки DirectX. Подход в реализации программ при помощи графического конвейера XNA Framework полностью исключает ра( боту с фиксированным конвейером, присущим всем программам с использовани( ем DirectX. Как вы помните, DirectX до последней девятой и десятой версии имел два конвейера: фиксированный и программируемый. Второй конвейер характе( рен тем, что все работы с вершинами объектов, пикселями текстур, материалом,
Рис. 2.3. Работа Content Pipeline в XNA Framework
светом, матрицами происходил посредством шейдеров. Тогда как в фиксирован( ном конвейере можно было и не использовать шейдеры. В XNA Framework используются только программируемый конвейер и шей( деры. Все операции со светом, материалом, матрицами, текстурами и т. д. необхо( димо производить только через шейдеры. В связи с этим в XNA Framework пре( дусмотрены два разных класса – BasicEffect и Effect. Первый класс BasicEffect позволяет не писать и не использовать код шей( дера или, в крайнем случае, не писать супермощные шейдерные программы. Тог( да все вышеперечисленные операции будут выполняться в простом штатном ре( жиме, а система сама будет обрабатывать данные с внутренними значениями по умолчанию. Такой подход позволяет создавать игры не только профессиональ( ным, но и начинающим программистам. Второй класс Effect представляет полный спектр возможностей по работе с шейдерами всех трех версий. По умолчанию применяется вторая и выше версия шейдеров, при желании можно использовать и первую версию шейдеров, но толь( ко для компьютерных систем. Приставка Xbox 360 работает лишь со второй и выше версиями шейдеров.
2.4.5. Компонент Math Как видно из названия компонента, это библиотека для работы с математически( ми операциями. Компонент Math входит в пространство имен Microsoft.Xna. Framework и содержит большое количество классов, структур и методов для ра( боты с векторными данными, матрицами, операциями по трансформации, перено( су, преобразованию и т. д. В качестве основной системы координат в XNA
30
Платформа XNA
Framework применяется правосторонняя система координат, но об этом мы пого( ворим подробно в третьей части книги.
Глава 3
2.4.6. Компонент Input Компонент Input принадлежит к пространству имен Microsoft.Xna. Framework.Input и является, в свою очередь, надстройкой над XInput. Биб( лиотека Input содержит набор инструментов для работы с устройствами ввода информации. К устройствам ввода информации традиционно причисляются кла( виатура, мышь и джойстик. Клавиатура, мышь и джойстик повсеместно использу( ются в компьютерных системах, и в играх в том числе. В консольной приставке Xbox 360 можно работать только с джойстиком и клавиатурой, причем джой( стик – это основное устройство ввода данных. Клавиатура может использоваться, но вряд ли она будет применяться пользователями повсеместно. Поэтому при со( здании игр для Xbox 360 в качестве основного устройства лучше использовать джойстик.
2.4.7. Компонент Audio Работа со звуком в XNA Framework построена на базе Microsoft CrossPlatform Audio Creation Tool, или просто XACT. С помощью этой утилиты или даже от( дельной программы создаются специализированные звуковые проекты, которые затем при помощи компонента Audio загружают все звуковые данные в игру. Та( кой подход сделан намеренно, дабы иметь возможность работать со звуковыми дан( ными вне зависимости от платформы, будь то компьютер или Xbox 360. Компонент Audio принадлежит к пространству имен Microsoft.Xna.Framework.Audio.
2.4.8. Компонент Storage Компонент Storage, относящийся к пространству имен библиотеки Microsoft. Xna.Framework.Storage, организует работу с файловой системой устройства. Здесь доступен набор стандартных операций по записи и чтению данных, удале( нию, перезаписи и т. д. Это стандартный набор для любого устройства с файловой системой организации хранения данных, который может потребоваться в играх, например для сохранения игры или ее последующей загрузки. На этом обзор общей составляющей XNA Framework завершен. Я думаю, что смог убедить вас в том, что создавать игры стало действительно легче. В двух сле( дующих главах мы рассмотрим инструментальные средства, которые нам понадо( бятся для написания исходного кода игр, а затем перейдем к созданию игр.
Инструментарий Visual C# Express Edition Необходимыми атрибутами в создании любых игр и программ являются инстру( ментальные средства разработки приложений. Это так называемые визуальные инструментарии, обладающие встроенными средствами для работы с исходным кодом, компиляцией и отладкой, сборкой и упаковкой проектов в готовый к рас( пространению дистрибутив. На данный момент в качестве промышленного стан( дарта для работы с DirectX и XNA мы имеем инструментальные средства от кор( порации Microsoft. В активе этой корпорации имеется большой инструментарий Visual Studio 2005, а также ряд отдельных инструментариев для работы с различ( ными языками программирования. В этой обзорной главе вы познакомитесь с ос( новными методами работы в Visual C# Express, а также узнаете, как правильно инсталлировать этот инструментарий на свой компьютер, если у вас уже установ( лен Visual Studio 2005.
3.1. Инструментарий Visual Studio 2005 Visual Studio 2005 – это большой инструментальный пакет, включающий в себя мощный и емкий комплекс программных средств, необходимых для создания все( возможных приложений под Windows, Windows Mobile, Xbox 360, дополнительно можно работать и под Unix, Symbian OS, и т. д. Инструментарий Visual Studio 2005 (рис. 3.1) включает в себя ряд отдельных инструментариев для работы с несколькими языками программирования. В со( став пакета Visual Studio 2005 входят следующие средства:
Visual C++ – необходим для работы с языком программирования С++; Visual C# – программирование на языке С#; Visual J# – работа с Java(подобным языком программирования; Visual Basic – использование старого доброго языка Basic.
Инструментарий Visual Studio 2005 не бесплатен и стоит достаточно дорого. Откровенно говоря, не все могут позволить себе приобрести этот отличный про( граммный продукт, поэтому корпорация Microsoft трезво рассудила и предложи( ла ряд бесплатных инструментариев. Одним из таких средств и является Visual C# Express.
32
Инструментарий Visual C# Express Edition
Инструментарий Visual C# Express
33
Инструментарий Visual C# Express абсолютно полнофункционален, без каких( либо урезок и недочетов. Дополнительно к Visual C# Express также бесплатно предлагается справочная система MSDN 2005 Express Edition. Еще нужно доба( вить одно важное обстоятельство. Инструментарий Visual C# Express бесплатен, но требует обязательной регистрации на сайте Microsoft. Сам процесс не сложен, и в разделе 3.4 этой главы мы поговорим о нем более подробно. Для работы с этой книгой вам обязательно понадобятся Visual C# Express, бесплатная версия XNA Game Studio Express (о которой речь пойдет в следующей главе) и последняя версия Runtime DirectX, которую можно скачать с сайта Microsoft. Потребности непосредственно в DirectX SDK нет. Если вы являетесь счастливым обладателем Visual Studio 2005, то пользоваться XNA Game Studio Express не получится, но в следующем разделе объясняется, как все же можно на( строить Visual Studio 2005 на работу с XNA Game Studio Express. Все подробности об этом и многом другом вас ждут в следующей главе. Первоначально на компьютер устанавливается инструментарий Visual C# Express и Runtime DirectX (последовательность не имеет значения, но Run) time DirectX лучше поставить вперед), и только после этого происходит инсталляция XNA Game Studio Express.
Рис. 3.1. Рабочее окно Visual Studio 2005
3.2. Инструментарий Visual C# Express Около миллиона человек со всего мира на данный момент скачали и установили на свои компьютерные системы бесплатный инструментарий Visual C# Express. Инструментарий доступен для бесплатного скачивания с сайта корпорации Microsoft по адресу в Интернете: http://msdn.microsoft.com/vstudio/express/ visualcsharp (размер дистрибутива около 440 Мб). Для читателей этой книги (для тех читателей, которые приобрели эту книгу в магазине, а не скачали из Интерне( та ее пиратскую копию, которая, я не сомневаюсь, в скором времени появится в сети) инструментарий Visual C# Express поставляется на диске, идущем в комп( лекте с книгой. Инструментарий Visual C# Express распространяется в формате ISO. Это образ диска, который вам просто надо записать на любой чистый компакт(диск или воспользоваться программами для чтения образов и создания виртуальных дисков. Самый простой способ – это обычная запись образа из папки \VCS на компакт(диск. После чего запуск данного компакт(диска инициализирует сис( тему установки Visual C# Express. Сам процесс установки инструментария стандартен.
Рис. 3.2. Рабочее окно Visual C# Express
34
Инструментарий Visual C# Express Edition
3.3. Установка Visual C# Express вместе с Visual Studio 2005 В последнее время начинающие программисты, особенно после появления XNA Game Studio Express, очень часто сталкиваются с проблемой установки Visual C# Express поверх ранее установленной на их компьютер Visual Studio 2005. Сама установка Visual C# Express на Visual Studio 2005 – естественно, не проблема, а вот последующая установка XNA Game Studio Express выдает массу различных ошибок. Одна из них – это полная невозможность установки XNA Game Studio Express на компьютерную систему, или другой вариант – отсутствие интеграции документации и примеров, шаблонов проектов XNA Game Studio Express в инст( рументарий Visual C# Express. В этом разделе объясняется механизм правильной и, что самое главное, кор( ректной установки инструментария Visual C# Express поверх уже ранее установ( ленной среды программирования Visual Studio 2005. Более того, если вы имеете на своей машине установленную среду Visual Studio 2005 с компонентом Visual C#, то вам НЕТ НЕОБХОДИМОСТИ скачивать из Интернета полностью Visual C# Express весом в 440 Мб!!! Достаточно простого Dialup(соединения с сетью Интер( нет и совсем немного денежных средств на своем счету для загрузки Visual C# Express размером примерно в 30–50 Мб (в зависимости от установленных у вас на компьютерной системе компонентов). То есть при установленной среде програм( мирования Visual Studio 2005 с компонентом Visual C# достаточно скачать толь( ко часть инструментария Visual C# Express, поскольку все остальное вы уже имеете на своем компьютере. В чем же заключается механизм такой установки Visual C# Express поверх Visual Studio 2005?
Установка Visual C# Express вместе с Visual Studio 2005
ходимо лишь подгрузить некоторые дополнительные компоненты Visual C# Ex( press в Visual Studio 2005. Как правильно и корректно произвести установку Visual C# Express поверх Visual Studio 2005, вам объяснит следующая пошаговая инструкция.
Пошаговая инструкция 1. На компакт(диске в папке \VSС находится установочный пакет Visual C# Express Edition Setup под названием vcssetup.exe. Это последняя версия пакета на сегодняшний день. Дополнительно можно зайти на сайт корпо( рации Microsoft и проверить обновление этого установочного пакета. Для этого соединитесь с сетью Интернет и откройте по нижеприведенной ссыл( ке страницу сайта корпорации Microsoft. Откроется домашняя страница инструментария Visual C# Express, изображенная на рис. 3.3. Здесь вы об( наружите некоторую полезную информацию и, самое главное, ссылку (Download) на автоматический пакет установщика Visual C# Express Edi( tion Setup через сеть Интернет. По ссылке Download загрузите пакет на свой компьютер и разъединитесь с Интернетом. Ссылка на страницу загрузки установщика Visual C# Express: http://msdn.microsoft.com/vstudio/express/visualcsharp/download
3.3.1. Механизм установки Корпорация Microsoft предусмотрела возможность корректной установки Visual C# Express поверх Visual Studio 2005, правильно рассудив, что этот шаг со стороны многих программистов все равно неизбежен после появления такого прекрасного инструмента, как XNA Game Studio Express. Для этих целей был специально со( здан так называемый установочный пакет Visual C# Express Edition Setup весом в 2,8 Мб. Если у вас на компьютере установлен инструментарий Visual Studio 2005, то нельзя прямо поверх него инсталлировать Visual C# Express. Необходи) мо использовать установочный пакет Visual C# Express Edition Setup и связь с Интернетом, иначе у вас возникнут некоторые проблемы с функ) циональностью XNA Game Studio Express. Этот установочный пакет регулирует установку Visual C# Express на ваш компьютер прямо из сети Интернет и позволяет определить необходимые для за( грузки компоненты с сайта корпорации Microsoft. С помощью этого пакета необ(
35
Рис. 3.3. Страница загрузки инструментария Visual C# Express
36
Инструментарий Visual C# Express Edition 2. Далее запустите установочный пакет vcssetup.exe на своем компьютере. На экране монитора вы увидите первое небольшое по размеру информаци( онное окно Setup, представленное на рис. 3.4. На этом этапе установочный пакет определит все установленные у вас на машине компоненты (на это может уйти несколько минут времени) и затем запустит инсталлятор Visual C# Express, загрузка которого будет происходить через Интернет.
Установка Visual C# Express вместе с Visual Studio 2005
37
ном переводе это обозначает, что после установки Visual C# Express в кор( порацию Microsoft будет отослан отчет о вашем опыте этой установки. Здесь можете выбрать этот флажок, а можете и снять его, после чего на( жмите кнопку Next для продолжения этапа установки. 4. Следующее диалоговое окно Visual C# Express Edition Setup – End User License Agreement, изображенное на рис. 3.6, содержит текст лицензион( ного соглашения. Выберите в этом окне флаг I accept the terms of the License Agreement и нажмите кнопку Next. В противном случае установка будет не возможна.
Рис. 3.4. Информационное окно Setup
3. После оценки установленных компонентов на экране монитора появится диалоговое окно Visual C# Express Edition Setup – Welcome to Setup (рис. 3.5). Это окно приветственного характера, объясняющее вам на анг( лийском языке, что именно вы будете инсталлировать на ваш компьютер. При этом в нижней части диалогового окна расположен флаг Yes, send information about my setup experiences Microsoft Corporation. В дослов(
Рис. 3.6. Лицензионное соглашение
Рис. 3.5. Диалоговое окно Visual C# Express Edition Setup – Welcome to Setup
5. Последующее диалоговое окно Visual C# Express Edition Setup – Installa tion Options (рис. 3.7) предоставляет возможность дополнительной за( грузки документации Microsoft MSDN 2005 Express Edition (размер 248 Мб). Если вам необходима эта документация, то выберите флажок с этой надписью, если нет, то флажок необходимо снять и нажать кнопку Next. 6. Поле этого откроется новое окно Visual C# Express Edition Setup – Desti nation Folder, изображенное на рис. 3.8. Если вы устанавливаете инстру( ментарий Visual C# Express поверх Visual Studio 2005, то возможности сменить директорию установки в этом окне не будет. Если вы устанавли( ваете Visual C# Express с нуля, то поле Install in folder будет активным. Для начала процесса скачивания Visual C# Express нажмите кнопку Ins tall, но перед этим не забудьте подключиться к сети Интернет.
38
Инструментарий Visual C# Express Edition
Установка Visual C# Express вместе с Visual Studio 2005
39
7. Пакет Visual C# Express Edition Setup по установленному соединению с сетью Интернет подключится к сайту Microsoft и начнет закачивать к вам на компьютер недостающие компоненты инструментария Visual C# Express (рис. 3.9). Обрывы связи с сетью Интернет установочному пакету не страш( ны. Если произошел обрыв связи, то соединитесь с Интернетом снова, и ус( тановочный пакет продолжит закачку компонентов с того места, с которого его прервали. При этом диалоговое окно Visual C# Express Edition Setup – Download and Install Progress (рис. 3.9) закрывать нет необходимости. При разрыве связи с сетью докачка оставшихся компонентов происходит в авто( матическом режиме. Если по какой(то причине вы вдруг все же закрыли это окно, то запустите установочный пакет Visual C# Express Edition Setup вновь, выполнив все шаги до этого места (1–6), и установочный пакет про( должит закачивать оставшиеся компоненты Visual C# Express.
Рис. 3.7. Диалоговое окно Visual C# Express Edition Setup – Installation Options
Рис. 3.9. Диалоговое окно Visual C# Express Edition Setup – Download and Install Progress
Рис. 3.8. Диалоговое окно Visual C# Express Edition Setup – Destination Folder
8. По окончании процесса скачивания инструментария Visual C# Express по( явится новое окно, изображенное на рис. 3.10. На этом этапе уже можно отключиться от сети Интернет, а процесс скачивания Visual C# Express плавно перейдет к фазе его инсталляции на компьютерную систему. 9. По окончании установки Visual C# Express появится последнее окно Visual C# Express Edition Setup – Setup Complete (рис. 3.11). Это окно оповестит об удачной установке инструментария и предупредит вас о том, что в вашем ак(
40
Инструментарий Visual C# Express Edition
Регистрация Visual C# Express
41
тиве имеется всего 30 дней пользования Visual C# Express без регистрации. О процессе регистрации мы поговорим в следующем разделе. Для окончания установки нажмите кнопку Exit либо выберите ссылку Register now.
3.4. Регистрация Visual C# Express Регистрация инструментария Visual C# Express бесплатна. На работу с инстру( ментарием без регистрации вам отводится всего 30 дней. За это время необходимо обязательно зарегистрировать Visual C# Express через сеть Интернет. Процесс регистрации не сложен, и в нижеприведенной инструкции перечислен пошаговый этап прохождения всей процедуры. Постарайтесь зарегистрировать версию Visual C# Express до установки XNA Game Studio Express. При регистрации инструментария через сеть Ин) тернет от вас потребуется наличие учетной записи в Hotmail.com, Microsoft Passport или MSN либо Windows Live ID. Если ничего из перечисленного нет, то проще всего создать почтовый ящик на сервере Hotmail.com.
Рис. 3.10. Установка Visual C# Express на компьютер
Рис. 3.11. Диалоговое окно Visual C# Express Edition Setup – Setup Complete
1. Итак, откройте на компьютере инструментарий Visual C# Express и выбе( рите команды Help ⇒ Registry Product. Откроется диалоговое окно, в ко( тором необходимо избрать ссылку Register now. Система запросит под( ключение к сети, откроет Интернет(браузер, установленный у вас по умолчанию, и загрузит на компьютер страницу регистрации инструмента(
Рис. 3.12. Интернет&страница для входа в систему
42
Инструментарий Visual C# Express Edition рия (рис. 3.12). На этой странице в поле Email address введите адрес элект( ронной почты на Hotmail.com. В поле Password необходимо ввести пароль к почтовому ящику, который был указан при регистрации учетной записи. Затем нажмите кнопку Sign in. 2. Если имя пользователя и пароль были указаны правильно, то вы войдете в систему и переместитесь на следующую страницу (рис. 3.13). Эта страни( ца представляет собой обычный шаблон с набором параметров для сбора информации о пользователе. Аккуратно заполните на английском языке все поля, в особенности те, которые обозначены красными звездочками. В поле My Email Address необходимо указать рабочий адрес почтового ящика, именно на этот адрес после регистрации придет письмо со ссылкой для продолжения регистрации. Адрес электронной почты можно указы( вать любой, привязанности к Hotmail.com нет. После того как вы заполни( те все поля, нажмите внизу страницы кнопку Continue. 3. Следующая страница носит информационный характер (рис. 3.14) и объ( яснит вам последующие шаги для удачной регистрации Visual C# Express. В частности, вам будет сказано, что на адрес электронной почты, который был указан в поле My Email Address, выслано письмо со ссылкой для ре( гистрации. Следовательно, вам необходимо получить это письмо и перейти по имеющейся в тексте письма ссылке. Эту Интернет(страницу сейчас мож( но закрыть, а нажатие кнопки Continue приведет вас на домашнюю страни( цу инструментария Visual C# Express, что сейчас совсем не обязательно.
Основы работы с Visual C# Express
43
Рис. 3.14. Страница, объясняющая следующие этапы регистрации Visual C# Express
4. Получив письмо и перейдя по имеющейся ссылке, вы попадете на страницу входа в систему (рис. 3.15). Здесь опять необходимо ввести свой адрес электронной почты с Hotmail.com и пароль, а затем нажать кнопку Sign in. 5. После входа в систему вы попадете на Интернет(страницу, оповещающую вас об успешной регистрации Visual C# Express (рис. 3.16). В тексте стра( ницы в строке Your registration key is: будет указан регистрационный ключ, с помощью которого вы сможете зарегистрировать Visual C# Express. Скопируйте этот код в буфер обмена, откройте Visual C# Express, выпол( ните команды Help ⇒ Registry Product. В открывшемся диалоговом окне в поле Registration key вставьте код и нажмите кнопку Complete regist ration. Все, на этом этап регистрации Visual C# Express окончен, и вы мо( жете спокойно работать с этим мощным и бесплатным инструментарием. Кстати, нажатие кнопки Continue на этой последней странице приведет вас на домашнюю страницу инструментария Visual C# Express.
3.5. Основы работы с Visual C# Express
Рис. 3.13. Сбор информации о пользователе
Эта книга предполагает, что вы, по крайней мере, знакомы с языком программи( рования С# или С++ и умеете работать с инструментарием Visual Studio, но на всякий случай для тех, кто только(только вступает в наши ряды или мигрирует с других платформ, предлагается этот раздел. В этом небольшом разделе мы рас(
44
Инструментарий Visual C# Express Edition
Основы работы с Visual C# Express
45
смотрим основные способы работы с инструментарием Visual C# Express. В част( ности, создадим простой проект, откомпилируем код, а затем соберем и запустим программу на компьютере.
3.5.1. Создаем простой проект Открываем Visual C# Express и в меню инструментария выполняем команды File ⇒ New Project (горячие клавиши Ctrl+Shift+N). Дополнительно эти самые действия также доступны со стартовой страницы Visual C# Express (Start Page) по выбору ссылки Create Project. В ответ на это откроется диалоговое окно New Project (рис. 3.17).
Рис. 3.15. Вход в систему
Рис. 3.17. Диалоговое окно New Project
Рис. 3.16. Получение регистрационного ключа
В этом диалоговом окне в текстовой области Visual Studio installed templates перечислены все шаблоны проектов, которые можно использовать при создании своих программ. Дополнительно вы можете создавать свои шаблоны или приме( нять шаблоны, созданные другими людьми. Кстати, после установки XNA Game Studio Express в эту область добавится ряд отличных шаблонов для создания сво( их игр и программ, но об этом в следующей главе. Для создания проекта из списка выбираем необходимый шаблон. В качестве примера был задействован самый первый шаблон Windows Application, который создает простое приложение на основе формы. Выбор шаблона происходит щелч( ком левой кнопки мыши на его названии и иконки в области Visual Studio installed templates. Затем в поле Name задается имя будущего проекта, и далее нажимается
46
Основы работы с Visual C# Express
Инструментарий Visual C# Express Edition
кнопка OK. Инструментарий Visual C# Express на основе полученных данных фор( мирует проект и открывает его в рабочем окне нового проекта (рис. 3.18).
47
3.5.2. Компиляция и запуск проекта Для компиляции проекта необходимо вы( полнить команды Build ⇒ Build Solution или воспользоваться горячей клавишей F6. Эта операция компилирует в первый раз весь проект, а впоследствии команды Build ⇒ Build Solution производят компиляцию только измененных файлов проекта. Если вы хотите откомпилировать весь проект в це( лом, то необходимо выполнить команды Build ⇒ Rebuild Solution. После удачной компиляции для запус( Рис. 3.20. Запуск проекта ка проекта необходимо выполнить коман( WindowsApplication1 ды Debug ⇒ Start Debug (F5) либо Debug ⇒ Start With Debugging (Ctrl+F5). В первом случае происходят запуск программы и одновременный запуск отладчика Visual C# Express, а во втором
Рис. 3.18. Рабочее окно проекта в Visual C# Express
После создания проекта его необходимо явно сохранить, для этого выполните команды File ⇒ Save All (Ctrl+Shift+S) или используйте панель инструментов Visual C# Express и кнопку Save All (с изображением нескольких дискет). Откро( ется диалоговое окно Save Project (рис. 3.19). В поле Location необходимо ука( зать директорию для сохранения проекта и нажать кнопку Save.
Рис. 3.19. Явное сохранение проекта в определенном каталоге
Рис. 3.21. Ошибки во время компиляции
48
Инструментарий Visual C# Express Edition
случае программа запускается без отладчика. Результат работы программы по( казан на рис. 3.20. Если на этапе компиляции возникают ошибки, то в нижней части рабочего окна Visual C# Express появляется дополнительная панель Error List со списком ошибок, как показано на рис. 3.21. В данном случае я просто инициализировал ошибку, удалив из имени одного метода букву в названии. Все ошибки в коде вы( деляются синим цветом, а в текстовой области панели Error List поясняется тип допущенной ошибки. Откровенно говоря, интеллектуальная система Visual C# Express в плане выявления ошибок, различных подсказок и подсветок реализова( на выше всяких похвал! Для примера в исходном коде любого проекта наведите курсор мыши на одно из ключевых слов или системную функцию, класс и посмот( рите на уровень инициализации подсказок (рис. 3.22).
3.5.3. Сборка проекта
Основы работы с Visual C# Express
того как вы подготовили программу и готовы ее распространять, необходимо вы( брать версию Release. В версии Release инструментарий Visual C# Express убе( рет различную отладочную информацию и минимизирует размер конечной про( граммы. Для задания типа конфигурации воспользуйтесь панелью инструментов (рис. 3.23) и выберите из списка версию Debug или Release. Эту операцию можно проделать также из меню Visual C# Express, выполнив команды Build ⇒ Confi guration Manager. На эти действия открывается диалоговое окно Configuration Manager, где в списке Active Solution configuration можно задать тип конфигура( ции для приложения (рис. 3.24). После компиляции и сборки проекта в его ката( логе будет сформирована рабочая программа. В зависимости от типа конфигура( ции в каталоге создаются две папки с названиями Debug и Release. Каждая последующая компиляция и сборка проекта подменяет предыдущую версию про( граммы.
В работе над проектами у вас есть возможность создавать два типа конфигурации программы – это отладочная версия (Debug) и окончательная версия (Release). По ходу работы над проектом лучше пользоваться отладочной версией, а после Рис. 3.23. Выбор типа конфигурации приложения
Рис. 3.24. Диалоговое окно Configuration Manager
Рис. 3.22. Подсказки в Visual C# Express
49
Студии разработки игр
Глава 4 Студия разработки игр XNA Game Studio Express Чтобы создать компьютерную игру для Windows в XNA Game Studio Express, вам понадобятся относительно небольшие познания языка программирования С#, умение работать с бесплатным инструментарием Visual C# Express, наличие этой книги, трудолюбие, фантазия, а также компакт(диск к этой книге, на котором на( ходится потрясающая подборка инструментов и материалов.
51
Корпорация Microsoft организовала клуб разработчиков игр XNA Creators Club, членство в котором позволит вам иметь доступ к большой базе данных ис( ходных кодов, различных бесплатных проектов и информации. Для создания игр под ПК членство в клубе не обязательно. При этом покупка приставки Xbox 360 – дело обязательное, поскольку регистрация в XNA Creators Club происходит не( посредственно через приставку и сервис Marketplace. На сайте http://creators.xna.com вы найдете множество различной информа( ции, примеров, статей, исходных кодов, пакетов Starter Kits и для приставки Xbox 360 и системы Windows (рис. 4.1). Дополнительно в книге, посвященной разработке игр для Xbox 360, имеются все пошаговые инструкции, связанные со вступлением в клуб XNA Creators Club, настройкой Xbox Live, переносом игр на консоль и многим другим.
4.1. Студии разработки игр Студия XNA Game Studio Express не имеет своего интерфейса, а представляет со( бой пакет или программный набор средств, позволяющих значительно упростить разработку компьютерных игр для Windows. Студия XNA Game Studio Express бесплатна и при инсталляции интегрируется в бесплатный инструментарий Visual C# Express. Всего имеются три разные версии студий: XNA Game Studio Express; XNA Game Studio Professional; XNA Studio. Все три версии идентичны в механизме работы и программной составляющей. Отличительной особенностью каждой последующей версии в порядке увеличе( ния (XNA Game Studio Express, XNA Game Studio Professional и XNA Studio) яв( ляется наличие дополнительных возможностей, предусмотренных для создания игр и попутных игровых сервисов.
4.1.1. XNA Game Studio Express Финальная версия XNA Game Studio Express появилась на свет 11 декабря 2006 года, но до этого времени было еще два бета(релиза. Переход от первой беты XNA Game Studio Express был значительно болезненнее для разработчиков, чем переход от второй беты к релизу. Бесплатная версия студии XNA Game Studio Express ориен( тирована в первую очередь на новичков в программировании игр, разработчиков( любителей, студентов и т. д. С помощью этой версии студии вы сможете создавать только некоммерческие проекты для приставки Xbox 360, но и любые виды ком( мерческих и бесплатных проектов для системы Windows.
Рис. 4.1. Ресурс http://creators.xna.com
В XNA Game Studio Express имеется возможность в подключении Starter Kit (Стартовый комплект разработчика). Один такой комплект представляет, как правило, одну готовую игру. На основе такого пакета, или шаблона, вы можете создавать свои игры. В комплект поставки XNA Game Studio Express входит
52
Студия разработки игр XNA Game Studio Express
Spacewar Starter Kit (для Xbox 360 и ПК), однако дополнительно можно исполь( зовать наработки других компаний или людей. Подписчикам клуба XNA Creators Club такая возможность предоставляется бесплатно, но более серьезные наработ( ки предлагаются за деньги, соответственно вам также никто не мешает продавать и свои наработки через сервис Xbox Live. Здесь мы фактически ведем речь об иг( ровых движках и возможных путях их сбыта.
4.1.2. XNA Game Studio Professional Студия XNA Game Studio Professional появится в ближайшее время. Эта версия студии является платной и направлена уже на создание коммерческих игр. Сту( дия XNA Game Studio Professional построена полностью на программном карка( се XNA Game Studio Express, то есть изучение первой версии ведет к автомати( ческому изучению второй версии студии. В чем разница между этими версиями студий? Во(первых, Microsoft выпустила XNA Game Studio Express раньше для того, чтобы обкатать весь программный комплекс в целом и выявить очевидные недо( четы и недоработки в студии. На базе этих полученных данных соответственно подкорректировать уже коммерческую версию XNA Game Studio Professional. Во( вторых, в XNA Game Studio Professional будет добавлен ряд дополнительных воз( можностей, например по работе с сервисом Xbox Live. Сама версия XNA Game Studio Professional ориентирована на профессиональ( ных разработчиков, и здесь в качестве инструментария уже может использоваться не только Visual C# Express, но и Visual Studio 2005. То есть версия XNA Game Studio Professional – это полностью коммерческий продукт, рассчитанный на не( большие игровые студии или коллектив единомышленников, желающих делать консольные и компьютерные игры на языке С#. Не стоит забывать о том, что че( рез год или даже меньше платформа XNA и С# станут стандартом и для другой платформы Windows Mobile. В итоге одну игру, созданную в XNA Game Studio, можно будет практически без затрат и усилий портировать на мобильное устрой( ство, Xbox 360 и ПК!
Установка XNA Game Studio Express
4.2. Установка XNA Game Studio Express Запустите установочный пакет XNA Game Studio Express, инициализировав тем самым инсталляцию студии на свой компьютер. Далее в пошаговом режиме мы проследим за процессом установки XNA Game Studio Express. Перед установкой студии у вас на компьютере должен быть установлен инструментарий Visual C# Express с полноценной регистрацией (см. гла) ву 3). Если у вас сейчас на машине установлена одна из предыдущих версий XNA Game Studio Express, удалите ее через команды Панель управления ⇒ Установка/удаление программ. После удаления студии запустите ин) струментарий Visual C# Express и закройте его, а затем перезагрузите компьютер. Только после перезагрузки компьютера устанавливайте позд) нюю версию XNA Game Studio Express. 1. Двойной клик левой кнопкой мыши на установочном файле xnagse_ setup.msi запустит процесс инсталляции XNA Game Studio Express на компьютере. Откроется окно приветствия Microsoft XNA Game Studio Express Setup – Welcome to the Microsoft XNA Game Studio Express Se tup (рис. 4.2). Для продолжения установки нажмите кнопку Next. 2. Следующее диалоговое окно Microsoft XNA Game Studio Express Setup – EndUser License Agreement содержит текст лицензионного соглашения, регулирующего использование этого продукта у себя на компьютере
4.1.3. XNA Studio Версия XNA Studio выйдет несколько позже, чем две предыдущие версии и будет предназначена для больших мощных и профессиональных игровых студий разра( батывающих игры для Xbox 360 и ПК. Эта версия студии будет построена на двух предыдущих версиях с большими возможностями в планировании и разработке игр. К сожалению, на момент написания книги информации по XNA Studio очень мало и она ужасно скудна, а по одним данным эта версия может и вовсе не по( явиться на свет, хотя и декларировалась Microsoft ранее. Пожалуй, это все что я могу вам сообщить на данный момент по версии XNA Studio, больше информации можно найти в Интернете по адресу http://msdn.com/xna.
53
Рис. 4.2. Окно Microsoft XNA Game Studio Express Setup – Welcome to the Microsoft XNA Game Studio Express Setup
54
Студия разработки игр XNA Game Studio Express
Знакомство с XNA Game Studio Express
55
(рис. 4.3). Чтобы продолжить установку, выберите в этом окне флажок I accept the terms of the License Agreement и нажмите кнопку Next.
Рис. 4.4. Диалоговое окно Microsoft XNA Game Studio Express Setup – Ready to install Microsoft XNA Game Studio Express Setup
Рис. 4.3. Диалоговое окно Microsoft XNA Game Studio Express Setup – End&User License Agreement
3. Откроется информационное окно Microsoft XNA Game Studio Express Setup – Ready to install Microsoft XNA Game Studio Express Setup (рис. 4.4). На этом этапе вам необходимо отключить у себя на компьютере любой брандмауэр, отличный от встроенного в Windows. Выполнение это( го условия обязательно, и оно необходимо для корректной работы при( ставки Xbox 360 и компьютера. Для запуска процесса инсталляции студии нажмите кнопку Install. Инсталлятор XNA Game Studio Express запустит установку студии на компьютер (рис. 4.5). 4. Последнее информационное окно Microsoft XNA Game Studio Express Se tup – Completed Microsoft XNA Game Studio Express Setup (рис. 4.6) опо( вестит вас об окончании установки студии. Нажмите кнопку Finish, чтобы закончить процесс инсталляции XNA Game Studio Express на машину.
4.3. Знакомство с XNA Game Studio Express Как мы уже выяснили, студия XNA Game Studio Express не имеет своего интер( фейса, или программной оболочки, и интегрируется непосредственно в инстру( ментарий Visual C# Express. Если вы откроете меню Пуск ⇒ Все программы, то обнаружите следующие установленные компоненты:
Рис. 4.5. Инсталляция студии на компьютер
XNA Game Studio Express – запускает работу студии, а точнее инструмен( тарий Visual C# Express; XNA Game Studio Express Readme – файл Readme;
56
Студия разработки игр XNA Game Studio Express
Знакомство с XNA Game Studio Express
57
Рис. 4.6. Информационное окно Microsoft XNA Game Studio Express Setup – Completed Microsoft XNA Game Studio Express Setup
XNA Game Studio Express Documentation – документация по XNA Game Studio Express; Tools – набор дополнительных утилит, включающих в себя следующие средства: – Microsoft CrossPlatform Audio Creation Tool (XACT) – кросс(плат( форменная утилита для работы со звуком в XNA; – XACT Auditioning Utility – утилита аудиотюнинга, работает из команд( ной строки; – XNA Framework Remote Performance Monitor for Xbox 360 – монито( ринг подключения приставки Xbox 360 к компьютеру.
Рис. 4.7. Стартовая страница инструментария Visual C# Express
4.3.1. Настройка получения новостей в XNA Game Studio Express Выбор в меню команды XNA Game Studio Express откроет Visual C# Express. Пос( ле открытия инструментария на его стартовой странице (если вы подключены к Интернету) отобразятся последние новости, как показано на рис. 4.7. Времен( ной интервал загрузки новостей в Visual C# Express можно регулировать через команды меню инструментария Tools ⇒ Options ⇒ Startup (рис. 4.8). В окне Options на вкладке Startup в списке At Startup можно выбрать вид стартовой страницы инструментария, а в поле Start Page news channel опреде( лить новостной канал загрузки новостей. Выбор флажка Download Content Every в этом окне включает режим загрузки новостей. В инкрементном регулято(
Рис. 4.8. Настройка загрузки новостей
ре minutes необходимо определить сам временной интервал в минутах. Если снять флажок Download Content Every, то загрузка новостей происходить не бу( дет. Дополнительно перемещаясь по вкладкам окна Options, вы можете настроить и другие различные параметры Visual C# Express.
58
Студия разработки игр XNA Game Studio Express
4.3.2. Шаблоны XNA Game Studio Express При формировании проекта в Visual C# Express (File ⇒ New Project) в окне New Project после установки XNA Game Studio Express добавятся несколько хороших и нужных шаблонов, которые будут нам помогать в создании всех примеров для этой книги (рис. 4.9). Новые шаблоны проектов значительно упрощают и автома( тизируют процесс создания игр для компьютерных систем. Выбрав один из шаб( лонов, вы фактически создаете готовый костяк игры, в который необходимо встроить свой исходный код. Конечно, саму игру за вас никто не напишет, но имею( щиеся средства и сервисы значительно облегчают создание игр. Например, за( грузку в проект моделей и, в частности, X(файлов. Как было раньше, кто помнит? А было так: строк двадцать исходного кода или свой сформированный ранее класс для загрузки мэшей. А сейчас одна строка кода – и ваш мэш загружен в игру!
Рис. 4.9. Окно New Project
Но вернемся к шаблонам. Итак, в XNA Game Studio Express появилось шесть следующих шаблонов: Windows Game – шаблон, создающий каркас классов, и игровой механизм, реализующий простой игровой процесс в приложении для Windows; Windows Game Library – шаблон для создания собственных игровых биб( лиотек;
Знакомство с XNA Game Studio Express
59
Xbox 360 – шаблон, создающий каркас классов и игровой механизм, реа( лизующий простой игровой процесс в приложении для Xbox 360; Xbox 360 Game Library – шаблон для создания собственных игровых биб( лиотек; Spaceware Windows – Starter Kit игры Spaceware для Windows; Spaceware Xbox 360 – Starter Kit игры Spaceware для Xbox 360. Выбрав один из шаблонов, вы сформируете структуру классов, на основе ко( торой легко будет создать свою собственную игру. Мы на протяжении всей книги будем работать с шаблоном Windows Game. Дополнительно вы можете создавать свои шаблоны (Starter Kit) или использовать сторонние разработки. Например, сейчас на сайте http://creators.xna.com доступен для свободного скачивания комп( лект Marblets Starter Kit, с помощью которого вы можете создать интересную двухмерную аркаду.
Формируем проект в Visual C# Express
Часть 2 Создаем двухмерную игру
61
5.1. Формируем проект в Visual C# Express Откройте на компьютере Visual C# Express. Для создания проекта выполните ко( манды File ⇒ New Project (Ctrl+Shift+N). Откроется диалоговое окно New Project, изображенное на рис. 5.1. В этом окне в списке Templates изберите курсо( ром мыши шаблон с названием Windows Game. Затем в поле Name укажите имя будущего проекта, у нас это FirstProject. В поле Location необходимо задать ката( лог для хранения создаваемого проекта, и вот тут вас могут поджидать некоторые неприятности.
Глава 5 Формируем каркас игровых классов С этой главы мы начинаем работать с исходным кодом игры, применяя инстру( ментальные средства, которые были рассмотрены в предыдущих главах. За время изучения этой главы мы создадим два проекта. Первый проект будет создан на базе шаблона Windows Game студии XNA Game Studio Express с использованием Application Model. Затем уже во втором проекте мы приступим к модификации исходного кода. В итоге каждая новая глава – это модификация проекта из преды( дущей главы. Предложенный подход в видоизменении или развитии проекта бу( дет сохранен до конца книги. Таким образом, вы шаг за шагом, как и в реальной разработке игр, создадите к концу книги две игры. В связи с этим рекомендуется изучать все главы книги исключительно в хронологическом порядке. Первый проект под названием FirstProject мы сформируем с использованием шаблона Windows Game, который добавился в Visual C# Express после установки на компьютер XNA Game Studio Express. Это простой шаблонный проект, форми( рующий каркас будущей игры. Шаблон хорош тем, что он автоматически создает необходимую структуру программы, включая все этапы – от инициализации ком( понентов игры до освобождения всех ресурсов, захваченных программой. Этот шаблон как раз и иллюстрирует в действии работу Application Model. Второй проект с названием FullScreen станет логическим продолжением пер( вого проекта. По умолчанию шаблон Windows Game создает программу, работаю( щую в оконном режиме. Этот режим для игр не всегда интересен, поэтому во вто( ром проекте мы перейдем из оконного режима в полноэкранный. После перехода в полноэкранный режим закрытие работающей программы с помощью курсора мыши станет недоступным, поэтому будет добавлен механизм выхода из прило( жения по нажатию одной из клавиш.
Рис. 5.1. Диалоговое окно New Project
Использование имен папок, написанных кириллицей, как, впрочем, и назва( ний проектов, не допустимо. В названиях необходимо применять только англий( ский алфавит. Возможны также проблемы, если вы храните ваши проекты на дру( гом жестком диске или в другом разделе одного диска. Все эти проблемы могут быть, а могут и не быть, в зависимости от того, какие средства будут задействова( ны в текущем проекте. Например, XNA Content Pipeline весьма капризен к папкам с русскими именами. В связи с этим лучше всего создать в корневом каталоге дис( ка «С» папку с любым английским названием и формировать в этой папке свои проекты. Тогда вы гарантированно будете избавлены от разного рода ошибок при компиляции проекта. Я для всех примеров книги создал папку Code, в которой формировались отдельные папки для каждой главы, например для этой главы это
62
Класс Program
Формируем каркас игровых классов
63
папка Chapter5. Каждый новый проект одной главы также имеет свою папку, совпадающую с названием этого проекта. В диалоговом окне New Project присутствует флаг Create directory for solution. Выбор этого флажка автоматически формирует вложенную папку в ка( талоге создаваемого проекта для хранения данных Solution Explorer. Этот флаг лучше не использовать и снять. Поле всех действий в окне New Project нажмите кнопку ОК. Инструментарий Visual C# Express на основе избранного вами шаб( лона создаст каркас проекта.
5.2. Структура проекта После создания проекта в рабочем каталоге диска «С» появится папка с названи( ем программы. В этой папке будут находиться все файлы проекта, а также папка под названием bin. В папке bin после компиляции и сборки проекта создаются еще две вложенные папки: для отладочной версии программы – папка Debug, а для окончательной версии – папка Release. После создания проекта FirstProject в рабочем пространстве Visual C# Express с правой стороны открывается панель Solu tion Explorer, где отображается древовидная структура текущего проекта, которая состо( ит из следующих компонентов (рис. 5.2). Папка Properties – эта папка со( держит файл AssemblyInfo.cs с описательными свойствами всего проекта. Вы можете открыть этот файл и редактировать его через тек( стовый редактор. А если щелкнуть Рис. 5.2. Панель Solution Explorer правой кнопкой мыши на файл AssemblyInfo.cs и в появившем( ся контекстном меню выбрать команду Properties, то в нижней части пане( ли Solution Explorer появится дополнительная панель Properties, через которую также возможно редактирование файла AssemblyInfo.cs. До( полнительно если щелкнуть правой кнопкой мыши уже на названии всего проекта в панели Solution Explorer и в появившемся меню выбрать коман( ду Properties, то в текстовом редакторе откроется добавочная вкладка с од( ноименным названием проекта (рис. 5.3). Здесь также можно задавать раз( личные дополнительные свойства для всего проекта. Папка References – в этой папке отображаются все задействованные в проекте ссылки на библиотечные классы платформы XNA. Выбор любого класса на панели Solution Explorer откроет в текстовом редакторе вкладку с представлением этого класса (рис. 5.4). В какой(то мере этот механизм можно использовать даже как справочную систему.
Рис. 5.3. Вкладка со свойствами проекта
Графический файл Game.ico – это иконка программы размером 32 на 32 пикселя, которая назначена вашему проекту по умолчанию студией XNA Game Studio Express. Вы можете отредактировать эту иконку конкретно для вашей игры. Для редакции иконки можно использовать любой графи( ческий редактор. Чтобы заменить в проекте иконку на новую, откройте до( бавочную вкладку (щелчок правой кнопкой мыши на названии проекта и выбор команды Properties) со свойствами проекта и выберите Application. Далее в поле Icon укажите путь к новой иконке (рис. 5.3). Класс Game1.cs – шаблонный класс Game1, сформированный Visual C# Ex( press, содержит костяк программы с набором необходимых методов и объектов. Класс Program.cs – это также шаблонный класс, который является вход( ной точкой для всех приложений. С рассмотрения этого файла мы сейчас приступим к изучению исходного кода созданной программы.
5.3. Класс Program Класс Program – это класс(шаблон, который Visual C# Express автоматически создает для любой программы вне зависимости от ее направленности. Этот класс служит входной точкой для начала работы всей программы. В любой программе
64
Класс Program
Формируем каркас игровых классов
65
using System; namespace FirstProject { static class Program { static void Main(string[] args) { using (Game1 game = new Game1()) { game.Run(); } } } }
Рис. 5.4. Обзор библиотек
на любом языке есть свой метод или функция, которая выступает в роли зажига( ния (если брать аналогию с машиной) и заводит программу. После чего в работу подключаются уже остальные винтики всего механизма. Класс Program и есть зажигание для всей программы, и очень редко, когда необходимо вносить изменения в исходный код класса Program, но это возмож( но, и подчас в больших проектах такой подход действительно оправдан. Теперь давайте посмотрим на листинг 5.1 класса Program проекта FirstProject и поста( раемся разобраться, что здесь к чему. //========================================================================= /// <summary> /// Ëèñòèíã 5.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 5 /// Ïðîåêò: FirstProject /// Êëàññ: Program /// Ñîçäàíèå ïåðâîãî ïðîåêòà /// <summary> //=========================================================================
Обратите внимание на комментарии. Использование в комментариях трой( ных слэшей и различных ключевых слов, таких как или <param name=“èìÿ ïàðàìåòðà”>, для языка программирования C# – нормальная практика. Этот подход позволяет сделать хорошо документированный исходный код программы. В начале исходного кода в файле Program.cs происходит подключение раз( личных системных классов посредством оператора using, а также для всего про( екта объявляется пространство имен оператором namespace FirstProject. Пространство имен организовывается для избежания возможных конфликтов при обращении к библиотекам с одинаковыми названиями, созданными различ( ными производителями. Пространство имен официально ограничивает использо( вание конкретного имени в пределах только этого программного кода или целого проекта, в частности. За объявлением пространства имен в исходном коде следуют объявление класса Program и вызов метода Main(). Среда исполнения CLR при запуске программы начинает поиск метода Main(). С запуска этого метода начинается работа всего приложения. В программе можно создать несколько методов Main(), но в этом случае все равно один из методов будет главным, а чтобы обо( значить, какой именно метод главный, придется использовать командную строку, дабы показать компилятору C# точку входа в программу. Технически подобную операцию сделать можно, но у нас такой необходимости нет. Внутри метода Main() происходит создание объекта game класса Game1, после чего в работу включается системный метод Run(). Этот метод и есть зажи( гание, который запускает бесконечный цикл, где и происходит выполнение всей программы. Пока программа работает, бесконечный цикл функционирует; как только пользователь закрывает программу, работа бесконечного цикла автомати( чески прекращается. После запуска метода Run() управление работой приложе( ния передается в класс Game1, где начинается выполнение исходного кода файла Game1.cs, к рассмотрению которого мы переходим.
66
Класс Game1
Формируем каркас игровых классов
5.4. Класс Game1
protected override void Initialize() { base.Initialize(); }
Каркас исходного кода класса Game1 формируется посредством шаблона инстру( ментария Visual C# Express и студии XNA Game Studio Express. Набор методов этого класса имеет стандартные функции, к которым вы вправе добавлять свои новые методы либо изменять имеющиеся. Рассмотрим исходный код файла Game1.cs из листинга 5.2. //========================================================================= /// <summary> /// Ëèñòèíã 5.2 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 5 /// Ïðîåêò: FirstProject /// Êëàññ: Game1 /// Ñîçäàåì ïåðâûé ïðîåêò /// <summary> //=========================================================================
/// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } }
#region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion
/// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { if(GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit();
namespace FirstProject { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); Window.Title = “Ïåðâûé ïðîåêò. Îêîííîå ïðèëîæåíèå”; } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary>
67
base.Update(gameTime); } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); } } }
В самом начале исходного кода класса Game1 происходит подключение сис( темных библиотек и определение пространства имен проекта. Название класса Game1 предлагается Visual C# Express по умолчанию, но если вам не нравится
68
Класс Game1
Формируем каркас игровых классов
это имя класса, естественно, вы его можете изменить. Для изменения имени клас( са необходимо щелкнуть в Solution Explorer правой кнопкой мыши на названии файла Game1.cs и в появившемся контекстном меню выбрать команду Rename. Соответственно вам придется изменить и название класса в исходном коде файла Program.cs, а также в других классах программы, где вы упоминаете этот класс. На самом деле, как вы понимаете, особой необходимости в изменении имени клас( са нет, поскольку для проекта все равно определяется свое пространство имен. Далее идет объявление самого класса Game1: public class Game1 : Microsoft.Xna.Framework.Game
Эта строка кода говорит о том, что класс Game1 является производным от системного класса Game библиотеки Microsoft.Xna.Framework. Иначе класс Game1 наследует все возможности своего базового класса Microsoft.Xna. Framework.Game, что дает ему право использовать всю силу и мощь системного класса, а также возможность модификации всех методов базового класса. Затем в исходном коде Game1.cs происходит глобальное объявление двух объектов: GraphicsDeviceManager graphics; ContentManager content;
Первая строка определяет объект graphics класса GraphicsDeviceManager. С помощью этого объекта в исходном коде происходят настройка и конфигурация графического устройства, или, говоря простым языком, видеоадаптера, установ( ленного в компьютере. Объект content необходим для загрузки различной гра( фической составляющей в проект при помощи XNA Content Pipeline. В этой главе объект content мы использовать не будем, поэтому поговорим о нем подробнее в следующей главе. Далее в исходном коде следует конструктор класса Game1(). public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); Window.Title = “Ïåðâûé ïðîåêò. Îêîííîå ïðèëîæåíèå”; }
Здесь происходит создание двух объявленных ранее объектов стандартным образом. Фактически объект graphics класса GraphicsDeviceManager за( менил всю ту череду вызовов различных функций в DirectX, необходимых для инициализации и настройки графического устройства. Единственное, что впо( следствии стоит добавить в конструктор, – так это несколько параметров ини( циализации графического устройства через объект graphics, например для пе( рехода в полноэкранный режим, но об этом позже в этой главе.
69
Следующая строка кода Window.Title = “Ïåðâûé ïðîåêò. Îêîííîå ïðèëîæåíèå”;
не входит в определение шаблона, создавшего наш проект. Эта строка была добав( лена в код специально, чтобы заменить заголовок окна с именем проекта FirstProject на более звучное и понятное название. За конструктором класса Game1() следует определение метода Initialize(). protected override void Initialize() { // çäåñü ñëåäóåò êîä èíèöèàëèçàöèè base.Initialize(); }
Этот метод предназначен для инициализации различных компонентов игры, которые вам понадобятся перед самым первым стартом игры. Из этого метода вы можете передавать управление и в другие методы, связанные с инициализацией различных начальных данных (позиции на экране, инициализация различных пе( ременных и т. д.). Единственные ограничения касаются того, что в этом методе нельзя загружать графические компоненты игры (текстуры, модели), для этих целей служит метод LoadGraphicsContent(). protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { // çäåñü çàãðóæàþòñÿ ìîäåëè è ãðàôè÷åñêèå ýëåìåíòû } }
Метод LoadGraphicsContent() необходим для загрузки графической со( ставляющей вашей игры, используя мощь XNA Content Pipeline, который позво( ляет загрузить в одну строку кода любую 3D(модель или графику определенного формата. Поддерживаемых форматов много, но об этом в следующей главе, когда мы с этим непосредственно столкнемся. Следующим в исходном коде идет метод UnloadGraphicsContent(), ко( торый в автоматическом режиме производит выгрузку загруженных 3D(моделей и графики на этапе выхода из игры. Это своего рода менеджер, работающий на автомате и освобождающий захваченные ресурсы системы по сигналу выхода из программы. В стандартном использовании загрузчика добавлять в метод UnloadGraphicsContent() дополнительный исходный код не нужно. Но если применяются различные дополнительные загрузчики, то выгрузку загруженных элементов в методе UnloadGraphicsContent() необходимо прописать явно. Затем в исходном коде класса Game1 идет метод Update() со следующим ис( ходным кодом:
70
Класс Game1
Формируем каркас игровых классов
protected override void Update(GameTime gameTime) { if(GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); // êîä èãðû base.Update(gameTime); }
Этот метод предназначен для обновления состояния игры. В методе Update() необходимо расположить все методы, отвечающие за логику игры, реализовать механизм получения событий с клавиатуры, мыши или джойстика, воспроизведе( ние звука. Определить, что именно будет происходить после столкновений, вы( стрелов, ударов и т. д. Этот метод вызывается с циклической периодичностью, а точнее через определенный промежуток времени (таймер). Промежуток време( ни реализуется в автоматическом режиме через объект gameTime. Первая и вторая строки кода в методе Update() получают события с джой( стика и реализуют закрытие программы. Когда мы создадим проект, работающий в полноэкранном режиме, то нам может понадобиться выход из программы по( средством клавиатуры (не у всех пользователей есть джойстик). Тогда и рассмот( рим возможность обработки событий, полученных с клавиатуры, а пока выход из программы осуществляется кнопкой мыши. Последний метод класса Game1 – это метод Draw().
71
ект, затем откомпилируйте и запустите программу. На экране монитора появится простое оконное приложение, как показано на рис. 5.5. По умолчанию в XNA Game Studio Express создается оконный режим работы программы, где ширина окна равна 800 пикселей, а высота – 600 пикселей. По желанию вы можете изме( нить размеры окна, а также перейти в полноэкранный режим, об этом мы погово( рим в разделе 5.6. Созданное окно по умолчанию не может быть развернуто на весь дисплей, но если добавить в конструктор класса Game1 следующий исход( ный код: Window.AllowUserResizing = true;
то в правом верхнем углу окна станет доступна кнопка с командой Развернуть.
protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); // çäåñü ðèñóåì ãðàôèêó base.Draw(gameTime); }
Данный метод необходим для отрисовки всей графики на экране монитора. Метод Draw(), как и метод Update(), работает в циклическом режиме, постоян( но обновляя состояние игры и перерисовывая игровую графику. Периодичность перерисовки графики зависит от объекта gameTime. Но еще раз повторяю, весь этот механизм работает самостоятельно и организовывает обновление состояния игры на автомате, стремясь достичь максимально возможной смены кадров в игре (FPS). Такой шаблонный подход в конструкции классов Program и Game1 дает вам возможность сосредоточить свои силы на написании игры, а не на инициализации и создании окна Windows, обработке циклов, объявлении кучи различных объек( тов для работы с DirectX и многом другом. Все это за вас уже сделали два класса – Program, Game1, инструментарий XNA Game Studio Express и системные классы платформы XNA Framework. Расслабьтесь и наслаждайтесь написанием исходно( го кода игры! Исходный код этого примера вы найдете на компакт(диске в папке Code\ Chapter5\FirstProject. Откройте инструментарий Visual C# Express и этот про(
Рис. 5.5. Работа программы FirstProject
Рабочее пространство окна в проекте FirstProject закрашено в синий цвет. Это достигается посредством вызова метода Clear(). Параметр этого метода CornflowerBlue как раз определяет синий цвет закраски экрана в программе,
72
Формируем каркас игровых классов
и, естественно, эту цветовую составляющую можно изменять по своему усмотре( нию. Чаще всего используют черный цвет, а закраска экрана цветом – на самом деле целый отработанный механизм. Этот механизм заключается в том, чтобы перед показом нового кадра (фрейма) стирать содержимое экрана одним из цветов и рисовать графику вновь. То есть если вы рисуете на экране какой(то объект и затем желаете его переместить по экрану, то необходимо сначала стереть этот объект со старого места и нарисовать уже в новом месте. Метод Clear() выступает в роли стирательной резинки, очищая экран мо( нитора. Если не стирать экран цветом, то за движущимся объектом будет оставать( ся большой шлейф, состоящий из предыдущих сформированных кадров.
5.5. Механизм работы программы Изучив исходный код обоих классов Program и Game1, необходимо разобраться с общим механизмом работы всей программы и тем, как все рассмотренные нами методы на самом деле функционируют. Итак, откомпилировав и собрав проект FirstProject, вы тем самым создали готовую программу, которая находится в папках Code\Chapter5\FirstProject\bin\x86\Release или Debug. После запуска программы среда времени выполнения CLR ищет входную точку в приложении. Этой точкой является метод main() класса Program. Вы( полнение этого метода приводит к созданию нового класса Game1. В момент со( здания класса Game1 управление программой на какой(то момент переходит к файлу Game1.cs. Там происходит инициализация всех глобальных переменных класса Game1, а затем выполнение исходного кода конструктора класса. Как толь( ко конструктор создал класс Game1, управление программой опять возвращается к классу Program и исходному коду файла Program.cs. После этого в работу вступает метод game.Run() класса Program. Этот ме( тод создает системный поток и запускает цикл, в котором происходит выполнение соответствующих методов класса Game1. В частности, первым в дело вступает метод Initialize() класса Game1. В методе Initialize() происходит ини( циализация всех компонентов игры, которые в нем расположены. Затем управле( ние программой переходит к методу LoadGraphicsContent(). Здесь происхо( дит загрузка всей графической составляющей проекта, после чего в цикле, созданном методом game.Run() класса Program, происходит цикличное выпол( нение двух методов – Update() и Draw(). Метод Update() постоянно следит за обновлением состояния игры, а метод Draw() рисует графику на экране монитора. Работа этих методов выполняется до тех пор, пока не последует команда выхода из программы. Таким образом, меха( низм работы программы сводится к организации фактически бесконечного цикла, в котором происходит выполнение методов Update() и Draw() для постоянной смены состояния игры или цикличной смены кадров в этой самой игре. Не так все и сложно на самом деле, не правда ли?! Достаточно хорошо проду( мать общую концепцию структуры будущих классов и на базе механизма работы
Переходим в полноэкранный режим
73
классов Program и Game1 реализовать свою идею. В дальнейшем мы так и будем поступать, с той лишь разницей, что параллельно изучению платформы XNA и XNA Game Studio Express мы будем создавать еще свою собственную неболь( шую игру. Для того чтобы более подробно изучить работу проекта FirstProject, ис) пользуйте отладчик инструментария Visual C# Express. Для этого необхо) димо откомпилировать проект и выполнить команды Debug ⇒ Step Into либо нажать на клавиатуре клавишу F11. Инструментарий Visual C# Express запустит механизм пошаговой отладки. Каждое последующее на) жатие клавиши F11 (Debug ⇒ Step Into) будет перемещать вас на один шаг вперед в работе программы. Таким образом, можно проследить в поша) говом режиме весь механизм функционирования программы и ее устрой) ство.
5.6. Переходим в полноэкранный режим Итак, двигаемся дальше и из оконного режима работы программы переходим в полноэкранный режим. Полноэкранный режим работы программы – это режим, в котором приложение захватывает всю рабочую поверхность дисплея и работает в приоритетном режиме. Чаще всего этот режим используется в играх, поэтому мы с самого первого примера переходим в полноэкранный режим работы. Продолжим модифицировать исходный код первого проекта. Далее в книге все новые строчки исходного кода, добавляемые в каждый последующий проект, я буду выделять жирным шрифтом, чтобы вам было легче следить за изменения( ми в исходном коде. Создадим второй проект с названием FullScreen и скопируем в него исходный код из первого проекта. В качестве названия проекта будем ис( пользовать имя FullScreen. Исходный код проекта находится на компакт(диске к книге в папке Code\Chapter5\ FullScreen. Для того чтобы создать полноэкранный режим, в исходный код конструктора класса Game1() необходимо добавить следующие строки кода. graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.ToggleFullScreen();
Первые две строки задают размер экрана по горизонтали и вертикали, ус( танавливая тем самым разрешение экрана. Для игры используется разрешение 1024 × 768 пикселей. Если у вас другой режим разрешения экрана, то задайте свои значения для этих двух параметров настроек. Параметры PreferredBackBufferHeight и PreferredBackBufferHeight задают разрешение для так называемого заднего буфера, или внеэкранной по( верхности. Этот буфер необходим для создания плавной анимации в играх, и его
74
Переходим в полноэкранный режим
Формируем каркас игровых классов
механизм функционирования чрезвычайно прост. Чтобы достичь плавной игро( вой анимации, создается дополнительный задний буфер, идентичный по своим параметрам первичной поверхности экрана. Каждый последующий кадр графики рисуется непосредственно в этом буфере. Когда графика на экране стирается оп( ределенным цветом, то происходит показ информации, находящейся в заднем бу( фере, а новый кадр формируется уже в следующем заднем буфере. Такое цикли( ческое переключение между внеэкранными поверхностями дисплея приводит к плавной анимации игрового процесса. Если стирать графику непосредственно на экране и тут же ее рисовать заново, то ваш глаз обязательно все это увидит, и будет казаться, что графика на экране дерга( ется, поэтому в свое время в DirectX и была придумана концепция работы заднего буфера. Смена поверхностей экрана происходит в автоматическом режиме и осу( ществляется системными сервисами, поэтому специально беспокоиться об этом не нужно. Просто задайте размер клиентской области экрана для заднего буфера па( раметрами PreferredBackBufferHeight и PreferredBackBufferHeight, а система выполнит всю черновую работу сама. Далее мы отключаем в приложении работу Multisampling, выставив его значение в false, а последняя строка кода в этом блоке graphics. ToggleFullScreen() включает полноэкранный режим отображения информа( ции. Вот и все настройки. Если не использовать параметры настройки режима дисплея PreferredBackBufferHeight и PreferredBackBufferHeight, то разрешение экрана даже в полноэкранном режиме будет установлено по умолча( нию в 800 × 600 пикселей. После того как работа программы будет происходить в полноэкранном режи( ме, необходимо предусмотреть корректный выход из программы. В предыдущем проекте FirstProject в качестве механизма выхода из программы в методе Update() использовался следующий исходный код: protected override void Update(GameTime gameTime) { if(GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); base.Update(gameTime); }
Метод Update() следит за обновлением состояния игры, а значит, именно в этом методе можно реализовать обработку событий, получаемых с устройств ввода информации (джойстика, мыши и клавиатуры). Вышеприведенный исход( ный код направлен исключительно на работу с джойстиком и формируется авто( матически шаблоном Visual C# Express. Этот код и механизм выхода из програм( мы также актуален и сейчас, но только в том случае, если в качестве устройства ввода используется джойстик. Если нет, то нужно реализовать дополнительный код для выхода из программы по нажатии одной из клавиш клавиатуры. Обычно в качестве кнопки выхода используется клавиша Esc.
75
Для этих целей в глобальных переменных класса Game1 объявим дополни( тельный объект. KeyboardState keyboardState;
Этот объект класса KeyboardState предназначен для работы с клавиатурой. Затем в методе Update() добавим три строки исходного кода. keyboardState = Keyboard.GetState(); if(keyboardState.IsKeyDown(Keys.Escape)) this.Exit();
В первой строке этого кода объект keyboardState инициализируется по( средством вызова метода Keyboard.GetState(), что позволяет нам получать события с клавиатуры и хранить их в keyboardState. Метод Update() работа( ет в циклическом режиме, то есть код этого метода выполняется строка за строкой до самого конца. После чего выполнение метода начинается снова в том же ключе от первой строки кода метода на протяжении работы всей программы. Все это означает, что один проход по коду метода Update() от начала до конца – это и есть один кадр в игре, соответственно состояние объекта keyboardState будет обновляться в каждом новом кадре. Во второй строке блока этого исходного кода используется логическая конст( рукция операторов if/else, позволяющая производить слежение за состоянием нажатия клавиш. Здесь все просто, если клавиша Esc на клавиатуре нажата – keyboardState.IsKeyDown(Keys.Escape), то происходит вызов метода Exit(), который останавливает работу программы и закрывает приложение. Ме( тод Exit() – это системный метод, необходимый для корректного завершения программы. В ином случае исходный код метода Update() продолжает выполняться дальше строка за строкой. По необходимости конструкцию if/else можно за( ключить в фигурные скобки, чтобы код стал читабельным и более понятным. keyboardState = Keyboard.GetState(); if(keyboardState.IsKeyDown(Keys.Escape)) { this.Exit(); }
Ниже приведен полный листинг 5.3 исходного кода файла Game1.cs проекта FullScreen. Все добавленные строки кода выделены жирным шрифтом. На ком( пакт(диске проект находится в папках Code\Chapter5\ FullScreen. //========================================================================= /// <summary> /// Ëèñòèíã 5.3
76
Переходим в полноэкранный режим
Формируем каркас игровых классов {
/// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 5 /// Ïðîåêò: FullScreen /// Êëàññ: Game1 /// Ïåðåõîä â ïîëíîýêðàííûé ðåæèì /// <summary> //=========================================================================
if (loadAllContent) { } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } }
#region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion
/// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); if(keyboardState.IsKeyDown(Keys.Escape)) this.Exit();
namespace FullScreen { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent)
base.Update(gameTime); } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); base.Draw(gameTime); } } }
77
Проект DrawSprite
Глава 6 Работа с двухмерной графикой
79
ность работы с форматами BMP, JPG, PNG, TGA и DDS. Это самые распростра( ненные и чаще всего используемые в играх графические форматы. Отображая на экране монитора спрайт, необходимо понимать следующее ус( ловие. Любой спрайт – это изображение, которое заключено в прямоугольник. Рисуя спрайт на экране, его начальной точкой отчета всегда будет оставаться верхний левый угол этого самого прямоугольника (рис. 6.2). Поэтому если вы оп( ределяете, допустим, столкновения между спрайтами, то вам необходимо знать ширину и высоту изображения, чтобы прибавить эти значения к начальной точке координат спрайта (левый верхний угол). Соответственно середина спрайта будет находиться в половине ширины и высоты графического изображения (рис. 6.2).
В этой главе мы начнем работать с двухмерными изображениями и создадим два проекта. В первом проекте мы загрузим в игру и выведем на экран монитора про( стое двухмерное изображение, или, как принято говорить, спрайт. В этом проекте общая структура классов останется неизменной, такой, какой она была в преды( дущих двух проектах пятой главы. Весь код по загрузке и выводу изображения на экран будет написан непосредственно в классе Game1. После того как вы разбере( тесь и поймете, как загружаются и рисуются двухмерные изображения на экране, мы создадим второй, более сложный проект. Идея второго проекта (DrawSpriteClass) также будет заключаться в загрузке и выводе на экран спрайта, но в этом случае структура проекта будет значительно улучшена. В частности, добавится новый класс Sprite, отвечающий за создание и загрузку двухмерных изображений. Впоследствии с каждой новой главой класс Sprite будет постоянно обновляться и улучшаться, обрастая новыми функцио( нальными возможностями.
6.1. Система координат Двухмерная система координат, предназначенная для представления графики в компьютерных играх, перевернута по отношению к обычной декартовой систе( ме координат. Начало этой системы координат находится в левом верхнем углу экрана. Положительная ось Х проходит по верхней кромке экрана слева направо, а положительная ось Y – сверху вниз по левой боковой стороне экрана. Отрица( тельная часть обеих осей координат лежит за областью экрана монитора, как это показано на рис. 6.1. Игровая графика, которая не по( падает в область экрана, отсекается системными серви( сами и не участвует в построении сцены. Ключевым понятием в двухмерной графике также является спрайт. Спрайт – это простое двухмерное изображение, нарисованное в любом графическом ре( дакторе и сохраненное в одном из графических форма( тов. В XNA Game Studio Express реализована возмож( Рис. 6.1. Система координат
Рис. 6.2. Представление спрайта на экране
6.2. Проект DrawSprite В этом проекте нам предстоит нарисовать спрайт на экране, или, иначе, вывести определенное изображение в заданном месте дисплея. Что для этого нужно? Преж( де всего необходимо само изображение в формате, понятном XNA Game Studio Express. Такое изображение можно создать в любом графическом редакторе. За( тем в нарисованном изображении нужно вырезать фон, чтобы при выводе спрайта на экран вокруг исходного изображения фона не было видно. Делается это с по( мощью графического редактора, и чаще всего для этих целей используется Adobe Photoshop, который уже давно стал своего рода промышленным стандартом.
80
Проект DrawSprite
Работа с двухмерной графикой
После того как вы подготовили изображение, можно приступать к работе с исходным кодом. Создайте новый проект (у нас это DrawSprite) и скопируйте в него программный код из предыдущего примера пятой главы. Либо можно прос( то модернизировать предыдущий проект, или открыть готовую программу с ком( пакт(диска, которая находится в папке Code\Chapter6\DrawSprite. Чтобы работать с изображениями в проектах, необходимо эти изображения добавить в ваш проект. При этом простое добавление спрайта в каталог с програм( мой никаких результатов не даст, необходимо явно добавить спрайт в проект. Де( лается это следующим образом. Все изображения игры, а также модели, шрифты, звуковые файлы можно хра( нить прямо в корневом каталоге проекта, но значительно лучше организовать для этих целей простую и понятную иерархию каталогов. В справочной информации по XNA Game Studio Express предлагается следующая структура папок для хране( ния в проекте различных компонентов игры. Папка Content – это большая общая папка, в которой хранится весь так называемый игровой контент или компоненты игры. Создав такую папку в каталоге проекта, впоследствии в ней вы будете создавать дополнитель( ные папки для различных файлов, например графических изображений, моделей, звуковых файлов, шрифтов и т. д. Папка Textures – это вложенная папка в папке Content, которая содержит все графические изображения, будь то спрайты, текстуры, игровые карты и т. д. Папка Models – это также вложенная подпапка в папке Content, где содер( жатся все трехмерные модели проекта. Дополнительно если при создании модели текстурная составляющая этой модели была прописана под один ка( талог модели, то можно хранить необходимые текстуры прямо в этой папке. К этим двум подпапкам папки Content можно добавлять любое количество папок для хранения, например, звуковых файлов, шрифтов, эффектов, вплоть до создания папок для каждого уровня или нескольких однотипных файлов исход( ного кода. Конечно, на самом деле все эти разделения по папкам – дело личных предпочтений, и никто вам не мешает хранить весь контент игры в корневом ката( логе, но в книге используется именно такая модель разделения файлов по папкам. Для того чтобы создать структуру папок, необходимо в текущем проекте, в панели Solution Explorer щелкнуть правой кнопкой мыши на названии проекта и в контекстном меню выбрать команды Add ⇒ New Folder. По выполнении этой команды в каталоге проек( та сформируется новая папка, которой необходимо дать название Content. Затем в этой папке создайте еще одну папку, но уже с названием Textures, где у нас и бу( дет находиться графический файл sprite.png (рис. 6.3). Чтобы явно добавить файл в текущий проект, в панели Solution Explorer щелкните правой кнопкой Рис. 6.3. Папка Content
81
мыши на названии папки Textures и в контекстном меню выберите команды Add ⇒ Exiting Item. Откроется диалоговое окно Add Exiting Item (рис. 6.4). В этом окне в списке Files of Types нужно выбрать вид файла, который требуется добавить в проект. Все графические изображения и модели принадлежат к типу Content Pipeline. Выберите этот тип, найдите спрайт, где бы он у вас ни находился, выделите его курсором и нажмите кнопку Add. Вне зависимости от того, в какой директории лежал графический файл, Visual C# Express скопирует его в выбран( ную папку проекта.
Рис. 6.4. Добавляем файл в проект
В нашем проекте изображение sprite.png представляет собой девушку, кото( рая падает, а точнее будет падать сверху вниз (рис. 6.5). Рисунок был нарисован специально под нашу задачу, поэтому девушка представлена в такой необычной позе. Изображение девушки заключено в прямоугольник, но при этом белый фон рисунка вырезан средствами Photoshop. Сейчас изображение статично и состоит из одного фрейма, или одного кадра, но в следующей главе мы дорису( ем еще несколько фреймов для создания иллюзии анимации. Добавив спрайт в проект, можно приступать к работе над исходным кодом класса Game1. В гло( бальных переменных к уже имеющимся объявлени( ям добавляем следующие три строки кода. Рис. 6.5. Статичное изображение девушки
82
Проект DrawSprite
Работа с двухмерной графикой
Texture2D spriteTexture; Vector2 spritePosition; SpriteBatch spriteBatch;
В первой строке кода объявляется объект spriteTexture класса Texture2D. Это системный класс, позволяющий работать с двухмерными текстурами. Объект этого класса spriteTexture как раз и будет представлять или содержать графи( ческий файл sprite.png. Объект spriteTexture отвечает только за загрузку в программу изображений, а с помощью объекта spriteBatch класса SpriteBatch происходит рисование спрайтов на дисплее. Во второй строке кода этого блока объявляется переменная spritePosition структуры Vector2. Эта структура дает возможность задавать сразу две коорди( наты по осям X и Y. С помощью переменной spritePosition мы сможем задать точку вывода спрайта на экран. Платформа XNA также имеет в своем составе еще структуры Vector3 и Vector4 для работы с тремя и четырьмя координатами со( ответственно (X, Y, Z и W). В этом случае ось Z представляет буфер глубины, а величина W – это коэффициент перспективы. В последней строке блока создается еще один объект spriteBatch класса SpriteBatch. Класс SpriteBatch имеет в своем составе сервисы, необходимые для вывода или рисования графических изображений на экране монитора. Далее в методе LoadGraphicsContent() создадим, или инициализируем (кто к чему привык), объект spriteBatch. protected override void LoadGraphicsContent(bool loadAllContent) { if(loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); spriteTexture = content.Load(“Content\\Textures\\sprite”); } }
В этом примере создание объекта spriteBatch происходит типичным, или стандартным, образом. Эта конструкция кода имеет место во всех программах. В следующей строке кода происходит загрузка изображения sprite.png в програм( му. Для этих целей используется метод Load(), а в качестве его параметра указывается полный путь к графическому изображению. В этой записи два слэша обозначают одну из папок в рабочем каталоге проекта. Если вы хотите загрузить текстуру, например, из корневого каталога, то достаточно указать про( сто имя файла без слэшей, если, конечно, ваш файл действительно находится в корневом каталоге. Загрузка графического файла в программу происходит посредством сервисов XNA Content Pipeline. В строке загрузки графического файла указывается только название файла без его расширения. Загрузчику XNA Content Pipeline абсолютно все равно, в каком формате выполнено изображение, главное, чтобы это были
83
форматы, с которыми он умеет работать (BMP, JPG, PNG, TGA и DDS), а все ос( тальное – дело техники. После компиляции приложения XNA Content Pipeline преобразует все графические ресурсы игры в свой специфический формат XNB, и программа будет читать уже преобразованные файлы. Далее в методе Initialize() мы зададим две координаты по осям X и Y для вывода спрайта на экран монитора. protected override void Initialize() { spritePosition.X = Window.ClientBounds.Width / 2; spritePosition.Y = Window.ClientBounds.Height / 2; base.Initialize(); }
Начальная точка координат для спрайта – это его верхний левый угол. Пере( менная spritePosition представляет собой вектор, который позволяет зада( вать координаты по двум осям. Обращение к осям X и Y происходит посредством оператора точка. Дополнительно можно использовать и другую конструкцию кода с явным созданием объекта spritePosition следующим образом. spritePosition = new Vector2(Window.ClientBounds.Width / 2, Window.ClientBounds.Height / 2);
Эта запись идентична предыдущей записи. Кстати, параметры Window. ClientBounds.Width и Window.ClientBounds.Height автоматически оп( ределяют размер экрана по ширине и высоте соответственно. А деление этих зна( чений на 2 позволяет найти центр экрана. В ином случае можно было записать следующие строки. spritePosition.X = 512; /* 1024/2 */ spritePosition.Y = 384; /* 768/2 */
Если не определять координаты для вывода спрайта на экран, то по умолча( нию значение положения спрайта задается двумя нулями, то есть верхний левый угол экрана или окна. После всех этих действий мы будем иметь загруженный в программу спрайт, а точнее механизм, который будет загружать в программу спрайт и точку вывода спрайта на экране. Теперь нам осталось только нарисовать/вывести/отобразить спрайт на экране. Для этих целей служит метод Draw(), код которого изменяется следующим образом: protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(spriteTexture, spritePosition, Color.White);
84
Проект DrawSprite
Работа с двухмерной графикой spriteBatch.End(); base.Draw(gameTime);
}
Методы Begin() и End() класса SpriteBatch задают начало и окончание представления всей двухмерной сцены на экране монитора. Между двумя вызова( ми этих методов необходимо производить вывод или рисование всех графических изображений на экране. На каждый вызов метода Begin() должен обязательно следовать вызов метода End(). Это необходимое условие! Между вызовами методов Begin() и End() происходит вызов метода Draw() класса SpriteBatch. spriteBatch.Draw(spriteTexture, spritePosition, Color.White);
Первым параметром этого метода является спрайт, представленный объектом spriteTexture. Второй параметр задает расположение спрайта на экране по осям X и Y, а последний параметр устанавливает цветовую составляющую спрай( та или то, каким цветом будет закрашен спрайт. Сейчас используется белый цвет, что означает рисование спрайта в оригинале без какой(либо дополнительной ре( туши цветом. В этом параметре для окраски спрайтов можно использовать любые доступные цвета. После компиляции и запуска проекта DrawSprite на экране монитора ото( бразится графическое изображение. В листинге 6.1 класса Game1 проекта DrawSprite находится полный исходный код рассматриваемого примера. //========================================================================= /// <summary> /// Ëèñòèíã 6.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 6 /// Ïðîåêò: DrawSprite /// Êëàññ: Game1 /// Âûâîä ñïðàéòà íà ýêðàí ìîíèòîðà /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace DrawSprite
{ public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; Texture2D spriteTexture; Vector2 spritePosition; SpriteBatch spriteBatch; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { spritePosition.X = Window.ClientBounds.Width / 2; spritePosition.Y = Window.ClientBounds.Height / 2; base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if(loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); spriteTexture = content.Load(“Content\\Textures\\sprite”); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if(unloadAllContent == true) {
85
86
Работа с двухмерной графикой content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { … }
Проект DrawSprite Class
87
ект, а точнее создать в проекте еще один дополнительный класс с названием Sprite. Делается это следующим образом. В открытом проекте в панели Solution Explorer щелкните правой кнопкой мыши на названии проекта и в контекстном меню выберите команды Add ⇒ New Item. В появившемся диалоговом окне Add New Item – DrawSpriteClass выдели( те курсором мыши шаблон Class и в поле Name задайте имя будущему классу (рис. 6.6). После этого в рабочем каталоге проекта инструментарий Visual C# сформирует новый файл под названием Sprite.cs.
/// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(spriteTexture, spritePosition, Color.White); spriteBatch.End(); base.Draw(gameTime); } } }
6.3. Проект DrawSpriteClass В первом проекте, работая с изображением, мы использовали класс Game1, в кото( ром непосредственно обеспечивалась загрузка спрайта в программу. Такой под( ход оправдан для небольших демонстрационных примеров, где программный код может составлять несколько десятков строк. В больших играх этот подход не го( дится, поскольку количество строк исходного кода может составлять несколько тысяч и в итоге можно элементарно запутаться в своем коде, не говоря уже о сто( ронних программистах. Поэтому перед созданием игры всегда необходимо тща( тельно продумывать структуру будущих классов. Тем более что язык программи( рования C# тем и хорош, что он объектно(ориентированный и все его лучшие качества заключены именно в этом стиле программирования. В проекте DrawSpriteClass мы создадим дополнительный класс Sprite, который будет отвечать за все спрайты, загружаемые в игру. Далее с каждой новой главой функционал класса Sprite будет пополняться новыми методами, что по( зволит нам разработать универсальный класс для работы с графическими изобра( жениями, который в дальнейшем вы можете усовершенствовать по своему усмот( рению. Проект DrawSpriteClass основан на исходном коде предыдущего проекта DrawSprite, но с некоторыми дополнениями и переработками. Проект будет включать в себя классы Program, Game1 и Sprite. Исходный код класса Sprite находится в отдельном файле Sprite.cs. Этот файл необходимо добавить в про(
Рис. 6.6. Добавляем в проект новый класс Sprite
6.3.1. Класс Sprite проекта DrawSpriteClass Рассмотрим исходный код класса Sprite (листинг 6.2), а затем перейдем к его подробному анализу. //========================================================================= /// <summary> /// Ëèñòèíã 6.2 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 6 /// Ïðîåêò: DrawSpriteClass /// Êëàññ: Sprite /// Ñîçäàåì êëàññ Sprite
88
Работа с двухмерной графикой
/// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace DrawSpriteClass { public class Sprite { public Texture2D spriteTexture; public Vector2 spritePosition; /// <summary> /// Êîíñòðóêòîð /// <summary> public Sprite() { } /// <summary> /// Çàãðóçêà ñïðàéòà â èãðó /// <summary> public void Load(ContentManager content, String stringTexture) { spriteTexture = content.Load(stringTexture); } /// <summary> /// Ðèñóåì ñïðàéò /// <summary> public void DrawSprite(SpriteBatch spriteBatch) { spriteBatch.Draw(spriteTexture, spritePosition, Color.White); } } }
В сформированном файле Sprite.cs в области подключения библиотечных классов добавляются дополнительные библиотеки, которые при формировании шаблона Visual C# добавлены не были. Затем в исходном коде класса Sprite идет объявление двух объектов. public Texture2D spriteTexture; public Vector2 spritePosition;
Проект DrawSprite Class
89
Первый объект spriteTexture класса Texture2D будет содержать загру( женное в программу изображение, а второй объект spritePosition класса Vector2 – задавать местоположение спрайта на экране. Здесь наблюдается пря( мая аналогия с примером, рассмотренным в предыдущем проекте, но все объявле( ния и действия по загрузке изображений будут происходить непосредственно в классе Sprite. Конструктор класса Sprite остается без реализации. В следующей главе мы будем изучать спрайтовую анимацию и создадим дополнительный конструктор для класса Sprite, направленный на создание объекта с возможностью анима( ции. В итоге у нас получится два разных конструктора: один – для создания не( анимированных объектов, а другой – для анимированных изображений. Затем в исходном коде файла Sprite.cs следует описание метода Load(), ко( торый в качестве параметров принимает объект content класса ContentManager и объект stringTexture класса String. public void Load(ContentManager content, String stringTexture) { spriteTexture = content.Load(stringTexture); }
Объект content необходим для вызова метода Load(), а значит, и для возможности загрузки в программу изображения непосредствен( но через класс Sprite. Второй объект stringTexture будет содержать строку текста, в которой мы будем передавать путь к загружаемому изображению. На этом этапе можно было сразу прописать путь к графическому файлу, но тогда метод Load() можно будет использовать только один раз для загрузки одного жестко заданного изображения. В нашем случае метод Load() универсален и годится для загрузки любого спрайта. Достаточно лишь вызвать этот метод в классе Game1 и в качестве параметров передать объект content и указать путь к спрайту. После метода Load() идет реализация метода DrawSprite(), отвечающего за рисование спрайта на экране монитора. public void DrawSprite(SpriteBatch spriteBatch) { spriteBatch.Draw(spriteTexture, spritePosition, Color.White); }
В этом методе происходит вызов системного метода Draw(), а в качестве па( раметров выступают спрайт, позиция на экране и цветовая составляющая. В сам метод DrawSprite() в качестве параметра передается объект spriteBatch класса SpriteBatch, для возможности вызова метода Draw() непосредственно через класс Sprite. На этом реализация класса Sprite закончена, давайте пе( рейдем к рассмотрению исходного кода класса Game1.
90
Проект DrawSprite Class
Работа с двухмерной графикой
91
protected override void Initialize() { sprite.spritePosition = new Vector2(Window.ClientBounds.Width / 2, Window.ClientBounds.Height / 2);
6.3.2. Класс Game1 проекта DrawSpriteClass Исходный код класса Game1 представлен в листинге 6.3. //========================================================================= /// <summary> /// Ëèñòèíã 6.3 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 6 /// Ïðîåêò: DrawSpriteClass /// Êëàññ: Game1 /// Ñîçäàåì êëàññ Sprite /// <summary> //=========================================================================
base.Initialize(); }
/// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); sprite.Load(content, “Content\\Textures\\sprite”); } }
#region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion
/// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { … }
namespace DrawSpriteClass { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch; Sprite sprite;
/// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); base.Update(gameTime);
/// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; sprite = new Sprite(); }
}
/// <summary> /// Èíèöèàëèçàöèÿ /// <summary>
} }
/// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); sprite.DrawSprite(spriteBatch); spriteBatch.End(); base.Draw(gameTime);
}
92
Работа с двухмерной графикой
Заметьте, что код из предыдущего примера по загрузке спрайта в классе Game1 был удален, потому что загрузка изображения теперь происходит через класс Sprite. В глобальных переменных класса Game1 добавляется объявление нового объекта sprite класса Sprite. Sprite sprite;
А в конструкторе класса Game1 происходит создание этого объекта строкой кода. sprite = new Sprite();
Затем в методе LoadGraphicsContent() через объект sprite вызывается метод Load() класса Sprite, который загружает изображение в игру. В качестве второго параметра этого метода передается путь к графическому файлу sprite.png. protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); sprite.Load(content, “Content\\Textures\\sprite”); } }
Потом в методе Initialize() через объект sprite мы обращаемся к объек( ту spritePosition и задаем начальную точку вывода изображения на экран монитора. protected override void Initialize() { sprite.spritePosition = new Vector2(Window.ClientBounds.Width / 2, Window.ClientBounds.Height / 2); base.Initialize(); }
И в конце в методе Draw() вызываем метод DrawSprite() класса Sprite для вывода спрайта на дисплей в заданном месте.
Глава 7 Спрайтовая анимация Изображения в играх могут быть как анимированными, так и неанимированны( ми. Неанимированное изображение – это некий рисунок заданного размера, сос( тоящий из одного кадра, или фрейма. Примером неанимированного изображения может послужить статический или двигающийся объект, рассмотренный нами в предыдущей главе. Анимированное изображение – это изображение, состоящее из определенного набора фреймов или ряда последовательных, взаимосвязанных изображений (анимационная последовательность). Переход по всем имеющимся фреймам анимационной последовательности создает иллюзию анимации в игро( вом процессе. Анимированное изображение может состоять из любого количества фреймов, задающих анимационную последовательность для персонажей игры. Отсчет фреймов изображения происходит от нуля, точно так же, как это делается в про( стом массиве данных. Количество фреймов анимационной последовательности не ограничено, но весь набор последующих фреймов анимационной последова( тельности должен совпадать с размером самого первого фрейма как по ширине, так и высоте. То есть все фреймы должны быть одинакового размера. Располагать фреймы изображения можно горизонтально, вертикально или компоновать лю( бым удобным для вас способом. Отсчет по фреймам изображения всегда происхо( дит слева направо и сверху вниз. Посмотрите на рис. 7.1, где изображен шагаю( щий робот. Изображение шагающего робота на рис. 7.1 состоит из четырех фреймов ани( мационной последовательности, где каждый фрейм определяет одну из фаз дви( жения робота. Циклическое перемещение по фреймам этого изображения или по( стоянная перерисовка фреймов на экране монитора создаст эффект ходьбы робота в игре. Этот механизм работы с анимацией используется в компьютерных и мобильных играх, а также в мультипликационных фильмах.
sprite.DrawSprite(spriteBatch);
На этом этапе это все улучшения и новшества, добавленные в исходный код бу( дущей игры. После запуска игры вы увидите на экране девушку в интересной позе. Рассматриваемый пример находится на компакт(диске в папке Code\Chapter6\ DrawSpriteClass. Откомпилируйте и запустите программу, а также поэкспери( ментируйте с выбором различных значений для задания позиций спрайта на экра( не монитора.
Рис. 7.1. Шагающий робот
94
Проект Animation
Спрайтовая анимация
В этой главе вашему вниманию будет представлено два проекта: Animation и Background. В первом проекте Animation вы научитесь загружать в программу анимационные последовательности и создавать анимацию на экране. Во втором проекте или во второй части этой главы мы добавим в игру статическое фоновое изображение (как принято говорить, Background, или задний фон), заметно улуч( шив тем самым графический интерфейс всей игры. Итак, переходим к работе над проектами.
7.1. Проект Animation Задача этого проекта заключается в загрузке в программу анимационной после( довательности, а также реализации механизма перебора всех имеющихся фрей( мов. Сам же спрайт рисуется в центре экрана без возможности его перемещения по экрану.
7.1.1. Анимационная последовательность В нашей игре анимационная последовательность будет состоять из набора не( скольких фреймов, имитирующих движение объекта в момент его падения сверху вниз. В предыдущей главе в проект загружалось изображение девушки, состоящее из одного фрейма. Чтобы сделать анимационную последовательность, необходимо к имеющемуся фрейму дорисовать еще несколько дополнительных фреймов. Коли( чество фреймов и содержание самого рисунка может быть любым, главное, чтобы циклический переход по анимационной последовательности создавал эффект ка( кого(то движения, которое было близко к естественным движениям. Наша задача в этой связи понятна и не очень проста. Необходимо нарисовать падающую сверху вниз девушку, которая должна «трепыхать» руками, ногами, телом и т. д. Заметьте, что чем больше фреймов вы нарисуете, тем плавнее будет анимация, но и, с другой стороны, уж очень большое количество фреймов может несколько притормаживать игру. Конечно, в компьютерных играх количество фреймов одной анимационной последовательности не критично и может состав( лять 10–20 и более фреймов, тогда как в мобильных играх разработчикам прихо( дится себя сильно ограничивать в этом плане. На рис. 7.2 представлен набор фреймов для падающей с небес девушки.
Рис. 7.2. Анимационная последовательность девушки
95
7.1.2. Класс Sprite проекта Animation В исходный код класса Sprite проекта Animation необходимо добавить несколь( ко новых элементов по сравнению с предыдущим проектом. Как вы помните, все наши последующие разработки модифицируют предыдущие. В первую очередь в классе Sprite нужно создать четыре новые переменные, объявление которых происходит в глобальной области исходного кода файла Sprite.cs. private private private private
int frameÑount; double timeFrame; int frame; double totalElapsed;
Первая переменная frameÑount впоследствии будет содержать в себе коли( чество фреймов нашей анимационной последовательности и примет прямое учас( тие в организации циклического показа фреймов на экране. Вторая переменная timeFrame получит значение времени, измеряемое в миллисекундах, отведенное для показа одного из фреймов на экране. Именно с помощью этой переменной мы будем регулировать время задержки показа одного фрейма на экране. Если не де( лать задержки для показа одного из фреймов анимации, то переход по всей анима( ционной последовательности пройдет с такой скоростью, что вы не успеете даже заметить, что мы там пытались анимировать и зачем. Следующая переменная frame – это своего рода счетчик, представляющий в текущий момент один из фреймов. Этот счетчик по прошествии заданного вре( мени задержки (переменная timeFrame) увеличивается на единицу для перехо( да к показу следующего фрейма. Последняя переменная totalElapsed будет со( держать значение времени, необходимое для расчета времени задержки показа одного фрейма, а точнее, расчета прошедшего времени, выделенного на показ од( ного из фреймов. После объявления в исходном коде всех переменных их необходимо инициа( лизировать заданными значениями. Для этих целей в классе Sprite формирует( ся второй конструктор, необходимый для создания анимированных объектов это( го класса. public Sprite(int frameCount, int framesPerSec) { frameÑount = frameCount; timeFrame = (float)1 / framesPerSec; frame = 0; totalElapsed = 0; }
Теперь в классе Sprite имеются два конструктора этого класса, позволяющие создавать как статические, так и анимированные спрайты или объекты этого класса. Второй конструктор класса Sprite содержит два целочисленных параметра frameCount и framesPerSec, которые передаются в создаваемый объект класса.
96
Проект Animation
Спрайтовая анимация
Первый параметр frameCount задает общее количество фреймов для загру( жаемого при создании этого объекта изображения. Количество фреймов загру( жаемой анимационной последовательности необходимо точно знать, и они опре( деляются на этапе создания рисунка. В нашем случае мы имеем 12 фреймов. На основе полученного значения или количества фреймов изображения происходит расчет времени задержки для показа одного фрейма на экране. Второй параметр framesPerSec конструктора класса Sprite в момент со( здания объекта этого класса получает количество кадров, которые необходимо показать за одну секунду. Иначе говоря, в этом параметре задается определенное количество кадров, показываемых на экране за одну секунду. Это значение позво( ляет рассчитать время задержки, необходимое для показа одного фрейма на экра( не. Параметр framesPerSec в какой(то мере позволяет задавать скорость пере( мещения по фреймам или задает скорость перебора фреймов анимационной последовательности изображения. Этот механизм дает возможность реализовы( вать различную степень скорости анимации объектов. Такой подход в реализации анимированного конструктора позволяет созда( вать множество различных объектов класса Sprite, каждый из которых волен содержать любое количество фреймов анимационной последовательности, а так( же определять количество фреймов изображения, показываемых за одну секунду. Далее в исходном коде класса Sprite добавляется новый метод UpdateFrame(), реализующий механику перебора фреймов изображения на основании получен( ного системного времени и его сравнения со временем задержки, отведенным на показ одного фрейма. public void UpdateFrame(double elapsed) { totalElapsed += elapsed; if (totalElapsed > timeFrame) { frame++; frame = frame % (frameÑount - 1); totalElapsed -= timeFrame; } }
В метод UpdateFrame() передается значение времени, полученное с момен( та предыдущего вызова этого метода, и его увеличение на единицу (получение и передача времени реализуются в классе Game1). Затем в конструкции кода if/else проверяется условие, в котором время задержки для показа одного фрейма (переменная timeFrame) не должно быть больше полученного значения времени. Как только это время больше или время задержки для показа одного фрейма истекает, то происходит увеличение счетчика фреймов на единицу и соот( ветственно переход к показу следующего фрейма. И в конце исходного кода класса Sprite добавляется новый метод DrawAnimationSprite(), предназначенный для рисования анимированных спрайтов, созданных посредством анимированного конструктора класса Sprite.
97
public void DrawAnimationSprite(SpriteBatch spriteBatch) { int frameWidth = spriteTexture.Width / frameÑount; Rectangle rectangle = new Rectangle(frameWidth * frame, 0, frameWidth, spriteTexture.Height); spriteBatch.Draw(spriteTexture, spritePosition, rectangle, Color.White); }
В первой строке кода этого метода переменная frameWidth инициализируется значением ширины одного фрейма анимационной последовательности, измеряе( мой в пикселях. Строка spriteTexture.Width позволяет получить текущую ши( рину графического изображения (длину всех 12 фреймов изображения sprite.png), и далее происходит деление этого значения на общее количество фреймов анимаци( онной последовательности (например: 200 пикселей / 5 фреймов = 40 пикселей), что в итоге дает нам искомую ширину одного фрейма. Вот поэтому все фреймы ани( мационной последовательности и должны быть одинаковыми! Дополнительно в этом исходном коде можно использовать и числовые значения для каждого конк( ретного изображения, но тогда для каждого загружаемого в игру спрайта вам при( дется писать отдельный метод DrawAnimationSprite(). Во второй строке кода метода DrawAnimationSprite() создается прямо( угольник или видимая прямоугольная область, которая по своему размеру равна ширине и высоте одного фрейма анимационной последовательности. То есть со( здается механизм отсечения всех фреймов анимационной последовательности, кроме одного(единственного фрейма, который рисуется в текущий момент на эк( ране монитора. Если не создавать такой механизм, то на экране будет нарисована вся анимационная последовательность разом, состоящая из 12 фреймов. Факти( чески в коде создается этакая дыра в стене заданного размера, за которой мы под( ставляем необходимые рисунки, чередуя их с заданной скоростью, что в итоге и создает эффект анимации. В последней строке кода метода DrawAnimationSprite() происходит про( рисовка одного из фреймов на экране монитора: spriteBatch.Draw(spriteTexture, spritePosition, rectangle, Color.White);
Где параметры: spriteTexture – это загруженное в игру изображение; spritePosition – позиция на экране монитора; rectangle – ограничивающий или отсекающий прямоугольник, равный ширине и высоте одного фрейма; Color.White – задает цвет для рисуемого изображения. Полный исходный код класса Sprite проекта Animation представлен в лис) тинге 7.1. //========================================================================= /// <summary>
98
Проект Animation
Спрайтовая анимация
/// Ëèñòèíã 7.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 7 /// Ïðîåêò: Animation /// Êëàññ: Sprite /// Ñîçäàåì àíèìàöèþ /// <summary> //=========================================================================
{
#region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics;
/// <summary> /// Çàãðóçêà ñïðàéòà â èãðó /// <summary> public void Load(ContentManager content, String stringTexture) { spriteTexture = content.Load(stringTexture); }
99
totalElapsed += elapsed; if (totalElapsed > timeFrame) { frame++; frame = frame % (frameÑount - 1); totalElapsed -= timeFrame; } } }
#endregion
/// <summary> /// Ðèñóåì ïðîñòîé ñïðàéò /// <summary> public void DrawSprite(SpriteBatch spriteBatch) { spriteBatch.Draw(spriteTexture, spritePosition, Color.White); }
namespace Animation { public class Sprite { public Texture2D spriteTexture; public Vector2 spritePosition; private int frameÑount; private double timeFrame; private int frame; private double totalElapsed;
/// <summary> /// Ðèñóåì àíèìèðîâàííûé ñïðàéò /// <summary> public void DrawAnimationSprite(SpriteBatch spriteBatch) { int frameWidth = spriteTexture.Width / frameÑount; Rectangle rectangle = new Rectangle(frameWidth * frame, 0, frameWidth, spriteTexture.Height); spriteBatch.Draw(spriteTexture, spritePosition, rectangle, Color.White); } }
/// <summary> /// Êîíñòðóêòîð /// <summary> public Sprite() { } } /// <summary> /// Êîíñòðóêòîð äëÿ àíèìèðîâàííîãî ñïðàéòà /// <summary> public Sprite(int frameCount, int framesPerSec) { frameÑount = frameCount; timeFrame = (float)1 / framesPerSec; frame = 0; totalElapsed = 0; } /// <summary> /// Öèêëè÷íûé ïåðåõîä ïî ôðåéìàì - àíèìàöèÿ /// <summary> public void UpdateFrame(double elapsed)
7.1.3. Класс Game1 проекта Animation Теперь давайте перейдем к рассмотрению класса Game1 проекта Animation. Ниже в листинге 7.2 представлен исходный код этого класса. Сначала посмотрим на весь код этого класса, а затем приступим к его изучению. //========================================================================= /// <summary> /// Ëèñòèíã 7.2 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 7
100
Проект Animation
Спрайтовая анимация
/// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); sprite.Load(content, “Content\\Textures\\sprite”); } }
/// Ïðîåêò: Animation /// Êëàññ: Game1 /// Ñîçäàåì àíèìàöèþ /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion
/// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } }
namespace Animation { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch; Sprite sprite;
/// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); double elapsed = gameTime.ElapsedGameTime.TotalSeconds; sprite.UpdateFrame(elapsed);
/// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; sprite = new Sprite(12, 10); }
base.Update(gameTime); } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
/// <summary> /// Èíèöèàëèçàöèèÿ /// <summary> protected override void Initialize() { sprite.spritePosition = new Vector2(Window.ClientBounds.Width / 2, Window.ClientBounds.Height / 2); base.Initialize(); } /// <summary>
101
spriteBatch.Begin(SpriteBlendMode.AlphaBlend); sprite.DrawAnimationSprite(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } } }
В этом коде в области глобальных переменных происходит объявление объек( та sprite класса Sprite.
102
Проект Background
Спрайтовая анимация
Sprite sprite;
103
{ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
Затем в конструкторе класса Game1 создается полноценный объект sprite.
spriteBatch.Begin(SpriteBlendMode.AlphaBlend); sprite.DrawAnimationSprite(spriteBatch); spriteBatch.End();
sprite = new Sprite(12, 10);
Для формирования этого объекта используется конструктор класса Sprite, подготовленный для создания анимированных объектов. В качестве параметров в конструктор класса Sprite передаются два целочисленных значения. Первое значение – это количество фреймов анимационной последовательности для за( гружаемого в игру изображения. Второй целочисленный параметр конструктора класса Sprite задает определенное значение, на базе которого происходит расчет времени задержки для показа одного фрейма анимационной последовательности. Далее в исходном коде класса Game1 в методе Initialize() назначается позиция для спрайта на экране монитора, а в методе LoadGraphicsContent() происходит загрузка изображения sprite.png. Исходное изображение располага( ется в рабочем каталоге проекта в папках Content\Textures. Затем в методе Update() мы обновляем состояние игры и вызываем метод UpdateFrame() класса Sprite.
base.Draw(gameTime); }
Полный исходный код проекта Animation вы найдете на компакт(диске в пап( ке Code\Chapter7\Animation.
7.2. Проект Background Фоновые изображения в играх могут быть различного плана, здесь все зависит от конкретной задачи в реализации самой игры. В нашем случае фон представлен сразу двумя статическими изображениями размером в 1024 пикселя по ширине и 768 пикселей по высоте (размер экрана монитора). Поэтому начальной точкой вывода этих изображений служит начало отсчета системы координат (рис. 7.3).
protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); double elapsed = gameTime.ElapsedGameTime.TotalSeconds; sprite.UpdateFrame(elapsed); base.Update(gameTime); }
В качестве параметра в методе UpdateFrame() класса Sprite передается значение переменной elapsed. Эта переменная при каждой итерации игрового цикла, то есть при каждом новом проходе по методу Update(GameTime gameTime), получает текущее значение времени, прошедшее за один игровой цикл или за один проход по коду метода Update(GameTime gameTime). А уже в методе UpdateFrame() класса Sprite происходит сравнение времени задерж( ки на показ одного фрейма с прошедшим временем и соответственно решается, показывать по(прежнему этот фрейм анимационной последовательности или пе( реходить к следующему фрейму. В самом конце исходного кода класса Game1 происходит вызов метода Draw(), где на экран выводится один из фреймов всей анимационной последова( тельности изображения. protected override void Draw(GameTime gameTime)
Рис. 7.3. Расположение фона на экране монитора
В качестве фоновой картинки мы используем два изображения. Первое изображение – это основной фон игры, а второе изображение представляет спе( циальную рамку и бортик. Рамка рисуется по окантовке всего экрана, а внутрен( нее пространство этой рамки вырезается (рис. 7.4). Впоследствии в рамке будет выводиться подсчет очков и другая сопутствующая игровая информация. Шири( на рамки с правой стороны составляет 300 пикселей. Это значение нам понадо(
104
Проект Background
Спрайтовая анимация
105
но. Если же в других ваших играх фоновое изображение будет иметь гораздо боль( ше функциональных возможностей, например механизм скроллинга, то стоит со( здать для этих целей отдельный класс. Пример реализации скроллинга в играх показан в документации к XNA Game Studio Express (Раздел Programming Guide ⇒ Graphics ⇒ 2D Graphics ⇒ Make a Scrolling Background). В листинге 7.3 представлен код класса Game1 проекта Background. Полный исходный код всего проекта находится на компакт(диске в папке Code\Chapter7\ Background.
Рис. 7.4. Фоновое изображение
бится в дальнейшем при расчете местоположения падающих с неба объектов. Но возникает вопрос: а зачем нам два изображения, когда все это можно было нарисо( вать в одном рисунке? Делается это только с одной целью – для того чтобы украсить игру. Сначала на экране мы нарисуем фоновый рисунок, затем на этом фоне будем рисовать падающие объекты, а затем поверх всей графики наложим рамку. Получится, что все игровые объекты (кроме платформы) будут перемещаться (падать вниз) меж( ду фоном и рамкой. К самой рамке мы еще подрисуем трос и поверх рамки в сле( дующих главах нарисуем платформу. Таким образом, все объекты будут красиво падать между фоном и рамкой, а платформа – скользить по тросу. Если пользова( тель не успеет поймать объект своего желания, то он красиво упадет под рамкой и тросом, но поверх фонового изображения. Такой механизмы слоев позволяет формировать в двухмерных играх полноценный Z(буфер или ось Z. Графические файлы фонов background1.png и background2.png располага( ются в рабочем каталоге проекта в папке Content\Textures. Добавление изобра( жений в проект необходимо произвести явно через выполнение команд Add ⇒ Exiting Item (глава 6, раздел 6.2), как это мы делали для изображения sprite.png. Загружать фоновое изображение мы будем напрямую прямо в классе Game1, поскольку для фона не определено никаких дополнительных функций, то и созда( ние отдельного класса для фона или использование класса Sprite не обязатель(
//========================================================================= /// <summary> /// Ëèñòèíã 7.3 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 7 /// Ïðîåêò: Background /// Êëàññ: Game1 /// Äîáàâëÿåì ôîí /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace Background { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch; Sprite sprite; private Texture2D background1; private Texture2D background2; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024;
106
Проект Background
Спрайтовая анимация graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; sprite = new Sprite(12, 10);
107
} /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
} /// <summary> /// Èíèöèàëèçàöèÿ /// <summary protected override void Initialize() { sprite.spritePosition = new Vector2(300, 200);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); sprite.DrawAnimationSprite(spriteBatch); spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); spriteBatch.End();
base.Initialize(); }
base.Draw(gameTime);
/// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); sprite.Load(content, “Content\\Textures\\sprite”); background1 = content.Load(“Content\\Textures\\background1”); background2 = content.Load(“Content\\Textures\\background2”); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit();
} } }
В исходном коде класса Game1 в области глобальных переменных происходит объявление двух объектов background1 и background2. Эти объекты пред( ставляют в игре фоновое изображение. private Texture2D background1; private Texture2D background2;
Затем в методе LoadGraphicsContent() происходит загрузка фона в игру из файла background1.png и background1.png. В данном случае механизм загруз( ки фона идентичен механизму загрузки простого спрайта без создания дополни( тельного класса Sprite. Этот механизм мы изучали в начале главы 6. Background1 = content.Load(“Content\\Textures\\background1”); Background2 = content.Load(“Content\\Textures\\background2”);
В свою очередь, в методе Draw() фон, а также анимированный спрайт рисует( ся на экране монитора. protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); sprite.DrawAnimationSprite(spriteBatch); spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); spriteBatch.End();
double elapsed = gameTime.ElapsedGameTime.TotalSeconds; sprite.UpdateFrame(elapsed);
base.Draw(gameTime); base.Update(gameTime);
}
108
Спрайтовая анимация
Главное в исходном коде метода Draw() – это последовательность вывода ри( сунков на экран монитора. Каждое последующее рисуемое изображение наклады( вается поверх предыдущего. Запомнить это очень легко, просто помните, что пер( вая строка кода метода Draw(), которая рисует изображение, – это первый слой, следующая строка кода метода Draw() – соответственно следующий слой, кото( рый накладывается поверх предыдущего слоя, и т. д. (рис. 7.5). Этот механизм позволяет формировать многослойные фоновые и игровые сцены, где, например, главный герой может заходить за дом или дерево, формируя тем самым потен( циальную ось Z, которая удалена от глаз пользователя как бы внутрь монитора (трехмерный мир).
Рис. 7.5. Наложение слоев в игровом мире
Если в нашем коде взять и поменять местами строки с выводом на экран фона и спрайта, то на дисплее, кроме фона, вы ничего не увидите, потому что фоновый рисунок размером во весь экран просто(напросто закроет собой нашу анимацию. Это, кстати, очень частая ошибка со стороны начинающих программистов, поэто( му не забывайте: каждый последующий слой накладывается поверх предыду щего! На этом все, переходим к следующей главе и поговорим о движении объектов в пространстве, а также о некоторых особенностях реализации простого искусст( венного интеллекта.
Глава 8 Движение спрайтов в пространстве Одной из главных составляющих любой игры является перемещение объектов в игровом пространстве. Часть таких объектов может перемещаться в соответствии с командами, получаемыми от пользователя через устройство ввода информации (клавиатуру, джойстик или мышь). Как правило, это либо главный герой игры, либо определенный инструмент, играющий главную роль в игровом процессе, как это задумано в нашей игре. Как вы помните, у нас в качестве такого инструмента выступает платформа, висящая над пропастью и ловящая различные объекты, па( дающие с неба. Другая часть объектов игры может перемещаться на основе своей логики, за( программированной создателем. Такая логика в играх называется игровым ис) кусственным интеллектом. В этой главе мы наделим наши игровые объекты простейшими алгоритмами игровой логики, научив их перемещаться в заданном направлении, а также выбирать себе случайное место в игровом пространстве. Ис( пользование более мощных алгоритмов искусственного интеллекта в нашей игре не понадобится, но если вы интересуетесь этим вопросом серьезно, то рекомен( дую вам изучить книгу издательства ДМК Пресс «Программирование искусст( венного интеллекта в приложениях», автор М. Тим Джонс. Итак, что такое движение в пространстве? Каждый спрайт при выводе на эк( ран монитора получает свои координаты по двум осям X и Y, например: Vector2 spritePosition = new Vector2(100, 300);
Также может иметь место и следующая запись: Vector2 spritePosition; spritePosition.X = 100; spritePosition.Y = 300;
В этих строках кода позиция для спрайта задается по оси X в 100 пикселей (от левого верхнего угла экрана) и по оси Y в 300 пикселей вниз все от того же верхне( го левого угла. Чтобы переместить спрайт в заданном направлении, необходимо просто изменить позицию спрайта или стереть спрайт с экрана в старом месте и нарисовать его уже на новом месте. То есть если вы желаете переместить спрайт сверху вниз (только по оси Y), то необходимо на каждой итерации игрового цикла
110
Движение спрайтов в пространстве
или в один проход исходного кода метода Update() класса Game1 изменить или увеличить позицию спрайта на определенное количество пикселей, например:
Движение спрайтов в пространстве
111
ва налево. Посмотрите на рис. 8.1, где схематически представлен механизм дви( жения спрайта по различным осям со скоростью в 3 пикселя.
// Îáíîâëÿåì ñîñòîÿíèå èãðû protected override void Update(GameTime gameTime) { spritePosition.Y += 3; }
В этом примере мы на каждой итерации игрового цикла, то есть за один кадр, увеличиваем позицию спрайта на 3 пикселя. Циклическое увеличение позиции спрайта по оси Y на 3 пикселя в каждой итерации игрового цикла (перерисовка спрайта на новом месте) создаст эффект движения спрайта по экрану сверху вниз. Получается, что значение в 3 пикселя задает фактическую скорость для дви( жения спрайта по экрану. Меньшее от 3 пикселей значение (например, 1 пиксель) уменьшит скорость перемещения спрайта по экрану, а большее значение (напри( мер, 5 пикселей) соответственно увеличит скорость движения спрайта. В целом выбирать скорость для движения объектов в игре необходимо аккуратно и на ос( нове большого количества тестов игры. Дело в том, что очень много хороших и красивых игр из(за неправильно выбранной скорости объектов и главного героя проигрывают менее удачным играм. Если выбрать маленькую скорость для объ( екта, то пользователю может казаться, что игра несколько тормозит и лишена ди( намики. Если, в свою очередь, скорость будет сильно большой, то осуществлять контроль над игрой будет чрезвычайно трудно. Но в любом случае все зависит именно от той задачи, которую вы реализовываете в вашей игре. Задавая скорость только по одной оси координат, вы соответственно будете передвигать спрайт вдоль этой оси, а если задавать скорость по обеим осям коор( динат, то спрайт будет перемещаться под определенным углом. Например, если скорость по осям X и Y равна по 3 пикселя, то этот угол будет равен четко 45 гра( дусам. Если используются неравные значения по осям, то угол движения будет либо меньше, либо больше. Дополнительно можно использовать и отрицательные значения для скорости, в этом случае позиция спрайта будет уменьшаться. На( пример, в приведенном выше примере это будет выглядеть следующим образом: Vector2 spritePosition; spritePosition.X = 100; spritePosition.Y = 300; ... // Îáíîâëÿåì ñîñòîÿíèå èãðû protected override void Update(GameTime gameTime) { spritePosition.Y -= 3; }
В этом исходном коде мы получаем движение спрайта снизу вверх по оси Y. Если такой подход использовать для оси X, то движение будет происходить спра(
Рис. 8.1. Техника движения спрайтов на экране
Надеюсь, теперь смысл перемещения спрайтов в пространстве вам стал поня( тен, поэтому мы можем смело переходить к работе над проектами этой главы. В частности, вашему вниманию будут предложены два проекта: MoveSprite и MoveSpriteArray. В первом проекте MoveSprite нашей целью будет перемещение спрайта по экрану сверху вниз. При этом как только спрайт будет достигать нижней части кромки экрана, то он будет исчезать из нашего поля зрения, поскольку его теку( щая позиция станет уже за пределами нашей зоны видимости. Чтобы корректно обрабатывать данную ситуацию, мы создадим простой механизм, который дол( жен будет следить за спрайтом на экране, и как только объект исчезнет с экрана, ему будет назначаться новая позиция в верхней части дисплея и перемещение во( зобновится снова в том же ключе. Таким образом, спрайт в игре будет двигаться циклично сверху вниз. Во втором проекте MoveSpriteArray задача несколько усложнится, и в игру добавятся еще четыре дополнительных спрайта. В итоге в игре задействуются сразу пять различных объектов. Для всех пяти объектов будут организованы ме( ханизм обработки ситуации выхода за пределы экрана в нижней части дисплея и установка на новые позиции. Теперь перейдем к новому проекту MoveSprite, который вы должны создать на базе последнего проекта предыдущей главы.
112
Проект MoveSprite
Движение спрайтов в пространстве
8.1. Проект MoveSprite Прежде всего в новом сформированном проекте MoveSprite в классе Sprite необходимо добавить новую переменную speedSprite, которая будет задавать скорость движения спрайта в пикселях. public Vector2 speedSprite = new Vector2(0, 6);
В этом коде мы задаем скорость по оси X, равную нулю, а скорость по оси Y – равную 6 пикселям. Это означает, что по оси X положение спрайта будет оставать( ся всегда неизменным, а вот по оси Y на каждой новой итерации или одном прохо( де метода Update() класса Game1 текущее положение спрайта будет увеличи( ваться на 6 пикселей. Теперь перейдем к исходному коду класса Game1 проекта MoveSprite. По( смотрим на весь исходный код этого класса в листинге 8.1, после чего приступим к его детальному анализу. Исходный код проекта MoveSprite находится на ком( пакт(диске в папках Code\Chapter8\MoveSprite. //========================================================================= /// <summary> /// Ëèñòèíã 8.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 8 /// Ïðîåêò: MoveSprite /// Êëàññ: Game1 /// Äâèæåíèå ñïðàéòà ïî ýêðàíó /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace MoveSprite { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch;
113
Sprite sprite; private Texture2D background1; private Texture2D background2; Random rand = new Random(); /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; sprite = new Sprite(12, 10); } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { sprite.spritePosition = new Vector2(300, -200); base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); sprite.Load(content, “Content\\Textures\\sprite”); background1 = content.Load(“Content\\Textures\\background1”); background2 = content.Load(“Content\\Textures\\background2”); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } }
114
Проект MoveSprite
Движение спрайтов в пространстве
/// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit();
Системный класс Random позволяет в программах генерировать для заданной переменной какое(то новое случайное значение. В состав класса входит метод Next(), который при каждом новом вызове формирует последовательность слу( чайных чисел. Метод Next() имеет три следующие модификации: Random.Next () – возвращает положительное случайное число; Random.Next(int32) – возвращает положительное случайное число, где единственный параметр метода – это верхняя граница или максималь( ное число, выше которого не может быть сгенерировано новое значение. Например, если вызвать метод вот так: Next(20), – то сгенерированное число не будет выходить из диапазона от 0 до 20; Random.Next(int32, int32) – этот вид метода с двумя параметрами позволяет задавать диапазон от выбранного минимального числа до мак( симального числа. Например, вызов метода со значениями Next(15, 20) задаст диапазон для случайного числа в пределах от 15 до 20.
double elapsed = gameTime.ElapsedGameTime.TotalSeconds; sprite.UpdateFrame(elapsed); MoveSprite(); base.Update(gameTime); } /// <summary> /// Äâèæåíèå ñïðàéòà ïî âåðòèêàëè /// <summary> public void MoveSprite() { sprite.spritePosition += sprite.speedSprite; if (sprite.spritePosition.Y > Window.ClientBounds.Height) { sprite.spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width-300-sprite.spriteTexture.Width/12),-200); } } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); sprite.DrawAnimationSprite(spriteBatch); spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); spriteBatch.End(); base.Draw(gameTime); } } }
В самом начале исходного кода класса Game1 происходит объявление гло( бальных объектов и добавляется новая строка кода. Random rand = new Random();
115
Класс Random и метод Next() нам будут необходимы для задания новой по( зиции на экране монитора в методе MoveSprite() класса Game1. Это новый ме( тод, который добавляется в текущий проект и выглядит следующим образом: public void MoveSprite() { sprite.spritePosition += sprite.speedSprite; if (sprite.spritePosition.Y > Window.ClientBounds.Height) { sprite.spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width-300-sprite.spriteTexture.Width/12),-200); } }
В первой строке кода метода MoveSprite() мы изменяем позицию объекта по оси Y, а значит, перемещаем спрайт на экране сверху вниз. Если развернуть эту строку записи (чтобы вам было понятно), то исходный код движения спрайта бу( дет выглядеть следующим образом: sprite.spritePosition.X += sprite.speedSprite.X; // ãäå ñêîðîñòü ïî X ðàâíà 0 sprite.spritePosition.Y += sprite.speedSprite.Y; // ãäå ñêîðîñòü ïî Y ðàâíà 6
Такая запись также может иметь место в исходном коде метода MoveSprite(), но представленный первый вариант несколько профессиональнее и быстрее ра( ботает. Оставшиеся три строки кода метода MoveSprite() обрабатывают ситуацию выхода спрайта за пределы экрана с нижней стороны дисплея. Как только пози( ция спрайта оказывается больше, чем максимальный размер высоты экрана, то в исходном коде метода MoveSprite() происходит вход в конструкцию if/ else, где определяется новая точка вывода спрайта на экран.
116
Проект MoveSpriteArray
Движение спрайтов в пространстве
117
sprite.spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width-300-sprite.spriteTexture.Width/12),-200);
Здесь мы используем метод Next(), с помощью которого при определении координаты по оси X задаем случайное значение в диапазоне от 10 пикселей до Window.ClientBounds.Width – 300-sprite.spriteTexture.Width/12 или в числовом эквиваленте 1024–300-îäèí ôðåéì àíèìàöèîííîé ïîñëåäîâàòåëüíîñòè пикселей, что позволяет нам при каждом новом определении значения точки вывода спрайта на экран по оси X задавать новое значение в пре( делах видимой области рамки фона. С выбором случайных значений для новой позиции спрайта игрок не будет знать, в каком месте должен выводиться спрайт на экран, и впоследствии не смо( жет приспособиться к вашей игре. Новое место будет всегда неожиданным как для игрока, так, собственно, и для вас, потому что используется метод Next() с двумя параметрами, задающими определенный диапазон значений. Дополни( тельные 300 пикселей, которые мы отняли от ширины дисплея, отводятся на ин( формационную рамку игры, а также мы отнимаем ширину одного фрейма, дабы спрайт не заходил за саму рамку. Что касается задания координаты по оси Y, то она, как вы заметили, отрица( тельна. Это позволяет рисовать спрайт в верхней части дисплея за пределами ви( димости экрана и затем перемещать спрайт вниз (рис. 8.2). Таким образом дости( гается эффект плавного появления, или выезда, спрайта из верхней части экрана и всего игрового пространства. Это красиво, эффектно, и если этого не делать и рисовать спрайт в положительных значениях оси Y, то объекты будут появляться резко и как бы ни с того ни с сего. Так делать нельзя, по крайней мере, в нашей игре точно. В других играх могут быть свои задачи, поэтому этот подход имеет право на жизнь. После создания метода MoveSprite() необходимо поместить вызов этого метода в метод Update(GameTime gameTime) класса Game1. Тогда на каждой итерации игрового цикла будет происходить вызов метода MoveSprite() и со( ответственно обновление состояния спрайта, а также обработка ситуации с выхо( дом спрайта из зоны видимости. Исходный код проекта вы найдете на компакт( диске в папке Code\Chapter8\MoveSprite, а мы переходим к рассмотрению следующего проекта MoveSpriteArray.
8.2. Проект MoveSpriteArray Задача этого проекта заключается в том, чтобы добавить в игру дополнительные объекты. Сделать это можно несколькими способами, но самый «продвинутый» и профессиональный способ сводится к организации простого массива данных. По( этому в проекте MoveSpriteArray мы реорганизуем способ объявления и созда( ния спрайта на создание массива данных, состоящего из пяти спрайтов. Для этого в исходном коде класса Game1 проекта MoveSpriteArray вместо одиночного объявления спрайта появляется объявление массива спрайтов.
Рис. 8.2. Назначение спрайту новой позиции в игровом пространстве
Sprite[] sprite = new Sprite[5];
Здесь мы объявляем массив данных, исчисляемый пятью спрайтами. Все эти пять спрайтов, а точнее каждый из спрайтов будет загружать в игру свое уникаль( ное изображение и представлять тот или иной объект. Каждый спрайт использует анимационную последовательность для создания динамики в игре, посмотрите на рис. 8.3, где представлены пять изображений, загружаемых в игру. Все изображения, загружаемые в игру, размещаются в рабочем каталоге про( екта в папках Content\Textures. Не забывайте, что все пять рисунков необходимо явно добавить в проект посредством команд Add ⇒ Exiting Item, как мы это дела( ли в предыдущих главах. Далее в конструкторе класса Game1 происходит создание пяти объектов. Для этих целей используется обычный цикл for. for (int i = 0; sprite.Length > i; i++) { sprite[i] = new Sprite(12, 10); }
118
Проект MoveSpriteArray
Движение спрайтов в пространстве sprite[0].Load(content, sprite[1].Load(content, sprite[2].Load(content, sprite[3].Load(content, sprite[4].Load(content,
119
“Content\\Textures\\0”); “Content\\Textures\\1”); “Content\\Textures\\2”); “Content\\Textures\\3”); “Content\\Textures\\4”);
Названия для изображений я специально задал в виде цифровых значений, чтобы был понятен механизм загрузки графики для каждого элемента массива данных. Кстати, поскольку имена изображений совпадают с данными массива, то можно реорганизовать вышеприведенный код в цикл, но эти действия выполняй( те самостоятельно – домашнее задание ;) Теперь перейдем к первичной установке всех спрайтов на свои игровые пози( ции, которые реализованы в методе Initialize(). protected override void Initialize() { j = 0; for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 500), j = j - 300); } base.Initialize(); }
Рис. 8.3. Изображения, загружаемые в игру
Единственное условие в этой конструкции кода заключается в том, чтобы все изображения имели одинаковую анимационную последовательность, то есть ко( личество фреймов всех изображений должно быть одинаково (как у нас в игре). Если одно и более изображений имеют различное количество фреймов, то придет( ся загружать каждый спрайт по отдельности, сделать это можно, например, сле( дующим образом: sprite[0] sprite[1] sprite[2] sprite[3] sprite[4]
= = = = =
new new new new new
Sprite(5, 3); Sprite(8, 2); Sprite(2, 2); Sprite(3, 1); Sprite(15, 2);
После создания массива данных переходим к методу LoadGraphicsContent() и к загрузке в массив данных графических изображений.
Для инициализации используется цикл. В качестве координаты по оси X для каждого спрайта применяется механизм случайного выбора позиции по ширине игровой области экрана. Этот механизм мы подробно рассмотрели в предыдущем проекте. А вот установка точки отсчета по оси Y проходит в несколько другом ключе с использованием переменной j = j - 300. Здесь мы используем промежуточную переменную j, которая инициализиру( ется нулевым значением. На каждой итерации цикла значение этой переменной уменьшается на 300 пикселей, что позволяет установить все спрайты как бы друг за другом на расстоянии 300 пикселей, и они не накладываются один на другой (рис. 8.4). Этот простой алгоритм позволяет нам избежать наложений спрайтов друг на друга. Далее в методе MoveSprite() переделываем код для движения массива объектов. public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition += sprite[i].speedSprite; if (sprite[i].spritePosition.Y > Window.ClientBounds.Height) {
120
Проект MoveSpriteArray
Движение спрайтов в пространстве
121
protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite[i].UpdateFrame(elapsed); } MoveSprite(); base.Update(gameTime); }
Для обновления анимационной последовательности методом UpdateFrame() используется цикл. Точно такой же цикл мы применяем и в методе Draw() для рисования спрайтов на экране. Полный исходный код класса Game1 приведен в листинге 8.2, а полный код проекта находится на компакт(диске в папках Code\Chapter8\MoveSpriteArray.
Рис. 8.4. Установка спрайтов на игровые позиции в момент первого запуска игры
sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[i].spriteTexture.Width / 12), -500); } } }
Обработка ситуации с выходом спрайта из зоны видимости реализована так же, как и в предыдущем проекте, но по оси Y задается значение в –500 пикселей. Делается это для того, чтобы уже исчезнувшие с экрана спрайты ставились на но( вые позиции повыше (подальше) и дали возможность пройти всем предыдущим спрайтам, еще не достигнувшим конца экрана. Затем в методе Update() класса Game1 мы вызываем метод UpdateFrame() класса Sprite для обновления анимационной последовательности спрайтов и метод MoveSprite() для движения спрайтов по экрану.
//========================================================================= /// <summary> /// Ëèñòèíã 8.2 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 8 /// Ïðîåêò: MoveSpriteArray /// Êëàññ: Game1 /// Ìàññèâ ñïðàéòîâ /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace MoveSpriteArray { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics;
122
Проект MoveSpriteArray
Движение спрайтов в пространстве
ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch; Sprite[] sprite = new Sprite[5]; private Texture2D background1; private Texture2D background2; Random rand = new Random(); int j; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; for (int i = 0; sprite.Length > i; i++) { sprite[i] = new Sprite(12, 10); } } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { j = 0; for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 500), j = j - 300); } base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); background1 = content.Load(“Content\\Textures\\background1”); background2 = content.Load(“Content\\Textures\\background2”); sprite[0].Load(content, “Content\\Textures\\0”); sprite[1].Load(content, “Content\\Textures\\1”); sprite[2].Load(content, “Content\\Textures\\2”); sprite[3].Load(content, “Content\\Textures\\3”);
sprite[4].Load(content, “Content\\Textures\\4”); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite[i].UpdateFrame(elapsed); } MoveSprite(); base.Update(gameTime); } /// <summary> /// Äâèæåíèå ñïðàéòà ïî âåðòèêàëè /// <summary> public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition += sprite[i].speedSprite; if (sprite[i].spritePosition.Y > Window.ClientBounds.Height) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[i].spriteTexture.Width / 12), -500); } } } /// <summary>
123
124
Движение спрайтов в пространстве
/// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); for (int i = 0; sprite.Length > i; i++) { sprite[i].DrawAnimationSprite(spriteBatch); } spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); spriteBatch.End(); base.Draw(gameTime); } } }
Глава 9 Устройства ввода В этой главе рассматриваются два проекта: Platform и PauseGame. В первом проекте мы разберем обработку событий, получаемых с устройства ввода или кла( виатуры. В игру у нас добавится новый элемент – платформа, которая будет ло( вить объекты, падающие с неба. Для того чтобы двигать платформу в определен( ном направлении, мы будем использовать некоторые клавиши клавиатуры. Второй проект этой главы – PauseGame – реализует механизм паузы в игре. Требования к этому механизму чрезвычайно просты: необходимо по нажатии од( ной из клавиш остановить работу игры, а потом по нажатии все той же клавиши запустить игру вновь. При этом игра должна продолжать работать именно с того места, на котором была остановлена. Теперь перейдем к исходным кодам и начнем работать над первым проектом Platform.
9.1. Проект Platform По задумке в нашей игре мы обязаны ловить всех людей, падающих с неба в про( пасть, с помощью платформы. Дополнительно с неба, кроме людей, также падают различные запчасти сгоревшего самолета, и от этих частей нам необходимо увора( чиваться. Платформа находится в нижней части экрана и ездит горизонтально по тросу слева направо на одном уровне. Чтобы управлять такой платформой, можно использовать компьютерную клавиатуру, джойстик или мышь. В игре мы будем использовать клавиатуру (работать с мышью вы научитесь в главе 12). В качестве управляющих клавиш на клавиатуре используются клави( ши с командами Up (Вверх), Down (Вниз), Left (Влево) и Right (Вправо) с нане( сенными на них стрелками по направлению движения. Нам понадобятся только команды Влево и Вправо, поскольку платформа будет всегда находиться в ниж( ней части экрана и двигаться только горизонтально на одном уровне. Изображение платформы состоит из одного фрейма, а это значит, что приме( няется неанимированный спрайт. Платформу можно нарисовать как угодно, но она была нарисована в виде тележки с матрацем, как показано на рис. 9.1. Сам рисунок располагается в рабочем каталоге проекта в папках Content\Textures, механизм добавления графического изображе( ния в проект остается прежним.
Рис. 9.1. Платформа
126
Проект Platform
Устройства ввода
Начнем работу над проектом Platform и добавим в исходный код класса Game1 в область глобальных переменных объявления нового объекта platform класса Sprite.
platform.spritePosition.X -= 10; else if (keyboardState.IsKeyDown(Keys.Right)) platform.spritePosition.X += 10; // Îáðàáîòêà ñòîëêíîâåíèé ñ ëåâîé è ïðàâîé ÷àñòüþ ýêðàíà if(platform.spritePosition.X < 10) platform.spritePosition.X = 10; else if(platform.spritePosition.X > Window.ClientBounds.Width - 400) platform.spritePosition.X = Window.ClientBounds.Width - 400;
Sprite platform;
Объект platform не будет анимированным, поэтому при создании объекта используется неанимированный конструктор класса Sprite. platform = new Sprite();
Затем в методе LoadGraphicsContent() класса Sprite происходит за( грузка изображения платформы в игру. platform.Load(content, “Content\\Textures\\platform”);
Далее платформе в методе Initialize() класса Sprite задается позиция на экране монитора. platform.spritePosition = new Vector2(Window.ClientBounds.Width / 2, Window.ClientBounds.Height - 90);
По оси X выбирается место примерно посередине экрана, в целом это положе( ние по оси может быть любым. Другое дело – положение платформы по оси Y. Нам нужно, чтобы платформа располагалась четко на тросе, который нарисован на фоновом изображении, поэтому от нижней части дисплея отнимается нужное количество пикселей и платформа встает точно на трос. Эта операция несложна – просто когда рисовался задний фон, а вместе с ним и трос, то было сразу просчита( но количество необходимых пикселей. В любом случае, если вы ошибетесь и плат( форма не станет четко на трос, ее всегда можно подвинуть, изменив отнимаемое значение от высоты экрана. При этом если бы я делал коммерческую игру из этого проекта, то обязательно добавил бы дополнительных красивостей. Например, платформу можно аними( ровать или приделать ей колесики, которые будут крутиться при движении. Сам трос можно сделать отдельным спрайтом и тоже анимировать, или отказаться от платформы и троса и создать мост, рельсы, помпу, лодку на воде, тележку, ковер( самолет, летающий в воздухе, или что(то еще, фантазировать на самом деле в этом плане можно до бесконечности. Для реализации движения платформы на экране в классе Game1 создается метод MovePlatform(), код которого выглядит следующим образом. public void MovePlatform() { // Äâèæåíèå ïëàòôîðìû âëåâî è âïðàâî if(keyboardState.IsKeyDown(Keys.Left))
127
}
Метод MovePlatform() состоит из двух независимых блоков исходного кода. В первом блоке происходит обработка нажатий клавиш с командами Влево и Вправо. Для обработки событий, получаемых с клавиатуры, используются кон( струкция кода if/else и метод IsKeyDown(). В переводе на русский язык эта конструкция обозначает следующее: Åñëè íàæàòà êëàâèøà Âëåâî, òî: Ïîçèöèÿ ïëàòôîðìû ïî îñè Õ óìåíüøàåòñÿ íà 10 ïèêñåëåé À åñëè íàæàòà êëàâèøà Âïðàâî, òî: Ïîçèöèÿ ïëàòôîðìû ïî îñè Õ óâåëè÷èâàåòñÿ íà 10 ïèêñåëåé
Уменьшение или увеличение позиции по оси X приводит к движению плат( формы по горизонтали влево и вправо. Заданная скорость в размере 10 пикселей за один кадр тестировалась много раз в игре и оказалась наиболее удачным значе( нием. Второй блок исходного кода обрабатывает ситуацию в столкновении плат( формы с левой и правой частями экрана. Если такое столкновение имеет место, то по оси X спрайту присваиваются значения, равные соответственно левой и правой кромкам экрана (в правой части мы не забываем о размере рамки). То есть спрайт при столкновении с двумя крайними точками экрана остается на месте без движе( ния. Делается это для того, чтобы платформа не уезжала из нашей области види( мости и все время оставалась на экране. Метод MovePlatform() вызывается в методе Update() класса Game1, где происходит постоянное обновление состояния игры. В конце исходного кода класса Game1 в методе Draw() платформа рисуется на экране. Поскольку пози( ция платформы platform.spritePosition постоянно меняется, то в методе Draw() платформа перерисовывается постоянно в новом месте. Полный исход( ный код класса Game1 представлен в листинге 9.1. На компакт(диске рассмотрен( ный пример находится в папке Code\Chapter9\ Platform. //========================================================================= /// <summary> /// Ëèñòèíã 9.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 9
128
Проект Platform
Устройства ввода
/// Ïðîåêò: Platform /// Êëàññ: Game1 /// Äîáàâëÿåì ïëàòôîðìó /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace Plarform { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch; Sprite[] sprite = new Sprite[5]; private Texture2D background1; private Texture2D background2; Random rand = new Random(); int j; Sprite platform; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; for (int i = 0; sprite.Length > i; i++) { sprite[i] = new Sprite(12, 10); } platform = new Sprite(); } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { for (int i = 0; sprite.Length > i; i++)
129
{ sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 500), j = j - 300); } platform.spritePosition = new Vector2(Window.ClientBounds.Width / 2, Window.ClientBounds.Height - 90); base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); background1 = content.Load(“Content\\Textures\\background1”); background2 = content.Load(“Content\\Textures\\background2”); platform.Load(content, “Content\\Textures\\platform”); sprite[0].Load(content, “Content\\Textures\\0”); sprite[1].Load(content, “Content\\Textures\\1”); sprite[2].Load(content, “Content\\Textures\\2”); sprite[3].Load(content, “Content\\Textures\\3”); sprite[4].Load(content, “Content\\Textures\\4”); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite[i].UpdateFrame(elapsed); } MoveSprite(); MovePlatform();
130
Проект PauseGame
Устройства ввода
131
} }
base.Update(gameTime); } } /// <summary> /// Äâèæåíèå ñïðàéòà ïî âåðòèêàëè /// <summary> public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition += sprite[i].speedSprite; if (sprite[i].spritePosition.Y > Window.ClientBounds.Height) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[i].spriteTexture.Width / 12), -500); } } } /// <summary> /// Äâèæåíèå ïëàòôîðìû ïî ýêðàíó /// <summary> public void MovePlatform() { if (keyboardState.IsKeyDown(Keys.Left)) platform.spritePosition.X -= 10; else if (keyboardState.IsKeyDown(Keys.Right)) platform.spritePosition.X += 10; if (platform.spritePosition.X < 10) platform.spritePosition.X = 10; else if (platform.spritePosition.X>Window.ClientBounds.Width - 400) platform.spritePosition.X = Window.ClientBounds.Width - 400; } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); for (int i = 0; sprite.Length > i; i++) { sprite[i].DrawAnimationSprite(spriteBatch); } spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); platform.DrawSprite(spriteBatch); // âåðõíèé ñëîé íàä òðîñîì spriteBatch.End(); base.Draw(gameTime);
После того как вы откомпилируете и запустите проект Platform, вы увидите в нижней части платформу, которую можно передвигать клавишами Влево и Вправо. С неба на вас будут падать различные предметы и люди, которые вы долж( ны ловить платформой, но на этом этапе, естественно, поймать вам ничего и нико( го не удастся, поскольку в игре еще не описан механизм игровых столкновений. Этим мы займемся в следующей главе, а здесь добавим еще несколько строк кода, для того чтобы сделать в игре режим паузы.
9.2. Проект PauseGame Вводим в проект PauseGame две новые булевы переменные, объявление которых происходит в области глобальных переменных, чтобы переменные были доступ( ны на любом участке исходного кода класса Game1. private bool paused = false; private bool pauseKeyDown = false;
Эти две переменные будут отражать соответственно состояние паузы в игро( вом процессе и состояние нажатия заданной клавиши. На каждой новой итерации игрового цикла (метод Update()) мы будем опрашивать клавиатуру и узнавать, нажата определенная клавиша или нет. Для паузы избрана стандартная в этом случае клавиша с изображением английской буквы «P». Если клавиша «P» нажата, то переменной paused присваивается значение true. В этом случае создается код на проверку условия, чему именно равна пере( менная paused, и если ее значение равно true, то, значит, выполнение игрового цикла пропускается, если false, то игра продолжает свою работу (рис. 9.2). По( смотрите на исходный код класса Game1 проекта PauseGame в листинге 9.2. //========================================================================= /// <summary> /// Ëèñòèíã 9.2 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 9 /// Ïðîåêò: PauseGame /// Êëàññ: Game1 /// Ðåàëèçóåì â èãðå ïàóçó /// <summary> //========================================================================= #region Using Statements using System;
132
Проект PauseGame
Устройства ввода
133
graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; for (int i = 0; sprite.Length > i; i++) { sprite[i] = new Sprite(12, 10); } platform = new Sprite(); }
Рис. 9.2. Схема работы паузы в игре using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace PauseGame { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch; Sprite[] sprite = new Sprite[5]; private Texture2D background1; private Texture2D background2; Random rand = new Random(); int j = 0; Sprite platform; private bool paused = false; private bool pauseKeyDown = false; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() {
/// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 500), j = j - 300); } platform.spritePosition = new Vector2(Window.ClientBounds.Width / 2, Window.ClientBounds.Height - 90); base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); background1 = content.Load(“Content\\Textures\\background1”); background2 = content.Load(“Content\\Textures\\background2”); platform.Load(content, “Content\\Textures\\platform”); sprite[0].Load(content, “Content\\Textures\\0”); sprite[1].Load(content, “Content\\Textures\\1”); sprite[2].Load(content, “Content\\Textures\\2”); sprite[3].Load(content, “Content\\Textures\\3”); sprite[4].Load(content, “Content\\Textures\\4”); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } }
134
Проект PauseGame
Устройства ввода
/// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) {
else if (platform.spritePosition.X >Window.ClientBounds.Width -400) platform.spritePosition.X = Window.ClientBounds.Width - 400; } /// <summary> /// Ïàóçà â èãðå /// <summary> public void Pause() { if (keyboardState.IsKeyDown(Keys.P)) { pauseKeyDown = true; } else if (pauseKeyDown) { pauseKeyDown = false; paused = !paused; } }
keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); Pause(); if (paused == false) { double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite[i].UpdateFrame(elapsed); }
/// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
MoveSprite(); MovePlatform(); } base.Update(gameTime); }
spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); for (int i = 0; sprite.Length > i; i++) { sprite[i].DrawAnimationSprite(spriteBatch); } spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); platform.DrawSprite(spriteBatch); spriteBatch.End();
/// <summary> /// Äâèæåíèå ñïðàéòà ïî âåðòèêàëè /// <summary> public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition += sprite[i].speedSprite; if (sprite[i].spritePosition.Y > Window.ClientBounds.Height) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[i].spriteTexture.Width / 12), -500); } } } /// <summary> /// Äâèæåíèå ïëàòôîðìû ïî ýêðàíó /// <summary> public void MovePlatform() { if (keyboardState.IsKeyDown(Keys.Left)) platform.spritePosition.X -= 10; else if (keyboardState.IsKeyDown(Keys.Right)) platform.spritePosition.X += 10; if (platform.spritePosition.X < 10) platform.spritePosition.X = 10;
135
base.Draw(gameTime); } } }
Из блока кода, направленного на обработку паузы в состоянии игры, видно, что мы сначала определяем состояние переменной paused, и если ее значение равно false (нет паузы), то выполняется содержимое блока if. Если состояние булевой переменной paused изменилось на true (пауза в игре), то выполнение блока if пропускается, что равносильно необновлению состояния игры, то есть игра замирает. Чтобы выйти из этого состояния, достаточно второй раз нажать клавишу «Р», и в соответствии с исходным кодом метода Pause() значение буле( вой переменной paused изменяется на противоположное. Заметьте, что вызов метода Pause() в методе Update() совершается до обработки событий, полу( ченных с клавиши «Esc» – выход из программы. Делается это намеренно, чтобы пользователь в режиме паузы мог свободно выйти из игры и закрыть программу.
Структура BoundingBox
Глава 10 Игровые столкновения В играх объекты могут пересекаться друг с другом или сталкиваться между со( бой. Такой вид пересечения объектов называется игровым столкновением. Для определения столкновения объектов пишут исходный код, который создает своего рода детектор столкновений объектов. Если столкновение имеет место, тогда необходимо производить определенные действия. Обработка ситуации при столкновении двух и более объектов между собой в играх может быть раз( личной. В одних играх при столкновении вам придется уничтожать объекты, в других играх необходимо будет оттолкнуться от этого объекта и т. д. Все зави( сит именно от логики игры. В нашей игре все объекты падают с неба (сверху вниз). В нижней части дис( плея курсирует платформа, которая обязана ловить людей и уворачиваться от других падающих объектов. В связи с этим нам нужно реализовать механизм, ко( торый должен определять, коснулся ли один из объектов платформы или нет. После касания объекта и платформы мы будем устанавливать объект на новую позицию, за верхней кромкой экрана, и перемещать его опять вниз. На первый взгляд, проблема сложна, но на самом деле все решается достаточно легко и про( сто, в чем вы сейчас и убедитесь.
чивающие прямоугольники, надетые на спрайты, или нет. Если пересеклись, то объекты столкнулись, а значит, необходимо выполнять определенные события, если нет, то столкновение не имело места и ничего делать не нужно. Просто, не правда ли? Создать переменную структуры BoundingBox очень просто, как и просто оп( ределить размер ограничивающего прямоугольника, смотрим на приведенный пример блока кода. // Ñîçäàåì ñòðóêòóðíóþ ïåðåìåííóþ public BoundingBox bb; // Çàäàåì ðàçìåð ïðÿìîóãîëüíèêà bb.Min = new Vector3(10, 20, 0); bb.Max = new Vector3(60, 80, 0);
Структурные переменные Min и Max определяют две точки в пространстве, на основе которых происходит построение ограничивающего прямоугольника. Точ( ка Min задает левый верхний угол ограничивающего прямоугольника, что впо( следствии будет соответствовать левому верхнему углу изображения. В свою очередь, точка Max определяет правый нижний угол прямоугольника, что уже соот( ветствует правому нижнему углу изображения. На основе этих данных сервисами XNA в пространстве строится прямоугольник. Если этот прямоугольник подо( гнать к размеру спрайта, то мы получаем тот самый ограничивающий прямоу( гольник, представленный структурой BoundingBox (рис. 10.1).
10.1. Структура BoundingBox В XNA Framework детектором столкновений между объектами служит структура BoundingBox. Дословный перевод этой структуры звучит как ограничивающий прямоугольник. Идея определения столкновений структурой BoundingBox со( стоит в следующем. В игре специально создается структурная переменная структуры BoundingBox. Структура BoundingBox в своей сущности описывает невидимый прямоуголь( ник, а значит, структурная переменная и есть тот самый невидимый прямоуголь( ник. Размер прямоугольника вы определяете сами с помощью элементов структу( ры. Далее вы надеваете это прямоугольник на определенный спрайт, максимально подгоняя размер прямоугольника под размер спрайта. То есть фактически раз( мер ограничивающего прямоугольника, представленный переменной структуры BoundingBox, должен соответствовать размеру одного фрейма изображения спрайта. Такую операцию необходимо проделать с каждым спрайтом, участвующим в обработке событий по столкновению объектов. Затем встроенными механизма( ми структуры BoundingBox вы определяете, пересекаются между собой ограни(
137
Рис. 10.1. Создание ограничивающего прямоугольника
138
Проект Collision
Игровые столкновения
Чем плотнее вы наденете ограничивающий прямоугольник на спрайт, тем точнее будет обрабатываться столкновение между объектами. Дополнительно можно искусственно уменьшать или увеличивать ограничивающий прямоуголь( ник, чтобы соответственно уменьшить или увеличить зону столкновения. Здесь все зависит от игровых задач. Например, прямоугольник для платформы мы спе( циально уменьшим, чтобы столкновения были реалистичными. Единственное, о чем еще следует сказать, – это то, что элементы Min и Max структуры BoundingBox задаются трехмерным вектором, состоящим из трех ко( ординат осей X, Y и Z. Vector3(10, 20, 0);
В двухмерном пространстве ось Z присутствует, но она работает несколько иначе, чем в трехмерных играх. Поэтому для простых двухмерных спрайтов тре( тий параметр вектора ставится всегда в ноль и не используется. В трехмерных иг( рах ось Z используется, а в векторе обязательно задается значение по оси Z. Тогда мы получим уже не ограничивающий прямоугольник, а ограничивающий куб (все модели объемные), но об этом вы узнаете в свое время и в своей главе.
10.2. Проект Collision Итак, приступим к работе над проектом Collision, в котором мы добавляем об( работку столкновений между платформой и падающими с неба объектами. В лис) тинге 10.1 представлен полный программный код класса Game1. Сначала давайте посмотрим на весь исходный код этого класса, а затем перейдем к его подробному анализу. Остальные классы проекта Collision остаются неизменными, как и в предыдущих проектах игры. //========================================================================= /// <summary> /// Ëèñòèíã 10.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 10 /// Ïðîåêò: Collision /// Êëàññ: Game1 /// Èãðîâûå ñòîëêíîâåíèÿ /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace Collision { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch; Sprite[] sprite = new Sprite[5]; private Texture2D background1; private Texture2D background2; Random rand = new Random(); int j = 0; Sprite platform; private bool paused = false; private bool pauseKeyDown = false; public BoundingBox bbplatform; public BoundingBox[] bb = new BoundingBox[5]; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; for (int i = 0; sprite.Length > i; i++) { sprite[i] = new Sprite(12, 10); } platform = new Sprite(); } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 500), j = j - 300); } platform.spritePosition = new Vector2(Window.ClientBounds.Width / 2,
139
140
Проект Collision
Игровые столкновения MovePlatform(); Collisions();
Window.ClientBounds.Height - 90); base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); background1 = content.Load(“Content\\Textures\\background1”); background2 = content.Load(“Content\\Textures\\background2”); platform.Load(content, “Content\\Textures\\platform”); sprite[0].Load(content, “Content\\Textures\\0”); sprite[1].Load(content, “Content\\Textures\\1”); sprite[2].Load(content, “Content\\Textures\\2”); sprite[3].Load(content, “Content\\Textures\\3”); sprite[4].Load(content, “Content\\Textures\\4”); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); Pause(); if (paused == false) { double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite[i].UpdateFrame(elapsed); } MoveSprite();
141
} base.Update(gameTime); } /// <summary> /// Äâèæåíèå ñïðàéòà ïî âåðòèêàëè /// <summary> public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition += sprite[i].speedSprite; if (sprite[i].spritePosition.Y > Window.ClientBounds.Height) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[i].spriteTexture.Width / 12), -500); } } } /// <summary> /// Äâèæåíèå ïëàòôîðìû ïî ýêðàíó /// <summary> public void MovePlatform() { if (keyboardState.IsKeyDown(Keys.Left)) platform.spritePosition.X -= 10; else if (keyboardState.IsKeyDown(Keys.Right)) platform.spritePosition.X += 10; if (platform.spritePosition.X < 10) platform.spritePosition.X = 10; else if (platform.spritePosition.X>Window.ClientBounds.Width - 400) platform.spritePosition.X =Window.ClientBounds.Width - 400; } /// <summary> /// Ïàóçà â èãðå /// <summary> public void Pause() { if (keyboardState.IsKeyDown(Keys.P)) { pauseKeyDown = true; } else if (pauseKeyDown) { pauseKeyDown = false; paused = !paused; }
142
Проект Collision
Игровые столкновения
Window.ClientBounds.Width 300 - sprite[4].spriteTexture.Width / 12), -500);
} /// <summary> /// Ñòîëêíîâåíèÿ /// <summary> public void Collisions() { bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 25, 0); bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 25 + platform.spriteTexture.Height, 0);
} } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); for (int i = 0; sprite.Length > i; i++) { sprite[i].DrawAnimationSprite(spriteBatch); } spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); platform.DrawSprite(spriteBatch); spriteBatch.End();
for (int i = 0; bb.Length > i; i++) { bb[i].Min = new Vector3(sprite[i].spritePosition.X, sprite[i].spritePosition.Y, 0); bb[i].Max = new Vector3(sprite[i].spritePosition.X + sprite[i].spriteTexture.Width / 12, sprite[i].spritePosition.Y+ sprite[i].spriteTexture.Height, 0); }
base.Draw(gameTime);
if (bbplatform.Intersects(bb[0])) { sprite[0].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[0].spriteTexture.Width / 12), -500); }
}
if (bbplatform.Intersects(bb[1])) { sprite[1].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[1].spriteTexture.Width / 12), -500); }
public BoundingBox bbplatform; public BoundingBox[] bb = new BoundingBox[5];
if (bbplatform.Intersects(bb[2])) { sprite[2].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[2].spriteTexture.Width / 12), -500); } if (bbplatform.Intersects(bb[3])) { sprite[3].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[3].spriteTexture.Width / 12), -500); } if (bbplatform.Intersects(bb[4])) { sprite[4].spritePosition = new Vector2(rand.Next(10,
143
} }
Первое, что необходимо сделать в исходном коде класса Game1 – это создать переменные структуры BoundingBox.
В первой строке этого кода создается переменная bbplatform структуры BoundingBox, отвечающая за создание ограничивающего прямоугольника вокруг платформы. Во второй строке блока исходного кода происходит создание массива переменных структуры BoundingBox, состоящего из пяти элементов. Как вы уже догадались, эти пять ограничивающих прямоугольников, или пять структурных переменных bb[0]-bb[4], назначены для имеющихся в игре пяти спрайтов. После создания переменных структуры BoundingBox необходимо опреде( лить размеры ограничивающих прямоугольников для каждого спрайта игры. То есть нужно наложить один конкретный ограничивающий прямоугольник на один конкретный спрайт, четко подогнав его под прямоугольный размер самого спрай( та. Более того, необходимо также неотступно следовать за спрайтом, а значит, пе( ремещать ограничивающий прямоугольник в соответствии с изменившимися ко( ординатами спрайта, поскольку у нас все спрайты динамичны и двигаются по экрану.
144
Проект Collision
Игровые столкновения
Для этих целей создается отдельный метод Collisions(), который мы бу( дем вызывать непосредственно в игровом цикле. Метод большой и несколько за( путанный, поэтому я сейчас его прокомментирую и дополнительно разверну не( которые строки исходного кода с использованием простого языка. Смотрим, что получилось.
145
300 - sprite[0].spriteTexture.Width / 12), -500); } if (bbplatform.Intersects(bb[1])) { sprite[1].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[1].spriteTexture.Width / 12), -500); }
public void Collisions() { /* Ïåðâàÿ ÷àñòü */
if (bbplatform.Intersects(bb[2])) { sprite[2].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[2].spriteTexture.Width / 12), -500); }
// Ñîçäàåì îãðàíè÷èâàþùèé ïðÿìîóãîëüíèê äëÿ ïëàòôîðìû bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 25, 0); bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 25 + platform.spriteTexture.Height, 0);
if (bbplatform.Intersects(bb[3])) { sprite[3].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[3].spriteTexture.Width / 12), -500); }
// Ñîçäàåì îãðàíè÷èâàþùèé ïðÿìîóãîëüíèê äëÿ sprite[0] bb[0].Min = new Vector3(sprite[0].spritePosition.X, sprite[0].spritePosition.Y, 0); bb[0].Max = new Vector3(sprite[0].spritePosition.X + øèðèíà îäíîãî ôðåéìà, sprite[0].spritePosition.Y + âûñîòà îäíîãî ôðåéìà, 0);
if (bbplatform.Intersects(bb[4])) { sprite[4].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[4].spriteTexture.Width / 12), -500); }
// Ñîçäàåì îãðàíè÷èâàþùèé ïðÿìîóãîëüíèê äëÿ sprite[1] bb[1].Min = new Vector3(sprite[1].spritePosition.X, sprite[1].spritePosition.Y, 0); bb[1].Max = new Vector3(sprite[1].spritePosition.X + øèðèíà îäíîãî ôðåéìà, sprite[1].spritePosition.Y + âûñîòà îäíîãî ôðåéìà, 0); } // Ñîçäàåì îãðàíè÷èâàþùèé ïðÿìîóãîëüíèê äëÿ sprite[2] bb[2].Min = new Vector3(sprite[2].spritePosition.X, sprite[2].spritePosition.Y, 0); bb[2].Max = new Vector3(sprite[2].spritePosition.X + øèðèíà îäíîãî ôðåéìà, sprite[2].spritePosition.Y + âûñîòà îäíîãî ôðåéìà, 0); // Ñîçäàåì îãðàíè÷èâàþùèé ïðÿìîóãîëüíèê äëÿ sprite[3] bb[3].Min = new Vector3(sprite[3].spritePosition.X, sprite[3].spritePosition.Y, 0); bb[3].Max = new Vector3(sprite[3].spritePosition.X + øèðèíà îäíîãî ôðåéìà, sprite[3].spritePosition.Y + âûñîòà îäíîãî ôðåéìà, 0); // Ñîçäàåì îãðàíè÷èâàþùèé ïðÿìîóãîëüíèê äëÿ sprite[4] bb[4].Min = new Vector3(sprite[4].spritePosition.X, sprite[4].spritePosition.Y, 0); bb[4].Max = new Vector3(sprite[4].spritePosition.X + øèðèíà îäíîãî ôðåéìà, sprite[4].spritePosition.Y + âûñîòà îäíîãî ôðåéìà, 0); /* Âòîðàÿ ÷àñòü */ // Ïðîâåðÿåì ïåðåñå÷åíèå âñåõ íàçíà÷åííûõ ïðÿìîóãîëüíèêîâ if (bbplatform.Intersects(bb[0])) { sprite[0].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width -
Исходный код метода Collisions() делится на два блока, или две разные по своим функциям части. В первой части мы для каждого объекта (платформа и пять спрайтов) создаем ограничивающие прямоугольники. Каждый прямоуголь( ник соответствует размеру конкретно взятого спрайта, возьмем, например, плат( форму и переменную Min. bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 25, 0);
В этих строках мы задаем начальную точку отсчета для платформы, что соот( ветствует у нас левому верхнему углу изображения платформы, а затем помещаем это значение в переменную Min. Дополнительно прибавляется еще 25 пикселей к значению по оси Y, чтобы уменьшить ограничивающий прямоугольник, а точнее опустить границу столкновения с платформой. Сделано это потому, что платфор( ма в своей центральной части имеет углубление, и чтобы столкновение было реа( листичным, мы уменьшаем прямоугольник (рис. 10.2). Затем необходимо дорисовать оставшуюся часть ограничивающего прямо( угольника, для этого достаточно задать координату для нижнего правого угла прямоугольника и сохранить ее значение в переменой Max.
146
Проект Collision
Игровые столкновения
147
if (bbplatform.Intersects(bb[0])) { sprite[0].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[0].spriteTexture.Width / 12), -500); }
Рис. 10.2. Ограничивающий прямоугольник для платформы bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 25 + platform.spriteTexture.Height, 0);
Вторая координата ограничивающего прямоугольника соответствует право( му нижнему углу изображения платформы. Все оставшиеся стороны прямоуголь( ника дорисовываются автоматически на основе этих двух точек. В итоге получа( ется, что мы надели на спрайт ограничивающий прямоугольник размером в сам спрайт, а поскольку метод Collisions() постоянно вызывается в итерациях иг( рового цикла, то и координаты прямоугольника будут постоянно изменяться и соответствовать текущему положению платформы на экране. Такой подход по( зволяет нам не только создавать ограничивающие прямоугольники, но и отслежи( вать положение спрайтов на экране монитора. Создание ограничивающих прямоугольников для падающих объектов проис( ходит аналогичным образом, но уже применительно к каждому элементу массива данных. // Ñîçäàåì îãðàíè÷èâàþùèé ïðÿìîóãîëüíèê äëÿ sprite[0] bb[0].Min = new Vector3(sprite[0].spritePosition.X, sprite[0].spritePosition.Y, 0); bb[0].Max = new Vector3(sprite[0].spritePosition.X + øèðèíà îäíîãî ôðåéìà, sprite[0].spritePosition.Y + âûñîòà îäíîãî ôðåéìà, 0);
При задании значений для переменных Max каждого из прямоугольников спрайта используются не числовые значения, а размер текстуры. Единственное, что нужно помнить, – это то, что при получении ширины изображения вам будет выдан весь фактический размер анимационной последовательности спрайта (все 12 фреймов), поэтому полученное значение необходимо поделить на количество имеющихся фреймов анимации. Вторая часть метода Collisions() направлена на определение столкно( вений между платформой и каждым спрайтом, падающим с неба. Для этого ис( пользуется метод Intersects() структуры BoundingBox. Дословный перевод названия метода обозначает пересечение. То есть этот метод определяет, пересек( лись между собой прямоугольники или нет, что равносильно определению столк( новения между двумя спрайтами.
Дословно на русском языке строка исходного кода if(bbplatform. Intersects(bb[0])) обозначает следующее: если ограничивающий прямо( угольник платформы будет пересекаться с ограничивающим прямоугольником спрайта под номером ноль, то необходимо выполнить заданные действия. В ка( честве заданных действий в столкновении мы определяем, что нужно убрать пря( моугольник с экрана и поставить его на новую игровую позицию в верхней части дисплея. После чего метод MoveSprite() заставит перемещаться этот спрайт в направлении сверху вниз или падать с неба. Если столкновение или пересечение ограничивающих прямоугольников не происходит, то спрайт уходит за пределы экрана и затем опять устанавливается на новую игровую позицию. В приведенном выше исходном коде для каждого спрайта происходит вызов метода Intersects() и соответственно идет слежение за пересечением всех прямоугольников между собой. Но поскольку мы имеем дело с массивом данных, то весь этот исходный код можно поместить в цикл, как первую часть, так и вто( рую часть метода Collisions(). В итоге мы значительно уменьшим и упростим исходный код данного метода. Помещаем все в цикл и смотрим, что получилось. public void Collisions() { /* Ïåðâàÿ ÷àñòü */ // Ñîçäàåì îãðàíè÷èâàþùèé ïðÿìîóãîëüíèê äëÿ ïëàòôîðìû bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y, 0); bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + platform.spriteTexture.Height, 0); // Ñîçäàåì îãðàíè÷èâàþùèå ïðÿìîóãîëüíèêè äëÿ ñïðàéòîâ for (int i = 0; bb.Length > i; i++) { bb[i].Min = new Vector3(sprite[i].spritePosition.X, sprite[i].spritePosition.Y, 0); bb[i].Max = new Vector3(sprite[i].spritePosition.X + sprite[i].spriteTexture.Width / 12, sprite[i].spritePosition.Y+ sprite[i].spriteTexture.Height, 0); } /* Âòîðàÿ ÷àñòü */ // Ïðîâåðÿåì ñòîëêíîâåíèÿ ìåæäó ïëàòôîðìîé è ñïðàéòàìè for (int i = 0; bb.Length > i; i++)
148
Игровые столкновения
{ if (bbplatform.Intersects(bb[i])) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[i].spriteTexture.Width / 12), -500); } } }
И напоследок один небольшой секрет. Когда вы работаете с циклом for, то, как правило, вызов этого цикла происходит следующим образом: for(int i = 0; 5 > i; i++) {…}
Но есть и другой, более интересный способ создания цикла for: for(int i = 5; —i >= 0;) {…}
Главным в этой записи является то, что подобная формулировка цикла позво( ляет работать циклу примерно на 10–13% быстрее, чем его стандартная запись. В компьютерных играх (мощные системные ресурсы) это, конечно, не столь ак( туально, но вот в мобильных играх я постоянно использую только такую запись. Теперь вы можете запустить рассматриваемый в этой главе проект и с по( мощью платформы ловить падающие с неба объекты. Постепенно все становится похожим на действительно полноценную игру. Единственное, что сейчас можно несколько видоизменить, – так это уменьшить ограничивающий прямоугольник для платформы. В частности, сделать его по высоте не в размер всей текстуры платформы, а, скажем, всего в пару пикселей (рис. 10.3). Полный размер высоты платформы кажется несколько большим, и иногда достаточно коснуться падаю( щего объекта боком – и считается, что вы его поймали. Если уменьшить площадь ограничивающего прямоугольника, а именно его боковую часть, то игровой про( цесс станет более реалистичным. bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 25 + 2, 0);
Рис. 10.3. Уменьшение ограничивающего прямоугольника платформы
Глава 11 Подсчет очков и вывод текста на экран В этой главе мы добавим в игру несколько дополнительных игровых элементов, которые позволят улучшить качество игрового процесса, да и сама игра приобре( тет некоторую цель. Что касается цели, то по сюжету игры пользователь обязан ловить и не ловить различные предметы, падающие с неба. В связи с этим в игре необходимо реализовать механизм, который будет вести простой подсчет набран( ных очков. Путей реализации этой задачи может быть несколько. Прежде всего вы должны проработать стратегию начисления очков игроку. Вариантов здесь много, все зависит от самой цели игры. Например, можно под( считывать очки только от тех предметов, которые действительно нужно ловить, а за пойманный предмет, который ловить было не нужно, – уменьшать баллы или даже отнимать жизнь у игрока. Можно также весь игровой процесс поставить на счетчик и отводить игроку определенное количество времени, за которое игрок должен набрать как можно больше очков. В этом случае за каждый пойманный предмет, который ловить было не нужно, можно отнимать у игрока очки или уменьшать игровое время. Система начисления очков в целом определяет всю иг( ровую стратегию и саму цель игры. В демонстрационном примере вам просто будет показано, как именно ведется подсчет очков. Мы будем подсчитывать каждый пойманный объект, организовав для каждого предмета свой счетчик. Но в своих играх вам нужно придумать более изощренную систему подсчета очков, чтобы пользователю было действительно интересно играть в вашу игру, поскольку каждый игровой процесс должен иметь определенную конечную цель. Во второй части главы мы рассмотрим проект Font, где выведем на экран все набранные пользователем очки. Для этих целей будет использоваться специаль( ный класс для работы со шрифтом, благодаря которому вы в своих играх сможете организовать вывод любого текста на экран. Класс BitmapFont доступен для свободного использования в любых программах, создан он программистом Gary Kacmarcik, работающим в корпорации Microsoft. Подход в использовании клас( сов или библиотек, написанных сторонними программистами, значительно облег( чает создание игры, уменьшая затраты – как временные, так и денежные. А вы на конкретном примере сможете ознакомиться с тем, как можно использовать сто( ронний код в своих программах.
150
Работа с текстом
Подсчет очков и вывод текста на экран
11.1. Подсчет очков Продолжаем улучшать игру, а в качестве базового проекта используем последний проект из десятой главы. В этой главе не будет создано два отдельных проекта по каждой теме, поскольку схематика подсчета очков вносит незначительные изме( нения в общий исходный код игры. Поэтому был сделан всего один проект под общим названием Font. Все изменения при подсчете очков касаются только ос( новного класса Game1. В работе с текстом добавится дополнительный класс BitmapFont, но об этом подробнее вы узнаете во второй части этой главы. Как всегда, исходный код этого проекта вы найдете на компакт(диске в папке Code\Chaper11\Font. Как мы уже выяснили, любой подсчет очков определяет всю игровую страте( гию. В нашем случае за каждый пойманный объект будет начисляться одно очко, иначе говоря, мы будем вести простой подсчет пойманных предметов. Для под( счета очков по каждому падающему объекту нам понадобится отдельная целочис( ленная переменная. Переходим к работе над проектом и в классе Game1 в области глобальных пе( ременных класса Game1 объявим пять новых переменных. int score0, score1, score2, score3, score4;
Все пять переменных пока не имеют никаких значений, и нам необходимо инициализировать их. В методе Initialize() зададим каждой переменной ну( левое значение, поскольку игрок еще не поймал ни одного объекта.
151
В этом коде видно, что как только платформа сталкивается с объектом или происходит пересечение двух ограничивающих прямоугольников, значение пере( менной score2 увеличивается на единицу. Для этого используется упрощенный вариант увеличения значения на единицу, но если развернуть эту строку кода, то она выглядит вот так: score2 = score2 + 1;
Точно такой же подход в подсчете очков необходимо использовать и для ос( тавшихся четырех объектов (см. листинг 11.1). Кстати, если вашей целью являет( ся только подсчет падающих предметов, то в коде можно создать массив данных для подсчета очков (если нужно считать очки по каждому объекту) или выделить одну переменную для общего подсчета всех очков. … // î÷êè ïî êàæäîìó îáúåêòó â ìàññèâå äàííûõ score[i] += 1; // ïîäñ÷åò îáùåãî êîëè÷åñòâà î÷êîâ äëÿ âñåõ îáúåêòîâ ðàçîì score += 1;
Здесь все определяет общая стратегия игры. Например, при столкновении платформы и объекта (который ловить не нужно) можно отнимать набранные баллы как по каждой переменной отдельно, так и по общему количеству очков. Score2 -= 1;
или // Îáíóëÿåì èìåþùèåñÿ î÷êè score0 = 0; score1 = 0; score2 = 0; score3 = 0; score4 = 0;
Теперь мы имеем пять переменных для каждого падающего с неба объекта. Чтобы начислить одно очко за каждый пойманный объект или подсчитать коли( чество пойманных объектов, нам необходимо при столкновении платформы с од( ним из объектов увеличить соответствующую перемену. Посмотрите на исход( ный код обработки столкновения для падающего с неба объекта под номером два. if (bbplatform.Intersects(bb[2])) { sprite[2].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[2].spriteTexture.Width / 12), -500); score2 += 1; }
score -= 1;
При этом если вы ведете подсчет жизней игрока, то за каждое ненужное столк( новение платформы и объекта, кроме уменьшения очков, можно также уменьшать игровую жизнь пользователя или даже уменьшать время, отведенное на игру. По( вторяюсь, все определяет игровая стратегия, которую необходимо очень тщатель( но продумать. Все, что касается подсчета очков, мы выяснили, и, как оказалось, дело это совсем не сложное, поэтому переходим ко второй части главы, цель которой со( стоит в том, чтобы научиться выводить на экран набранные пользователем очки и другую сопутствующую информацию.
11.2. Работа с текстом Рисовать текст на экране монитора в приложениях можно различными способа( ми. Самый простой способ – это использование системных классов XNA Frame( work, но тогда весь текст, а точнее шрифт будет иметь стандартный вид, и в краси(
152
Работа с текстом
Подсчет очков и вывод текста на экран
во оформленный интерфейс игры он может элементарно не вписываться и даже портить общее впечатление от игры. Поэтому обычно для игр рисуются отдель( ные шрифты, которые впоследствии загружаются в программный код. Графическое изображение шрифта представляет собой набор всех или почти всех букв алфавита, чисел, знаков препинания, специальных символов, выполнен( ных в одной стилистике. Каждый символ рисуется в отдельном фрейме, а затем все символы компонуются в единое графическое изображение (рис. 11.1). Затем в игре пишется определенный класс, который загружает изображение шрифта в про( граммный код. При выводе текста на экран происходят автоматическое считывание нарисованных символов с изображения (которые, естественно, совпадают с симво( лами выводимого текста) и показ строки текста, предложения, числовых значений на экране уже новым красиво оформленным шрифтом. Мы поступим точно так же, но сами делать ничего не будем, а используем готовое решение.
153
статочно его просто усовершенствовать. Такие решения могут предо) ставляться бесплатно, а могут предоставляться и на базе платных ли) цензионных отчислений (игровые движки). Соберите нужную информа) цию, уточните все детали и приступайте к работе. Сейчас на руках мы имеем готовое решение, и море благодарностей человеку по имени Gary Kacmarcik. Приступаем к работе над проектом, в котором будем вести подсчет очков и полученные результаты выводить на экран монитора. Прежде чем перейти к исходному коду, давайте сначала посмотрим, что представ( ляет собой готовое решение XNAExtras.
11.2.1. Готовое решение XNAExtras Библиотека XNAExtras (наверное, ее можно так назвать, пусть она и небольшая) содержит следующий набор функциональных элементов.
Рис. 11.1. Набор цифр, нарисованных в графическом редакторе
Работа программиста сводится не только к написанию исходного кода, но и к поиску уже готовых решений, и хорошо, если эти решения будут бесплатными. Поэтому я решил потратить немного рабочего времени на поиск уже готового ре( шения. Спустя буквально десять минут поиска было найдено абсолютно бесплат( ное решение с названием XNAExtras от программиста Gary Kacmarcik, работаю( щего в корпорации Microsoft. Это решение позволяет формировать изображения символов на основе установленных у вас в системе шрифтов. В новой версии XNA Game Studio Express, которая должна появиться в ап) реле)марте 2006 года, работа с текстом будет осуществляться встроен) ными механизмами студии. Аналогичная книга по программированию игр для Xbox 360, будет писаться по завершению этой книги, поэтому в рабо) те с текстом мы задействуем уже новые механизмы XNA Game Studio Express. Если вы желаете работать со встроенными средствами, то обра) титесь к книге «Программирование игр для приставки Xbox 360 в XNA Game Studio Express». В этой же книге для работы с текстом задейству) ется сторонняя библиотека, которая ко всему прочему еще и покажет вам технику подключения в игру сторонних наработок. Дополнительно в папке XNAExtras еще находился ряд интересных примеров по выводу текста в рамках, столкновению объектов в попиксельном режиме и многому другому. Все это добро вы можете найти на компакт(диске в папке Soft\XNAExtras. Прежде чем начинать разработку любого проекта, поищите готовые ре) шения по необходимой вам тематике. Не нужно изобретать велосипед, до)
Класс BitmapFont – этот класс реализует загрузку шрифтов в программу, вы( вод текста на экран с возможностью его форматирования. Загрузка шрифтов в программу производится из XML(файла, который, в свою очередь, загружа( ет графическое изображение с любым нарисованным шрифтом. XMLфайлы – поставляется несколько XML(файлов с решением XNAExtras для различных языков мира. Файл XML регулирует загрузку шрифта в игру, а также занимается его непосредственным форматированием, а точ( нее создает спецификацию, на основе которой класс BitmapFont работает с текстовыми строками и числами. Изображения – готовые изображения шрифтов для нескольких языков, включая даже японский. В примере используется достаточно простой систем( ный шрифт. Для того чтобы сгенерировать необходимый шрифт из графиче( ского изображения, необходимо воспользоваться утилитой BMFontGen, ко( торая есть в составе XNAExtras. BMFontGen – эта утилита, входящая в состав XNAExtras, позволяет гене( рировать на основе имеющихся у вас графических изображений подборку шрифтов и компоновать шрифт в единичный DDS(файл, который с по( мощью класса BitmapFont вы можете загрузить в игру.
11.2.2. Как это работает? Чтобы воспользоваться готовым решением XNAExtras, необходимо создать в проекте дополнительный класс BitmapFont и скопировать в него исходный код оригинального класса BitmapFont библиотеки XNAExtras. Затем добавить в ваш проект графическое изображение шрифта, а к нему – дополнительный XML(файл. Каждый XML(файл описывает свойства созданного шрифта. После этих манипуляций вы переходите к классу Game1, где создаете объект нового до( бавленного класса BitmapFont, и далее пользуетесь методами этого класса для
154
Подсчет очков и вывод текста на экран
вывода текста на экран, особо не заботясь о том, как устроен и как работает этот самый класс BitmapFont. Классическое решение черного ящика и объектно(ори( ентированного подхода реализации проектов на базе сторонних библиотек.
11.2.3. Как создать шрифт? Чтобы создать шрифт, необходимо воспользоваться командной строкой. Выпол( ните на компьютере в меню Пуск команды Все программы ⇒ Стандартные ⇒ Командная строка. Откроется окно командной строки. По умолчанию путь в окне командной строки указывает на папку Document and Settings\Имя пользовате ля. Чтобы не прописывать в командной строке путь к утилите BMFontGen, про( сто перенесите ее в эту папку с компакт(диска, и именно в этой папке в дальней( шем вы будете получать готовые шрифты. Для того чтобы создать шрифт, необходимо использовать определенные коман( ды, например следующая строка текста создает шрифт Courier New размером в 12 pt. bmfontgen -name “Courier New” -size 12 -output “courier”
Первая команда bmfontgen запускает работу программы BMFontGen. Вторая команда -name «Courier New» будет создавать изображение сим( волов из установленного в системе шрифта Courier New. Третья команда -size 12 задает размер создаваемого шрифта. И последняя часть строки – (output «courier» – определяет выходное название файла изображения и XML(файла. Используя такую запись, вы можете создать любой набор символов из установленных у вас в системе шрифтов. Все шрифты создаются белого цвета на черном фоне в формате PNG. В дальнейшем в своем исходном коде можно закрашивать шрифт любым цветом. Еще одна интересная команда с названием -range 0020-007e позволяет задавать диапазон символов, участвующих в создании шрифта, например:
Работа с текстом
155
11.2.4. Добавляем в проект необходимые компоненты Теперь давайте, чтобы ничего не упустить, в пошаговом режиме добавим в проект все перечисленные компоненты, а уже потом перейдем к работе с исходным ко( дом. Создайте в Visual C# новый проект или модифицируйте последний рассмот( ренный проект из десятой главы и переходите к выполнению следующих шагов (в примере у нас проект Font). 1. Сформируйте в проекте новый класс под названием BitmapFont. Для этого щелкните правой кнопкой мыши на названии проекта и в появившемся кон( текстном меню изберите команды Add ⇒ New Item. Откроется диалоговое окно Add New Item Font. В списке Templates выберите курсором шаблон Class, а в поле Name задайте имя классу как BitmapFont (рис. 11.2). Затем откройте на компакт(диске папку Soft\XNAExtras\XNA Samples\ Common, найдите файл BitmapFont.cs и скопируйте его содержимое в буфер обмена, вставив далее скопированный код в ваш новый класс с од( ноименным названием. 2. Далее необходимо явно добавить в проект файл XML и графическое изоб( ражение шрифта. И то и другое находится также на компакт(диске в папке Soft\XNAExtras\XNASamples\BitmapFontDemo\Content. В этой папке проект XNAExtras содержит много шрифтов для различных языков. Если вы работаете с русским, то необходимо выбрать файлы russian.xml, russian-0.png и russian-1.png. Но все эти файлы нужно добавить
bmfontgen -name “Century” -size 14 -output “centr” -range 0020-007e
Цифровые значения 0020-007e указывают на начало и конец диапазона сим( волов, входящих в состав создаваемого шрифта. Дело в том, что некоторые шриф( ты по умолчанию представляют только латинские символы от A до Z. Чтобы со( здать, например, кириллический шрифт, необходимо использовать команду: –range Íà÷àëî äèàïàçîíà ñèìâîëîâ – Êîíåö äèàïàçîíà ñèìâîëîâ.
Для каждого шрифта этот диапазон может быть разным, поэтому универсаль( ных рекомендаций по цифрам диапазона, к сожалению, нет. Дополнительно при создании шрифтов вы можете использовать еще ряд команд. Описание этих ко( манд можно найти в файле BMFontGen.html, который находится в каталоге вмес( те с программой. Рис. 11.2. Создаем в проекте новый класс BitmapFont
156
Работа с текстом
Подсчет очков и вывод текста на экран Рис. 11.3. Добавляем в проект файлы изображений и XML&файл
в проект явно, а не просто скопировать в рабочий ка( талог проекта. Для этого создаем в папке проекта Content новую папку с названием Font (чтобы систематизировать весь контент игры). Затем нажи( маем на ее названии правой кнопкой мыши и в по( явившемся контекстном меню выбираем команды Add ⇒ Existing Item. Через появившееся диалоговое окно Add Existing Item – Font добавляем в проект нужные три файла с компакт(диска (рис. 11.3). 3. После этих шагов необходимо совершить одно важное действие, без выполне( ния которого у вас будут проблемы в момент компиляции проекта. Все три до( бавленных файла – russian.xml, russian-0.png и russian-1.png – по умолчанию имеют защиту от копирования со значением Do not copy. Это значение для всех трех добавленных файлов нужно обязательно изменить на значение Copy if newer, то есть разрешить копирование. Если этого не сделать, то при компиляции проекта будет проявляться ошибка с малозна( чительными объяснениями, разобраться в которых без опыта достаточно сложно (рис. 11.4). Чтобы изменить в свойствах файлов значение Do not copy на значение Copy if newer, нужно открыть окно свойств отдельно взя( того файла. Для этого щелкните правой кнопкой мыши на названии файла (russian.xml, russian-0.png или russian-1.png) и в появившемся контекстном меню выберите команду Properties. В нижней части Solution Explorer откроется дополнительное окно Properties для конкретно выбран( ного файла. Затем в списке Copy to Output Directory изберите значение Copy if newer (рис. 11.5). Окно свойств можно не закрывать, достаточно вы( делить курсором следующий файл – и его свойства отобразятся в этом окне. 4. Данную операцию необходимо проделать для каждого из трех добавлен( ных файлов: russian.xml, russian-0.png и russian-1.png. Если вы добавляете и другие файлы для работы с прочими языками, то эта опе( рация должна быть выполнена по каждому отдельно взятому файлу. На
157
Рис. 11.5. Изменяем свойства файлов
этом все предварительные манипуляции окончены, и можно смело приступать к ра( боте с исходным кодом проекта Font.
11.2.5. Работа с текстом Очень полезно будет поверхностно просмот( реть исходный код нового класса BitmapFont. Изучать его не обязательно, главное – знать, как с этим классом работать. Мы детально разбирать( ся с классом BitmapFont тоже не будем (черный ящик он и есть черный ящик), а лишь используем необходимые нам методы этого класса для вывода текста на экран. Откройте проект Font и в классе Game1 объявите в области глобальных пере( менных новый объект font класса BitmapFont, который был добавлен нами в игру ранее: BitmapFont font;
После объявления объекта его нужно создать. Сделать это можно в конструк( торе класса Game1. font = new BitmapFont(“Content\\Font\\russian.xml”);
Создавая объект font в исходном коде приложения, вы автоматически загру( жаете файл XML, который регулирует загрузку шрифта в игру, а также определя( ет специфику вывода строк текста на экран. Если вы используете в игре несколько разных по размеру или языкам шрифтов, то вам необходимо создать несколько объектов класса BitmapFont и соответственно в каждый объект загрузить соот( ветствующий файл XML. После создания объекта font класса BitmapFont переходим к методу LoadGraphicsContent() и к загрузке в игру графического контента. В этом методе необходимо выполнить стандартную операцию (для класса BitmapFont) по выбору графического устройства. font.Reset(graphics.GraphicsDevice);
Рис. 11.4. Ошибка при компиляции
На этом все операции по созданию объекта font класса BitmapFont оконче( ны, и вы можете использовать методы этого класса для вывода любого текста на экран. Например, давайте нарисуем на экране строку текста. Сделаем это в тот момент, когда игрок нажимает на клавиатуре клавишу «Р», и выведем на экран
158
Работа с текстом
Подсчет очков и вывод текста на экран
слово «Пауза». Все, что связано с выводом на экран графики, пишется, как вы по( мните, в методе Draw(). if(paused == true) { font.DrawString(450, 300, Color.Black, “Ïàóçà”); }
Метод DrawString() класса BitmapFont выводит на экран строку текста, где первые два параметра задают точку вывода по осям X и Y для левого верхнего угла прямоугольника, внутри которого пишется сам текст. Высота прямоугольни( ка определяется автоматически высотой шрифта. Ширина прямоугольника также подстраивается под количество символов в строке, и эти действия за нас делает класс BitmapFont. Третий параметр закрашивает шрифт любым цветом на вы( бор, а последний параметр – это и есть строка текста, рисуемая на экране, которая будет замещаться буквами из графического изображения шрифта. Вот и все! Чтобы вывести на экран цифры, необходимо использовать вот такую схему: int fy = 245; font.DrawString(Window.ClientBounds.Width - 95, fy, Color.Black, “{0}”, score0); font.DrawString(Window.ClientBounds.Width - 95, fy += font.LineHeight + 12, “{0}”, score1); font.DrawString(Window.ClientBounds.Width - 95, fy += font.LineHeight + 12, “{0}”, score2); font.DrawString(Window.ClientBounds.Width -95,fy += font.LineHeight + 12, “{0}”, score3); font.DrawString(Window.ClientBounds.Width - 95, fy += font.LineHeight + 12, “{0}”, score4);
Здесь первые два параметра определяют точку вывода прямоугольника по осям X и Y, третий параметр не используется, а такой вид записи показывает на то, что будет выводиться числовое значение. Последний параметр есть число. Место вывода в игре текущих набранных очков было определено еще на этапе создания фонового изображения. По оси X это ширина дисплея минус 95 пиксе( лей, а по оси Y первая строчка определена на расстоянии 245 пикселей от верхней части экрана и затем каждая последующая строка удаляется на высоту шрифта плюс 12 пикселей на междустрочие. В классе BitmapFont имеется несколько разных методов DrawString() для вывода различного вида текста с форматированием и без него, в рамке и т. д. Поэкспериментируйте с этими методами ради практического интереса. Весь ис(
159
ходный код класса Game1 проекта Font, где мы подсчитываем очки, набранные пользователем, и рисуем на экране текст, приведен в листинге 11.1. Исходный код проекта Font вы найдете на компакт(диске в папке Code\Chapter11\Font. //========================================================================= /// <summary> /// Ëèñòèíã 11.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 11 /// Ïðîåêò: Font /// Êëàññ Game1 /// Äîáàâëÿåì òåêñò è ïîäñ÷åò î÷êîâ /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace Font { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch; Sprite[] sprite = new Sprite[5]; private Texture2D background1; private Texture2D background2; Random rand = new Random(); int j = 0; Sprite platform; private bool paused = false; private bool pauseKeyDown = false; public BoundingBox bbplatform; public BoundingBox[] bb = new BoundingBox[5]; BitmapFont font; int score0, score1, score2, score3, score4; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1()
160
Работа с текстом
Подсчет очков и вывод текста на экран
{ graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; for (int i = 0; sprite.Length > i; i++) { sprite[i] = new Sprite(12, 10); } platform = new Sprite(); font = new BitmapFont(“Content\\Font\\russian.xml”); } /// <summary> /// Èíèöèàëèçàöèèÿ /// <summary protected override void Initialize() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 500), j = j - 300); } platform.spritePosition = new Vector2(Window.ClientBounds.Width / 2, Window.ClientBounds.Height - 90); score0 = 0; score1 = 0; score2 = 0; score3 = 0; score4 = 0; base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); background1 = content.Load(“Content\\Textures\\background1”); background2 = content.Load(“Content\\Textures\\background2”); platform.Load(content, “Content\\Textures\\platform”); sprite[0].Load(content, “Content\\Textures\\0”); sprite[1].Load(content, “Content\\Textures\\1”); sprite[2].Load(content, “Content\\Textures\\2”); sprite[3].Load(content, “Content\\Textures\\3”); sprite[4].Load(content, “Content\\Textures\\4”); font.Reset(graphics.GraphicsDevice); } }
/// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); Pause(); if (paused == false) { double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite[i].UpdateFrame(elapsed); } MoveSprite(); MovePlatform(); Collisions(); } base.Update(gameTime); } /// <summary> /// Äâèæåíèå ñïðàéòà ïî âåðòèêàëè /// <summary> public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition += sprite[i].speedSprite; if (sprite[i].spritePosition.Y > Window.ClientBounds.Height) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[i].spriteTexture.Width / 12), -500); } } }
161
162
Работа с текстом
Подсчет очков и вывод текста на экран
/// <summary> /// Äâèæåíèå ïëàòôîðìû ïî ýêðàíó /// <summary> public void MovePlatform() { if (keyboardState.IsKeyDown(Keys.Left)) platform.spritePosition.X -= 10; else if (keyboardState.IsKeyDown(Keys.Right)) platform.spritePosition.X += 10; if (platform.spritePosition.X < 10) platform.spritePosition.X = 10; else if(platform.spritePosition.X> Window.ClientBounds.Width - 400) platform.spritePosition.X= Window.ClientBounds.Width - 400; } /// <summary> /// Ïàóçà â èãðå /// <summary> public void Pause() { if (keyboardState.IsKeyDown(Keys.P)) { pauseKeyDown = true; } else if (pauseKeyDown) { pauseKeyDown = false; paused = !paused; } } // Ñòîëêíîâåíèÿ public void Collisions() { bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 25, 0); bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 25 + 2, 0); for (int i = 0; bb.Length > i; i++) { bb[i].Min = new Vector3(sprite[i].spritePosition.X, sprite[i].spritePosition.Y, 0); bb[i].Max = new Vector3(sprite[i].spritePosition.X + sprite[i].spriteTexture.Width / 12, sprite[i].spritePosition.Y + sprite[i].spriteTexture.Height, 0); } if (bbplatform.Intersects(bb[0])) { sprite[0].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width -
300 - sprite[0].spriteTexture.Width / 12), -500); score0 += 1; } if (bbplatform.Intersects(bb[1])) { sprite[1].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[1].spriteTexture.Width / 12), -500); score1 += 1; } if (bbplatform.Intersects(bb[2])) { sprite[2].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[2].spriteTexture.Width / 12), -500); score2 += 1; } if (bbplatform.Intersects(bb[3])) { sprite[3].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[3].spriteTexture.Width / 12), -500); score3 += 1; } if (bbplatform.Intersects(bb[4])) { sprite[4].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[4].spriteTexture.Width / 12), -500); score4 += 1; } } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); for (int i = 0; sprite.Length > i; i++) { sprite[i].DrawAnimationSprite(spriteBatch); } spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); platform.DrawSprite(spriteBatch); spriteBatch.End();
163
164
Подсчет очков и вывод текста на экран if (paused == true) { font.DrawString(450, 300, Color.Black, “Ïàóçà”); } int fy = 245; font.DrawString(Window.ClientBounds.Width - 95, fy, Color.Black, “{0}”, score0); font.DrawString(Window.ClientBounds.Width - 95, fy += font.LineHeight + 12, “{0}”, score1); font.DrawString(Window.ClientBounds.Width - 95, fy += font.LineHeight + 12, “{0}”, score2); font.DrawString(Window.ClientBounds.Width -95,fy += font.LineHeight + 12, “{0}”, score3); font.DrawString(Window.ClientBounds.Width - 95, fy += font.LineHeight + 12, “{0}”, score4); base.Draw(gameTime);
} } }
Глава 12 Создаем игровое меню Ни одна хорошо сделанная компьютерная или консольная игра не обходится без меню. Меню – это не просто стартовая страница всего приложения, это хорошо отлаженный механизм, позволяющий пользователю управлять работой програм( мы. В связи с этим необходимо очень тщательно продумывать и планировать ра( боту меню. Старайтесь избегать множественных вложений, непонятных команд и лишних неоправданных диалоговых окон плана: А вы действительно хотите вый( ти, А вы точно не передумали и т. д. Все должно быть очень просто и в то же время красиво оформлено, и не забывайте о том, что страница меню – это первая страни( ца, на которую попадет игрок после вступления. Если ваше меню будет вызывать у человека полное уныние или он будет «блукать» по нему в поисках кнопки для старта игры, то ваша игра точно надолго не задержится на машине пользователя. В этой главе рассматриваются два проекта– Menu и MenuCursor. В обоих проектах мы изучим два разных способа создания интерактивного меню. В пер( вом проекте для управления работой меню будет использоваться клавиатура, а во втором задействуется компьютерная мышка. Что касается способов создания и реализации меню, то тут все зависит только от вашей фантазии. Можно приду( мать что угодно и сколько угодно, но главное – знать и понимать, как работает в целом вся система меню. В этой главе мы займемся изучением этой проблемати( ки и начнем с того, что разберемся с тем, где именно необходимо вызывать игро( вое меню и как правильно спланировать его вывод на экран монитора.
12.1. Планируем запуск меню Сейчас при запуске игры мы сразу попадаем в игровой процесс, минуя какие(либо заставки. Течение всего игрового процесса начинается с вызова метода Update() класса Game1, и на основании полученных данных метод Draw() рисует графику на экране. Посему именно в методе Update(), где происходит обновление состоя( ния игры, нам необходимо изначально запускать показ игрового меню, через кото( рое проводить старт или выход из игры. То есть сначала необходимо вывести на экран меню и только потом запускать течение игрового процесса. Меню должно стать механизмом управления всей игры. Методов реализации смены игровых состояний очень много. Мы воспользу( емся одним из алгоритмов, основанных на проверке состояния булевой перемен( ной menuState, которую объявим и инициализируем в исходном коде класса Game1. Эта переменная при старте игры будет иметь значение true. В этом слу(
166
Проект Menu
Создаем игровое меню
167
чае при старте игры в методе Update() достаточно создать проверку следующего условия: Åñëè menuState ðàâíî true òî ïîêàçûâàåì ìåíþ Åñëè íåò òî íà÷èíàåì èãðîâîé ïðîöåññ
или на языке программирования С#: if(menuState == true) { // ïîïàäàåì â ìåíþ èãðû } else{ // çàïóñêàåì èãðó }
Тогда при старте игры пользователь всегда будет попадать в меню, а уже с меню после выполнения заданной команды – запускать непосредственно сам иг( ровой процесс. Чтобы реализовать из меню запуск игры, достаточно по опреде( ленной команде просто изменить состояние переменной menuState с true на false – и тогда блок кода с вызовом меню будет пропускаться, а выполняться будет другой блок кода, следующий за ключевым словом else. Как видите, в тех( нике запуска меню особых сложностей нет. Теперь давайте перейдем к реализа( ции первого проекта с названием Menu и поговорим о том, каким способом, или, точнее, какими клавишами, мы будем управлять работой меню. В третьей части книги вашему вниманию будет предложен другой, более профессиональный подход смены игровых состояний.
12.2. Проект Menu Какие команды и клавиши у нас были отведены для управления игрой? Клавиша Esc закрывает игру, а клавиша с буквой P включает режим паузы, а ее повторное нажатие отменяет паузу в игре. Этого нам будет мало, придется добавить в игру еще несколько команд, но при этом необходимо строго разграничить их работу, поскольку, например, для выхода и запуска игры будет использоваться одна кла( виша Enter. Посмотрите на рис. 12.1, где схематично показан предлагаемый меха( низм функционирования игры. Идея следующая. Происходит запуск игры, и пользователь попадает в меню. В меню мы нарисуем на экране две таблички с надписями Игра и Выход (рис. 12.2). Фактически этим мы создадим две команды для запуска игры и выхода из игры. Переход по обеим командам будет осуществляться командами клавиатуры Вверх и Вниз (это те клавиши, что со стрелочками). Выполняя команду Вверх (Игра), мы будем активировать данное состояние и дезактивировать команду Выход. Для
Рис. 12.1. Схема работы игры
переключения этого состояния можно использовать дополнительную перемен( ную как булеву, так и целочисленную, например cursorState. Единственное отличие при выборе типа – это то, что в булевой переменной есть только два со( стояния, тогда как целочисленной переменной можно присваивать сколько угод( но состояний. Например, если cursorState равна 1, то активируем игру, если cursorState равна 2, то активируем экран опций, а если cursorState равна 3 – активируем заставку «Об авторе» и т. д. В итоге если нажата клавиша «вверх», то мы присваиваем переменной cursorState значение, равное 1, а если нажата клавиша «вниз», то значение этой переменной изменяется на 2. Что нам это дает? А дает нам это многое, в част( ности далее мы будем проверять нажатие пользователем клавиши Enter. Если клавиша Enter нажата и переменная cursorState равна 1, то выполня( ется запуск игры, а вот если переменная cursorState равна 2, то по нажатии клавиши Enter будет осуществлен выход из игры. Таким образом, для одной кла( виши Enter мы создаем два разных условия, а пользователю будет комфортно уп( равлять работой игры. Теперь о том, как после выполнения команд клавиатуры Вверх (Игра) и Вниз (Выход) дать понять пользователю, какая из команд сейчас активна. Способов здесь очень много, и все зависит только от вашей фантазии. В одних играх теку( щая активная команда подсвечивается, в других играх команда анимируется, в третьих – существует курсор, указывающий на то, какая из команд меню сей( час активна, и т. д. То есть это дело фантазии и стилистики оформления всей игры в целом. В нашей с вами игре в качестве команд Игра и Выход применяются две таб( лички, или доски, с соответствующими надписями (рис. 12.2). Чтобы показать пользователю, какая из команд активна в данный момент, мы будем сдвигать до( щечку с надписью выбранной команды влево на 50 пикселей, а неактивную ко( манду слегка подкрашивать желтым цветом, дабы несколько затемнить цветом эту команду. Таким образом, пользователь сразу заметит, какая из команд актив( на, а выбрав одну из команд, нажмет клавишу Enter, для которой, как вы помните, мы назначаем различные состояния.
168
Проект Menu
Создаем игровое меню
169
/// Ëèñòèíã 12.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð äëÿ Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 12 /// Ïðîåêò: Menu /// Êëàññ: Menu /// Äîáàâëÿåì â èãðó ìåíþ /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; #endregion
Рис. 12.2. Графическая заставка меню игры
Теперь еще пара слов о выходе в меню непосредственно из игрового процесса. Итак, игрок зашел в меню, выбрал команду Игра и нажал клавишу Enter. Произо( шел запуск игрового процесса. В режиме паузы мы ничего не меняем, здесь и так все нормально, а вот для выхода из игры в меню нужна дополнительная команда. Как правило, для выхода в меню используется клавиша с командой Esc. Это весь( ма распространенная и стандартная практика. Ранее эта клавиша у нас служила для закрытия игры, но теперь мы эту клавишу переопределим. По нажатии клави( ши с командой Esc будет производиться выход в меню игры, а уже из самого меню пользователь может как выйти из игры, так и запустить игру сначала. Теперь переходим к работе над проектом Menu, исходный код которого вы можете найти на компакт(диске в папке Code\Chapter12\Menu.
12.2.1. Класс Menu Начнем с того, что сформируем новый проект с названием Menu, в который ско( пируем исходный код из предыдущей главы. Вы также можете просто модифици( ровать наш последний проект Font, где мы рассматривали работу со шрифтом и вывод текста на экран монитора. Очевидно, что для описания игрового меню лучше всего создать отдельный класс. Мы так и поступим, создав специальный класс Menu, которому отведем роль представления меню игры. В листинге 12.1 показан полный исходный код класса Menu. //========================================================================= /// <summary>
namespace Menu { class Menu { public Texture2D menuTexture; public Vector2 menuPosition; public Texture2D cursorGame; public Vector2 cursorPositionGame; public Texture2D cursorExit; public Vector2 cursorPositionExit; /// <summary> /// Êîíñòðóêòîð /// <summary> public Menu() { menuPosition = new Vector2(0, 0); cursorPositionGame = new Vector2(650, 400); cursorPositionExit = new Vector2(700, 550); } /// <summary> /// Çàãðóçêà ñïðàéòîâ /// <summary> public void Load(ContentManager content) { menuTexture = content.Load(“Content\\Textures\\menu”); cursorGame = content.Load(“Content\\Textures\\cursorGame”); cursorExit = content.Load(“Content\\Textures\\cursorExit”); } /// <summary> /// Ðèñóåì ìåíþ
170
Проект Menu
Создаем игровое меню
/// <summary> public void DrawMenu(SpriteBatch spriteBatch, int state) { spriteBatch.Draw(menuTexture, menuPosition, Color.White); switch(state) { case 1: spriteBatch.Draw(cursorGame, cursorPositionGame, Color.White); spriteBatch.Draw(cursorExit, cursorPositionExit, Color.Yellow); break; case 2: spriteBatch.Draw(cursorGame, cursorPositionGame, Color.Yellow); spriteBatch.Draw(cursorExit, cursorPositionExit, Color.White); break; } }
cursorPositionGame = new Vector2(650, 400); cursorPositionExit = new Vector2(700, 550);
Заметьте, что верхняя табличка с командой Игра изначально сдвинута влево по отношению к нижней табличке. Это говорит о том, что при входе в меню коман( да Игра будет активироваться первой. Впоследствии, естественно, мы добавим код для этой команды меню в классе Game1, а также закрасим неактивную коман( ду желтым цветом. В методе Load() происходит загрузка всех трех графических изображений в игру, которые предварительно явно добавляются в каталог проекта в папку Content\Textures. И в самом конце исходного кода класса Menu создается метод DrawMenu() для вывода графики на экран монитора. public void DrawMenu(SpriteBatch spriteBatch, int state) { spriteBatch.Draw(menuTexture, menuPosition, Color.White);
} }
switch(state) { case 1:
С помощью класса Menu нам необходимо загрузить в игру общий фон меню, а также две дополнительные таблички, или доски, с командами Игра и Выход. Для этих целей необходимо определить ряд объектов и переменных. Поэтому в начале исходного файла Menu.cs в области глобальных переменных следует шесть строк кода. // Ôîí ìåíþ è åãî ïîçèöèÿ íà ýêðàíå public Texture2D menuTexture; public Vector2 menuPosition; // Äîñêà ñ êîìàíäîé Èãðà è åå ïîçèöèÿ íà ýêðàíå public Texture2D cursorGame; public Vector2 cursorPositionGame; // Äîñêà ñ êîìàíäîé Âûõîä è åå ïîçèöèÿ íà ýêðàíå public Texture2D cursorExit; public Vector2 cursorPositionExit;
Затем в конструкторе класса Menu мы задаем позиции на экране как для фона, так и для табличек. Поскольку фон меню идет с размером в 1024 × 768 пикселей (в размер 17(дюймового экрана монитора), то в качестве точки отсчета для фона назначаются нулевые координаты по обеим осям системы. menuPosition = new Vector2(0, 0);
Для дощечек с командами Игра и Выход позиции задаются в правой части экрана и чуть ближе к его нижней кромке. Эти значения были вычислены еще на этапе проектирования меню, но их всегда можно подкорректировать в исходном коде, если вам вдруг не понравилось их расположение.
171
spriteBatch.Draw(cursorGame, cursorPositionGame, Color.White); spriteBatch.Draw(cursorExit, cursorPositionExit, Color.Yellow); break; case 2: spriteBatch.Draw(cursorGame, cursorPositionGame, Color.Yellow); spriteBatch.Draw(cursorExit, cursorPositionExit, Color.White); break; } }
Принцип работы метода DrawMenu() следующий. Вызывая этот метод в классе Game1, в качестве второго параметра мы будем передавать в метод пере( менную cursorState, которая, как вы помните, показывает, какая из команд в текущий момент выбрана. Затем на базе оператора switch происходит отбор нужного ветвления для выполнения исходного кода метода DrawMenu(). Если переменная cursorState равна 1, то это значит, что в данный момент активна команда Игра и выбрана верхняя табличка. Поэтому нижнюю табличку необходимо подкрасить цветом, а верхнюю – красим белым цветом. Как только состояние переменной cursorState изменяется на двойку, то работает блок кода case 2, и так до бесконечности, пока одна из команд не будет выбрана (ко( манда Enter). В качестве цвета, закрашивающего табличку, был выбран желтый цвет, и мы закрашиваем именно не активную в данный момент команду, но можно делать и наоборот, выделять активную команду цветом. В следующем проекте мы так и
172
Проект Menu
Создаем игровое меню
сделаем – будем выделять цветом уже активную команду. Дополнительно рас( смотренный механизм закрашивания спрайтов цветом можно использовать пря( мо в игровом процессе, накладывая на текстуры различные оттенки, гаммы цветов и даже новые текстуры. С исходным классом Menu мы разобрались, переходим к основному коду игры и классу Game1. По традиции все нововведения в исходном коде выделены жир( ным шрифтом для облегчения чтения произошедших изменений.
12.2.2. Загружаем в игру меню Начнем с того, что объявим в области глобальных переменных класса Game1 объект класса Menu и создадим две переменные menuState и cursorState для отслеживания состояния меню и выбранной пользователем команды. Menu menu; private bool menuState; private int cursorState;
После этих объявлений переходим в конструктор класса Game1, где создаем полноценный объект menu класса Menu и инициализируем обе переменные. menu = new Menu(); menuState = true; cursorState = 1;
Переменная menuState при запуске игры получает значение true, поэтому первым делом на экран будет выводиться меню игры. В свою очередь, переменная cursorState равна 1, а значит, в игре изначально активируется табличка с надписью Игра. Далее в методе LoadGraphicsContent() мы загружаем всю игровую графику, включая меню. В предыдущих версиях игры в методе Initialize() класса Game1 мы опре( деляли все игровые позиции объектов. Сейчас также можно использовать этот метод, но был реализован улучшенный механизм, в частности создан отдельный метод NewGame(), исходный код которого выглядит следующим образом: /// <summary> /// Íîâàÿ èãðà /// <summary> public void NewGame() { j = 0; for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 500), j = j - 300); } platform.spritePosition = new Vector2(Window.ClientBounds.Width/ 2,
173
Window.ClientBounds.Height - 90); score0 = 0; score1 = 0; score2 = 0; score3 = 0; score4 = 0; base.Initialize(); }
Зачем это нужно? Дело в том, что нам необходимо реализовать обратный старт игры в момент, когда пользователь выйдет из игры в меню и потом захочет опять начать игру сначала. Для этих целей и создается метод NewGame(). Вызов этого метода будет происходить непосредственно в меню игры, и каждый раз, ког( да пользователь выходит в меню, а потом вновь запускает игру, происходит уста( новка объектов на новые игровые позиции. Исходный код метода NewGame() почти не претерпел изменений, за исключе( нием того, что переменная j, увеличивающая координату по отрицательной оси Y, инициализируется именно в этом методе нулевым значением. Сделано это по од( ной причине. Как вы знаете, переменная j помогает нам удалить друг от друга спрайты по вертикали на 300 пикселей. Каждое ее увеличение будет содержать большое отрицательное значение, например для пяти спрайтов это значение бу( дет равно –1500 пикселей. Если пользователь зайдет в игру, потом выйдет в меню, а затем опять вернется в игру, то переменная j по(прежнему будет содержать зна( чение в –1500 пикселей, и установка игровых объектов будет происходить от это( го значения. В результате при повторном старте игры пользователю придется подождать, пока на экране появится первый спрайт, а при третьем или даже десятом старте игры на это уйдет еще больше времени. Поэтому при каждом новом вызове метода NewGame() необходимо обнулять все игровые переменные. Учтите этот нюанс во всех ваших последующих играх. Каждый новый старт игры должен обязательно обнулять все предыдущие игровые состояния, если, конечно, не предусмотрена система сохранения игры или перехода на новый уровень. Теперь переходим к методу Update() класса Game1. В этом методе мы поме( няем конструкцию обновления состояния игры с учетом появления в игре меню и сделаем это следующим образом. protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); // Âûõîä â ìåíþ èç èãðû if (keyboardState.IsKeyDown(Keys.Escape)) { menuState = true; } // Ïîêàçûâàåì ìåíþ
174
Проект Menu
Создаем игровое меню
if (menuState == true) { // Ïåðåõîä êóðñîðà ïî ìåíþ if (keyboardState.IsKeyDown(Keys.Up)) { menu.cursorPositionGame = new Vector2(650, menu.cursorPositionExit = new Vector2(700, cursorState = 1; } else if (keyboardState.IsKeyDown(Keys.Down)) { menu.cursorPositionGame = new Vector2(700, menu.cursorPositionExit = new Vector2(650, cursorState = 2; }
400); 550);
400); 550);
// Îáðàáàòûâàåì íàæàòèå êëàâèøè Enter if (keyboardState.IsKeyDown(Keys.Enter) && cursorState == 1) { this.NewGame(); menuState = false; } else if (keyboardState.IsKeyDown(Keys.Enter) && cursorState == 2) { this.Exit(); } } else // Çàïóñêàåì èãðó { Pause(); if (paused == false) { double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite[i].UpdateFrame(elapsed); } MoveSprite(); MovePlatform(); Collisions(); } } base.Update(gameTime); }
Когда вы только запускаете игру, то попадаете в меню игры, потому что пере( менная menuState равна значению true. Следовательно, вы попадаете в первый блок кода после ключевого слова if, а все, что следует после ключевого слова else, пропускается и не выполняется. В первом блоке кода, следующем после ключевого слова if, все связано толь( ко с работой меню. Здесь мы организуем механизм перехода курсора по таблич(
175
кам, а также обрабатываем нажатие клавиши Enter. Механизм здесь такой: если нажата клавиша Вверх, то мы сдвигаем позицию таблички с надписью Игра влево и присваиваем переменной cursorState значение, равное единице. В этом со( стоянии, если пользователь нажмет клавишу, сработает блок кода, запускающий игру. // Îáðàáàòûâàåì íàæàòèå êëàâèøè Enter if (keyboardState.IsKeyDown(Keys.Enter) && cursorState == 1) { this.NewGame(); menuState = false; }
В этом коде мы вызываем метод NewGame() для определения новых игровых позиций и меняем значение булевой переменной menuState на false, что авто( матически выводит работу программы из блока кода, следующего за ключевым словом if. В данной ситуации уже будет выполняться исходный код, следующий за ключевым словом else. Таким образом, из меню игры мы переходим непосред( ственно в игровой процесс, где будем постоянно отслеживать состояние перемен( ной menuState. Как только пользователь нажмет клавишу Esc, переменная menuState меняет свое состояние на true, а значит, игра останавливается, и мы выходим из игры в меню. Последние дополнения в исходном коде текущего проекта происходят в мето( де Draw(), который рисует всю игровую графику. if (menuState == true) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); menu.DrawMenu(spriteBatch, cursorState); spriteBatch.End(); }
Здесь мы также добавляем ветвление с конструкцией if/else, и если пере( менная menuState равна true, то отображаем меню, а если нет, то рисуем на экране игровую графику. Дополнительно в метод DrawMenu() передается значе( ние переменной cursorState, на основе которой, как вы помните, происходит определение, какую из табличек в текущий момент необходимо затенять цветом. Полный исходный код класса Game1 содержится в листинге 12.2, а весь исходный код проекта располагается на компакт(диске в папке Code\Chapter12\Menu. //========================================================================= /// <summary> /// Ëèñòèíã 12.2 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 12
176
Проект Menu
Создаем игровое меню
sprite[i] = new Sprite(12, 10); } platform = new Sprite(); font = new BitmapFont(“Content\\Font\\russian.xml”); menu = new Menu(); menuState = true; cursorState = 1;
/// Ïðîåêò: Menu /// Êëàññ: Game1 /// Äîáàâëÿåì â èãðó ìåíþ /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace Menu { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch; Sprite[] sprite = new Sprite[5]; private Texture2D background1; private Texture2D background2; Random rand = new Random(); int j = 0; Sprite platform; private bool paused = false; private bool pauseKeyDown = false; public BoundingBox bbplatform; public BoundingBox[] bb = new BoundingBox[5]; BitmapFont font; int score0, score1, score2, score3, score4; Menu menu; private bool menuState; private int cursorState; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; for (int i = 0; sprite.Length > i; i++) {
177
} /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); background1 = content.Load(“Content\\Textures\\background1”); background2 = content.Load(“Content\\Textures\\background2”); platform.Load(content, “Content\\Textures\\platform”); sprite[0].Load(content, “Content\\Textures\\0”); sprite[1].Load(content, “Content\\Textures\\1”); sprite[2].Load(content, “Content\\Textures\\2”); sprite[3].Load(content, “Content\\Textures\\3”); sprite[4].Load(content, “Content\\Textures\\4”); font.Reset(graphics.GraphicsDevice); menu.Load(content); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) {
178
Проект Menu
Создаем игровое меню }
keyboardState = Keyboard.GetState(); // Âûõîä â ìåíþ if (keyboardState.IsKeyDown(Keys.Escape)) { menuState = true; }
/// <summary> /// Äâèæåíèå ñïðàéòà ïî âåðòèêàëè /// <summary> public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition += sprite[i].speedSprite;
// Ïîêàçûâàåì ìåíþ if (menuState == true) { // Ïåðåõîä êóðñîðà ïî ìåíþ if (keyboardState.IsKeyDown(Keys.Up)) { menu.cursorPositionGame = new Vector2(650, menu.cursorPositionExit = new Vector2(700, cursorState = 1; } else if (keyboardState.IsKeyDown(Keys.Down)) { menu.cursorPositionGame = new Vector2(700, menu.cursorPositionExit = new Vector2(650, cursorState = 2; }
} base.Update(gameTime);
if (sprite[i].spritePosition.Y > Window.ClientBounds.Height) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width -300 - sprite[i].spriteTexture.Width / 12), -500); } }
400); 550); }
400); 550);
// Îáðàáàòûâàåì íàæàòèå êëàâèøè Enter if (keyboardState.IsKeyDown(Keys.Enter) && cursorState == 1) { this.NewGame(); menuState = false; } else if (keyboardState.IsKeyDown(Keys.Enter) && cursorState == 2) { this.Exit(); } } else // Çàïóñêàåì èãðó { Pause(); if (paused == false) { double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite[i].UpdateFrame(elapsed); } MoveSprite(); MovePlatform(); Collisions(); }
179
/// <summary> /// Äâèæåíèå ïëàòôîðìû ïî ýêðàíó /// <summary> public void MovePlatform() { if (keyboardState.IsKeyDown(Keys.Left)) platform.spritePosition.X -= 10; else if (keyboardState.IsKeyDown(Keys.Right)) platform.spritePosition.X += 10; if (platform.spritePosition.X < 10) platform.spritePosition.X = 10; else if (platform.spritePosition.X > Window.ClientBounds.Width-400) platform.spritePosition.X = Window.ClientBounds.Width- 400; } /// <summary> /// Ïàóçà â èãðå /// <summary> public void Pause() { if (keyboardState.IsKeyDown(Keys.P)) { pauseKeyDown = true; } else if (pauseKeyDown) { pauseKeyDown = false; paused = !paused; } } /// <summary> /// Ñòîëêíîâåíèÿ
180
Проект Menu
Создаем игровое меню
/// <summary> public void Collisions() { bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 25, 0); bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 25 + platform.spriteTexture.Height, 0); for (int i = 0; bb.Length > i; i++) { bb[i].Min = new Vector3(sprite[i].spritePosition.X, sprite[i].spritePosition.Y, 0); bb[i].Max = new Vector3(sprite[i].spritePosition.X + sprite[i].spriteTexture.Width / 12, sprite[i].spritePosition.Y + sprite[i].spriteTexture.Height, 0); }
Window.ClientBounds.Width 300 - sprite[4].spriteTexture.Width / 12), -500); score4 += 1; } } /// <summary> /// Íîâàÿ èãðà /// <summary> public void NewGame() { j = 0; for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 500), j = j - 300); } platform.spritePosition = new Vector2(Window.ClientBounds.Width/ 2, Window.ClientBounds.Height - 90); score0 = 0; score1 = 0; score2 = 0; score3 = 0; score4 = 0; base.Initialize();
if (bbplatform.Intersects(bb[0])) { sprite[0].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[0].spriteTexture.Width / 12), -500); score0 += 1; } } if (bbplatform.Intersects(bb[1])) { sprite[1].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[1].spriteTexture.Width / 12), -500); score1 += 1; } if (bbplatform.Intersects(bb[2])) { sprite[2].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[2].spriteTexture.Width / 12), -500); score2 += 1; } if (bbplatform.Intersects(bb[3])) { sprite[3].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[3].spriteTexture.Width / 12), -500); score3 += 1; } if (bbplatform.Intersects(bb[4])) { sprite[4].spritePosition = new Vector2(rand.Next(10,
181
/// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); if (menuState == true) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); menu.DrawMenu(spriteBatch, cursorState); spriteBatch.End(); } else { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); for (int i = 0; sprite.Length > i; i++) { sprite[i].DrawAnimationSprite(spriteBatch); } spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); platform.DrawSprite(spriteBatch); spriteBatch.End(); if (paused == true)
182
Проект MenuCursor
Создаем игровое меню { font.DrawString(450, 300, Color.Black, “Ïàóçà”); } int fy = 245; font.DrawString(Window.ClientBounds.Width “{0}”, score0); font.DrawString(Window.ClientBounds.Width font.LineHeight + 12, “{0}”, score1); font.DrawString(Window.ClientBounds.Width font.LineHeight + 12, “{0}”, score2); font.DrawString(Window.ClientBounds.Width font.LineHeight + 12, “{0}”, score3); font.DrawString(Window.ClientBounds.Width font.LineHeight + 12, “{0}”, score4);
- 95, fy, Color.Black, - 95, fy += - 95, fy += - 95, fy += - 95, fy +=
} base.Draw(gameTime); } }
183
между собой. Именно на основе этого подхода и реализуется обработка ситуации в наведении курсора мыши на две таблички. Здесь нам достаточно создать огра( ничивающий прямоугольник для курсора мыши, а также два прямоугольника для табличек меню Игра и Выход. Затем в игровом цикле нужно постоянно проверять условие по пересечению этих прямоугольников. Если ограничивающий прямо( угольник курсора пересекается с ограничивающим прямоугольником одной из табличек, то эта табличка с командой становится активной в данный момент, со всеми вытекающими отсюда последствиями. Все последствия, как вы помните, приводят либо к запуску игры, либо к ее закрытию. Перейдем к исходному коду. Нам понадобятся несколько дополнительных объектов и переменных, которые мы объявляем в области глобальных перемен( ных класса Game1. Исходный код класса Menu остается неизменным. Sprite mouse; MouseState mouseState; public BoundingBox bbMouse; public BoundingBox bbCursorGame; public BoundingBox bbCursorExit;
}
12.3. Проект MenuCursor В этом проекте для управления работой меню мы задействуем компьютерную мышку. Все(таки большинство игр используют именно этот механизм, да и пользователю значительно удобнее работать с мышью, чем с клавиатурой. В осно( ву нового проекта ляжет исходный код предыдущего рассмотренного в этой главе проекта Menu. Все изменения будут связаны с добавлением нового метода UpdateMouse(), который будет следить за состоянием мыши и изменением ме( тодики обработки команд Игра и Выход. Создайте новый проект или модифици( руйте предыдущий и приступайте вместе со мной к работе. Первым делом в новый проект MenuCursor необходимо добавить графиче( ское изображение курсора, выполненное в одном из графических редакторов. Это изображение заменит стандартный курсор мыши, используемый Windows по умолчанию. В нашем проекте в качестве такого курсора выступает изображение стрелы (рис. 12.3), которое мы явно добавляем в папку проекта Content\Textures. Обратите внимание, что изображение стрелы в иконке курсора находится под не( большим углом, но если вы посмотрите на изображение курсора на своем компью( тере, то заметите, что он также расположен под определенным углом. Это стан( дартная практика создания курсоров для операционной системы Windows, и не только. Техника наведения курсора мыши на одну из двух табличек в проекте MenuCursor строится на базе проверки пересечения прямоугольников. В главе 10 мы изучали вопросы по обработке ситуации, возникающей при столкновении двух и более объектов Рис. 12.3. Графическое изображение курсора
Первый объект mouse класса Sprite будет представлять неанимированный объект этого класса, но вы в своих проектах можете спокойно создать и анимиро( ванное изображение курсора. Во второй строке кода этого блока мы создаем структурную переменную mouseState структуры MouseState. Эта структура аналогична по своей направленности структуре KeyboardState, с той лишь раз( ницей, что она отслеживает состояние мыши и имеет свои механизмы работы. И в последних трех строках этого блока создаются три ограничивающих прямо( угольника для курсора мыши и двух табличек. Теперь переходим к конструктору класса и инициализируем все объекты и переменные, а в методе LoadGraphicsContent() добавляем загрузку изобра( жения курсора в игру. mouse = new Sprite(); bbMouse = new BoundingBox(); bbCursorGame = new BoundingBox(); bbCursorExit = new BoundingBox(); …. mouse.Load(content, “Content\\Textures\\mouse”);
Далее в методе Update(), в том месте, где мы в предыдущем проекте запускали работу меню и обрабатывали перемещение по клавиатуре Вверх и Вниз, добавляем вызов нового метода UpdateMouse(), к работе над которым сейчас приступим. Что касается исходного кода в меню, отвечающего за обработку событий, по( лученных клавиатуры, то этот код можно удалить, а можно оставить. Если остави( те, то пользователь сможет работать как с мышью, так и с клавиатурой, соответ( ственно если удалите, то остается возможность работы только с мышью. Здесь
184
Проект MenuCursor
Создаем игровое меню
поступайте, как вам больше нравится, но я удалил код клавиатуры, чтобы вы эле( ментарно не путались в лишних нагромождениях строк кода. Сейчас мы работаем исключительно с мышью, поэтому давайте создадим в коде класса Game1 новый метод UpdateMouse() и возложим на него несколько ответственных задач, а в частности получение координат курсора, слежение за пересечением прямоугольников и обработку нажатия левой клавиши мыши. public void UpdateMouse() { // Ïîëó÷àåì ìåñòîíàõîæäåíèå êóðñîðà ìûøè mouseState = Mouse.GetState(); mouse.spritePosition.X = mouseState.X; mouse.spritePosition.Y = mouseState.Y; //——————————————————————————————————— // Ñîçäàåì îãðàíè÷èâàþùèå ïðÿìîóãîëüíèêè äëÿ êóðñîðà è òàáëè÷åê bbMouse.Min = new Vector3(mouse.spritePosition.X, mouse.spritePosition.Y, 0); bbMouse.Max = new Vector3(mouse.spritePosition.X + mouse.spriteTexture.Width, mouse.spritePosition.Y + mouse.spriteTexture.Height, 0); bbCursorGame.Min = new Vector3(650, 400, 0); bbCursorGame.Max = new Vector3(950, 500, 0); bbCursorExit.Min = new Vector3(650, 600, 0); bbCursorExit.Max = new Vector3(950, 700, 0); //——————————————————————————————————— // Îáðàáàòûâàåì ïåðåñå÷åíèå ïðÿìîóãîëüíèêîâ if (bbMouse.Intersects(bbCursorGame)) { menu.cursorPositionGame = new Vector2(650, 400); menu.cursorPositionExit = new Vector2(700, 550); cursorState = 1; } if (bbMouse.Intersects(bbCursorExit)) { menu.cursorPositionGame = new Vector2(700, 400); menu.cursorPositionExit = new Vector2(650, 550); cursorState = 2; } //——————————————————————————————————— // Îáðàáàòûâàåì íàæàòèå ëåâîé êíîïêè ìûøè if(mouseState.LeftButton == ButtonState.Pressed && bbMouse.Intersects(bbCursorGame)) { this.NewGame(); menuState = false; } else if(mouseState.LeftButton == ButtonState.Pressed && bbMouse.Intersects(bbCursorExit)) {
185
this.Exit(); } }
Весь код метода Update() можно разделить на четыре условные части: 1) получение координат курсора мыши; 2) создание трех ограничивающих прямоугольников; 3) пересечение прямоугольников; 4) нажатие пользователем левой клавиши мыши.
Получение координат курсора мыши mouseState = Mouse.GetState(); mouse.spritePosition.X = mouseState.X; mouse.spritePosition.Y = mouseState.Y
В первой строке этого блока кода мы с помощью системного метода Mouse.GetState() получаем все без исключения текущие состояния мыши, включая местоположение и возможные нажатия кнопок мыши. Полученные дан( ные сохраняются в структурной переменной mouseState. Затем мы обращаемся к каждой координате курсора мыши по отдельности, а полученные значения сохраняем в позиции спрайта mouse, который у нас в игре представляет графическое изображение курсора мыши. Поскольку весь метод UpdateMouse() циклично вызывается на каждой итерации игрового цикла, то координаты курсора будут постоянно «свежими». Таким образом, мы имеем на руках текущее положение курсора мыши, а в методе Draw() класса Game1 на ос( нове полученных координат будет рисоваться изображение стрелы на экране мо( нитора. mouse.DrawSprite(spriteBatch);
Создание ограничивающих прямоугольников bbMouse.Min = new Vector3(mouse.spritePosition.X, mouse.spritePosition.Y, 0); bbMouse.Max = new Vector3(mouse.spritePosition.X + mouse.spriteTexture.Width, mouse.spritePosition.Y + mouse.spriteTexture.Height, 0); bbCursorGame.Min = new Vector3(650, 400, 0); bbCursorGame.Max = new Vector3(950, 500, 0); bbCursorExit.Min = new Vector3(650, 600, 0); bbCursorExit.Max = new Vector3(950, 700, 0);
Для создания ограничивающих прямоугольников используются структурные переменные bbMouse, bbCursorGame и bbCursorExit структуры BoundingBox, для курсора мыши и двух табличек Игра и Выход. Ограничивающий прямоуголь( ник курсора мыши мы рисуем в соответствии с текущими координатами курсора
186
Проект MenuCursor
Создаем игровое меню
и размером изображения стрелы. Для задания координат и размера прямоуголь( ников табличек были использованы простые целочисленные значения. Таблички у нас отчасти статичны и сильно не изменяют своего расположения на экране, по( этому достаточно задать максимальные значения по горизонтали и вертикали для каждой из табличек. Если вам не нравится привязанность к целочисленным пере( менным, то можно дополнительно описать код, использующий точные размеры и координаты табличек, так, как это было сделано для курсора мыши.
Пересечение прямоугольников if (bbMouse.Intersects(bbCursorGame)) { menu.cursorPositionGame = new Vector2(650, menu.cursorPositionExit = new Vector2(700, cursorState = 1; } if (bbMouse.Intersects(bbCursorExit)) { menu.cursorPositionGame = new Vector2(700, menu.cursorPositionExit = new Vector2(650, cursorState = 2; }
400); 550);
400); 550);
В исходном коде, определяющем пересечение ограничивающих прямоуголь( ников курсора и табличек, все достаточно просто. Если прямоугольник курсора пересекает прямоугольник таблички с командой Игра, то эта табличка сдвигается влево на 50 пикселей, а переменная cursorState становится равной единице. При этом данная переменная нам необходима только для того, чтобы определить, каким цветом красить активную в текущий момент табличку. Пересечение прямо( угольника курсора и второй таблички происходит по той же схеме, что и для пер( вой таблички.
Нажатие пользователем левой кнопки мыши if(mouseState.LeftButton == ButtonState.Pressed && bbMouse.Intersects(bbCursorGame)) { this.NewGame(); menuState = false; } else if(mouseState.LeftButton == ButtonState.Pressed && bbMouse.Intersects(bbCursorExit)) { this.Exit(); }
Для определения условия, нажата кнопка мыши или нет, применяется сле( дующая конструкция кода: mouseState.LeftButton == ButtonState.Pressed
187
Это стандартная проверка на нажатие пользователем кнопки мыши, и этот механизм определен системно в XNA Framework. Работает все очень просто: если кнопка нажата, то приведенное выше условие равно, а значит, требуется выпол( нить действия, которые вы определяете для этого условия. Как видно из исходного кода, переменная cursorState не участвует в обра( ботке состояния нажатой кнопки мыши, как это было сделано в работе с клавиату( рой. Вместо этого происходит проверка нажатия кнопки и обязательного пересе( чения одного из прямоугольников. Если одно из предопределенных условий неверно, то выполнение блока кода за оператором if не происходит. Если определять нажатие кнопки мыши вкупе с переменной cursorState, то весь механизм будет работать несколько некорректно. Представьте, вы навели курсор мыши на одну из табличек, сделав ее активной, а затем отвели курсор в другую часть экрана. Состояние переменной cursorState в данный момент остается принадлежать этой самой активной табличке, и любое нажатие кнопки мыши в другой части экрана приведет либо к запуску игры, либо к ее закрытию, в зависимости от того, какая из команд была активна… Решать эту проблему можно многими способами, а не только тем способом, что я вам показал. Можно добавить еще одну конструкцию операторов if/else и менять состояние переменной cursorState, как только курсор мыши убирается с одной из табличек, либо отказаться от этой переменной вовсе и слегка услож( нить весь код. Полный исходный код класса Game1 вы найдете в листинге 12.3, а код проекта MenuCursor – на компакт(диске в папке Code\Chapter12\MenuCursor. Дополни( тельно я добавил в исходный код механизм, изменяющий скорость падающих предметов после их столкновения с платформой и нижней частью экрана. //========================================================================= /// <summary> /// Ëèñòèíã 12.3 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 12 /// Ïðîåêò: MenuCursor /// Êëàññ: Game1 /// Ðàáîòà ñ ìûøåé /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input;
188
Проект MenuCursor
Создаем игровое меню
189
bbCursorGame = new BoundingBox(); bbCursorExit = new BoundingBox();
using Microsoft.Xna.Framework.Storage; #endregion } namespace MenuCursor { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch; Sprite[] sprite = new Sprite[5]; private Texture2D background1; private Texture2D background2; Random rand = new Random(); int j = 0; Sprite platform; private bool paused = false; private bool pauseKeyDown = false; public BoundingBox bbplatform; public BoundingBox[] bb = new BoundingBox[5]; BitmapFont font; int score0, score1, score2, score3, score4; Menu menu; private bool menuState; private int cursorState; Sprite mouse; MouseState mouseState; public BoundingBox bbMouse; public BoundingBox bbCursorGame; public BoundingBox bbCursorExit; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; for (int i = 0; sprite.Length > i; i++) { sprite[i] = new Sprite(12, 10); } platform = new Sprite(); font = new BitmapFont(“Content\\Font\\russian.xml”); menu = new Menu(); menuState = true; cursorState = 1; mouse = new Sprite(); bbMouse = new BoundingBox();
/// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); background1 = content.Load(“Content\\Textures\\background1”); background2 = content.Load(“Content\\Textures\\background2”); platform.Load(content, “Content\\Textures\\platform”); sprite[0].Load(content, “Content\\Textures\\0”); sprite[1].Load(content, “Content\\Textures\\1”); sprite[2].Load(content, “Content\\Textures\\2”); sprite[3].Load(content, “Content\\Textures\\3”); sprite[4].Load(content, “Content\\Textures\\4”); font.Reset(graphics.GraphicsDevice); menu.Load(content); mouse.Load(content, “Content\\Textures\\mouse”); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); // Âûõîä â ìåíþ if (keyboardState.IsKeyDown(Keys.Escape))
190
Проект MenuCursor
Создаем игровое меню
platform.spritePosition.X -= 10; else if (keyboardState.IsKeyDown(Keys.Right)) platform.spritePosition.X += 10;
{ menuState = true; } // Ïîêàçûâàåì ìåíþ if (menuState == true) { UpdateMouse(); } else // Çàïóñêàåì èãðó { Pause(); if (paused == false) { double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite[i].UpdateFrame(elapsed); } MoveSprite(); MovePlatform(); Collisions(); } } base.Update(gameTime); } /// <summary> /// Äâèæåíèå ñïðàéòà ïî âåðòèêàëè /// <summary> public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition += sprite[i].speedSprite; if (sprite[i].spritePosition.Y > Window.ClientBounds.Height) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width -300 - sprite[i].spriteTexture.Width / 12), -500); sprite[i].speedSprite.Y = rand.Next(5, 9); } }
if (platform.spritePosition.X < 10) platform.spritePosition.X = 10; else if (platform.spritePosition.X > Window.ClientBounds.Width-400) platform.spritePosition.X = Window.ClientBounds.Width- 400; } /// <summary> /// Ïàóçà â èãðå /// <summary> public void Pause() { if (keyboardState.IsKeyDown(Keys.P)) { pauseKeyDown = true; } else if (pauseKeyDown) { pauseKeyDown = false; paused = !paused; } } /// <summary> /// Ñòîëêíîâåíèÿ /// <summary> public void Collisions() { bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 25, 0); bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 25 + platform.spriteTexture.Height, 0); for (int i = 0; bb.Length > i; i++) { bb[i].Min = new Vector3(sprite[i].spritePosition.X, sprite[i].spritePosition.Y, 0); bb[i].Max = new Vector3(sprite[i].spritePosition.X + sprite[i].spriteTexture.Width / 12, sprite[i].spritePosition.Y + sprite[i].spriteTexture.Height, 0); }
} /// <summary> /// Äâèæåíèå ïëàòôîðìû ïî ýêðàíó /// <summary> public void MovePlatform() { if (keyboardState.IsKeyDown(Keys.Left))
191
if (bbplatform.Intersects(bb[0])) { sprite[0].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[0].spriteTexture.Width / 12), -500); sprite[0].speedSprite.Y = rand.Next(5, 9); score0 += 1;
192
Проект MenuCursor
Создаем игровое меню score1 = 0; score2 = 0; score3 = 0; score4 = 0; base.Initialize();
} if (bbplatform.Intersects(bb[1])) { sprite[1].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[1].spriteTexture.Width / 12), -500); sprite[1].speedSprite.Y = rand.Next(5, 9); score1 += 1; } if (bbplatform.Intersects(bb[2])) { sprite[2].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[2].spriteTexture.Width / 12), -500); sprite[2].speedSprite.Y = rand.Next(5, 9); score2 += 1; } if (bbplatform.Intersects(bb[3])) { sprite[3].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[3].spriteTexture.Width / 12), -500); sprite[3].speedSprite.Y = rand.Next(5, 9); score3 += 1; } if (bbplatform.Intersects(bb[4])) { sprite[4].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[4].spriteTexture.Width / 12), -500); sprite[4].speedSprite.Y = rand.Next(5, 9); score4 += 1; } } /// <summary> /// Íîâàÿ èãðà /// <summary> public void NewGame() { j = 0; for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 500), j = j - 300); } platform.spritePosition = new Vector2(Window.ClientBounds.Width/ 2, Window.ClientBounds.Height - 90); score0 = 0;
} /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå ìûøè /// <summary> public void UpdateMouse() { // Ïîëó÷àåì ìåñòîíàõîæäåíèå êóðñîðà ìûøè mouseState = Mouse.GetState(); mouse.spritePosition.X = mouseState.X; mouse.spritePosition.Y = mouseState.Y; // Ñîçäàåì îãðàíè÷èâàþùèå ïðÿìîóãîëüíèêè äëÿ êóðñîðà è òàáëè÷åê bbMouse.Min = new Vector3(mouse.spritePosition.X, mouse.spritePosition.Y, 0); bbMouse.Max = new Vector3(mouse.spritePosition.X + mouse.spriteTexture.Width, mouse.spritePosition.Y + mouse.spriteTexture.Height, 0); bbCursorGame.Min = new Vector3(650, 400, 0); bbCursorGame.Max = new Vector3(950, 500, 0); bbCursorExit.Min = new Vector3(650, 600, 0); bbCursorExit.Max = new Vector3(950, 700, 0); // Îáðàáàòûâàåì ïåðåñå÷åíèå ïðÿìîóãîëüíèêîâ if (bbMouse.Intersects(bbCursorGame)) { menu.cursorPositionGame = new Vector2(650, menu.cursorPositionExit = new Vector2(700, cursorState = 1; } if (bbMouse.Intersects(bbCursorExit)) { menu.cursorPositionGame = new Vector2(700, menu.cursorPositionExit = new Vector2(650, cursorState = 2; }
400); 550);
400); 550);
// Îáðàáàòûâàåì íàæàòèå ëåâîé êíîïêè ìûøè if(mouseState.LeftButton = = ButtonState.Pressed && bbMouse.Intersects(bbCursorGame)) { this.NewGame(); menuState = false; } else if(mouseState.LeftButton = = ButtonState.Pressed && bbMouse.Intersects(bbCursorExit))
193
194
Создаем игровое меню { this.Exit(); }
} /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); if (menuState == true) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); menu.DrawMenu(spriteBatch, cursorState); mouse.DrawSprite(spriteBatch); spriteBatch.End(); } else { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); for (int i = 0; sprite.Length > i; i++) { sprite[i].DrawAnimationSprite(spriteBatch); } spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); platform.DrawSprite(spriteBatch); spriteBatch.End(); if (paused == true) { font.DrawString(450, 300, Color.Black, “Ïàóçà”); } int fy = 245; font.DrawString(Window.ClientBounds.Width - 95, fy, Color.Black, “{0}”, score0); font.DrawString(Window.ClientBounds.Width-95, fy += font.LineHeight + 12, “{0}”, score1); font.DrawString(Window.ClientBounds.Width-95, fy += font.LineHeight + 12, “{0}”, score2); font.DrawString(Window.ClientBounds.Width-95, fy += font.LineHeight + 12, “{0}”, score3); font.DrawString(Window.ClientBounds.Width-95, fy += font.LineHeight + 12, “{0}”, score4); } base.Draw(gameTime); } } }
Глава 13 Звуковые эффекты Работа со звуком в играх до недавнего времени не отличалась особой легкостью. В том же DirectX SDK на организацию звукового движка уходило очень много времени, а с появлением Xbox 360 программирование звука в играх на базе DirectX SDK стало и вовсе невозможным. Поэтому для платформы XNA был со( здан новый компонент под названием Microsoft Cross(Platform Audio Creation Tool, или просто XACT. Это кросс(платформенный механизм, позволяющий со( здавать отдельные звуковые проекты, которые впоследствии вы можете встроить в свою игру, как для ПК, так и для Xbox 360, затратив при этом минимум усилий и времени. Все проекты с использованием XACT создаются по одному принципу. Вам необходимо произвести стандартный набор операций, и у вас на руках будет готовый звуковой проект. В этой главе вы научитесь создавать XACT(проекты и интегрировать их в свои программы. Что касается нашей игры, то мы добавим в программу два зву( ковых эффекта. Один звуковой эффект будет проигрываться в момент касания платформы и падающего объекта, а второй звуковой эффект будет воспроизво( диться, когда объект упадет в пропасть. Сейчас вам главное – разобраться с об( щим принципом работы проектов XACT, а затем вы сможете интегрировать в свои игры любое количество звуковых эффектов или отдельных мелодий. Начнем с того, что рассмотрим технику и механику создания проектов XACT.
13.1. Создаем проект XACT Прежде чем начинать создавать проект XACT, необходимо подготовить все звуко( вые файлы, которые вы собираетесь использовать в игре. Причем отнеситесь к этому делу ответственно, поскольку заменить один или несколько не понравив( шихся вам звуковых файлов в проекте простой операцией удалить/вставить нельзя. Придется редактировать созданный ранее проект XACTзаново. Алгоритм действий по созданию проекта XACT и добавлению его в програм( му следующий. Вы создаете определенное количество звуковых файлов и явно добавляете их в свой проект. Затем с помощью программы Microsoft Cross( Platform Audio Creation Tool создается XACT(проект, в котором выполняется ряд простых действий (пара(тройка кликов кнопкой мыши) для присоединения к XACT(проекту звуковых файлов. Когда все операции выполнены, нужно просто
196
Создаем проект ХАСТ
Звуковые эффекты
сохранить проект XACT в рабочем каталоге вашего проекта в том месте, где хра( нятся звуковые файлы. После этого начинается работа над исходным кодом про( екта, в котором вам необходимо описать ряд стандартных действий для воспроиз( ведения звуковых эффектов в игре. Программа Microsoft Cross(Platform Audio Creation Tool понимает три форма( та: WAV, AIF и AIFF. Самый распространенный и часто используемый в играх – это, конечно, WAV(формат. Кстати, очень много начинающих программистов склоняются к формату MP3, но на самом деле не учитывают один недостаток это( го формата. Если вы не в курсе, то знайте, за использование формата MP3 в играх необходимо платить, то есть приобретать соответствующую лицензию, которая стоит приличных денег. Поэтому бесплатный WAV(формат, по крайней мере для начинающих программистов, смотрится проще, да и всегда можно преобразовать звуковые файлы из формата MP3 в формат WAV. Для игры я использовал два звуковых файла в формате WAV, которые были взяты из проекта Spacewar Windows Starter Kit, идущего в составе инструмента( рия XNA Game Studio Express. Файлы были подобраны, насколько это было воз( можно, под игру и переименованы в boom.wav и platform.wav. Первый файл boom.wav мы будем воспроизводить, когда пользователь не сможет поймать па( дающий объект и он выйдет за пределы экрана. Второй файл используется при столкновении платформы и объекта. На самом деле в коммерческих проектах необходимо очень ответственно под( ходить к созданию звуковой дорожки игры. Лучше всего для этих целей привле( кать хороших аранжировщиков, которые действительно могут без видимых уси( лий сыграть на слух любую мелодию. Таких предложений в том же Интернете очень много, и цена на эту работу может быть различной, вплоть до того, что вы сможете найти единомышленника, желающего вписать свое имя в историю созда( ваемой игры. Если таковые единомышленники сейчас читают эту книгу, то можно зайти на мой сайт в Интернете http://www.gornakov.ru и оставить там свою заявку на сотрудничество. Соответствующая информация находится в разделе Работа для читателей. Предложение также действительно для программистов и особенно модельеров, художников, дизайнеров… Возвращаемся на землю и переходим к созданию XACT(проекта, который изучим скрупулезно в пошаговом режиме.
13.1.1. Пошаговая инструкция 1. Создайте в каталоге рабочего проекта Audio новую папку под названием Sound (команды Add ⇒ New Folder). Эту папку лучше всего переместить или изначально создать как вложенную в папке Content (рис. 13.1). 2. В дальнейшем в эту папку мы добавим два звуковых файла, предназначен( ных для воспроизведения в игре. Звуковые файлы могут находиться где угодно, поскольку их добавление в проект, как вы помните, должно проис( ходить явно. Для добавления в проект звуковых файлов в панели Solution
197
Explorer щелкните правой кнопкой мыши на названии папки Sound и в контекстном меню выберите команды Add ⇒ Exiting Item. Откроется диалоговое окно Add Exiting Item. В этом окне в списке Files of Types нуж( но выбрать тип Audio files и найти на вашем компьютере звуковые файлы. В нашей игре в качестве таких файлов выступают WAV(файлы под назва( ниями boom и platform. После этой операции в папку Sound добавятся два этих файла (рис. 13.2). Всегда сначала явно добавляйте звуковые фай лы в свой проект и только потом приступайте к созданию проекта XACT, который, в свою очередь, берет эти звуковые файлы из папки вашего проекта!
Рис. 13.1. Создание в каталоге проекта новой папки Sound Рис. 13.2. Добавление звуковых файлов в проект
3. Сейчас можно на некоторое время отложить в сторону работу с Visual C# Express и перейти к созданию XACT(проекта. Инструментарий Visual C# Express можно оставить открытым, а можно закрыть, существенной разни( цы в этом нет. Открываем на компьютере программу Microsoft Cross( Platform Audio Creation Tool, которая поставляется вместе с инструмента( рием XNA Game Studio Express. Для этого выполните на компьютере команды Пуск ⇒ Все программы ⇒ Microsoft XNA Game Studio Ex press ⇒ Tools ⇒ Microsoft CrossPlatform Audio Creation Tool. Откроется рабочее окно программы, изображенное на рис. 13.3. Приступим к созда( нию нового проекта. 4. Поочередно в рабочем окне программы выполните следующие команды. Сначала в линейке меню изберите команды Wave Banks ⇒ New Wave Bank, а затем команды Sound Banks ⇒ New Sound Bank. Итогом этих опе( раций станет открытие двух пустых окон в рабочей области Microsoft Cross(Platform Audio Creation Tool с одноименными названиями выпол( няемых команд (рис. 13.4). Эти два пустых файла, входящих в один про( ект, позволяют в пару кликов мыши конвертировать ваши звуковые дан( ные для XACT(проекта. 5. Затем выберите в линейке меню команду View и в появившемся меню из( берите флажком платформу, для которой вы создаете проект. Есть всего два варианта – это Xbox 360 или Windows. Выбрав платформу, нажмите правой кнопкой мыши на пустом месте файла Wave Bank. Появится кон( текстное меню, где необходимо избрать команду Insert Wave File(s), как
198
Звуковые эффекты
Создаем проект ХАСТ
199
показано на рис. 13.5. Выполнение этой команды откроет простое диалого( вое окно выбора файлов. Проследуйте в этом окне в каталог вашего рабоче( го проекта в папку Sound и добавьте два звуковых файла – boom и platform. Оба этих файла моментально добавятся в файл Wave Bank (рис. 13.6). Этой операцией вы присоединили два звуковых файла к проекту XACT. Всегда сначала явно добавляйте звуковые файлы в каталог своего про екта и только потом уже из этого каталога добавляйте в Wave Bank все звуковые файлы, иначе в дальнейшем у вас может возникать мас са непонятных ошибок во время компиляции и сборки проекта!
Рис. 13.3. Рабочее окно Microsoft Cross&Platform Audio Creation Tool
Рис. 13.5. Команда добавления звуковых данных
Рис. 13.4. Создание двух пустых файлов Wave Bank и Sound Bank
6. Затем выделите курсором мыши два добавленных файла в Wave Bank и перетащите файлы в окно Sound Bank, непосредственно в часть окна с на( званием Cue Name (рис. 13.7). После перетаскивания файлов их названия появятся в Cue Name. Клик мыши на любом из файлов откроет более под( робную информацию о звуковом файле во всех текстовых частях окна Sound Bank. Убедитесь, что все добавленные файлы при клике на них мы( шью отображают свои свойства во всех текстовых частях окна Sound Bank. Если какой(то из файлов не представлен во всех текстовых областях окна Sound Bank, то нужно обязательно удалить этот файл из окна Sound Bank (щелчок правой кнопкой мыши на названии файла и выбор команды Delete). Затем необходимо вновь выполнить операцию по перетаскиванию
200
Класс Sound
Звуковые эффекты
Рис. 13.6. Добавление звуковых файлов в Wave Bank
данного файла из окна Wave Bank в окно Sound Bank. Но обычно все рабо( тает с первого раза. 7. Последний штрих к созданию кросс(платформенного проекта XACT – это сохранение всего того, что вы сейчас имеете в открытом окне про( граммы Microsoft Cross(Platform Audio Creation Tool. Выберите в меню команды File ⇒ Save Project As и в появившемся диалоговом окне Save Project As (рис. 13.8) проследуйте в каталог вашего проекта в папку Sound. В поле этого окна Имя файла задайте любое название проекту и нажмите кнопку Сохранить. Всегда сохраняйте проект XACT в той папке вашего проекта, где хранятся звуковые файлы! На этом все, вы создали и сохранили проект XACT в рабочем каталоге вашего проекта (рис. 13.9), и теперь можно спокойно приступать к работе над исходным кодом программы. Итогом всех этих незамысловатых действий стало создание проектного файла Sound.xap. Что нам это дало? В момент компиляции проекта в работу вступит Content Pipeline, который своими внутренними сервисами на основе проектного файла Sound.xap произведет преобразование всех звуковых файлов проекта в три
201
Рис. 13.7. Перетаскиваем файлы в окно Sound Bank
специфических файла: Wave Bank.xwb, Sound Bank.xsb и Sound.xgs. Именно из этих трех файлов в дальнейшем будет происходить воспроизведение звука в игре. То есть после того как вы откомпилируете проект, в рабочем каталоге проекта в папках Debug или Release ⇒ Content ⇒ Sound появятся новые файлы Wave Bank.xwb, Sound Bank.xsb и Sound.xgs – и вся работа программы со звуковыми данными будет происходить именно с этими файлами.
13.2. Класс Sound Для работы со звуковыми данными в игре лучше создать отдельный класс, кото( рый вы всегда можете со временем усовершенствовать, а также использовать в любых других своих проектах. Так поступим и мы, но свой класс писать не бу( дем, а возьмем уже готовое решение, поставляемое в комплекте с Spacewar Win( dows Starter Kit. В частности, класс Sound, который реализован достаточно про( сто и элегантно. Немного переделав его под свой проект, мы получаем вполне функциональный класс. Давайте посмотрим сначала на весь исходный код класса Sound, представленный в листинге 13.1, а затем перейдем к его анализу.
202
Класс Sound
Звуковые эффекты
203
} public static class Sound { private static AudioEngine engine; private static SoundBank soundbank; private static WaveBank wavebank;
Рис. 13.9. Рабочий каталог проекта с добавленными в него звуковыми файлами и проектом XACT
Рис. 13.8. Сохранение проекта XACT в каталоге создаваемого приложения
//========================================================================= /// <summary> /// Ãëàâà 13 /// Ëèñòèíã 13.1 /// Ïðîåêò: Audio /// Êëàññ: Sound /// <summary> //========================================================================= #region Dependencies using System; using System.Collections.Generic; using System.Text; using System.IO; using Microsoft.Xna.Framework.Audio; #endregion namespace Audio { public enum soundList { /// <summary> /// Ïðèêîñíîâåíèå ê ïëàòôîðìå /// Platform, /// <summary> /// Ïàäåíèå â ïðîïàñòü /// Boom,
/// <summary> /// Èíèöèàëèçàöèÿ /// public static void Initialize() { engine = new AudioEngine(“Content\\Sound\\Sound.xgs”); soundbank =new SoundBank(engine, “Content\\Sound\\Sound Bank.xsb”); wavebank = new WaveBank(engine, “Content\\Sound\\Wave Bank.xwb”); } private static string[] cueNames = new string[] { “platform”, “boom”, }; /// <summary> /// Âîñïðîèçâåäåíèå çâóêà ïîñëå îñòàíîâêè /// public static CuePlay(soundList sound) { Cue returnValue = soundbank.GetCue(cueNames[(int)sound]); returnValue.Play(); return returnValue; } /// <summary> /// Âîñïðîèçâåäåíèå /// public static void PlayCue(soundList sound) { soundbank.PlayCue(cueNames[(int)sound]); } /// <summary> /// Îáíîâëÿåì çâóê /// public static void Update() { engine.Update(); } /// <summary> /// Îñòàíàâëèâàåì âîñïðîèçâåäåíèå /// public static void Stop(Cue cue)
204
Класс Sound
Звуковые эффекты
{
205
soundbank.PlayCue(“boom”); cue.Stop(AudioStopOptions.Immediate);
} } }
Ключевыми фигурами в звуковых проектах, основанных на использовании XACT, являются три класса: AudioEngine, SoundBank и WaveBank. Объявле( ние и создание объектов этих классов, а также загрузка звука в игру всегда проис( ходят в одном и том же ключе. Посмотрите на нижеприведенные строки исходно( го кода. private static AudioEngine engine; private static SoundBank soundbank; private static WaveBank wavebank; … public static void Initialize() { engine = new AudioEngine(“Content\\Sound\\Sound.xgs”); soundbank = new SoundBank(engine, “Content\\Sound\\Sound Bank.xsb”); wavebank = new WaveBank(engine, “Content\\Sound\\Wave Bank.xwb”); }
В трех первых строках этого блока кода происходит объявление трех объек( тов. Затем создание или инициализация объектов, в данном случае делается в ме( тоде Initialize(), но это можно сделать в любом месте на этапе загрузки игры. Создаются объекты всегда именно таким способом, как показано в коде. Это их специфика, или черный ящик, как хотите. Главное, что нужно четко понимать, – это какие пути прописывать в параметрах конструкторов классов AudioEngine, SoundBank и WaveBank. Как вы помните, все звуковые файлы на основе проектных данных файла XACT (Sound.xap), добавленные в рабочий каталог проекта, в момент компиля( ции будут преобразованы к файлам под названиями Wave Bank.xwb, Sound Bank.xsb и Sound.xgs. И именно к этим файлам вам необходимо прописывать путь в параметрах конструкторах классов AudioEngine, SoundBank и WaveBank. Если у вас после компиляции появляются ошибки, связанные с тем, что програм( ма не может найти файлы Wave Bank.xwb, Sound Bank.xsb и Sound.xgs, то вы ука( зали неверно либо путь к файлам, либо названия самих файлов. В этом случае после компиляции откройте рабочий каталог проекта и посмотрите, где у вас точ( но располагаются эти файлы, какое название они имеют, и, что немаловажно, об( ратите внимание на расширение файлов, у всех файлов оно разное. Эта стандартная процедура по созданию объектов классов AudioEngine, SoundBank и WaveBank подгружает в игру имеющиеся звуковые данные. Те( перь, чтобы воспроизвести любой файл, необходимо просто вызвать метод PlayCue(), в параметрах которого указать название файла для воспроизве( дения.
В классе Sound реализация воспроизведения звуковых файлов несколько от( личается от того, что я вам показал, но отличается в лучшую сторону. Смысл здесь следующий. Вместо того чтобы в методе PlayCue() явно прописывать название воспроизводимого файла, используется более элегантная конструкция, состоя( щая из структуры soundList и массива данных cueNames. public enum soundList { /// <summary> /// Ïðèêîñíîâåíèå ê ïëàòôîðìå /// Platform, /// <summary> /// Ïàäåíèå â ïðîïàñòü /// Boom, } …. private static string[] cueNames = new string[] { “platform”, “boom”, };
Любые элементы структуры или массива данных всегда ведут свое числовое исчисление от нуля. В структуре soundList присутствуют два названия – Platform и Boom. Для читабельности исходного кода и дальнейшего использова( ния звуковых данных такая запись просто прекрасна, но на самом деле в любой структуре первый элемент (Platform) всегда равен нулю, второй элемент (Boom) – единице и т. д. Аналогичные явления присутствуют и в массиве данных, поэтому если взять метод PlayCue() класса Sound, то получится, что обращение к элементам структуры идет по имени. public static void PlayCue(soundList sound) { soundbank.PlayCue(cueNames[(int)sound]); }
В классе Sound, кроме метода PlayCue(), есть еще три метода. Первый ме( тод Stop() позволяет останавливать воспроизведение определенного звукового файла. /// <summary> /// Îñòàíàâëèâàåì âîñïðîèçâåäåíèå /// public static void Stop(Cue cue)
206
Воспроизведение звука в игре
Звуковые эффекты
{ cue.Stop(AudioStopOptions.Immediate); }
В свою очередь, второй метод CuePlay() позволяет воспроизводить файл после его остановки. В параметре этого метода необходимо просто передать на( звание структурной переменной, которая отвечает за один из звуковых файлов. /// Âîñïðîèçâåäåíèå çâóêà ïîñëå îñòàíîâêè /// public static CuePlay(soundList sound) { Cue returnValue = soundbank.GetCue(cueNames[(int)sound]); returnValue.Play(); return returnValue; }
Последний, очень важный метод занимается тем, что постоянно обновляет работу звукового движка. Вызов этого метода в дальнейшем должен производить( ся непосредственно в игровом цикле в методе Update(). Сейчас давайте перей( дем к классу Game1 и попрактикуемся в воспроизведении звука в игре. /// <summary> /// Îáíîâëÿåì çâóê /// public static void Update() { engine.Update(); }
13.3. Воспроизведение звука в игре Посмотрите на исходный код класса Game1 проекта Audio, приведенный в лис) тинге 13.2. Давайте сначала изучим код, а затем перейдем к его анализу. В некото( рых больших методах этого класса исходный код был вырезан, поскольку не изме( нился с предыдущего проекта. Весь исходный код проекта Audio вы найдете на компакт(диске в папке Code\Chapter13\Audio. //========================================================================= /// <summary> /// Ëèñòèíã 13.2 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 13 /// Ïðîåêò: Audio /// Êëàññ: Game1 /// Çâóêîâûå ýôôåêòû /// <summary>
207
//========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace MenuCursor { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch; Sprite[] sprite = new Sprite[5]; private Texture2D background1; private Texture2D background2; Random rand = new Random(); int j = 0; Sprite platform; private bool paused = false; private bool pauseKeyDown = false; public BoundingBox bbplatform; public BoundingBox[] bb = new BoundingBox[5]; BitmapFont font; int score0, score1, score2, score3, score4; Menu menu; private bool menuState; private int cursorState; Sprite mouse; MouseState mouseState; public BoundingBox bbMouse; public BoundingBox bbCursorGame; public BoundingBox bbCursorExit; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { …. } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() {
208
Воспроизведение звука в игре
Звуковые эффекты Sound.Initialize(); base.Initialize();
} /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { …. } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); // Âûõîä â ìåíþ if (keyboardState.IsKeyDown(Keys.Escape)) { menuState = true; } // Ïîêàçûâàåì ìåíþ if (menuState == true) { UpdateMouse(); } else // Çàïóñêàåì èãðó { Pause(); if (paused == false) { double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite[i].UpdateFrame(elapsed); } MoveSprite();
MovePlatform(); Collisions(); } } Sound.Update(); base.Update(gameTime); } /// <summary> /// Äâèæåíèå ñïðàéòà ïî âåðòèêàëè /// <summary> public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition += sprite[i].speedSprite; if (sprite[i].spritePosition.Y > Window.ClientBounds.Height) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width -300 - sprite[i].spriteTexture.Width / 12), -500); sprite[i].speedSprite.Y = rand.Next(5, 9); Sound.PlayCue(soundList.Boom); } } } /// <summary> /// Äâèæåíèå ïëàòôîðìû ïî ýêðàíó /// <summary> public void MovePlatform() { …. } /// <summary> /// Ïàóçà â èãðå /// <summary> public void Pause() { if (keyboardState.IsKeyDown(Keys.P)) { pauseKeyDown = true; } else if (pauseKeyDown) { pauseKeyDown = false; paused = !paused; } } /// <summary> /// Ñòîëêíîâåíèÿ
209
210
Воспроизведение звука в игре
Звуковые эффекты
/// <summary> public void Collisions() { bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 25, 0); bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 25 + platform.spriteTexture.Height, 0); for (int i = 0; bb.Length > i; i++) { bb[i].Min = new Vector3(sprite[i].spritePosition.X, sprite[i].spritePosition.Y, 0); bb[i].Max = new Vector3(sprite[i].spritePosition.X + sprite[i].spriteTexture.Width / 12, sprite[i].spritePosition.Y + sprite[i].spriteTexture.Height, 0); } if (bbplatform.Intersects(bb[0])) { sprite[0].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[0].spriteTexture.Width / 12), -500); sprite[0].speedSprite.Y = rand.Next(5, 9); score0 += 1; Sound.PlayCue(soundList.Platform); } if (bbplatform.Intersects(bb[1])) { sprite[1].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[1].spriteTexture.Width / 12), -500); sprite[1].speedSprite.Y = rand.Next(5, 9); score1 += 1; Sound.PlayCue(soundList.Platform); } if (bbplatform.Intersects(bb[2])) { sprite[2].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[2].spriteTexture.Width / 12), -500); sprite[2].speedSprite.Y = rand.Next(5, 9); score2 += 1; Sound.PlayCue(soundList.Platform); } if (bbplatform.Intersects(bb[3])) { sprite[3].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[3].spriteTexture.Width / 12), -500); sprite[3].speedSprite.Y = rand.Next(5, 9);
211
score3 += 1; Sound.PlayCue(soundList.Platform); } if (bbplatform.Intersects(bb[4])) { sprite[4].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[4].spriteTexture.Width / 12), -500); sprite[4].speedSprite.Y = rand.Next(5, 9); score4 += 1; Sound.PlayCue(soundList.Platform); } } /// <summary> /// Íîâàÿ èãðà /// <summary> public void NewGame() { j = 0; for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 500), j = j - 300); } platform.spritePosition = new Vector2(Window.ClientBounds.Width/ 2, Window.ClientBounds.Height - 90); score0 = 0; score1 = 0; score2 = 0; score3 = 0; score4 = 0; base.Initialize(); } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå ìûøè /// <summary> public void UpdateMouse() { …. } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); if (menuState == true) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
212
Цикличное воспроизведение музыки
Звуковые эффекты menu.DrawMenu(spriteBatch, cursorState); mouse.DrawSprite(spriteBatch); spriteBatch.End();
} else { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); for (int i = 0; sprite.Length > i; i++) { sprite[i].DrawAnimationSprite(spriteBatch); } spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); platform.DrawSprite(spriteBatch); spriteBatch.End(); if (paused == true) { font.DrawString(450, 300, Color.Black, “Ïàóçà”); } int fy = 245; font.DrawString(Window.ClientBounds.Width - 95, fy, Color.Black, “{0}”, score0); font.DrawString(Window.ClientBounds.Width-95, fy += font.LineHeight + 12, “{0}”, score1); font.DrawString(Window.ClientBounds.Width-95, fy += font.LineHeight + 12, “{0}”, score2); font.DrawString(Window.ClientBounds.Width-95, fy += font.LineHeight + 12, “{0}”, score3); font.DrawString(Window.ClientBounds.Width-95, fy += font.LineHeight + 12, “{0}”, score4); } base.Draw(gameTime); } } }
У нас в игре имеются два звуковых файла, которые определены для событий, связанных со столкновением платформы и объектов, а также падением объектов в пропасть. Соответственно и вызовы метода Sound.PlayCue() происходят в методах, обрабатывающих столкновения с платформой и окончанием экрана. Здесь все просто. Придерживаясь в дальнейшем аналогичной схемы работы со звуковыми дан( ными, можно добавить в игру любое количество звуковых эффектов или больших звуковых дорожек. Техника воспроизведения всех звуковых файлов, а также со( здания проектов XACT одинакова.
13.4. Цикличное воспроизведение музыки В своих играх вам обязательно захочется воспроизводить одну или более мелодий в цикличном режиме. В XNA Game Studio Express такая возможность, несомнен( но, имеется. При этом цикличное воспроизведение одного из файлов задается еще на этапе формирования проекта XACT. Посмотрите на рис. 13.7, где представлен этап добавления в проект XACT новых звуковых файлов. Так вот если в окне Sound Bank выделить курсором название файла, а затем в правой части этого окна выделить ветку структуры древовидной иерархии с названием Play Wave (рис. 13.10), то в левой части главного окна программы Microsoft Cross(Platform Audio Creation Tool откроется дополнительная панель со свойствами выбранного звукового файла. В перечислениях свойств файла имеется параметр LoopEvent. В данный мо( мент значение этого параметра выставлено в No, что означает: цикличное воспроиз( ведение для этого файла не задано (рис. 13.11). Для того чтобы установить циклич( ность для этого параметра, достаточно выбрать из списка значение Infinite и продолжать работу с проектом по рассмотренной в этой главе схеме. Затем, когда вы запустите проект, то для того файла, для которого назначили цикличное проиг( рывание, программа автоматически выберет заданный режим воспроизведения.
Первым делом в классе Game1 необходимо воспользоваться методом Sound.Initialize() и создать объекты AudioEngine, SoundBank и WaveBank. protected override void Initialize() { Sound.Initialize(); base.Initialize(); }
Затем мы переходим к игровому циклу и вызываем в нем метод Sound.Update(). Этот метод, как вы помните, обновляет работу звукового движка. На этом все под( готовительные действия выполнены, и можно заняться воспроизведением звука в игре или использовать любые другие методы класса Sound.
213
Рис. 13.10. Выделение названия файла
Рис. 13.11. Панель свойств звукового файла
Переход с уровня на уровень
Глава 14 Добавляем в игру новые уровни Играя в любую игру, пользователь всегда надеется на длительное продолжение игро( вого процесса. Поэтому большинство игр имеют определенный набор уровней. По прохождении одного из уровней игроку предлагается пройти следующий уровень и т. д. Если один из уровней не был пройден, то игрок должен пройти его вновь либо начать игру с места последнего игрового сохранения или пройденной контрольной точки. Любая из перечисленных методик прохождения уровней в конечном счете оп( ределяет общую стратегию прохождений игры в целом. Поэтому на этапе проектиро( вания игры этому очень важному аспекту нужно уделить много времени. В нашей игре этому вопросу на начальном этапе мы не уделяли должного вни( мания, поскольку вы тогда еще не знали, как делаются игры и из чего они состоят. Но прежде чем начинать работать над этой книгой, я сначала разработал схему игрового меню, механизм перехода с уровня на уровень, потом сконструировал модель игровых классов. Затем попросил свою жену нарисовать к игре несколько концептов игровой графики и только после всех этих стадий приступил к написа( нию исходного кода игры. По окончании работы над игрой был придуман подход в представлении вам материала книги (от простого к сложному), и соответственно весь исходный код был поделен на главы. И только потом я приступил к написанию самой книги. То есть за всей книгой (как и самой программой) скрывается на самом деле большая работа, которая практически идентична любой предварительной работе по созда( нию игры. Нельзя просто сесть за компьютер и написать игру с чистого листа, точ( нее сделать это, конечно, можно, но делать вы эту игру будете точно долго. Это как в том старом анекдоте: программист спрашивает другого программиста, что пи( шешь, а тот отвечает: а вот откомпилируем – и узнаем. Чтобы не пойти по тропинке программиста из анекдота, изначально необхо( димо уделить проектированию игры столько времени, сколько необходимо. И только после того как вы определитесь со всеми винтиками, гайками и шайбами вашей игры, можно приступать к ее написанию. В нашей игре для перехода с уровня на уровень также была изначально приду( мана определенная схема. В чем эта схема заключается и как ее реализовать, вы узнаете из этой главы. Вашему вниманию будет представлен последний проект в этой части книги под названием NewLevels. В этом проекте мы добавим в игру новые уровни и создадим механизм перехода с уровня на уровень.
215
14.1. Переход с уровня на уровень Продолжение игры, или переход с уровня на уровень, обязан происходить по за( данному (естественно, вами как программистом) событию. Такое событие может быть ликвидацией всех врагов на уровне или главного босса, окончание уровня – по прошествии определенного промежутка времени или после выполнения игро( вой задачи на этом уровне либо по достижении заданного количества очков и т. д. То есть важно создать определенное условие, после выполнения которого игрок может перейти к своей следующей миссии. В нашей игре переход с уровня на уровень происходит по количеству набран( ных очков. Каждый раз при переходе на следующий уровень планка набранных очков будет повышаться. Дополнительно на каждом уровне будет устанавливать( ся различное количество начисляемых очков за пойманный объект. В итоге каж( дый новый уровень будет несколько усложнен в отличие от предыдущего. Но в идеале, конечно, в каждом новом уровне игры вы можете изменять игровой фон, падающие объекты, добавлять различных сложностей и артефактов и т. д. Сейчас моя задача заключается лишь в том, чтобы научить и показать вам, как это делает( ся, а все остальное – это дело вашей фантазии. В игре «Летящие в прерии» после набора заданного количества очков пользова( тель сможет перейти на новый уровень. Для того чтобы реализовать этот механизм, нам понадобится еще одна булева переменная, которая представит промежуточный экран перехода с уровня на уровень, так, как мы это делали с меню. На этом экране мы покажем информацию о набранных очках, а также предложим игроку на выбор три действия: продолжение игры, выход в меню игры и закрытие программы. По( смотрите на рис. 14.1, где представлен дополнительный элемент в игровом меха(
Рис. 14.1. Игровые состояния
216
Проект NewLevels
Добавляем в игру новые уровни
низме. Единственное, что сейчас необходимо, – это проработать механизм набора очков и задать верхнюю планку, пройдя которую игроку будет представлен этот са( мый промежуточный экран или экран с выбором нового уровня.
14.2. Набранные очки В предыдущих примерах для подсчета очков нами использовались пять следую( щих переменных: int score0, score1, score2, score3, score4;
Эти переменные вели простой подсчет пойманных объектов, а затем мы выво( дили эту информацию на экран. В новом проекте NewLevels мы изменим назна( чение этих переменных. Теперь все перечисленные переменные будут содержать определенное количество баллов и будут привязаны к каждому из падающих объектов. Когда платформа поймает один из объектов, то согласно значению од( ной из переменных будет выполнен набор или снятие очков с игрока. Соответ( ственно для подсчета общего количества очков в исходный код вводится новая переменная totalScore. Дополнительно мы введем в программу еще одну переменную под названием endScore. В этой переменной мы будем хранить то количество очков, которое необходимо набрать пользователю для перехода к следующему уровню. Тогда, чтобы перейти на следующий уровень, нам необходимо всего лишь создать сле( дующую проверку условия: if(totalScore >= endScore) { // ìåõàíèçì ïîêàçà ýêðàíà ñ âûáîðîì íîâîãî óðîâíÿ. }
Такую конструкцию кода достаточно поместить в игровой цикл, и проверка условия будет осуществляться на каждой итерации игрового цикла. В свою оче( редь, увеличение переменной totalScore может происходить следующим об( разом: score0 = 5; score1 = -10; score2 = 5; score3 = -10; score4 = 5; totalScore = 0; endScore = 200; … totalScore += score0; totalScore += score1; totalScore += score2;
217
totalScore += score3; totalScore += score4;
Переменные score0, score1, score2, score3 и score4 мы жестко привя( жем к каждому падающему объекту и сделаем это в соответствии с числовыми обозначениями объектов в массиве данных и самих переменных. Касание объекта и платформы будет приводить к увеличению или уменьшению общего количества очков. Инициализация всех перечисленных переменных производится непосред( ственно в методе NewGame(), который также несколько видоизменяется. Но да( вайте обо всем по порядку и перейдем к работе над проектом NewLevels.
14.3. Проект NewLevels Начнем с того, что переработаем некоторые графические элементы интерфейса игры. В рамке с правой стороны у нас имелась табличка, в которой выводилось количество пойманных объектов. Теперь мы изме( ним назначение этой таблички и будем в ней пока( зывать пользователю то количество очков, кото( рое он может набрать либо потерять, поймав один из объектов (рис. 14.2). Дополнительно в рамке имеется еще одна табличка, в которой ранее до сих пор содержался информационный текст. Теперь пришло время использовать эту табличку по свое( му назначению. В частности, на этой табличке бу( дет выводиться информация о том, на каком сей( час уровне пользователь играет, сколько игроку нужно набрать очков для окончания уровня и те( кущее количество очков (рис. 14.2). Промежуточный экран, появляющийся между уровнями, представляется также новой табличкой (рис. 14.3). В этой табличке будут выводиться раз( личные сведения информационного характера по текущему состоянию игры. Когда пользователь пройдет уровень, то игра будет остановлена и по( явится промежуточный экран с этой табличкой, где пользователю будет предложено продолжить уровень, выйти в меню или закрыть программу. Последнее графическое нововведение в игре касается изменения режима паузы. Ранее в этом режиме мы писали на экране слово Пауза. Теперь вместо этого слова на экране будет появляться Рис. 14.2. Информационные таблички
218
Проект NewLevels
Добавляем в игру новые уровни
219
Первые две переменные, как мы уже выяснили, служат соответственно для подсчета общего количества набранных очков и количества очков, которое необ( ходимо набрать пользователю для перехода на следующий уровень. Следующие две булевы переменные – gameState и levelState – понадо( бятся нам для отслеживания состояния игры. О них мы поговорим подробнее чуть позже в этой главе, когда дойдем до игрового цикла. Следующие две переменные в этом блоке кода – level и tempLevel – пред( ставляют текущий уровень игры. Зачем нужны сразу две переменные для пред( ставления одного уровня? Дело в том, что переменная level будет изменяться по прошествии одного из уровней (увеличиваться на единицу), но для информаци( онной таблички, появляющейся между уровнями, нам понадобится показать, ка( кой из уровней был пройден только что, а переменная level к этому времени уже увеличится на единицу. Поэтому и применяется дополнительная переменная tempLevel. Два последних объекта gameLevel и pausedTexture класса Texture2D представляют соответственно междууровневую табличку (рис. 14.3) и табличку режима паузы (рис. 14.4). В методе LoadGraphicsContent() мы загрузим эти два графических файла, которые по традиции располагаются в рабочем каталоге проекта в папке \Content\Textures, после явного добавления их в проект. В конструкторе класса Game1 происходит инициализация объявленных пере( менных. Рис. 14.3. Табличка перехода с уровня на уровень level = 1; tempLevel = level; gameState = false; levelState = false;
красочная табличка с информацией о том, какие команды необходимо выполнить для продолжения игры или для выхода в меню программы (рис. 14.4).
14.3.1. Изменения в классе Game1
Рис. 14.4. Табличка для режима паузы
Начинаем работать над новым проектом и классом Game1. Прежде всего добавим в область глобальных переменных этого класса несколько новых переменных. private private private private private private private private
int totalScore; int endScore; bool gameState; bool levelState; int level; int tempLevel; Texture2D gameLevel; Texture2D pausedTexture;
На начальном этапе игры мы задаем значение переменной level, равное еди( нице, для того чтобы игрок мог начать играть непосредственного с первого уровня. Если в игре предусмотрена система сохранения прошедших уровней, то это как раз то место, где можно прочитать из памяти сохраненные данные и присвоить пере( менной level уровень, с которого пользователь должен продолжить играть. В справочной системе XNA Game Studio Express (Programming Guide ⇒ Storage) вы найдете всю необходимую информацию о механизме сохране) ния различных данных на жестком диске. Особенно советую обратить вни) мание на пример с названием: How to: Serialize Data. В этом примере со) зданы два универсальных метода для записи данных на диск и их чтения. Эти методы очень легко использовать для сохранения игровых данных на жестком диске. Исходный код примеров не сложен, и, надеюсь, вы уже в си) лах разобраться с ним самостоятельно. Две булевы переменные gameState и levelState устанавливаются в состоя( ние false. Перейдем к игровому циклу в метод Update() и посмотрим, для чего нам понадобились эти две новые переменные.
220
Проект NewLevels
Добавляем в игру новые уровни
if(levelState == true) { LevelSelect(); } else if(menuState == true) { UpdateMouse(); } else if(gameState == true) { Pause(); …
case 2: score0 = 10; score1 = -20; score2 = 5; score3 = -20; score4 = 5; totalScore = 0; endScore = 600; break; case 3: … break;
В этом блоке кода происходит выбор состояния игры на основе булевых пере( менных. Всего имеются четыре следующих состояния:
case 4: … break;
показывать меню, если menuState равно true; играть в игру, если gameState равно true; показывать межуровневый экран, если levelState равна true; выйти в меню игры, если нажата клавиша Esc.
case 5: … break;
После того как пользователь вошел в игру, запускается работа метода UpdateMouse() и показ на экране монитора меню. Если пользователь выбрал команду Играть, то происходит выполнение следующего блока кода.
case 6: … break; case 7: … break;
level = 1; this.NewGame(level); menuState = false; levelState = false; gameState = true;
В коде происходит присвоение переменной level значения уровня, на кото( ром пользователь будет играть (здесь также можно читать данные из памяти, со( храненные ранее). Затем вызывается метод NewGame(), в параметр которого пе( редается значение переменной level, а значит, и текущий уровень. В методе NewGame() добавляется конструкция кода с оператором switch. На базе входящей переменной level оператор switch выбирает новые значения для каждого последующего уровня. switch(curentLevel) { case 1: score0 = 5; score1 = -20; score2 = 5; score3 = -20; score4 = 5; totalScore = 0; endScore = 300; break;
221
case 8: … break; }
В игре было задано восемь уровней, и, честно говоря, я не сильно усердство( вал, когда назначал баллы за подборы объектов и порог набранных очков. В реаль( ной игре вам необходимо обязательно уделить этому моменту очень много вре( мени и протестировать игру и все имеющиеся в ней уровни. Это очень важно, и от этого зависит интересность и состоятельность вашей игры. Естественно, что ко( личество уровней в игре может быть любым, кроме того, вы можете добавить на каждый новый уровень новые объекты, фон и многое другое. Когда будете создавать конструкцию кода с использованием оператора switch, не забывайте в конце очередного блока case прописывать ключе) вое слово break. Иначе у вас вместо блочной работы этой конструкции будет происходить ее построчное выполнение. Продолжаем разбор метода UpdateMouse() и, в частности, место присвое( ния булевым переменным menuState и levelState значения false. Назна( ченные действия ведут к выходу из игрового меню и к пропуску показа межуров(
222
Проект NewLevels
Добавляем в игру новые уровни
невого экрана. В свою очередь, переменная gameState получит значение, равное true, а значит, запустится работа игрового процесса. В самом игровом процессе, как только пользователь достигает верхней планки очков, назначенных на текущий уровень, происходит вызов следующего блока исходного кода.
223
gameState = true; } if(keyboardState.IsKeyDown(Keys.B)) { menuState = true; levelState = false; gameState = false; }
if(totalScore >= endScore) { gameState = false; menuState = false; levelState = true; tempLevel = level;
if (keyboardState.IsKeyDown(Keys.Escape)) { this.Exit(); } }
level += 1; if(level > 8) { level = 1; } }
Здесь мы присваиваем переменной gameState значение false, для вывода из игрового процесса пользователя. Переменная menuState также имеет значе( ние false, но она и имела это значение раньше, тут мы просто подстраховываем( ся на всякий случай. Далее переменная levelState получает значение, равное true, что приведет нас в дальнейшем (после выхода из этого блока кода) к выпол( нению метода LevelSelect(). В конце блока кода переменной tempLevel при( сваивается значение текущего уровня. Сам уровень увеличивается на единицу, а в блоке кода
В методе Draw() класса Game1, где происходит прорисовка графической со( ставляющей, также формируется логическая конструкция, позволяющая выби( рать, что именно показывать в конкретный игровой момент. Особенно интересно состояние игры на межуровневом промежутке, когда переменная levelState равна значению true. Этот приведенный ниже блок кода относится к межуровне( вому состоянию игры. else if(levelState == true) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); menu.DrawMenu(spriteBatch, 3); spriteBatch.Draw(gameLevel, new Vector2(300, 220), Color.White); spriteBatch.End(); int f = 320; if (tempLevel == 8) { font.DrawString(440, f, Color.Black, “Âû ïîáåäèëè!!!”); } font.DrawString(420, f, Color.Black, “Ïðîäîëæåíèå èãðû”); font.DrawString(420, f += font.LineHeight, “Âû ïðîøëè óðîâåíü”); font.DrawString(500, f += font.LineHeight, “{0}”, tempLevel); font.DrawString(430, f += font.LineHeight + 35, “Íàáðàííûå î÷êè”); font.DrawString(490, f += font.LineHeight, “{0}”, totalScore); font.DrawString(440, f += font.LineHeight, “Íóæíî î÷êîâ”); font.DrawString(490, f += font.LineHeight , “{0}”, endScore); font.DrawString(415, f += font.LineHeight, “Ñëåäóþùèé óðîâåíü”); font.DrawString(500, f += font.LineHeight, “{0}”, level);
if(level > 8) { level = 1; }
происходит проверка, достиг ли игрок последнего уровня. Если достиг, то сбрасы( ваем level к первому уровню, если нет, то оставляем все как прежде. Метод LevelSelect() следит за нажатыми пользователем клавишами и вы( бирает в связи с полученной информацией с устройства ввода определенные дей( ствия. В работе этого метода, думается, все предельно ясно, интересно другое – что показывает нам игра, когда работает межуровневый экран. public void LevelSelect() { if (keyboardState.IsKeyDown(Keys.A)) { this.NewGame(level); menuState = false; levelState = false;
}
Интересное нововведение происходит в строке: menu.DrawMenu(spriteBatch, 3);
Ранее в классе Menu мы имели всего два состояния в методе Draw() этого класса. В первом и во втором состояниях на экран поверх основного фона меню
224
Проект NewLevels
Добавляем в игру новые уровни
выводились две таблички. Теперь в дополнительном третьем состоянии эти таб( лички не выводятся, а на фоне меню рисуется табличка для межуровневого сос( тояния. spriteBatch.Draw(gameLevel, new Vector2(300, 220), Color.White);
В идеале в игре вы можете создать базовую фоновую заставку и использовать ее на любом участке игровых состояний, определяя, что именно вы хотите видеть поверх этой заставки. Оставшиеся строки исходного кода в этом блоке выводят на табличке различ( ную текстовую информацию. Еще раз повторюсь, что шрифт используется по умолчанию, идущий с пакетом XNAExtras, но вы можете создать свой алфавит и значительно улучшить графику в игре. И не забывайте об очередности рисования на экране графики, все то, что вызывается в методе Draw() позже, соответственно и рисуется позже. В листинге 14.1 представлен полный исходный код класса Game1. Файлы проекта NewLevels находятся на компакт(диске в папке Code\Chapter14\NewLevels. //========================================================================= /// <summary> /// Ëèñòèíã 14.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 14 /// Ïðîåêò: NewLevels /// Êëàññ: Game1 /// Íîâûå óðîâíè /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace NewLevels { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; SpriteBatch spriteBatch;
Sprite[] sprite = new Sprite[5]; private Texture2D background1; private Texture2D background2; Random rand = new Random(); int j; Sprite platform; private bool paused = false; private bool pauseKeyDown = false; public BoundingBox bbplatform; public BoundingBox[] bb = new BoundingBox[5]; BitmapFont font; int score0, score1, score2, score3, score4; Menu menu; private bool menuState; private int cursorState; Sprite mouse; MouseState mouseState; public BoundingBox bbMouse; public BoundingBox bbCursorGame; public BoundingBox bbCursorExit; private int totalScore; private int endScore; private bool gameState; private bool levelState; private int level; private int tempLevel; private Texture2D gameLevel; private Texture2D pausedTexture; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1024; graphics.PreferredBackBufferHeight = 768; graphics.PreferMultiSampling = false; graphics.IsFullScreen = true; for (int i = 0; sprite.Length > i; i++) { sprite[i] = new Sprite(12, 10); } platform = new Sprite(); font = new BitmapFont(“Content\\Font\\russian.xml”); menu = new Menu(); menuState = true; cursorState = 1; mouse = new Sprite(); bbMouse = new BoundingBox(); bbCursorGame = new BoundingBox(); bbCursorExit = new BoundingBox(); level = 1;
225
226
Проект NewLevels
Добавляем в игру новые уровни tempLevel = level; gameState = false; levelState = false;
keyboardState = Keyboard.GetState(); if (levelState == true) { LevelSelect(); } else if(menuState == true) { UpdateMouse(); } else if(gameState == true) { if(keyboardState.IsKeyDown(Keys.Escape)) menuState = true;
} /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { Sound.Initialize(); base.Initialize(); }
Pause(); if(paused == false) { if (totalScore >= endScore) { gameState = false; menuState = false; levelState = true; tempLevel = level;
/// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); background1 = content.Load(“Content\\Textures\\background1”); background2 = content.Load(“Content\\Textures\\background2”); platform.Load(content, “Content\\Textures\\platform”); sprite[0].Load(content, “Content\\Textures\\0”); sprite[1].Load(content, “Content\\Textures\\1”); sprite[2].Load(content, “Content\\Textures\\2”); sprite[3].Load(content, “Content\\Textures\\3”); sprite[4].Load(content, “Content\\Textures\\4”); font.Reset(graphics.GraphicsDevice); menu.Load(content); mouse.Load(content, “Content\\Textures\\mouse”); pausedTexture = content.Load(“Content\\Textures\\paused”); gameLevel = content.Load(“Content\\Textures\\game”); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) {
level += 1; if (level > 8) { level = 1; } } double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite[i].UpdateFrame(elapsed); } MoveSprite(); MovePlatform(); Collisions(); } } Sound.Update(); base.Update(gameTime); } /// <summary> /// Äâèæåíèå ñïðàéòà ïî âåðòèêàëè /// <summary> public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition += sprite[i].speedSprite; if (sprite[i].spritePosition.Y > Window.ClientBounds.Height)
227
228
Проект NewLevels
Добавляем в игру новые уровни
229
{
{ sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[i].spriteTexture.Width / 12), -500); sprite[i].speedSprite.Y = rand.Next(6, 10); Sound.PlayCue(soundList.Boom); } } } /// <summary> /// Äâèæåíèå ïëàòôîðìû ïî ýêðàíó /// <summary> public void MovePlatform() { if (keyboardState.IsKeyDown(Keys.Left)) platform.spritePosition.X -= 10; else if (keyboardState.IsKeyDown(Keys.Right)) platform.spritePosition.X += 10; if (platform.spritePosition.X < 10) platform.spritePosition.X = 10; else if(platform.spritePosition.X> Window.ClientBounds.Width - 400) platform.spritePosition.X = Window.ClientBounds.Width - 400; } /// <summary> /// Ïàóçà â èãðå /// <summary> public void Pause() { if (keyboardState.IsKeyDown(Keys.P)) { pauseKeyDown = true; } else if (pauseKeyDown) { pauseKeyDown = false; paused = !paused; } } /// <summary> /// Ñòîëêíîâåíèÿ /// <summary> public void Collisions() { bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 25, 0); bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 25 + 2, 0); for (int i = 0; bb.Length > i; i++)
bb[i].Min = new Vector3(sprite[i].spritePosition.X, sprite[i].spritePosition.Y, 0); bb[i].Max = new Vector3(sprite[i].spritePosition.X + sprite[i].spriteTexture.Width / 12, sprite[i].spritePosition.Y + sprite[i].spriteTexture.Height, 0); } if (bbplatform.Intersects(bb[0])) { sprite[0].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[0].spriteTexture.Width / 12), -500); sprite[0].speedSprite.Y = rand.Next(6, 10); totalScore += score0; Sound.PlayCue(soundList.Platform); } if (bbplatform.Intersects(bb[1])) { sprite[1].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[1].spriteTexture.Width / 12), -500); sprite[1].speedSprite.Y = rand.Next(6, 10); totalScore += score1; Sound.PlayCue(soundList.Platform); } if (bbplatform.Intersects(bb[2])) { sprite[2].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[2].spriteTexture.Width / 12), -500); sprite[2].speedSprite.Y = rand.Next(6, 10); totalScore += score2; Sound.PlayCue(soundList.Platform); } if (bbplatform.Intersects(bb[3])) { sprite[3].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[3].spriteTexture.Width / 12), -500); sprite[3].speedSprite.Y = rand.Next(6, 10); totalScore += score3; Sound.PlayCue(soundList.Platform); } if (bbplatform.Intersects(bb[4])) { sprite[4].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width 300 - sprite[4].spriteTexture.Width / 12), -500); sprite[4].speedSprite.Y = rand.Next(6, 10);
230
Проект NewLevels
Добавляем в игру новые уровни score1 = -30; score2 = 5; score3 = -30; score4 = 8; totalScore = 0; endScore = 900; break;
totalScore += score4; Sound.PlayCue(soundList.Platform); } } /// <summary> /// Íîâàÿ èãðà /// <summary> public void NewGame(int curentLevel) { j = 0; for (int i = 0; sprite.Length > i; i++) { sprite[i].spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 500), j = j - 300); }
case 5: score0 = 10; score1 = -30; score2 = 5; score3 = -40; score4 = 8; totalScore = 0; endScore = 1000; break;
platform.spritePosition = new Vector2(Window.ClientBounds.Width / 2, Window.ClientBounds.Height - 90);
case 6: score0 = 10; score1 = -40; score2 = 5; score3 = -50; score4 = 8; totalScore = 0; endScore = 1100; break;
switch (curentLevel) { case 1: score0 = 5; score1 = -20; score2 = 5; score3 = -20; score4 = 5; totalScore = 0; endScore = 300; break;
case 7: score0 = 15; score1 = -60; score2 = 8; score3 = -80; score4 = 10; totalScore = 0; endScore = 1200; break;
case 2: score0 = 10; score1 = -20; score2 = 5; score3 = -20; score4 = 5; totalScore = 0; endScore = 600; break; case 3: score0 = 10; score1 = -30; score2 = 5; score3 = -30; score4 = 8; totalScore = 0; endScore = 800; break; case 4: score0 = 10;
case 8: score0 = 20; score1 = -100; score2 = 8; score3 = -100; score4 = 10; totalScore = 0; endScore = 1300; break; } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå ìûøè /// <summary> public void UpdateMouse()
231
232
Проект NewLevels
Добавляем в игру новые уровни if (keyboardState.IsKeyDown(Keys.A)) { this.NewGame(level); menuState = false; levelState = false; gameState = true; }
{ // Ïîëó÷àåì ìåñòîíàõîæäåíèÿ êóðñîðà ìûøè mouseState = Mouse.GetState(); mouse.spritePosition.X = mouseState.X; mouse.spritePosition.Y = mouseState.Y; // Ñîçäàåì îãðàíè÷èâàþùèå ïðÿìîóãîëüíèêè äëÿ êóðñîðà è òàáëè÷åê bbMouse.Min = new Vector3(mouse.spritePosition.X, mouse.spritePosition.Y, 0); bbMouse.Max = new Vector3(mouse.spritePosition.X + mouse.spriteTexture.Width, mouse.spritePosition.Y + mouse.spriteTexture.Height, 0);
if(keyboardState.IsKeyDown(Keys.B)) { menuState = true; levelState = false; gameState = false; }
bbCursorGame.Min = new Vector3(650, 400, 0); bbCursorGame.Max = new Vector3(950, 500, 0);
if (keyboardState.IsKeyDown(Keys.Escape)) { this.Exit(); }
bbCursorExit.Min = new Vector3(650, 600, 0); bbCursorExit.Max = new Vector3(950, 700, 0); // Îáðàáàòûâàåì ïåðåñå÷åíèå ïðÿìîóãîëüíèêîâ if (bbMouse.Intersects(bbCursorGame)) { menu.cursorPositionGame = new Vector2(650, menu.cursorPositionExit = new Vector2(700, cursorState = 1; } if (bbMouse.Intersects(bbCursorExit)) { menu.cursorPositionGame = new Vector2(700, menu.cursorPositionExit = new Vector2(650, cursorState = 2; }
}
400); 550);
400); 550);
// Îáðàáàòûâàåì íàæàòèå ëåâîé êíîïêîé ìûøè if(mouseState.LeftButton == ButtonState.Pressed && bbMouse.Intersects(bbCursorGame)) { level = 1; this.NewGame(level); menuState = false; levelState = false; gameState = true; } else if(mouseState.LeftButton == ButtonState.Pressed && bbMouse.Intersects(bbCursorExit)) { this.Exit(); } } /// <summary> /// Ïðîäîëæåíèå èãðû /// <summary> public void LevelSelect() {
233
/// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); if(menuState == true) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); menu.DrawMenu(spriteBatch, cursorState); mouse.DrawSprite(spriteBatch); spriteBatch.End(); } else if(levelState == true) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); menu.DrawMenu(spriteBatch, 3); spriteBatch.Draw(gameLevel, new Vector2(300, 220), Color.White);; spriteBatch.End(); int f = 320; if (tempLevel == 8) { font.DrawString(440, } font.DrawString(420, font.DrawString(420, font.DrawString(500, font.DrawString(430, font.DrawString(490, font.DrawString(440, font.DrawString(490, font.DrawString(415,
f, Color.Black, “Âû ïîáåäèëè!!!”); f, Color.Black, “Ïðîäîëæåíèå èãðû”); f += font.LineHeight, “Âû ïðîøëè óðîâåíü”); f += font.LineHeight, “{0}”, tempLevel); f += font.LineHeight + 35, “Íàáðàííûå î÷êè”); f += font.LineHeight, “{0}”, totalScore); f += font.LineHeight, “Íóæíî î÷êîâ”); f += font.LineHeight , “{0}”, endScore); f += font.LineHeight, “Ñëåäóþùèé óðîâåíü”);
234
Добавляем в игру новые уровни font.DrawString(500, f += font.LineHeight, “{0}”, level); } else if(gameState == true) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background1, new Vector2(0, 0), Color.White); for (int i = 0; sprite.Length > i; i++) { sprite[i].DrawAnimationSprite(spriteBatch); } spriteBatch.Draw(background2, new Vector2(0, 0), Color.White); platform.DrawSprite(spriteBatch); spriteBatch.End(); if(paused == true) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(pausedTexture, new Vector2(300, 300), Color.White); spriteBatch.End(); } int fy = 245; font.DrawString(Window.ClientBounds.Width Color.Black, “{0}”, score0); font.DrawString(Window.ClientBounds.Width font.LineHeight + 12, “{0}”, score1); font.DrawString(Window.ClientBounds.Width font.LineHeight + 12, “{0}”, score2); font.DrawString(Window.ClientBounds.Width font.LineHeight + 12, “{0}”, score3); font.DrawString(Window.ClientBounds.Width font.LineHeight + 12, “{0}”, score4);
- 95, fy, - 95, fy += - 95, fy += - 95, fy += - 95, fy +=
int fyy = 585; font.DrawString(Window.ClientBounds.Width - 110, fyy += font.LineHeight, “{0}”, level); font.DrawString(Window.ClientBounds.Width - 110, fyy += font.LineHeight, “{0}”, totalScore); font.DrawString(Window.ClientBounds.Width - 130, 674, “{0}”, endScore); } base.Draw(gameTime); } } }
Глава 15 Формируем инсталляционный пакет Вот и настал долгожданный момент, когда, несмотря на все трудности, вы все( таки создали свою игру, многократно ее протестировали и готовы продавать или распространять программу бесплатно. Для того чтобы это сделать, вам необходи( мо сформировать стандартный инсталляционный пакет. Для работы программ, созданных в XNA Game Studio Express, есть ряд требо( ваний. На компьютере пользователя должен быть обязательно установлен пакет .NET Framework от версии 2.0 и выше, как минимум Runtime DirectX 9.0c, а также пакет XNA Redistribute. Пакет XNA Redistribute вы можете найти у себя на компью( тере в рабочем каталоге студии. Он располагается в папке Каталог установки студии\XNA Game Studio Express\v1.0\Redist\XNA FX Redist\xnafx_redist. Этот файл небольшой по размеру (1,9 Мб), и его можно распространять с прило( жением либо прямо добавить в инсталляционный пакет программы. Пакет Runtime DirectX 9.0c также можно найти у себя на компьютере в папке Каталог установки студии\XNA Game Studio Express\v1.0\Redist\DX DXSETUP. Пакет .NET Framework 2.0 достаточно увесистый (24 Мб), поэтому на закачку этого пакета придется давать соответствующую ссылку на сайт Microsoft. Но опе( рационная система Windows Vista имеет встроенную версию пакета .NET Frame( work 2.0, а для Windows XP уже давно имеются обновления с .NET Framework 2.0. Без перечисленных пакетов ваша программа не будет работать на компьютере пользователя, поэтому необходимо обязательно упомянуть эти требования в ва( шем файле Readme, поставляемым вместе с игрой. Распространять игру можно, конечно, и без установочного пакета, достаточно просто взять файл игры в формате EXE и папку Content из папки Release рабоче( го каталога проекта и положить все это дело в любое место файловой системы компьютера. Запустив EXE(файл, вы автоматически запустите исполнение игры. Но, во(первых, это не профессионально, а во(вторых, если вы действительно со( бираетесь продавать свою программу, то необходимо позаботиться о пользовате( ле и создать как инсталлятор, так и корректную систему удаления программы с компьютера. Создать инсталляционный пакет можно с помощью специализированных про( грамм, которые специально предназначены для этих целей. В Интернете таких программ очень много, но я пользуюсь программой Smart Install Maker, поэтому в этой главе мы создадим пакет при помощи инсталлятора Smart Install Maker.
236
Формируем инсталляционный пакет
Тем более что программа имеет приятный русскоязычный интерфейс, автомати( ческую систему проверки установленной версии .NET Framework на компьютере пользователя, а также бесплатную и платную версии.
15.1. Программа Smart Install Maker На компакт(диске в папке Soft\Инсталлятор находится установочный пакет программы Smart Install Maker с названием simsetup_3_20. Эту полноценную версию программы читателям книги любезно предоставило руководство компа( нии InstallBuilders Company. Программа имеет как платный режим работы, так и бесплатный. Бесплатный режим работы программы отличается от платного лишь тем, что в сформированном установочном пакете вашей программы будет показана информация о том, каким инсталлятором был сделан этот установоч( ный пакет. Стоимость программы составляет всего 450 руб., и думается, что это очень маленький взнос из вашего личного начального капитала на создание собствен( ных игр. Бесплатный режим программы функционирует на базе бесплатного клю( ча, который обновляется каждые две недели. Это ключ вы можете скачать абсо( лютно бесплатно любое количество раз с сайта компании по адресу в Интернете http://www.sminstall.com/rus/. На сайте компании также доступен форум, систе( ма обратной связи, присутствует множество другой полезной информации. После установки и открытия программы Smart Install Maker вы попадаете на первую страницу с названием Информация. Далее переход по имеющимся стра( ницам проведет вас ко всем этапам формирования инсталляционного пакета, при этом доступен возврат к любым параметрам любой страницы. Затем мы рассмот( рим каждую страницу программы Smart Install Maker и в пошаговом режиме, раз( дел за разделом, создадим установочный пакет для игры «Летящие в прерии».
Программа Smart Install Maker
Флажок скрытая установка – эта опция позволяет производить скрытую установку программы на компьютер пользователя. Эта опция нам не нуж( на, поэтому этот флажок не выбираем. Сохранить как – по окончании создания инсталляционного пакета в месте, указанном в этом поле, будет создан установочный пакет. Конечное назва( ние пакета задается также в этом поле. Тип сжатия – это список, в котором можно избрать тип сжатия или упаков( ки программы. По умолчанию предлагается максимальный тип сжатия компонентов программы, его и выбираем. Тип разбивки – этот список позволяет разбить инсталляционный пакет на несколько томов. Данная опция необходима в том случае, если ваша про( грамма, допустим, не вмещается на один компакт(диск. Тогда вам необхо( димо разбить программу на два и более томов. Выбор различных значений из этого списка активизирует на этой странице еще две опции: инкремент( ный регулятор Размер тома, где необходимо указать размер одного тома, и поле Имена томов для указания имен томов. После заполнения всех полей этой страницы перейдите на страницу Файлы. При переходе на каждую новую страницу я вам рекомендую сохранять текущее состояние создавае(
15.1.1. Информация Страница Информация (рис. 15.1) содержит набор полей и списки, которые необ( ходимо заполнить определенной информацией. На этой странице имеются сле( дующие поля и списки. Имя программы – здесь необходимо прописать имя программы, соответ( ствующее вашему титульному названию игры. Версия – в этом поле определите номер для версии программы. Имя компании – в этом поле нужно указать свое имя или имя компании. Заголовок окна – инсталляция программы будет состоять из нескольких последовательно сменяющих друг друга окон. Надпись в этом поле будет определять заголовки некоторых окон. Надпись снизу – строка текста из этого поля будет видна в нижней части каждого окна установки программы. Как правило, здесь прописывается информация о компании или авторе программы.
237
Рис. 15.1. Страница Информация
238
Формируем инсталляционный пакет
Программа Smart Install Maker
239
мого инсталлятора. Сделать это можно по выполнении команд меню Файл ⇒ Сохранить как (рис. 15.2).
Рис. 15.2. Сохранение проекта Рис. 15.3. Окно Добавить запись
15.1.2. Файлы Страница Файлы является одной из главных страниц программы. Здесь необхо( димо определить, какие компоненты игры вы будете добавлять в установочный пакет. После того как вы создали игру в XNA Game Studio Express версии Release, в рабочем каталоге проекта находятся готовые к распространению компоненты игры. Вот все эти компоненты и нужно добавить в создаваемый инсталляционный пакет. В игре «Летящие в прерии» в версии Release мы получили следующие файлы: Файл Flying to prairies.exe – готовая программа; Папка Content – папка с компонентами игры. Все эти данные нам и необходимо добавить на странице Файлы в создаваемый пакет. Делается это следующим образом. Нажимаем кнопку Добавить (с изобра( жением зеленого крестика) и в появившемся окне Добавить запись в поле Файл указываем путь сначала к файлу Flying to prairies.exe (рис. 15.3). Для упрощения указания пути к файлам используйте кнопку Обзор (с изображением папки). Затем в следующем поле Путь к извлечению файла окна Добавить запись не( обходимо указать путь для извлечения этого добавленного файла. Файл с расши( рением EXE, как правило, остается всегда в корневом каталоге программы
(%InstallPath%), поэтому в этом поле нам менять ничего не нужно. Нажимаем в этом окне кнопку ОК и перемещаемся снова на страницу Файлы, где только что выбранный нами файл Flying to prairies.exe появится в списке (рис. 15.4). Теперь пришла очередь добавить в пакет графику и звуковые файлы, и здесь есть очень важный нюанс. Итак, добавим все графические данные. Нажимаем на странице Файлы кнопку Добавить и в окне Добавить запись используем кнопку Обзор. Следуем в папку Content\Textures. Выделяем в этой папке все файлы ра( зом и нажимаем кнопку ОК. Добавить сразу целую папку с данными не получит( ся, необходимо выбирать только файлы. В окне Добавить запись в поле Файл появится запись всех добавленных вами файлов, следующих через слэш (рис. 15.5). Чуть ниже, в поле Путь к извлечению файла, будет указан путь для извлечения компонентов игры. По умолчанию это корневой каталог программы, но, как вы помните, в игре для графики мы спе( циально отводили отдельную папку Content\Textures. Поэтому в поле Путь к из влечению файла нужно обязательно прописать строку текста %InstallPath%\ Content\Textures. Таким образом, все извлекаемые данные после установки программы на ком( пьютер пользователя попадут в папку Content\Textures, которую инсталлятор
240
Программа Smart Install Maker
Формируем инсталляционный пакет
241
После добавления графики на странице Файлы отобразятся все добавленные вами данные (рис. 15.6). Аналогичные действия нужно проделать и для осталь( ных папок игры Font и Sound, причем добавлять необходимо все файлы (из папки Release), которые имеются в этих папках, включая файлы с расширениями PNG, XML, XNB, XWB и т. д. (рис. 15.6). Дополнительно добавьте в пакет текстовый файл лицензионного соглашения, файл Readme и иконку игры, которую в даль( нейшем мы разместим на рабочем столе. Добавив необходимые компоненты игры, сохраняем проект и переходим к странице Требования.
Рис. 15.4. Добавление файла в список компонентов пакета
Рис. 15.6. Окно Файлы
15.1.3. Требования Рис. 15.5. Прописываем правильный путь для извлечения данных
сформирует в автоматическом режиме. Если этого не сделать, то ваша программа элементарно не будет работать. То есть все те папки, которые вы определяли для различных игровых компонентов, в ходе работы над проектом должны присут( ствовать и после установки программы на компьютер пользователя.
На странице Требования располагается ряд элементов управления, с помощью которых можно задать определенные требования для устанавливаемой програм( мы (рис. 15.7). Рассмотрим опции, необходимые для нашей с вами игры. Поддерживаемые версии операционных систем – в этой области с по( мощью флажков необходимо выбрать те операционные системы, которые поддержаны вашим приложением. Нам можно выбрать версии системы от Windows NT и выше.
242
Программа Smart Install Maker
Формируем инсталляционный пакет
243
Вкладка Фон позволяет включить или выключить полноэкранный вариант ин( сталляции программы. Вкладка Заголовок дает возможность подобрать иконку с изображением инсталляционного пакета для заголовка диалоговых окон (рис. 15.8). Здесь можно использовать несколько вариантов иконок, предлагаемых инсталля( тором, или нарисовать свои изображения размером 55 × 55 пикселей.
Рис. 15.7. Страница требований
Проверять версию .NET Framework – это очень важная опция программы, позволяющая определять на этапе инсталляции программы на компьютере пользователя версию .NET Framework. Для XNA Game Studio Express тре( буется версия как минимум 2.0 и выше. Поэтому выбираем из списка зна( чение 2.0. Если на компьютере пользователя будет меньшая версия, то ин( сталлятор не разрешит пользователю установку игры. Дополнительно в поле Если отсутствует открыть следующую Webстраницу, можно ука( зать адрес в Интернете для ссылки на установочный пакет .NET Frame( work 2.0. Это все опции, которые нам требуются, поэтому переходим на следующую страницу с названием Интерфейс.
15.1.4. Интерфейс На странице Интерфейс происходит графическое оформление интерфейса диа( логовых окон установочного пакета. Страница разделена на четыре вкладки: Фон, Заголовок, Логотип и Языки. Переход по вкладкам страницы и выбор раз( личных опций позволят вам настроить вид диалоговых окон.
Рис. 15.8. Подбор иконки для заголовка диалоговых окон
Следующая вкладка Логотип позволяет выбрать графическое изображение, которое будет отображаться с левой стороны диалоговых окон инсталляции про( граммы (рис. 15.9). На выбор предлагается ряд изображений инсталлятора, но вы можете нарисовать свой логотип и загрузить его в программу посредством кнопки Обзор (рис. 15.9). Последняя вкладка Языки содержит список языков, которые можно выбрать для поддержки инсталлятором.
15.1.5. Диалоги Страница Диалоги имеет пять вкладок: Общая, Папки, Лицензия, Пароль и За вершение. Каждая тематическая вкладка позволит вам выбрать те или иные эле( менты для создания инсталляционного пакета. На первой вкладке Общие с по(
244
Программа Smart Install Maker
Формируем инсталляционный пакет
Рис. 15.9. Загрузка логотипа
мощью флажков необходимо выбрать количество диалоговых окон, появляющих( ся при инсталляции программы (рис. 15.10). Имеются следующие варианты:
страница приветствия; страница выбора папки установки; страница выбора программной группы; страница выбора дополнительных ярлыков; страница готовности к установке; страница завершения установки.
Как видите, это стандартный набор диалоговых окон, появляющихся в про( цессе инсталляции программы. Выбирайте из списка необходимые окна и перехо( дите к следующим вкладкам страницы Диалоги. Следующая вкладка Папки позволит вам выбрать каталог для установки про( граммы и создать группу ярлыков в меню Пуск ⇒ Все программы (рис. 15.11). Ка( талог установки может быть любой, или каталогов может быть несколько. В нашем случае каталог для установки программы – это %ProgramFiles%\Летящие в пре( рии. Переменная %ProgramFiles% определяет папку на компьютере, в которую бу( дет устанавливаться программа. Таких переменных в инсталляторе предусмотрено очень много, ознакомиться подробнее со всеми можно через справку программы.
245
Рис. 15.10. Вкладка Общие
С помощью вкладки Лицензия в диалоговое окно Лицензия загружается текст лицензионного соглашения, заключаемого между пользователем и создате( лем программы. Дополнительно на вкладке Лицензия есть опция по загрузке текстовой информации Сведения, которая иногда используется в череде диалого( вых окон процесса инсталляции программы. Предпоследняя вкладка Пароль предоставляет возможность показа диалого( вого окна проверки программы на пароль или серийный номер. Вкладка Завер шение содержит несколько несложных элементов управления для завершающей стадии установки программы на компьютер.
15.1.6. Ярлыки На этой странице вам необходимо добавить ярлык программы, который впослед( ствии будет отображаться на рабочем столе компьютера. Чтобы сделать это, от( кройте страницу Ярлыки и нажмите кнопку Добавить (кнопка с изображением зеленого крестика). Откроется диалоговое окно Добавить запись, где в соответ( ствующих полях необходимо указать ряд значений (рис. 15.13). В нашем случае мы используем поля:
246
Формируем инсталляционный пакет
Инсталляция созданной программы
247
Рис. 15.11. Вкладка Папки
Рис. 15.12. Вкладка Лицензия
Место создания – здесь выбирается место создания ярлыка; Имя ярлыка – это имя для ярлыка, которое будет отображаться на рабочем столе; Имя файла – имя файла EXE, к которому мы «привяжем» данный ярлык программы; Текст подсказки – этот текст будет появляться при наведении на него кур( сора мыши; Файл иконки – здесь необходимо указать путь к файлу иконки програм( мы, которую необходимо загрузить в свою программу (см. раздел 15.1.2). Оставшиеся страницы программы мы в нашем инсталляционном пакете не используем, но в справочной информации к Smart Install Maker находится описа( ние этих страниц.
в Smart Install Maker предусмотрен полный спектр настроек, располагающихся в рабочем окне программы в области Деинсталлятор. Эта область содержит ссылки на четыре страницы: Настройки (рис. 15.14), Файлы, Реестр и Команды. На странице Настройки вы определяете необходи( мость создания деинсталлятора и избираете ряд необходимых для этого опций. Вкладка Файлы служит для выбора элементов установленной программы, необ( ходимых для удаления с компьютера пользователя. Рекомендации по настройке оставшихся двух вкладок даны в справочной информации программы. После выполнения всех настроек инсталлятора вам необходимо скомпилиро( вать установочный пакет программы командами меню Проект ⇒ Компилировать (Ctrl+F9). В каталоге для сохранения программы, который был отведен на на( чальном этапе (см. раздел 15.1.1), появится установочный пакет игры.
15.1.7. Деинсталлятор
15.2. Инсталляция созданной программы
Последняя и немаловажная стадия – это создание механизма корректного удале( ния установленной программы с компьютера пользователя. Для этих целей
Создав программу, ее можно и нужно сначала протестировать на своем компьюте( ре. Так мы и поступим. Смотрим в пошаговом режиме, что у нас в итоге получилось.
248
Инсталляция созданной программы
Формируем инсталляционный пакет
15.2.1. Окно приветствия
Рис. 15.13. Страница Ярлыки Рис. 15.15. Первое окно с приветствием программы
15.2.2. Лицензия
Рис. 15.14. Страница Настройки
Рис. 15.16. Диалоговое окно с лицензионным соглашением
249
250
Формируем инсталляционный пакет
15.2.3. Выбор директории
Рис. 15.17. Выбор каталога для установки программы
15.2.4. Выбор места установки ярлыков
Рис. 15.18. Предложение с помещением ярлыка на рабочий стол
Инсталляция созданной программы
15.2.5. Начало установки программы
Рис. 15.19. Информация о готовности начать установку программы
15.2.6. Установка программы
Рис. 15.20. Процесс инсталляции программы на компьютер пользователя
251
252
Формируем инсталляционный пакет
15.2.7. Окончание установки программы На компакт(диске в папке Final Game\2D вы найдете установочный пакет игры «Летящие в прерии», созданный программой Smart Install Maker, которая дей( ствительно за небольшие деньги создает хорошие, а главное – профессионально созданные инсталляционные пакеты.
Часть 3 Создаем трехмерную игру Глава 16 Основы программирования трехмерной графики
Рис. 15.21. Диалоговое окно об окончании установки программы
В отличие от двухмерного программирования графики, работать с 3D(графикой несколько сложнее. Для создания даже простых трехмерных программ вам пона( добится значительно больше математических познаний, чем для работы со спрай( товой графикой. В связи с этим в этой главе я предлагаю рассмотреть основы про( граммирования 3D(графики, без знания которых у вас не получится создать ни одной дельной программы. Прежде всего нам необходимо разобраться с тем, что на самом деле есть трех( мерное пространство и каким образом в этом пространстве представляются 3D( объекты. После этого мы обобщим все полученные знания и рассмотрим модель графического конвейера, используемого в XNA, а уже со следующей главы при( ступим к разработке трехмерной игры. Итак, приступим к изучению 3D(графики и начнем с понятия простой системы трехмерных координат.
16.1. Система трехмерных координат Система координат, применяемая в 3D(программировании, отличается от двух( мерной системы координат, но практически идентична простой декартовой систе( ме, за исключением некоторых нюансов. В этой системе координат начальная точ( ка отсчета находится в левом нижнем углу экрана. Ось X проходит по нижней части дисплея слева направо, а ось Y – снизу вверх. Дополнительно в трехмерном программировании добавляется новая ось Z, которая и позволяет работать с 3D( объектами. Смысловая нагрузка оси Z заключается в том, чтобы на двухмерной плоско( сти экрана правильно представить трехмерный объект, или, как часто говорят,
254
Точки и вершины
Основы программирования трехмерной графики
модель. Ось Z позволяет организовать глубину в двухмерном пространстве. Для начала это дает нам возможность отдалять или прибли( жать объекты по этой оси. В более сложных операциях ось Z принимает участие в транс( формации и масштабировании объектов, в раз( личных видовых и мировых преобразованиях, вычислении перспективы и многом другом, о чем мы, несомненно, поговорим в этой главе. Сейчас посмотрите на рис. 16.1, где изобра( жена трехмерная система координат. В DirectX Рис. 16.1. Трехмерная и OpenGL имеются две различные ориентации система координат трехмерных координат. Это так называемая правосторонняя система координат и левосто( ронняя система координат. В DirectX ранее использовалась только левосторон( няя система, а в OpenGL основной была правосторонняя система координат. В XNA Framework для представления трехмерного пространства применяется ис( ключительно правосторонняя система координат. В чем разница между этими двумя ориентациями? Разница небольшая, но она существенна и может повлиять на неправильное определение точки вывода объекта в пространстве. В правосторонней системе ко( ординат положительная ось Z идет в направлении прямо к вам. То есть если вы сейчас смотрите на страницу этой книги, то положительная часть оси Z выходит прямо со страницы книги и направляется в вашу сторону. В свою очередь, отрица( тельная часть плоскости оси Z удаляется от вас, и все то, что находится за кни( гой, – это уже есть отрицательная часть оси Z. На рис. 16.2 представлена право( сторонняя система координат. В левосторонней системе координат все наоборот. Положительная часть оси Z удаляется от вас, тогда как отрицательная часть оси Z направлена в вашу сторону. Поэтому если вы перепутаете эти две системы координат между собой и дадите объекту неправильные координаты по оси Z, то можете этот объект просто не уви( деть на экране. Например, вы указываете для вывода объекта по оси Z координату со значени( ем минус 100 пикселей. В этом случае в правосторонней системе координат (на( шей основной системе координат) объект будет располагаться как бы внутри мо( нитора и будет удален от поверхности дисплея в его глубь на 100 пикселей. Если вы перепутаете значения и укажете положительные сто пикселей, то объекта на экране вы не увидите, поскольку он будет находиться где(то между поверхностью дисплея и вашими глазами, то есть вне зоны видимости. Но при этом существуют дополнительные нюансы, связанные с положением камеры или положением ваших глаз. По умолчанию в правосторонней системе координат предполагается, что камера или ваши глаза находятся в положитель( ной части оси Z и соответственно, для того чтобы увидеть объект на экране, его
255
Рис. 16.2. Правосторонняя система координат
нужно удалять от себя или назначать ему небольшие положительные, нулевые или отрицательные значения по оси Z. Но можно изменять положение камеры и переносить ее в отрицательную плоскость, чтобы смотреть на объекты сзади. В этом случае для вывода объектов может потребоваться использовать уже поло( жительные значения по оси Z. Более подробно о камерах мы поговорим далее в этой главе, а сейчас перейдем к основным представлениям примитивов в трех( мерной плоскости.
16.2. Точки и вершины Очевидно, что для представления любой точки в трехмерной плоскости нужно задать координаты по всем трем осям X, Y и Z. Для представления на экране мо( нитора примитива (треугольник, квадрат, прямоугольник, ромб и т. д.) необходимо задать N(количество точек, соединив которые между собой, вы получите задан( ный примитив. То есть с помощью определенного количества точек в простран( стве можно нарисовать примитив любой формы. В компьютерной графике есть понятие полигон. Полигон – это площадь в про( странстве определенного размера, которая строится на основе точек. Как правило, в качестве полигона в графике применяется обыкновенный треугольник (примитив). Получается, что полигон – это простой треугольник, построение которого в про( странстве происходит на базе трех точек. В этом контексте точка приобретает более осмысленную роль, и ее в компьютерной графике принято называть вершиной. Вершина – это точка в пространстве, заданная по трем осям координат X, Y и Z. Имея три вершины, вы можете построить в пространстве полигон. Иначе гово(
256
Матрицы
Основы программирования трехмерной графики
ря, вершины и полигоны – это простые точки и треугольники, которые мы все изучали в геометрии. На базе вершин и полигонов в компьютерной графике про( исходит работа с трехмерными объектами или моделями.
257
даже с несколькими десятками тысяч полигонов будет по силам большинству со( временных видеоадаптеров.
16.4. Матрицы
16.3. Модель Любую трехмерную модель можно представить в пространстве определенным ко( личеством полигонов. Посмотрите на рис. 16.3, где показана работа утилиты DirectX Viewer, входящей в состав DirectX SDK. Эта утилита позволяет открыть модель в формате X(файла, или мэш, и показать количество полигонов, участвую( щих в построении этой самой модели. Работа любой видеокарты по рисованию трехмерной графики на экране мони( тора состоит в том, чтобы собрать из вершин полигоны и затем сформировать из этих полигонов конечную модель. Большое количество полигонов трехмерного объекта дает возможность создавать более детализированную модель. С другой стороны, большое количество полигонов одной модели может повлиять на ско( рость прорисовки графики на экране монитора. На сегодняшний день модель
Программирование трехмерной графики невозможно себе представить без ис( пользования матриц. Матрица – это двухмерный массив данных, заполненный определенными значениями. Матрицы позволяют легко и быстро производить любые манипуляции с вершинами объектов в пространстве. С помощью матриц можно связать несколько однородных операций над вершинами, что в результате позволяет выполнить колоссальные по объему математические вычисления за не( большой промежуток времени. Более того, матрицы можно использовать и в опи( сании координатных систем для переноса, масштабирования, вращения и транс( формации объектов в пространстве. Размерность матрицы может быть любой, но в компьютерной графике типич( ной размерностью является матрица 4 × 4, где имеются четыре строки и четыре столбца.
Матрица =
Если вам необходимо определить положение элемента внутри матрицы, то нужно сначала указать, в какой именно строке находится этот элемент, а затем указать, в каком столбце он располагается. В итоге получается, что искомый эле( мент матрицы с размерностью m × n находится в строке m столбца n.
16.4.1. Сложение и вычитание матриц Все математические операции над матрицами основаны на знаниях, которые мы изучали в школе. Единственным условием в сложениях и вычитаниях матриц яв( ляется их одинаковая размерность. Чтобы сложить между собой две матрицы, не( обходимо просто сложить поэлементно обе матрицы между собой, а результат записать в отдельную матрицу:
Вычитание двух матриц происходит по той же схеме:
Рис. 16.3. Полигоны модели
258
Матричные преобразования
Основы программирования трехмерной графики
16.4.2. Умножение матриц Умножение матриц бывает двух видов. Это скалярное произведение и матричное произведение. Скалярное произведение матрицы – это умножение матрицы на любое скалярное значение. В этом случае элементы матрицы поочередно пере( множаются на это самое скалярное значение, а результат записывается в итого( вую матрицу. При такой операции размерность матриц не имеет абсолютно ника( кого значения.
Матричное произведение отличается от скалярного произведения тем, что в этих операциях используются две и более матрицы, где обязательно должно соблюдаться следующее условие: количество столбцов матрицы А должно быть равно количеству строк матрицы В. Механизм перемножения двух матриц между собой заключается в последовательном произведении каждого элемента из пер( вой строки матрицы А на каждый элемент первого столбца матрицы В. Затем это произведение суммируется между собой, а результат записывается в отдельную матрицу
В этом показательном примере первый элемент матрицы А умножается на первый элемент первого столбца матрицы В. Затем второй элемент первой строки матрицы А умножается на второй элемент первого столбца матрицы В. И так да( лее, до окончания всех элементов в строке матрицы А и в столбце матрицы В. По( том эти результаты суммируются между собой, а итоговое значение записывается в первую строку матрицы С. Все записи в матрице С происходят слева направо и сверху вниз. Перемножение матриц между собой носит название матричной кон) катенации. Операция умножения матрицы А на матрицу В не коммутативна, то есть А × В = С, но В × А ≠ С.
16.5. Матричные преобразования В компьютерной графике определены понятия трех различных матриц. Это миро( вая матрица (World Matrix), матрица вида (View Matrix) и матрица проекции (Projection Matrix). С помощью этих матриц в исходном коде программы произ(
259
водятся матричные преобразования над моделями. Матричные преобразования подразумевают под собой умножение каждой вершины объекта на одну из мат( риц, а точнее последовательное умножение всех вершин объекта на каждую из трех матриц. Такой подход позволяет корректно представить модель в трехмерном про( странстве вашего двухмерного монитора. Техника прохода модели через три пе( речисленные матрицы представляет суть механизма работы с графическими дан( ными в трехмерной плоскости монитора. Чтобы было более понятно, о чем идет речь, давайте остановимся на каждой из трех матриц поподробнее.
16.5.1. Мировая матрица Мировая матрица – позволяет производить различные матричные преобразо( вания (трансформацию, масштабирование) объекта в мировой системе коорди( нат. Мировая система координат – это своя локальная система координат дан( ного объекта, которой наделяется каждый объект, скажем так, прошедший через мировую матрицу, поскольку каждая вершина участвует в произведении этой матрицы. Новая локальная система координат значительно упрощает аффинные преоб( разования объекта в пространстве. Например, чтобы перенести объект с левого верхнего угла дисплея в нижний правый угол дисплея, необходимо просто пере( нести его локальную точку отсчета, системы координат на новое место. А пред( ставьте, если бы не было мировой матрицы и этот объект пришлось переносить по одной вершине из угла в угол монитора... Поэтому любой объект, а точнее все вер( шины этого объекта проходят через мировую матрицу преобразования. Как уже упоминалось, мировое преобразование вершин объекта может со( стоять из любых комбинаций вращения, трансляции и масштабирования. В мате( матической записи вращение вершины по оси Х выглядит следующим образом:
где Ω — угол вращения в радианах. Вращение вершины вокруг оси Y выглядит так:
260
Матричные преобразования
Основы программирования трехмерной графики
А вращение вокруг оси Z происходит по следующей формуле:
Трансляция вершины позволяет переместить эту самую вершину с координа( тами x, y, z в новую точку с новыми координатами x1, y1, z1. В математической записи это выглядит так: x1 = x + Tx y1 = y + Ty z1 = z + Tz.
261
16.5.2. Матрица вида Матрица вида – задает местоположение камеры в пространстве, это вторая по счету матрица, на которую умножаются вершины объекта. Эта матрица способ( ствует определению направления просмотра трехмерной сцены. Трехмерная сце) на – это все то, что вы видите на экране монитора. Это как в театре, где вы сидите в портере или на галерке и наблюдаете за действиями на сцене. Так вот, в портере у вас будет одно местоположение камеры, а на галерке – уже совсем другое. Фактически эта матрица даже позволяет определять жанр игры. Например, игра DOOM от первого лица – это, можно сказать, первые ряды портера в теат( ре, тогда как игра Warcraft – это галерка, причем высоко на балконе. Матрица вида предназначена для определения положения камеры в пространстве, где вы можете смещать позицию камеры влево, вправо, вверх, вниз, удалять, прибли( жать ее и т. д.
Трансляция вершины в матричной записи выглядит следующим образом:
16.5.3. Матрица проекции
где Tx, Ty и Tz — значения смещения по осям X, Y и Z. Масштабировать вершину в пространстве (удалять или приближать) с коор( динатами x, y, z в новую точку с новыми значениями x1, y1, z1 можно посредством следующей записи:
Матрица проекции – это более сложная матрица, которая создает проекцию трех( мерного объекта на плоскость двумерного экрана монитора. С помощью этой мат( рицы определяются передняя и задняя области отсечения трехмерного простран( ства, что позволяет регулировать пространство отсечения невидимых на экране объектов, а заодно и снизить нагрузку процессора видеокарты. На рис. 16.4 изоб( ражены механизм проекции объекта в плоскости и отсечение пространства.
x1 = x × S y1 = y × S z1 = z × S. В матричной записи это выражается следующим образом:
где Sx, Sy, Sz — значения коэффициентов растяжения или сжатия по осям X, Y, Z. Все перечисленные операции можно делать в исходном коде программы вруч( ную, то есть вычислять приведенные записи так, как мы их только что описали. Но, естественно, так никто не делает (почти никто), потому что в XNA Framework имеется огромное количество методов, которые сделают все вышеприведенные операции за «один присест». В дальнейшем, когда мы начнем работать с объекта( ми, вы познакомитесь с этими методами вплотную и на практике.
Рис. 16.4. Работа матрицы преобразования
262
Шейдеры
Основы программирования трехмерной графики
263
16.6. Свет
16.7. Шейдеры
Понятия свет и освещенность в компьютерных играх идентичны понятиям окру( жающего нас с вами мира. В компьютерной графике применяются три типа осве( щения. Первый – это параллельный, или направленный, источник света. Этот тип освещения не имеет определенного источника света и светит отовсюду, но в опре( деленном направлении. Второй тип освещения имеет название точечный. Этот тип освещения уже имеет источник света, но светит во всех направлениях. При( мер: лампочка, люстра, торшер и т. д. Третий источник освещения носит название прожекторный. По названию этого типа освещения легко догадаться, что он собой представляет, а именно направленный конусообразный источник света (пример простого прожектора или фонарика). Освещение или свет задается цветовой компонентой, или просто цветом в формате ARGB, где A – это альфа(канал для определения степени прозрачности цветов, R представляет красный цвет (red), G – это зеленый цвет (green), B – си( ний цвет (blue). Сумма всех цветов в итоге приводит вас к одному из оттенков того или иного цвета. Здесь все по аналогии со школьными уроками рисования, где все мы смешивали краски разных цветов и получали, как правило, одну боль( шую черную кляксу. К слову, цвет можно использовать не только для освещения, но и для прямого закрашивания вершин полигонов цветом. В этом случае вы мо( жете закрасить всю модель или ее отдельные части (помним, что модель состоит из полигонов) одним из цветов. Для расчета правильности отображения освещения используются нормали. Нормаль – это единичный вектор, расположенный перпендикулярно от плоско( сти полигона или вершины (рис. 16.5). На базе расположения нормалей происхо( дит расчет освещения объекта. Грубо говоря, те нормали, которые не попадают под источник света, дают понять механизму освещения, какие части объекта бу( дут освещаться источником света, а какие – нет.
Это магическое слово приводит большинство начинающих программистов в страшный трепет, но на самом деле шейдеры – это обыкновенная программа, ко( торая пишется на специальном языке программирования и выполняется графи( ческим процессором видеокарты. И не более того. Базис программирования шей( деров аналогичен базису программирования обыкновенных приложений, но, естественно, со своими нюансами. Шейдеры – это всего лишь программа, состоящая из набора ключевых слов, которая позволяет программисту работать напрямую с вершинами и пикселями. Как и в любом другом языке программирования, понятие и освоение шейдеров сводятся к пониманию общей сущности работы шейдеров и изучению самого язы( ка программирования, на котором пишутся шейдеры. По окончании работы над этой книгой планируется создание книги по шей) дерной технологии и языку программирования HLSL. Будем надеяться, что к осени 2007 года книга будет готова и сможет поступить в продажу. По) дробную информацию о новинках, планах и всех вышедших книгах вы може) те найти на сайте http://www.gornakov.ru. Суть шейдеров, а точнее шейдерных программ заключается в том, что эта про( грамма исполняется графическим процессором видеоадаптера. Сделано это для того, чтобы избежать массы повторяющихся операций в коде программы, снизить нагрузку с центрального процессора и иметь возможность обращения к процессо( ру видеокарты напрямую. Все эти действия приводят к быстрой работе програм( мы и повышению качества графических эффектов. Графический процессор видеокарты работает несколько иначе, чем централь( ный процессор компьютера, и в связи с этим для доступа к процессору необходим свой набор команд, или язык программирования. Ранее в DirectX для этих целей использовался ассемблероподобный язык программирования шейдеров, состоя( щий из набора определенных инструкций. В те времена шейдерные программы были небольшими, поэтому языка, основанного на инструкциях, вполне хватало. Со временем шейдерные программы увеличивались, да и сама технология про( граммирования шейдеров развивается и по сей день. Если вас интересует ассемблерный язык программирования шейдеров, а также работа с инструментариями RenderMonkey и FX Composer, то могу вам порекомендовать книгу «Инструментальные средства отладки и программирования шейдеров в DirectX и OpenGL».
16.7.1. Шейдерная модель Рис. 16.5. Нормали вершин и полигона
Со временем на замену ассемблерному языку программирования шейдеров при( шел новый язык с названием HLSL (High(Level Shaders Language, или высоко( уровневый язык программирования шейдеров). Этот язык более прост в исполь(
264
Шейдеры
Основы программирования трехмерной графики
зовании и, главное, имеет большое сходство с языком программирования С. В мо( мент появления языка HLSL была предложена так называемая Shaders Models 1.0. Это своего рода версия описания шейдерной модели для использования шейдер( ных программ в программировании графики. Шейдерная модель предъявляет на( бор требований для графического процессора видеокарты, которые он обязан иметь или исполнять. На сегодняшний день имеются уже три версии Shaders Models. Каждая по( следующая шейдерная модель лишь усовершенствует свою предшественницу. То есть имеется набор определенных инструкций, которые может выполнить видео( адаптер с поддержкой Shaders Models 1.0, и если вы попробуете на этом видео( адаптере использовать шейдерную модель третьей версии, то произойдет ошибка в работе программы. Фактически шейдерная модель описывает тот набор опера( ций, который графический процессор видеокарты может или даже способен вы( полнить. Например, в Shaders Models 1.0 нет возможности использовать циклы, а во второй версии такая возможность (для видеоадаптеров) была добавлена. Технология не стоит на месте и развивается, мощности и возможности гра( фических процессоров растут, поэтому и Shaders Models постепенно усовершен( ствуется. Первая версия Shaders Models со временем отошла и уже практически не используется в программировании шейдеров, но поддерживается всеми видео( картами без исключения, если, конечно, производитель умышленно не исключил такой возможности. Сейчас отправной точкой в версии модели шейдеров считается версия Shaders Models 2.0, которая также используется в консольной приставке Xbox 360 (при( ставка не может использовать Shaders Models 1.0). Более дорогие компьютерные видеоадаптеры могут работать с Shaders Models 2.0 и с Shaders Models 3.0, но не далек тот день, когда появится Shaders Models 4.0, разработка которой идет пол( ным ходом. Соответственно появится и новый класс видеокарт, умеющих рабо( тать с этой шейдерной моделью. В исходном коде можно легко создать блок кода для проверки поддерживаемой видеоадаптером версии шейдерной модели. Например, в следующем исходном коде происходит проверка видеоадаптера на совместимость с Shaders Models 2.0. Если поддержки этой шейдерной модели нет, то на экран выводится сообщение. Исполь( зовать эту конструкцию кода необходимо на этапе инициализации программы, а также нужно предусмотреть аварийный выход из приложения, если видеокарта не поддерживает используемую в программе шейдерную модель. foreach (GraphicsAdapter adapter in GraphicsAdapter.Adapters) { GraphicsDeviceCapabilities caps = adapter.GetCapabilities(DeviceType.Hardware); // ïðîâåðêà âòîðîé âåðñèè øåéäåðíîé ìîäåëè if (caps.MaxPixelShaderProfile < ShaderProfile.PS_2_0) { System.Diagnostics.Debug.WriteLine(“Íåò ïîääåðæêè Shader Model 2.0.”); } }
265
Есть и более рациональные решения поддержки различных версий шейдеров. Например, написание универсальных шейдерных программ, использующих ис( ключительно синтаксис всех моделей шейдеров, или написание отдельных бло( ков кода для всех трех или двух последних версий шейдерных моделей.
16.7.2. Механизм работы шейдерных программ Любой язык программирования направлен на описание синтаксиса и семантики. На базе этих правил и ключевых слов пишутся все программы. Точно так же и язык HLSL обладает своим уникальным синтаксисом и семантикой. Язык HLSL имеет очень большое количество ключевых слов, как говорится, на все случаи жизни. Но часть ключевых слов может не поддерживаться определенной моделью видеоадаптера. Вот для этого и нужна шейдерная модель (Shaders Models), кото( рая описывает требования, предъявляемые к видеоадаптеру. В результате шей( дерная модель является неотъемлемой частью правил использования высоко( уровневого языка программирования шейдеров. Шейдерные программы пишутся в обычных инструментариях (продукты се( мейства Visual Studio, RenderMonkey, FX Composer…) и сохраняются в отдельных файлах с расширением FX. Затем этот файл или файлы подгружаются в программу, и когда в исходном коде программа доходит до места выполнения шейдерного фай( ла, то в этот момент программа переключается на выполнение файла шейдеров. Как только шейдерная программа выполнена, работа приложения опять воз( вращается к основному исходному коду программы. Но поскольку шейдеры очень «плотно» интегрируются в исходный код программы, то и выполнение шей( дерных программ происходит очень часто и быстро. Таким образом, все приложе( ние постоянно нагружает работой (в хорошем смысле этого слова) графический процессор видеокарты, минуя основной процессор компьютерной системы. Специфичность архитектуры графического процессора заключается в нали( чии входных и выходных регистров. С помощью языка HLSL происходит запись определенных данных во входные регистры видеоадаптера, и задается набор дей( ствий, которые графический процессор должен выполнить. После выполнения заданных операций все итоговые результаты помещаются в выходные регистры графического процессора, откуда посредством все той же шейдерной программы полученные данные считываются и выполняются. В данном контексте выполне( ние шейдерной программы означает рисование на экране итоговой графической картинки.
16.7.3. Вершинные и пиксельные шейдеры Шейдерная программа – это исходный код, который располагается в одном фай( ле. Несмотря на это, шейдерная программа незримо делится на две различные по своим направленностям части, которые выполняются строго друг за другом. Пер( вая часть программы работает исключительно с вершинами объектов, поэтому и носит название вершинный шейдер. После того как определенные операции над
266
Графический конвейер
Основы программирования трехмерной графики
вершинами были сделаны, в работу включается часть программы, отвечающая за работу с пикселями, или пиксельный шейдер. В юрисдикцию вершинного шейдера входит проход через мировую матрицу, видовую матрицу и матрицу проекции, любые матричные преобразования вер( шин объектов, нормализация, установка освещения, цвета, текстурных коорди( нат. Как только все перечисленные действия выполнены, итоговые данные пере( даются пиксельному шейдеру. Пиксельный шейдер на основании полученных данных рассчитывает и уста( навливает цвет для каждого пикселя поверхности дисплея. Дополнительно в пик( сельном шрейдере выполняются действия по добавлению в игру различных эф( фектов на пиксельном уровне. Например, туман, вода, огонь и т. д. Все эти эффекты исполняются непосредственно пиксельным шейдером. Никаких других операций над вершинами объектов в пиксельном шейдере быть не может. Этот шейдер работает исключительно с пикселями! Оба шейдера связаны друг с другом неразрывно, и очередность их выполне( ния одна: сначала в работу включается вершинный шейдер, а затем пиксельный. Это как стряпанье пирогов. Сначала вы замешиваете тесто и придаете форму пи( рогу, а затем ставите этот пирог в духовку. Не замешав тесто, вам соответственно и в духовку нечего будет поставить, как, впрочем, замешав тесто, но не поставив его в духовку, вы не сможете испечь сам пирог.
16.8. Графический конвейер Все то, что мы изучили в этой главе, в компьютерной графике известно под назва( нием графический конвейер. Если представить схематично все стадии представ( ления графики на экране монитора, то можно получить схему, изображенную на рис. 16.6. Когда графический процессор получает задание нарисовать на экране трех( мерную модель, то от программы он получает так называемый вершинный поток данных. Эти данные содержат позиции вершин в пространстве, цвет каждой вер(
16.7.4. Подводя итоги шейдерной технологии Подводя итоги вышеописанного материала, можно констатировать следующее. С помощью шейдеров происходит выполнение различных операций над верши( нами и пикселями графической составляющей. Это означает, что посредством ре( гистров графического процессора и кода шейдерной программы вы можете управ( лять как вершинами, так и пикселями графических компонентов игры. На практике такая методика работы с данными позволяет создавать потря( сающие по качеству спецэффекты. Вот лишь небольшой перечень всего того, что можно сделать с графикой, используя шейдерные программы: закрашивание объектов и полигонов цветом; установка освещения; работа с матричными преобразованиями; любые виды объектной анимации; текстурная анимация; наложение текстур; смешивание текстур; текстурирование ландшафтов; любые размытые и искаженные изображения; любимый некоторыми компаниями эффект черно(белых изображений; пиксельные взрывы и эффекты; пар, туман, огонь, вода, облака, зеркала, отражение и т. п.; все виды bump mapping и многое, многое, многое другое.
267
Рис. 16.6. Графический конвейер
268
Основы программирования трехмерной графики
шины, нормали, текстурные координаты. Набор этих данных передается на обра( ботку в вершинный шейдер. В этот момент в работу включается исходный код шейдера, а именно блок кода, отведенный для работы с вершинным шейдером. Операции, проводимые вершинным шейдером, зависят от тех задач, которые ставит сам программист, написавший шейдерную программу на языке HLSL, но как минимум происходят все матричные преобразования (мировые, видовые и проекционные), нормализация и установка освещения. На выходе вершинного шейдера поток имеющихся данных проходит этап растеризации и интерполяции. Растеризация содержит несколько правил представления примитивов на эк( ране монитора, суть которых заключается в точном заполнении всех примитивов своими пикселями. В свою очередь, интерполяция цвета позволяет добиться плавного перехода из одной цветовой компоненты в другую. Например, две вер( шины имеют разный цвет. Между двумя этими вершинами пространство должно быть заполнено плавно переходящей из одного цвета в другой цветовой компо( нентой. Задача интерполяции как раз и заключается в заглаживании, или плав( ном переходе от одного цвета к другому. Далее все данные поступают в пиксельный шейдер. На этом этапе ведется ра( бота попиксельно, на уровне каждого отдельно взятого пикселя. Для каждого пикселя вычисляются цвет и другие эффекты, и затем данные выходят из пик( сельного шейдера и проходят так называемый Z(буфер. Этот буфер еще известен как буфер глубины. Суть работы Z(буфера заключается в том, чтобы правильно представить объемную модель на двухмерной плоскости экрана монитора. Задний буфер рас( ставляет вершины объекта в пространстве как в реальном мире, удаляя от нас бо( лее дальние части модели и приближая ближние, создавая тем самым объемность мира. После этого все данные поступают в так называемый фрейм(буфер, или просто кадр. Вот такая модель работы используется графическим конвейером для пред( ставления графики на экране монитора. На этом знакомство с основами програм( мирования 3D(графики подошло к завершению, и мы переходим к работе над но( вым проектом трехмерной игры. Это не значит, что мы изучили всю концепцию программирования 3D(графики, но этих знаний вам вполне хватит для дальней( шей работы с книгой.
Глава 17 Смена игровых состояний Прежде чем перейти к созданию трехмерной игры, нам стоит уделить внимание оптимизации ранее изученных способов представления графики на экране мони( тора, а также значительно улучшить механизм смены игровых состояний в прило( жении. В этой главе мы последовательно изучим обе предложенные темы и нач( нем с автоматического подбора программой текущего разрешения дисплея.
17.1. Автоматический подбор разрешения экрана В двухмерной игре разрешение экрана задавалось постоянной величиной. Это простой и надежный способ выбора режима представления графики на экране монитора, но не всегда этот способ приемлем. В трехмерных играх, где полноэк( ранное представление графики является одним из важнейших и в то же время комфортных (для пользователя) факторов, очень важно иметь автоматический механизм подбора разрешения дисплея. В XNA имеется ряд дополнительных классов и методов, позволяющих выпол( нять подбор разрешения дисплея в автоматическом режиме. Давайте восполь( зуемся этими компонентами и реализуем пример подбора разрешения монитора в новом проекте Screen. Исходный код всего проекта вы найдете на компакт(дис( ке в папке Code\Chapter17\Screen. Это базовый проект для всех последующих наших проектов, связанных с трехмерной графикой. Проект Screen сформирован на базе шаблона Windows Game. Как и в предыдущей части книги, мы будем постоянно модернизировать исходный код программы и в результате реализуем пусть и не большую, но зато свою трехмерную игру. Перейдем к проекту Screen и классу Game1. Весь исходный код этого класса в книге давать не имеет смысла, поскольку он основан на шаблоне Windows Game и не содержит неизвестных для вас нововведений. Рассмотрим только один(един( ственный новый блок кода в методе Initialize(), который направлен на под( бор разрешения экрана. protected override void Initialize() { GraphicsAdapter adapter =
270
Улучшаем смену игровых состояний
Смена игровых состояний
graphics.GraphicsDevice.CreationParameters.Adapter; graphics.PreferredBackBufferWidth = adapter.CurrentDisplayMode.Width; graphics.PreferredBackBufferHeight = adapter.CurrentDisplayMode.Height; graphics.IsFullScreen = true; graphics.ApplyChanges(); screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; base.Initialize(); }
Работа предложенного блока кода, а в частности, применение класса GraphicsAdapter на начальном этапе инициализации игры, возможна только в методе Initialize(). Если вы попробуете использовать этот класс в конст( рукторе класса Game1, то компилятор моментально выдаст вам ошибку компиля( ции. Настраивать разрешение экрана (автоподбор или ручной подбор разрешения классом GraphicsAdapter) можно только от метода Initialize() или с по( мощью других методов сторонних классов, которые все равно должны выпол( няться лишь после окончания работы конструктора класса Game1. Для того чтобы использовать определенное разрешение экрана, нам необхо( димо сначала получить эти искомые параметры. Для этих и других целей в XNA существует класс GraphicsAdapter, представляющий на программном уровне графический видеоадаптер компьютерной системы. В первой строке кода метода Initialize() создается объект adapter клас( са GraphicsAdapter, который получает все технические параметры видеоадап( тера, в том числе и разрешение экрана. GraphicsAdapter adapter = graphics.GraphicsDevice.CreationParameters.Adapter;
Имея этот объект, мы можем использовать его для установки ширины и высо( ты рабочей поверхности экрана монитора следующим образом. graphics.PreferredBackBufferWidth = adapter.CurrentDisplayMode.Width; graphics.PreferredBackBufferHeight = adapter.CurrentDisplayMode.Height;
В этих строках кода мы получаем у видеоадаптера компьютерной системы те( кущую ширину и высоту экрана и используем эти значения в программе. Таким образом, на любой компьютерной системе ширина и высота экрана будут установ( лены в соответствии с текущими пользовательскими настройками. Затем в исходном коде метода Initialize() происходят установка полно( экранного режима отображения информации на дисплее, получение графическо( го состояния, а две новые переменные screenWidth и screenHeight инициа( лизируются значениями ширины и высоты дисплея. Две эти новые переменные впоследствии нам понадобятся для работы с матрицами. Запустите проект Screen и проверьте работу предложенного механизма на практике.
271
17.2. Улучшаем смену игровых состояний Как вы помните, для смены игровых состояний в двухмерной игре мы использова( ли конструкцию управляющих операторов if/else. Теперь пришло время со( здать более действенный и профессиональный подход смены игровых состояний, основанный на использовании структур и структурных переменных. В чем смысл этой методики? На самом деле все очень просто. Необходимо сформировать структуру данных, содержащую определенный набор переменных. Как вы знаете, структура – это совокупность переменных, предназначенных для хранения различной информации. Объявление структуры приводит к созданию набора данных, которые впоследствии может использовать любой объект струк( туры или структурная переменная. Создать структуру в нашем контексте можно следующим образом: private enum CurentGameState { SplashScreen, MenuScreen, AboutScreen, GameScreen, GameOverScreen, VictoryScreen }
Как видите, названия переменных соответствуют определенным фазам игро( вых состояний. У нас эти переменные будут означать следующее: SplashScreen – первая игровая заставка, появляющаяся при старте игры; MenuScreen – меню игры; AboutScreen – экран с информацией об игре; GameScreen – игровой процесс; GameOverScreen – экран проигрыша игры; VictoryScreen – экран выигрыша игры. Дополнительно можно ввести любое количество переменных для любого ко( личества фаз игровых состояний. Далее при старте игры создается структурная переменная и инициализируется одним из значений структуры, например: CurentGameState gameState = CurentGameState.SplashScreen;
Это значит, что в данный момент структурная переменная gameState ассо( циируется со значением SplashScreen. Затем в игровом цикле необходимо про( сто создать проверку значения переменной gameState и в соответствии с ее зна( чением выбрать текущие действия. switch(gameState)
272
Улучшаем смену игровых состояний
Смена игровых состояний
{ case CurentGameState.SplashScreen: { // ïîêàçàòü çàñòàâêó break; } case CurentGameState.MenuScreen: { // ïîêàçàòü ìåíþ break; } case CurentGameState.AboutScreen: { // ïîêàçàòü ýêðàí îá èãðå break; } case CurentGameState.GameScreen: { // ñòàðò èãðû break; } case CurentGameState.GameOverScreen: { // êîíåö èãðû break; } case CurentGameState.VictoryScreen: { // ýêðàí âûèãðûøà èãðû break; } }
Элегантное и в то же время действенное решение смены игровых состояний. Этот подход мы будем использовать в нашей трехмерной игре. На компакт(диске в папке Code\Chapter17\GameState находится новый проект под названием GameState. В этом проекте используется вышеописанная схема. Ниже в листин) ге 17.1 предложен исходный код класса Game1, иллюстрирующий работу смены игровых состояний на базе структуры CurentGameState. На данном этапе в исходном коде структурная переменная gameState пред( ставляет фазу игрового процесса. Так будет продолжаться до тех пор, пока мы не дойдем до главы, рассказывающей о создании меню и других игровых заставок. //========================================================================= /// <summary> /// Ëèñòèíã 17.1 /// Èñõîäíûé êîä ê êíèãå:
273
/// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 17 /// Ïðîåêò: GameState /// Êëàññ: Game1 /// Ñìåíà èãðîâûõ ñîñòîÿíèé /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace GameState { public class Game1 : Microsoft.Xna.Framework.Game { private enum CurentGameState { SplashScreen, MenuScreen, AboutScreen, GameScreen, GameOverScreen, VictoryScreen } CurentGameState gameState = CurentGameState.GameScreen; GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; int screenWidth, screenHeight; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() {
274
Улучшаем смену игровых состояний
Смена игровых состояний
GraphicsAdapter adapter = graphics.GraphicsDevice.CreationParameters.Adapter; graphics.PreferredBackBufferWidth = adapter.CurrentDisplayMode.Width; graphics.PreferredBackBufferHeight = adapter.CurrentDisplayMode.Height; graphics.IsFullScreen = true; graphics.ApplyChanges(); screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; base.Initialize(); }
{
/// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { } }
break; }
/// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); switch(gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.AboutScreen:
break; } case CurentGameState.GameScreen: { if(keyboardState.IsKeyDown(Keys.Escape)) this.Exit();
case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } } base.Update(gameTime); } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); switch(gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.AboutScreen: {
275
276
Смена игровых состояний break; } case CurentGameState.GameScreen: { break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; }
} base.Draw(gameTime); } } }
На диске, в примерах от клуба разработчиков игр XNA Creators Club нахо) дится проект с названием GameStateManagementSample. В этом примере представлен более сложный и профессиональный механизм смены игровых состояний, а также показана методика реализации интерактивного меню на базе шрифта, использование заставки для загрузки игры и техника плавного перехода с экрана на экран.
Глава 18 Загружаем в игру модель Если вы работаете сами на себя и не умеете создавать 3D(модели в графических редакторах, то вопрос выбора и поиска модели будет для вас одним из главных. Для простых и, скажем так, домашних игр трехмерная графика может быть какой угодно, но для игр, которые делаются на продажу, качество 3D(моделей – дело первостепенной важности. Вы можете покупать модели, можете найти единомышленника, готового со( трудничать на энтузиазме или близких к тому отношениях, или поискать бесплат( ные модели в Интернете. В использовании бесплатных моделей есть один важный нюанс. Не все модели в Интернете бесплатны, даже если на одном из сайтов об этом так говорят. Владельцы этих сайтов могут и не знать, что модели, предлагае( мые для свободного скачивания, на самом деле далеко не бесплатны. В этом во( просе нужно соблюдать осторожность, а лучше связаться с автором графики и узнать точно, на каком типе лицензии распространяются его модели. Что касается ресурсов в Интернете, посвященных графике и компьютерным играм в частности, то могу вам порекомендовать старейший и всем известный ре( сурс Turbosquid.com (адрес в Интернете: http:// www.turbosquid.com). На этом сайте вы найдете массу готовых моделей, уровней, текстур, звуковых эффектов и многого другого. В базе данных этого ресурса насчитывается более 160 000 раз( личных наименований продуктов, связанных с разработкой игр (рис. 18.1). Отличительной особенностью сайта Turbosquid.com является то, что на этом ресурсе можно не только купить или продать графику, но и скачать множество бесплатно распространяемых моделей. Если вам негде брать модели, то реко( мендую обратить внимание на этот ресурс. Что касается оплаты покупок на Turbosquid.com, то на сайте используются банковские или кредитные карты (рис. 18.2).
18.1. Рисуем модель на экране монитора В качестве основной модели для нашей трехмерной игры послужит футбольный мячик. Как вы помните из первой главы, идея заключается в том, чтобы поста( раться не упустить мячик или несколько мячей на поле, подбивая их все время курсором мыши.
278
Загружаем в игру модель
Рис. 18.1. Сайт Turbosquid.com
Модель футбольного мяча была взята с сайта Turbosquid.com. Это бесплат( ная модель без текстуры, но с материалом, сделана 3D(мастером, представлен( ным на сайте под именем RP3D (рис. 18.3). Модель распространяется в не( скольких форматах, в том числе в формате 3DS. Для преобразования модели из формата 3DS в формат Х(файла применялась программа 3D Exploration. Но вы можете использовать для преобразования моделей из одного формата в другой любую удобную вам программу или утилиту. Таких бесплатных программ очень много в Интернете. Для более профессиональных модельеров советую скачать с сайта компании Autodesk плагин к программе 3ds Max версии 8 и ниже, который позволяет прямо из 3ds Max сохранять модель в формате FBX (рис. 18.4). Ниже в тексте дана пря( мая ссылка на ряд утилит для различных операционных систем. К сожалению, согласно лицензионному соглашению эти программы нельзя размещать на циф( ровых носителях и распространять с какими бы то ни было печатными издания( ми. Все утилиты небольших размеров (от 1 до 2 Мб), поэтому скачать их самостоя( тельно вам не составит труда. Ссылка на конвертер FBX для 3ds Max: http://usa.autodesk.com/adsk/servlet/index?siteID=123112&id=6839916
Рисуем модель на экране монитора
279
Рис. 18.2. Ввод данных с кредитной карты
18.1.1. Механизм загрузки модели в игру Алгоритм по загрузке модели в игру достаточно простой. Первым делом необхо( димо явно загрузить в проект модель и все текстуры к этой модели, если таковые имеются. Название текстур и текстурные координаты для одной конкретной мо( дели прописываются во внутренних свойствах модели. Поэтому некоторые текс( туры могут быть жестко привязаны к папке с определенным названием. Здесь все зависит от модельера, которые делал эту модель. Например, в Starter Kit Spacewar студии XNA Game Studio Express для всех моделей задан каталог Models, а для текстур этих моделей определена папка Textures. В соответствии с этим, если вы используете в своих проектах графику из Starter Kit Spacewar, модели и текстуры у вас должны располагаться в папках с точно такими названиями. Если изменить базовое местонахождение моделей или текстур, то в момент компиляции программы студия разработки игр выдаст вам ошибку, из контекста которой будет следовать, что компилятор не нашел того или иного графического компонента. Определившись с расположением модели в проекте, нужно объявить и со( здать объект системного класса Model, затем загрузить в него модель из файла и
280
Рисуем модель на экране монитора
Загружаем в игру модель
281
Рис. 18.3. Модель мячика, взятая с сайта Turbosquid.com
Рис. 18.4. Страница компании Autodesk
нарисовать ее на экране. Сейчас давайте попрактикуемся и перейдем к рассмотре( нию исходного кода нового проекта LoadingModel, направленного на загрузку модели в игру и вывод ее на экран. Исходный код всего проекта находится на ком( пакт(диске в папке Code\Chapter18\ LoadingModel. Отправной точкой для нового проекта у нас, как всегда, является предыдущий проект, поэтому возьмите последний исходный код класса Game1 и приступайте к его модификации. Именно в классе Game1 на этом этапе работ развернутся все боевые действия. Начнем с того, что в области глобальных переменных класса Game1 объявим три матрицы.
Далее в области глобальных переменных класса Game1 создаются четыре но( вые переменные.
Matrix world; Matrix view; Matrix proj;
Как видно из названий матриц, это те самые три матрицы (мировая, видовая и проекционная), используемые для представления объектов в трехмерном про( странстве. Дополнительно структура Matrix в XNA Framework имеет большой набор встроенных методов, с помощью которых можно выполнить любые матрич( ные преобразования.
static static static static
float float float float
aspectRatio; FOV = MathHelper.PiOver4; nearClip = 1.0f; farClip = 1000.0f;
Все четыре переменные принимают участие в матричных расчетах, и это часть давно отлаженного механизма, передающегося по наследству от старых версий DirectX. Первая переменная aspectRatio в исходном коде представляет так на( зываемый коэффициент сжатия, на базе которого ведется расчет перспективы мо( дели для правильного соотношения геометрических размеров в пространстве. Следующая переменная FOV призвана определять поле зрения. Здесь типич( ное значение равно MathHelper.PiOver4. Две оставшиеся переменные nearClip и farClip позволяют задать соответственно переднюю и заднюю об( ласти отсечения и участвуют в проекционных расчетах. Затем в исходном коде у нас следует объявление объекта model класса Model, который будет представлять мячик в игре.
282
Рисуем модель на экране монитора
Загружаем в игру модель
private Model model; private Vector3 positionModel
Переменная positionModel необходима для выбора позиции модели на эк( ране. В конструкторе класса Game1 мы задаем позицию модели в пространстве нулевыми значениями. positionModel = new Vector3(0, 0, 0);
Нулевые значения для позиции модели при нахождении камеры в центре эк( рана устанавливают модель четко в центр экрана. Помните, в главе 16 мы говори( ли о преобразовании системы координат модели в свою локальную, или объект( ную, систему координат. Так вот, используя в исходном коде три ранее созданные матрицы (мировую, видовую и проекционную), мы создадим для объекта свою локальную систему координат. После этих действий все свои установки объектов на позиции в трехмерном мире вам придется вести относительно локальной системы координат объекта и позиции камеры (о которой мы поговорим позже в этой главе). Если камера стоит в центре экрана (X = 0 и Y = 0, по оси Z камера либо приближается, либо удаляет( ся от центра экрана) и у объекта нулевые координаты, то местоположение объекта всегда будет точно в центре монитора (рис. 18.5).
283
Переходим в тело метода Initialize(). Здесь появляется только одна но( вая запись, которая инициализирует переменную aspectRatio значением, рав( ным делению половины ширины экрана на половину высоты экрана. aspectRatio = (float)screenWidth / (float)screenHeight;
Эта стандартная запись, но ранее до метода Initialize() мы не могли ис( пользовать такую запись, поскольку лишь в этом методе несколькими строками выше (см. исходный код этого метода в листинге 18.1) определили свойства ви( деоадаптера, установленного в системе. Если использовать эту запись прежде, до определения настроек адаптера, то переменная aspectRatio получит системное значение, равное по умолчанию размеру 800/600 пикселей, и тогда ваш коэффи( циент сжатия будет некорректным. Значение этой переменной можно также определять и числовыми значениями (например, 1024/768), но тогда вы «привяжете» себя к конкретному размеру дис( плея. В предыдущей главе мы создали универсальный механизм по подбору теку( щего разрешения дисплея, поэтому значение переменной aspectRatio следует также определять на автомате. Переходим к методу LoadGraphicsContent(). Строка кода по загрузке мо( дели из рабочего каталога программы выглядит следующим образом: model = content.Load<Model>(“Content\\Models\\Soccerball”);
Как видите, такая запись практически идентич( на записи по загрузке спрайтового изображения, но вместо ключевого слова использует( ся слово <Model>. Такая команда дается для Con( tent Pipeline на выполнение загрузки в программу уже трехмерной модели, а не текстуры. Модель мя( чика находится в каталоге рабочего проекта в папке Content\Models (рис. 18.6). Здесь схема поиска мо( дели в каталоге проекта у загрузчика Content Pipe( line стандартна.
Рис. 18.6. Расположение модели в каталоге проекта
18.1.2. Метод DrawModel() Для рисования или представления модели на экране монитора в исходном коде класса Game1 создан метод DrawModel(). private void DrawModel(Model m) { Matrix[] transforms = new Matrix[m.Bones.Count]; m.CopyAbsoluteBoneTransformsTo(transforms);
Рис. 18.5. Объектная система координат
world = Matrix.CreateTranslation(positionModel);
284
Загружаем в игру модель
view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 150.0f), Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); foreach (ModelMesh mesh in m.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = transforms[mesh.ParentBone.Index] * world; effect.View = view; effect.Projection = proj; } mesh.Draw(); } }
Этот метод на первый взгляд кажется сложным, но на самом деле это отла( женный механизм построения модели в пространстве и вывода ее на экран. В этот метод в качестве параметра передается модель, и в первых двух строках происходят считывание и преобразование всех вершин модели в один массив данных. Затем происходит установка всех трех матриц. Мировая матрица получает позицию модели в пространстве. В матрице вида происходит установка камеры методом CreateLookAt(), где первый параметр как раз и определяет позицию камеры. В нашем случае мы удаляем камеру для модели на 150 пикселей в сторо( ну от монитора в направлении ваших глаз. Два оставшихся параметра метода CreateLookAt() – типично используемые величины. По окончании этой главы самостоятельно попробуйте изменять все значения матрицы вида, чтобы уяснить суть этих преобразований. Матрица проекции для своих расчетов использует че( тыре переменные, назначение которых вы уже знаете. Последний блок кода метода DrawModel() задействует механизм языка программирования С# и оператор foreach для цикличного перебора данных массива или, как в нашем случае, коллекции данных. Оператор foreach поза( имствован из языка программирования Visual Basic и позволяет циклично из( влекать каждый элемент коллекции данных по очереди, помещая извлеченный элемент в очередной объект массива, который вынесен в заголовок этого опера( тора. foreach (ModelMesh mesh in m.Meshes)
В результате этот механизм позволяет получить все компоненты мэша, объ( единить их воедино и сформировать трехмерную модель. Следующая строка кода foreach (BasicEffect effect in mesh.Effects)
Рисуем модель на экране монитора
285
делает ту же самую операцию по извлечению коллекции данных, но уже в отноше( нии шейдерных данных, а точнее вершинного потока, поступающего во входные данные видеоадаптера. В данном контексте используется простой эффект BasicEffect, который дает возможность работать с шейдерами в автоматическом режиме. В этом случае система сама рассчитывает освещение, материал, нормали, трансформацию и дру( гие виды преобразований в автоматическом режиме. Работать с шейдерами очень сложно, и освещение этой темы выходит за рамки книги, поэтому в программе применяется более простой в использовании класс BasicEffect. Далее сам метод DrawModel() мы вызываем в методе, отвечающем за рисова( ние графики на экране монитора. case CurentGameState.GameScreen: { graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; DrawModel(model); break; }
Здесь, думается, все понятно, за исключением самой верхней строки кода это( го блока. graphics.GraphicsDevice.RenderState.DepthBufferEnable = true;
На самом деле запись этой строки кода сейчас не имеет какой(либо смысловой нагрузки. Эта запись нам понадобится в следующих главах, когда мы добавим на экран вывод спрайтовых изображений. Дело в том, что использование в программе объектов класса SpriteBatch несколько изменяет настройки видеоадаптера, перестраивая эти настройки под работу с двухмерной графикой. Поэтому очень важно возвращать некоторые на( стройки назад. В данном случае необходимо вновь включить буфер глубины, ко( торый SpriteBatch отключил для работы с двухмерной графикой. В противном случае ваша модель на экране будет представлена с большими визуальными де( фектами. Кстати, может понадобиться и больше дополнительных настроек, для одновременной работы с моделями и двухмерной графикой. Теперь обратитесь к полному исходному коду класса Game1, который пред( ставлен в листинге 18.1. По обыкновению все новшества в коде выделены жир( ным шрифтом. В следующем разделе мы усовершенствуем работу программы и создадим отдельный класс для работы с трехмерными моделями. //========================================================================= /// <summary> /// Ëèñòèíãå 18.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã.
286
/// Ãëàâà 18 /// Ïðîåêò: LoadingModel /// Êëàññ: Game1 /// Çàãðóçêà ìîäåëè /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace LoadingModel { public class Game1 : Microsoft.Xna.Framework.Game { private enum CurentGameState { SplashScreen, MenuScreen, AboutScreen, GameScreen, GameOverScreen, VictoryScreen } CurentGameState gameState = CurentGameState.GameScreen; GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; int screenWidth, screenHeight; Matrix view; Matrix proj; Matrix world; static static static static
Рисуем модель на экране монитора
Загружаем в игру модель
float float float float
aspectRatio; FOV = MathHelper.PiOver4; nearClip = 1.0f; farClip = 1000.0f;
private Model model; private Vector3 positionModel; /// <summary> /// Êîíñòðóêòîð /// <summary public Game1()
{ graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); positionModel = new Vector3(0, 0, 0); } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { GraphicsAdapter adapter = graphics.GraphicsDevice.CreationParameters.Adapter; graphics.PreferredBackBufferWidth = adapter.CurrentDisplayMode.Width; graphics.PreferredBackBufferHeight = adapter.CurrentDisplayMode.Height; graphics.IsFullScreen = true; graphics.ApplyChanges(); screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; aspectRatio = (float)screenWidth / (float)screenHeight; base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { model = content.Load<Model>(“Content\\Models\\Soccerball”); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); switch (gameState) {
287
288
Рисуем модель на экране монитора
Загружаем в игру модель case CurentGameState.SplashScreen: {
} case CurentGameState.MenuScreen: {
break; }
break; }
case CurentGameState.MenuScreen: {
case CurentGameState.AboutScreen: {
break; }
break; }
case CurentGameState.AboutScreen: {
case CurentGameState.GameScreen: { graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; DrawModel(model); break; }
break; } case CurentGameState.GameScreen: { if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit();
case CurentGameState.GameOverScreen: {
break; }
break; }
case CurentGameState.GameOverScreen: {
case CurentGameState.VictoryScreen: {
break; }
break; } } base.Draw(gameTime);
case CurentGameState.VictoryScreen: { } break; } } base.Update(gameTime); } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); switch (gameState) { case CurentGameState.SplashScreen: { break;
/// <summary> /// Ðèñóåì ìîäåëü /// <summary> private void DrawModel(Model m) { Matrix[] transforms = new Matrix[m.Bones.Count]; m.CopyAbsoluteBoneTransformsTo(transforms); world = Matrix.CreateTranslation(positionModel); view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 150.0f), Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); foreach (ModelMesh mesh in m.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = transforms[mesh.ParentBone.Index] * world;
289
290
Создаем объект класса LoadingModelClass
Загружаем в игру модель effect.View = view; effect.Projection = proj;
} mesh.Draw(); } } } }
18.2. Класс ModelClass В шестой главе, когда мы только начинали работать с двухмерной графикой, был создан один общий класс для работы со спрайтами. В этой главе мы также созда( дим такой класс, но уже для представления трехмерных моделей. Для реализации этой задачи формируем новый проект LoadingModelClass на базе предыдуще( го примера и добавим в проект дополнительный класс ModelClass (см. лис) тинг 18.2). //========================================================================= /// <summary> /// Ëèñòèíã 18.2 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 18 /// Ïðîåêò: LoadingModelClass /// Êëàññ: ModelClass /// Çàãðóæàåì â èãðó ìîäåëü ÷åðåç êëàññ /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace LoadingModelClass { class ModelClass { public Model model; public Vector3 position; public ModelClass() { position = new Vector3(0, 0, 0); }
291
/// <summary> /// Çàãðóçêà ìîäåëè â èãðó /// <summary> public void Load(ContentManager content, String stringModel) { model = content.Load<Model>(stringModel); } /// <summary> /// Ðèñóåì ìîäåëü íà ýêðàíå /// <summary> public void DrawModel(Matrix world, Matrix view, Matrix proj) { Matrix[] transforms = new Matrix[model.Bones.Count]; model.CopyAbsoluteBoneTransformsTo(transforms); foreach (ModelMesh mesh in model.Meshes) { foreach (BasicEffect effect in mesh.Effects) { effect.EnableDefaultLighting(); effect.World = transforms[mesh.ParentBone.Index] * world; effect.View = view; effect.Projection = proj; } mesh.Draw(); } } } }
Исходный код нового класса чем(то напоминает технику работы с двухмерными изображениями. Код несложен, подробно разбирать его не имеет смысла, единствен( ное, что стоит заметить, – это то, что мы перенесли в класс ModelClass метод DrawModel() из класса Game1, а необходимые матрицы передаем в метод в качестве параметров. Сами матрицы будут устанавливаться и по необходимости изменяться непосредственно в классе Game1. Перейдем к программному коду этого класса.
18.3. Создаем объект класса LoadingModelClass Теперь необходимо создать в исходном коде класса Game1, объект ball класса ModelClass и загрузить в него модель мячика. После этого нужно установить матрицы и вызвать метод ball.DrawModel(world, view, proj) в цикле прорисовки графики на экране. В листинге 18.3 представлен полный исходный код обновленного класса Game1. Весь проект LoadingModelClass находится на компакт(диске в папке Code\Chapter18\LoadingModelClass. //========================================================================= /// <summary> /// Ëèñòèíã 18.3
292
/// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 18 /// Ïðîåêò: LoadingModelClass /// Êëàññ: Game1 /// Çàãðóæàåì â èãðó ìîäåëü ÷åðåç êëàññ /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace LoadingModelClass { public class Game1 : Microsoft.Xna.Framework.Game { private enum CurentGameState { SplashScreen, MenuScreen, AboutScreen, GameScreen, GameOverScreen, VictoryScreen } CurentGameState gameState = CurentGameState.GameScreen; GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; int screenWidth, screenHeight; Matrix view; Matrix proj; Matrix world; static static static static
Создаем объект класса LoadingModelClass
Загружаем в игру модель
float float float float
aspectRatio; FOV = MathHelper.PiOver4; nearClip = 1.0f; farClip = 1000.0f;
private ModelClass ball; /// <summary> /// Êîíñòðóêòîð
/// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); ball = new ModelClass(); } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { … } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { ball.Load(content, “Content\\Models\\Soccerball”); ball.position = new Vector3(0, 0, 0); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { … } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); switch (gameState)
293
294
Загружаем в игру модель
{ case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; // ìèðîâàÿ ìàòðèöà world = Matrix.CreateTranslation(ball.position); // ìàòðèöà âèäà view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 150.0f), Vector3.Zero, Vector3.Up); // ïðîåêöèîííàÿ ìàòðèöà proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); // ðèñóåì ìîäåëü íà ýêðàíå ball.DrawModel(world, view, proj); break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } } base.Draw(gameTime); } } }
Глава 19 Движение моделей в пространстве Продолжаем работать над игрой. В этой главе мы сформируем новый проект, в котором выведем на экран три разных мячика, а потом будем перемещать их в пространстве. Заметьте, что подход в представлении и освещении исходных ко( дов всех оставшихся проектов с этой главы несколько меняется. За время чтения этой книги вы уже хорошо поднаторели в программировании игр и разбирать каждую точку с запятой в коде программы смысла не имеет. Поэтому предлагает( ся разделить все нововведения на разделы и рассматривать их в порядке убыва( ния, а в конце всей главы изучать полный исходный код класса Game1, где, как всегда, все модификации кода выделены жирным шрифтом. Начнем работу над новым проектом BallArray. Полный исходный код этого проекта находится на компакт(диске в папке Code\Chapter19\BallArray. От( кройте в проекте BallArray файл ModelClass.cs и приступайте к изучению примера.
19.1. Задаем скорость движения модели В классе ModelClass добавляется одна новая переменная speed, с помощью ко( торой мы задаем скорость движения объектов в пространстве. public float speed; … public ModelClass() { position = new Vector3(0, 0, 0); speed = 20.0f / 60.0f; }
Переменная speed инициализируется значением 20.0f/60.0f. В данном слу( чае число 60 – это количество секунд в одной минуте. Такая конструкция выбора скорости очень часто встречается в играх. Для класса ModelClass это все нововведения. Переходим к исходному коду класса Game1, где нам необходимо создать массив объектов класса ModelClass, а затем реализовать механизм перемещения объектов сверху вниз.
296
Движение моделей в пространстве
19.2. Создаем массив данных Сначала нужно объявить массив объектов ball класса ModelClass. private ModelClass [] ball = new ModelClass[3]; MouseState mouseState; Random rand = new Random();
Попутно происходит объявление еще двух объектов: mouseState и rand. Первый объект mouseState необходим для работы с мышью, а второй объект rand позволит в программе генерировать случайные числа. В двухмерной игре, как вы помните, мы также применяли эти объекты. Далее в конструкторе класса Game1 происходит создание массива объектов ball при помощи цикла for. for (int i = 0; ball.Length > i; i++) { ball[i] = new ModelClass(); }
19.3. Инициализация и установка моделей на позиции
Инициализация и установка моделей на позиции Мячик Soccerball – это базовая модель с синими шашками. Мячик SoccerballGreen – эта модель с зелеными шашками. Мячик SoccerballRed – этот мяч имеет красные шашки.
В исходном коде метода LoadGraphicsContent() с помощью цикла for и метода rand.Next() генерируются случайные координаты в пространстве по всем трем осям для вывода мячей на игровые позиции. В этом блоке кода вас мо( жет заинтересовать выбор позиции по оси X. ball[i].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80));
Дело в том, что метод rand.Next() может генерировать только положитель( ные числа, но нам необходимо иметь еще и отрицательные значения. Как мы по( мним, каждая модель в пространстве наделяется своей собственной локальной системой координат, где нулевая точка отсчета модели находится в центре экрана (при нулевых значениях координат по осям X и Y для точки просмотра сцены). Это значит, что движение модели в левую сторону – это движение модели в отри( цательной плоскости оси X. Движение модели в правую сторону – это движение в положительной части оси Х. По оси Y для движения вверх нужно выбирать по( ложительные значения, а для движения вниз – отрицательные. Выбирая позицию для мячика по оси Х, для минимального значения задается диапазон чисел от 0 до –80. Для максимального числа используем значения от
Теперь переходим к методу LoadGraphicsContent(), который отвечает за за( грузку в игру различных графических данных. protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { ball[0].Load(content, “Content\\Models\\Soccerball”); ball[1].Load(content, “Content\\Models\\SoccerballGreen”); ball[2].Load(content, “Content\\Models\\SoccerballRed”); for(int i = 0; ball.Length > i; i++) { ball[i].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[i].position.Y = rand.Next(0, 80); ball[i].position.Z = -(rand.Next(20, 150)); } } }
Модель мячика, взятая с сайта Turbosquid.com, изначально имеет только два цвета – серый и синий. Чтобы разнообразить игру, я покрасил средствами 3ds Max шашки синего цвета в зеленый и красный цвета. Таким образом, у нас теперь имеет( ся три разных мячика с тремя разными цветами и тремя разными названиями.
297
Рис. 19.1. Выбор позиции на экране
298
Случайный выбор позиции на экране
Движение моделей в пространстве
0 до 80, но уже в положительной плоскости оси Х. Дополнительно по оси Z уста( навливается диапазон чисел от –20 до –150, что позволяет удалять или прибли( жать мячики в пространстве. Посмотрите на рис. 19.1, где показана техника выбо( ра позиции мячей на экране. Неплохо было бы вам на этом этапе добавить в код класс для работы с текстом и выводить на экран все установленные значения для мячей, ка) мер, матриц и т. д., для того чтобы четко понимать, как происходят вы) числения всех позиций в локальной системе координат модели.
19.4. Установка матриц Матрицы преобразований остаются у нас прежними. Сначала задается точка про( смотра сцены с помощью видовой матрицы. view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 150.0f), Vector3.Zero, Vector3.Up);
Затем устанавливается проекционная матрица, и здесь тоже ничего не изме( няется по сравнению с предыдущим примером.
Мячи в игре падают сверху вниз, а значит, нам нужно отнимать от текущих по( зиций объектов заданное количество пикселей (20.0f/60.0f). Метод MoveBall() вызывается в методе Update() класса Game1 на каждой новой итерации игрово( го цикла. Каждая новая позиция объекта, представленная переменной position, передается в мировую матрицу, где для движения объекта в работу включается механизм переноса всех вершин модели в пространстве (те самые мировые преоб( разования).
19.6. Случайный выбор позиции на экране Движение объектов на экране происходит сверху вниз. Через определенный про( межуток времени все мячики исчезнут с экрана; чтобы этого не происходило, до( бавим в исходный код простой метод под названием MouseClick(). В этом методе щелчок левой кнопкой мыши в игре позволит нам выбрать для мячей новые позиции на экране монитора. void MouseClick() { mouseState = Mouse.GetState(); for (int i = 0; ball.Length > i; i++) { if(mouseState.LeftButton == ButtonState.Pressed) { ball[i].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[i].position.Y = rand.Next(0, 80); ball[i].position.Z = -(rand.Next(20, 150)); } }
proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip);
После чего формируется цикл for, где устанавливается мировая матрица для трех мячиков, а все итоговые значения матриц передаются в качестве параметров в метод DrawModel(). for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world, view, proj); }
19.5. Формируем метод для перемещения моделей Теперь пришло время для создания метода, который будет перемещать мячик в пространстве. Здесь алгоритм действий простой: необходимо с помощью ранее созданной переменной speed изменять позицию мячей по оси Y так, как мы это делали в игре «Летящие в прерии». void MoveBall() { for (int i = 0; ball.Length > i; i++) { ball[i].position.Y -= ball[i].speed; } }
299
}
Методика выбора позиции аналогична той методике, которую мы рассмотре( ли в начале этой главы. В дальнейшем мы модифицируем исходный код этого ме( тода и будем его использовать уже для выстрелов по мячикам. В листинге 19.1 представлен полный исходный код класса Game1. Сам проект вы найдете на ком( пакт(диске в папке Code\Chapter19\BallArray. //========================================================================= /// <summary> /// Ëèñòèíã 19.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 19 /// Ïðîåêò: BallArray /// Êëàññ Game1 /// Çàãðóæàåì â èãðó òðè ìÿ÷èêà
300
Случайный выбор позиции на экране
Движение моделей в пространстве
for (int i = 0; ball.Length > i; i++) { ball[i] = new ModelClass(); }
/// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace BallArray { public class Game1 : Microsoft.Xna.Framework.Game { private enum CurentGameState { SplashScreen, MenuScreen, AboutScreen, GameScreen, GameOverScreen, VictoryScreen } CurentGameState gameState = CurentGameState.GameScreen; GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; int screenWidth, screenHeight; Matrix view; Matrix proj; Matrix world; static static static static
float float float float
aspectRatio; FOV = MathHelper.PiOver4; nearClip = 1.0f; farClip = 1000.0f;
private ModelClass [] ball = new ModelClass[3]; MouseState mouseState; Random rand = new Random(); /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services);
301
} /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { GraphicsAdapter adapter = graphics.GraphicsDevice.CreationParameters.Adapter; graphics.PreferredBackBufferWidth = adapter.CurrentDisplayMode.Width; graphics.PreferredBackBufferHeight = adapter.CurrentDisplayMode.Height; graphics.IsFullScreen = true; graphics.ApplyChanges(); screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; aspectRatio = (float)screenWidth / (float)screenHeight; base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { ball[0].Load(content, “Content\\Models\\Soccerball”); ball[1].Load(content, “Content\\Models\\SoccerballGreen”); ball[2].Load(content, “Content\\Models\\SoccerballRed”); for (int i = 0; ball.Length > i; i++) { ball[i].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[i].position.Y = rand.Next(0, 80); ball[i].position.Z = -(rand.Next(20, 150)); } } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary>
302
Случайный выбор позиции на экране
Движение моделей в пространстве
/// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); MoveBall(); MouseClick(); break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } }
/// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 150.0f), Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world, view, proj); } break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: {
base.Update(gameTime); } /// <summary> /// Ðèñóåì íà ýêðàíå
303
break; } } base.Draw(gameTime);
304
Движение моделей в пространстве
} /// <summary> /// Äâèæåíèå ìÿ÷åé /// <summary> void MoveBall() { for (int i = 0; ball.Length > i; i++) { ball[i].position.Y -= ball[i].speed; } } /// <summary> /// Ìûøêà /// <summary> void MouseClick() { mouseState = Mouse.GetState(); for (int i = 0; ball.Length > i; i++) { if (mouseState.LeftButton == ButtonState.Pressed) { ball[i].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[i].position.Y = rand.Next(0, 80); ball[i].position.Z = -(rand.Next(20, 150)); } } } } }
Глава 20 Стреляем по целям В игре движение мячей происходит сверху вниз, и задача игрока – не дать упасть мячикам на футбольное поле. С помощью выстрелов, которые должны попасть в мячик, игрок должен удерживать мячи на лету определенное время. Если ему это удалось, то пользователь переходит на новый уровень, если нет, то он пробует играть снова и снова. Наша задача на данном этапе заключается в том, чтобы добавить в игру при( цел для стрельбы по мячам и создать механизм обработки попаданий выстрелов в цель. Как обычно, продолжаем усовершенствовать пример из предыдущей гла( вы, и наш новый проект носит название CursorBall. На компакт(диске этот проект находится в папке Code\Chapter20. Открываем проект CursorBall и приступаем к изучению исходного кода примера.
20.1. Класс для работы с двухмерными изображениями В любой трехмерной игре обязательно применяются двух( мерные изображения. Наша игра в этом плане не исключение. Для представления на экране прицела мы будем использо( Рис. 20.1. вать спрайт, нарисованный в виде окружности с рисками и Изображение вырезанным фоном (рис. 20.1). прицела Для работы с прицелом в проект добавится новый класс Sprite, который мы создали за время работы над двухмер( ной игрой. Как видите, очень полезно создавать классы с фундаментальным подходом, для того чтобы в дальнейшем эти классы можно было использовать многократно, тем более что с каждой новой игрой этот самый класс можно усо( вершенствовать.
20.2. Задаем радиус для мячей Перейдем на время к классу ModelClass и добавим в его исходный код одну но( вую переменую radius. public float radius; … public ModelClass()
306
Получаем координаты прицела
Стреляем по целям
{ position = new Vector3(0, 0, 0); speed = 20.0f / 60.0f; radius = 8.0f; }
Как видно из названия переменной, она предназначается для определения ра( диуса мячиков. Впоследствии для определения пересечения моделей и курсора мыши будет использоваться класс BoundingSphere. Этот класс позволяет со( здавать ограничивающую сферу для моделей игры, и именно пересечение курсора с этой сферой будет означать, что мячик взят на прицел. Радиус в восемь единиц для мячика больше, чем это нужно на самом деле, но поскольку мячи постоянно находятся в движении, то попасть в цель будет не так просто. Поэтому мы немного помогаем игроку и увеличиваем зону пересечения прицела и модели.
20.3. Рисуем на экране прицел Прицел в игре представлен классом Sprite, поэтому нужно объявить и создать объект этого класса, а заодно и не забываем о классе SpriteBatch, который не( обходим для работы с 2D(графикой.
307
20.4. Получаем координаты прицела Основной проблемой в механизме обработки выстрелов по мячам является разли( чие координат в положении на экране двухмерного прицела и трехмерной моде( ли. Как вы знаете, двухмерная плоскость имеет свою систему координат, а трех( мерная – свою. У нас в игре одним из основных механизмов игровой логики является обработка выстрелов в мячики, где нам необходимо сопоставлять теку( щие координаты спрайта с текущими координатами модели. И эти координаты будут абсолютно разными, поскольку системы отсчета у спрайтов и моделей раз( ные. В связи с этим нам нужно создать механизм, который может перенести коор( динаты спрайта в трехмерную плоскость. В справочной информации по студии XNA Game Studio Express имеется показа( тельный пример получения и переноса двухмерных координат мыши в трехмерную плоскость. Мы воспользуемся этим примером, который состоит из двух частей. Первая часть примера представлена методом GetPickRay(), где происходят по( лучение текущих координат мыши и перенос их в трехмерное пространство. Ray GetPickRay() { mouseState = Mouse.GetState(); int mouseX = mouseState.X; int mouseY = mouseState.Y;
SpriteBatch spriteBatch; Sprite cursor;
Vector3 nearsource = new Vector3((float)mouseX, (float)mouseY, 0f); Vector3 farsource = new Vector3((float)mouseX, (float)mouseY, 1f); // ìèðîâàÿ ìàòðèöà, âñå çíà÷åíèÿ ñòàâèì â íîëü world = Matrix.CreateTranslation(0, 0, 0); // ìàòðèöà âèäà view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 150.0f), Vector3.Zero, Vector3.Up); // ìàòðèöà ïðîåêöèè proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); // áëèæíÿÿ òî÷êà Vector3 nearPoint = graphics.GraphicsDevice.Viewport.Unproject(nearsource, proj, view, world); // äàëüíÿÿ òî÷êà Vector3 farPoint = graphics.GraphicsDevice.Viewport.Unproject(farsource, proj, view, world);
В конструкторе класса Game1 создаем объект cursor класса Sprite. cursor = new Sprite();
В методе LoadGraphicsContent() загружаем в программу графическое изображение прицела из каталога проекта и папки Content\Textures. spriteBatch = new SpriteBatch(graphics.GraphicsDevice); cursor.Load(content, “Content\\Textures\\cursor”);
В методе Draw() после вывода на экран мячей добавляем отрисовку на экра( не прицела.
Vector3 direction = farPoint - nearPoint; direction.Normalize(); Ray pickRay = new Ray(nearPoint, direction);
spriteBatch.Begin(SpriteBlendMode.AlphaBlend); cursor.DrawSprite(spriteBatch); spriteBatch.End();
return pickRay; }
И в этом месте очень важна очередность вывода графики на экран. Не забыва( ем о том, что все то, что устанавливается для рисования на экране позже в исход( ном коде программы, соответственно и рисуется позже, поверх предыдущего все( го графического контента.
Сложный метод, но его можно рассматривать как часть объектно(ориентиро( ванного программирования, когда совсем не обязательно знать, как работает тот или иной метод, или класс. Суть метода GetPickRay() заключается в переносе
308
Целимся и стреляем
Стреляем по целям
координат мыши в трехмерное пространство посредством матричных преобразо( ваний, и здесь имеется один очень важный нюанс. Значения видовой и проекционной матриц для курсора мыши должны быть идентичны значениям аналогичных матриц для моделей, с которыми в дальней( шем будут обрабатываться условия по совпадению координат. Если видовая и проекционная матрицы будут разными, то курсор и модели будут находиться в разных виртуальных измерениях! Поэтому как для курсора, так и для моделей значение видовой и проекционной матриц у нас одинаково.
Затем создается проверка условия для обработки нажатия левой кнопки мыши. Это условие в переводе на русский язык звучит так. Если левая кнопка мыши нажата и координаты мыши совпадают с координатами ограничивающей сферы одного из мячей, то происходят выстрел и попадание, а мячик устанавлива( ется на новую позицию на экране. if (mouseState.LeftButton == ButtonState.Pressed) { Ray pickRay = GetPickRay();
20.5. Целимся и стреляем После того как вы сформировали метод GetPickRay() для переноса координат прицела из одной плоскости в другую, можно приступать к работе над вторым методом, который будет обрабатывать уже непосредственно столкновение объек( тов в пространстве. Для начала объявим в классе Game1 новый объект bb класса BoundingSphere.
Nullable result0 = pickRay.Intersects(bb[0]); if (result0.HasValue == true) {
В следующем блоке кода выбирается новая позиция на экране для мячика под номером ноль. Выбор позиции мячика аналогичен первоначальному выбору по( зиции при старте всей игры. ball[0].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[0].position.Y = rand.Next(0, 80); ball[0].position.Z = -(rand.Next(20, 150));
public BoundingSphere[] bb = new BoundingSphere[3];
В отличие от класса BoundingBox, с которым мы имели дело во второй части этой книги, класс BoundingSphere создает не ограничивающий прямоугольник или куб, а ограничивающую сферу. Наши модели мячиков круглые, поэтому этот класс подходит для нас как нельзя кстати. Сейчас давайте «залезем» прямо в исходный код метода MouseClick(), ко( торый направлен на определение столкновений между прицелом и мячами, и по ходу его изучения прокомментируем суть работы этого метода.
} Nullable result1 = pickRay.Intersects(bb[1]); if (result1.HasValue == true) { ball[1].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[1].position.Y = rand.Next(0, 80); ball[1].position.Z = -(rand.Next(20, 150)); } Nullable result2 = pickRay.Intersects(bb[2]); if (result2.HasValue == true) { ball[2].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[2].position.Y = rand.Next(0, 80); ball[2].position.Z = -(rand.Next(20, 150)); }
void MouseClick() {
Сначала мы получаем текущие координаты мыши и передаем их нашему при( целу. mouseState = Mouse.GetState(); cursor.spritePosition.X = mouseState.X; cursor.spritePosition.Y = mouseState.Y;
Здесь создается цикл, где на каждый мячик надевается своя сфера с радиусом, установленным в классе ModelClass. for (int i = 0; bb.Length > i; i++) { bb[i].Center = ball[i].position; bb[i].Radius = ball[i].radius; }
309
} }
Обработку событий по пересечению сферы и прицела можно поместить и в цикл, например следующим образом: for (int i = 0; ball.Length > i; i++) { Nullable result = pickRay.Intersects(bb[i]); if (result.HasValue == true)
310
Целимся и стреляем
Стреляем по целям
{ ….
} CurentGameState gameState = CurentGameState.GameScreen;
} }
Но в дальнейшем мы будем вести подсчет попадания в мячи по каждому от( дельному мячу, поэтому нам здесь цикл не подойдет. В результате два рассмотренных метода представляют отлаженный механизм по переносу координат курсора мыши в трехмерную плоскость и обработке столк( новений между спрайтом и моделью. Этот механизм вам может понадобиться в различных играх, например в шутерах от первого или второго лица, где, так же как и нашей игре, присутствуют двухмерный прицел и множество различных трехмерных моделей. В листинге 20.1 вы найдете полный исходный код класса Game1, а сам проект – на компакт(диске в папке Code\Chapter20\CursorBall. //========================================================================= /// <summary> /// Ëèñòèíã 20.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 20 /// Ïðîåêò: CursorBall /// Êëàññ: Game1 /// Ïîäáèâàåì ìÿ÷èêè /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace CursorBall { public class Game1 : Microsoft.Xna.Framework.Game { private enum CurentGameState { SplashScreen, MenuScreen, AboutScreen, GameScreen, GameOverScreen, VictoryScreen
GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; int screenWidth, screenHeight; Matrix view; Matrix proj; Matrix world; static static static static
float float float float
aspectRatio; FOV = MathHelper.PiOver4; nearClip = 1.0f; farClip = 1000.0f;
private ModelClass[] ball = new ModelClass[3]; MouseState mouseState; Random rand = new Random(); SpriteBatch spriteBatch; Sprite cursor; public BoundingSphere[] bb = new BoundingSphere[3]; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); for (int i = 0; ball.Length > i; i++) { ball[i] = new ModelClass(); } cursor = new Sprite(); } /// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { GraphicsAdapter adapter = graphics.GraphicsDevice.CreationParameters.Adapter; graphics.PreferredBackBufferWidth = adapter.CurrentDisplayMode.Width; graphics.PreferredBackBufferHeight = adapter.CurrentDisplayMode.Height; graphics.IsFullScreen = true; graphics.ApplyChanges(); screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; aspectRatio = (float)screenWidth / (float)screenHeight; base.Initialize();
311
312
Целимся и стреляем
Стреляем по целям
}
}
/// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); cursor.Load(content, “Content\\Textures\\cursor”); ball[0].Load(content, “Content\\Models\\Soccerball”); ball[1].Load(content, “Content\\Models\\SoccerballGreen”); ball[2].Load(content, “Content\\Models\\SoccerballRed”);
case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); MoveBall(); MouseClick(); break; }
for (int i = 0; ball.Length > i; i++) { ball[i].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[i].position.Y = rand.Next(0, 80); ball[i].position.Z = -(rand.Next(20, 150)); }
case CurentGameState.GameOverScreen: { break; }
} } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break;
case CurentGameState.VictoryScreen: { break; } } base.Update(gameTime); } /// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; }
313
314
Целимся и стреляем
Стреляем по целям
case CurentGameState.AboutScreen: { break; }
void MouseClick() { mouseState = Mouse.GetState(); cursor.spritePosition.X = mouseState.X; cursor.spritePosition.Y = mouseState.Y; for (int i = 0; bb.Length > i; i++) { bb[i].Center = ball[i].position; bb[i].Radius = ball[i].radius; }
case CurentGameState.GameScreen: { graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 150.0f), Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world, view, proj); }
Ray pickRay = GetPickRay(); for (int i = 0; ball.Length > i; i++) { Nullable result0 = pickRay.Intersects(bb[0]); if (result0.HasValue == true) { ball[0].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[0].position.Y = rand.Next(0, 80); ball[0].position.Z = -(rand.Next(20, 150)); }
spriteBatch.Begin(SpriteBlendMode.AlphaBlend); cursor.DrawSprite(spriteBatch); spriteBatch.End(); break; }
Nullable result1 = pickRay.Intersects(bb[1]); if (result1.HasValue == true) { ball[1].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[1].position.Y = rand.Next(0, 80); ball[1].position.Z = -(rand.Next(20, 150)); } Nullable result2 = pickRay.Intersects(bb[2]); if (result2.HasValue == true) { ball[2].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[2].position.Y = rand.Next(0, 80); ball[2].position.Z = -(rand.Next(20, 150)); }
case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } } base.Draw(gameTime); } /// <summary> /// Äâèæåíèå ìÿ÷åé /// <summary> void MoveBall() { for (int i = 0; ball.Length > i; i++) { ball[i].position.Y -= ball[i].speed; } } /// <summary> /// Êëèê ìûøåé /// <summary>
} } /// <summary> /// Ïðåîáðàçîâûâàåì êîîðäèíàòû ìûøè /// <summary> Ray GetPickRay() { mouseState = Mouse.GetState(); int mouseX = mouseState.X; int mouseY = mouseState.Y; Vector3 nearsource = new Vector3((float)mouseX, (float)mouseY, 0f); Vector3 farsource = new Vector3((float)mouseX, (float)mouseY, 1f);
315
316
Стреляем по целям
// ìèðîâàÿ ìàòðèöà world = Matrix.CreateTranslation(0, 0, 0); // ìàòðèöà âèäà view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 150.0f), Vector3.Zero, Vector3.Up); // ìàòðèöà ïðîåêöèè proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); // áëèæíÿÿ òî÷êà Vector3 nearPoint = graphics.GraphicsDevice.Viewport.Unproject(nearsource, proj, view, world); // äàëüíÿÿ òî÷êà Vector3 farPoint = graphics.GraphicsDevice.Viewport.Unproject(farsource, proj, view, world); Vector3 direction = farPoint - nearPoint; direction.Normalize(); Ray pickRay = new Ray(nearPoint, direction); return pickRay; } } }
Заметьте, что координаты прицела на самом деле находятся не в его цент) ре, а в левом верхнем углу изображения мушки. Это связано с тем, что по) лучая координаты курсора мыши, методом Mouse.GetState(), мы получаем координаты верхнего левого угла курсора. При желании вы можете переоп) ределить полученные значения непосредственно в центр прицела.
Глава 21 Формируем трехмерную сцену Способов создания полноценных трехмерных сцен очень много. Выбор того или ино( го способа целиком зависит от игровых задач. В игре «Футбольный стрелок» наша камера статична и находится в одном положении, поэтому для организации сцены был выбран механизм загрузки в игру модели стадиона. Суть этого способа заключа( ется в том, чтобы загрузить в игру модель стадиона и установить камеру под опреде( ленным углом, а затем на фоне стадиона развернуть все игровые действия. Для текущего примера был создан новый проект под названием Plane, который находится на компакт(диске в папке Code\Chapter21\Plane. Начиная с раздела 21.1, мы приступим к его изучению. Дополнительно в папке Code\Chapter21\ имеется еще один проект под названием PlaneMove. В нашей игре нет необходимости пере( мещаться по полю стадиона, но показать вам, как это делается, нужно обязательно. После изучения проекта Plane рассмотрите исходный код примера PlaneMove, ду( мается, что этот пример вам обязательно пригодится в своих собственных играх.
21.1. Изменяем позицию камеры Переходим к работе над игрой и начнем с того, что добавим в исходный код класса Game1 проекта Plane новую переменную camera. Vector3 camera = new Vector3(0.0f, 20.0f, 250.0f);
Эта переменная будет задавать одинаковую позицию просмотра сцены для мячей и футбольного стадиона. // ìåòîä Draw() view = Matrix.CreateLookAt(camera, Vector3.Zero, Vector3.Up);
А также для вычисления позиции курсора мыши. // ìåòîä GetPickRay() Ray GetPickRay() { … view = Matrix.CreateLookAt(camera, Vector3.Zero, Vector3.Up); … }
318
Новые игровые позиции для мячей
Формируем трехмерную сцену
Таким образом, мы создаем одну переменную и упрощаем возможность обо( юдного изменения точки просмотра сцены в двух и более местах исходного кода. Сама точка просмотра трехмерной сцены удаляется на 250 пикселей и поднимает( ся от центра экрана на 20 пикселей. Предложенные значения точки просмотра сцены взяты не с неба, эти значе( ния были сначала опробованы на загружаемой в игру модели стадиона, и только потом был выбран наиболее выгодный ракурс. В этом плане бесплатные модели имеют свою обратную сторону медали. Здесь уже приходится подстраиваться под имеющийся графический контент, вместо того чтобы модельер, делающий моде( ли, подстраивался под ваши требования…
21.2. Загружаем в игру стадион Переходим к загрузке модели стадиона в игру. В качестве основной игровой моде( ли для формирования сцены в игре мы используем модель стадиона, которая была найдена в закромах ресурса Turbosquid.com (рис. 21.1). Эта модель распространя( ется на бесплатной основе в формате 3ds и создана модельером, зарегистрирован( ным на Turbosquid.com под именем dwallcott (рис. 21.1).
319
К сожалению, в комплекте со стадионом текстуры не идут, поэтому я покра( сил все элементы стадиона просто в разные цвета, но при желании можно исполь( зовать любые текстуры, созданные специально для этих целей. Итак, перейдем к исходному коду класса Game1 проекта Plane и в области глобальных переменных объявим объект stadium класса ModelClass. private ModelClass stadium;
Затем в конструкторе класса Game1 или в методе Initialize() создаем но( вый объект stadium. stadium = new ModelClass();
Переходим к телу метода LoadGraphicsContent() и, используя стандарт( ные механизмы, загружаем в игру модель. stadium.Load(content, “Content\\Models\\stadium 1”);
В этом же методе выбираем позицию для стадиона на экране монитора. stadium.position = new Vector3(0, 0, 0);
Стадион мы ставим на экран в нулевые координаты, а за счет удаления каме( ры от центра экрана (см. раздел 21.1) мы получаем хороший ракурс просмотра всей трехмерной сцены. Последние установки, связанные с выводом стадиона на экран, происходят в методе Draw(). world = Matrix.CreateTranslation(stadium.position); stadium.DrawModel(world, view, proj);
Здесь все установки стандартны, а значения матрицы вида и матрицы проек( ции общие как для мячей, так и для стадиона. Если сейчас запустить проект, то на экране монитора вы увидите стадион и падающие сверху мячики (рис. 21.2), но прежде нам необходимо сделать еще несколько изменений и нововведений, чтобы трехмерная сцена стала несколько интереснее.
21.3. Новые игровые позиции для мячей
Рис. 21.1. Страница модели стадиона на сайте
В предыдущей главе для выбора случайных позиций мячиков мы использовали некоторые значения. Эти значения были промежуточными. Теперь, когда мы име( ем полноценную трехмерную сцену, нам необходимо особо тщательно подрегули( ровать случайный выбор позиций мячей на экране. По оси X и Y выбор позиции мячей на экране остается прежним.
320
Небо и тучи
Формируем трехмерную сцену
321
Это изменение было сделано после того, как текущий проект был протестиро( ван несколько раз. Как вы помните, к выбору скоростей в играх нужно подходить взвешенно и осторожно. К слову сказать, в игре «Футбольный стрелок» значение скорости задано жестко, но можно производить выбор скорости объекта на экране непосредственно с созданием этого объекта и далее на каждом уровне игры, внося элемент случайности (в последней главе книги мы так и поступим). В исходном коде класса Game1 и методе MoveBall() на этой стадии нас ждут небольшие изменения, которые выглядят следующим образом:
Рис. 21.2. Стадион в игре
ball[i].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[i].position.Y = rand.Next(0, 80);
А для оси Z мы теперь задаем своеобразный коридор от –50 пикселей до 150 пикселей. ball[i].position.Z = rand.Next(-(rand.Next(0, 50)), rand.Next(0, 150));
Дело в том, что сейчас камера у нас удалилась на целых 250 пикселей по оси Z от всей сцены и мячики на экране (если оставить старые позиции) выводятся да( лековато и местами даже падают за сам стадион, пропадая из области видимости. Поэтому мы задаем новые значения и приближаем мячи к игроку, а также делаем так, чтобы мячи в основном падали в районе футбольного поля, а не на здания. Изменение выбора случайных позиций происходит как в методе LoadGraphicsContent() при начальном запуске игры, так и в ходе игрового процесса в методе MouseClick() (см. листинг 21.1).
21.4. Падение мячей на поле стадиона По сравнению с предыдущим примером стоит несколько увеличить скорость па( дения мячей, которую мы устанавливали в классе ModelClass. speed = 25.0f / 60.0f;
void MoveBall() { for (int i = 0; ball.Length > i; i++) { if (ball[i].position.Y < -32) { ball[i].position.Y = ball[i].position.Y; } else { ball[i].position.Y -= ball[i].speed; } } }
Мячики в игре, падая сверху вниз, должны в определенный промежуток вре( мени коснуться поверхности поля. Определять пересечение поля стадиона и мя( чей можно по(разному. Самый простой способ – это выбор числового значения (в данном случае по оси Y), достигнув которого, мячи просто останавливаются. if (ball[i].position.Y < -32) { ball[i].position.Y = ball[i].position.Y; }
Целочисленное значение –32 пикселя по оси Y совпадает с поверхностью фут( больного поля. Когда мячики по оси Y достигнут –32 пикселя, то они перестают двигаться. Такой простой механизм позволяет создать в игре иллюзию остановки мячей на поверхности футбольного поля.
21.5. Небо и тучи Задний фон за стадионом в игре закрашивается синим цветом. Сейчас это уже не( актуально, и для формирования полноценной сцены необходимо создать небо, тучи и другие элементы пейзажа. Механика представления этих элементов в иг( рах достаточна обширна. Поскольку игра «Футбольный стрелок» имеет статич( ную камеру и точка просмотра сцены не изменяется, то можно воспользоваться текстурой пейзажа и поставить это изображение за элементами сцены.
322
Небо и тучи
Формируем трехмерную сцену
В большинстве игр этот механизм используется повсеместно, но если камера в игре двигается, то добавляется более сложный механизм, основанный либо на прокрутке текстуры фона в разных направлениях, либо на использовании куби( ческих текстур для представления небесной оболочки (Sky Box). У нас вся трех( мерная сцена попроще, поэтому вполне достаточно простой текстуры с пейзажем установленной на заднем фоне игры. Текстура пейзажа была взята с сайта Turbosquid.com. Это графическое изоб( ражение в формате JPG распространяется бесплатно. Пейзаж создан художником под именем Hal9000 (рис. 21.3).
323
Для добавления фона в игру объявим новую переменную background. private Texture2D background;
Изображение фона статично, поэтому можно обойтись и без объекта класса Sprite. После объявления объекта background в методе LoadGraphicsContent() загружаем пейзаж в игру. background = content.Load(“Content\\Textures\\hallake001”);
А затем в методе Draw() рисуем его на экране монитора. spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background, new Vector2(0, -50), Color.White); spriteBatch.End();
При этом само изображение фона мы рисуем самым первым, не забывайте, что каждый последующий рисуемый элемент графики всегда накладывается поверх предыдущего! Изображение hallake001 по высоте равно 500 пикселям. По оси Y мы смес( тили изображение вверх на 50 пикселей. Сделано это просто для красоты. То есть после того как изображение было загружено, а игра запущена на компьютере, я просто подобрал оптимальное местоположение фона, исходя из общей картинки на экране, и не более того. В листинге 21.1 вы найдете полный исходный код текущего примера, где все нововведения выделены жирным шрифтом. Сам проект располагается на ком( пакт(диске в папке Code\Chapter21\Plane.
Рис. 21.3. Страница изображения hallake001 на сайте Turbosquid.com
Графическое изображение этого пейзажа оказалось очень большим по ширине (3000 пикселей), поэтому пришлось обрезать его по бокам, укоротив изображение по ширине до 1500 пикселей. Связано это с тем, что некоторые видеокарты не в силах загрузить слишком большой по размеру (ширина или высота) графиче( ский файл. Если у вас при запуске программ возникает сообщение подобного ха( рактера, то эта проблема состоит в мощности видеоадаптера, и вам необходимо либо подкупить новую видеокарту (что дороже), либо уменьшить размер изобра( жения (что дешевле).
//========================================================================= /// <summary> /// Ëèñòèíã 21.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 21 /// Ïðîåêò: Plane /// Êëàññ: Game1 /// Òðåõìåðíàÿ ñöåíà /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics;
324
Небо и тучи
Формируем трехмерную сцену
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion
325
} cursor = new Sprite(); stadium = new ModelClass(); }
namespace Plane { public class Game1 : Microsoft.Xna.Framework.Game { private enum CurentGameState { SplashScreen, MenuScreen, AboutScreen, GameScreen, GameOverScreen, VictoryScreen } CurentGameState gameState = CurentGameState.GameScreen; GraphicsDeviceManager graphics; ContentManager content; KeyboardState keyboardState; int screenWidth, screenHeight; Matrix view; Matrix proj; Matrix world; static static static static
float float float float
aspectRatio; FOV = MathHelper.PiOver4; nearClip = 1.0f; farClip = 1000.0f;
private ModelClass[] ball = new ModelClass[3]; MouseState mouseState; Random rand = new Random(); SpriteBatch spriteBatch; Sprite cursor; public BoundingSphere[] bb = new BoundingSphere[3]; Vector3 camera = new Vector3(0.0f, 20.0f, 250.0f); private ModelClass stadium; private Texture2D background; /// <summary> /// Êîíñòðóêòîð /// <summary> public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); for (int i = 0; ball.Length > i; i++) { ball[i] = new ModelClass();
/// <summary> /// Èíèöèàëèçàöèÿ /// <summary> protected override void Initialize() { GraphicsAdapter adapter = graphics.GraphicsDevice.CreationParameters.Adapter; graphics.PreferredBackBufferWidth = adapter.CurrentDisplayMode.Width; graphics.PreferredBackBufferHeight = adapter.CurrentDisplayMode.Height; graphics.IsFullScreen = true; graphics.ApplyChanges(); screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; aspectRatio = (float)screenWidth / (float)screenHeight; base.Initialize(); } /// <summary> /// Çàãðóçêà êîìïîíåíòîâ èãðû /// <summary> protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); cursor.Load(content, “Content\\Textures\\cursor”); ball[0].Load(content, “Content\\Models\\Soccerball”); ball[1].Load(content, “Content\\Models\\SoccerballGreen”); ball[2].Load(content, “Content\\Models\\SoccerballRed”); for (int i = 0; ball.Length > i; i++) { ball[i].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, 80)); ball[i].position.Y = rand.Next(0, 80); ball[i].position.Z = rand.Next(-(rand.Next(0, 50)), rand.Next(0, 150)); } stadium.Load(content, “Content\\Models\\stadium 1”); stadium.position = new Vector3(0, 0, 0); background =content.Load(“Content\\Textures\\hallake001”); } } /// <summary> /// Îñâîáîæäàåì ðåñóðñû /// <summary> protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) {
326
Небо и тучи
Формируем трехмерную сцену content.Unload();
}
} } /// <summary> /// Îáíîâëÿåì ñîñòîÿíèå èãðû /// <summary> protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { if (keyboardState.IsKeyDown(Keys.Escape)) this.Exit(); MoveBall(); MouseClick(); break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } } base.Update(gameTime);
/// <summary> /// Ðèñóåì íà ýêðàíå /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background, new Vector2(0, -50), Color.White); spriteBatch.End(); graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; view = Matrix.CreateLookAt(camera, Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world, view, proj); } world = Matrix.CreateTranslation(stadium.position); stadium.DrawModel(world, view, proj); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); cursor.DrawSprite(spriteBatch); spriteBatch.End(); break; }
327
328
Небо и тучи
Формируем трехмерную сцену
{ ball[0].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, ball[0].position.Y = rand.Next(0, 80); ball[0].position.Z = rand.Next(-(rand.Next(0, 50)), rand.Next(0, } Nullable result1 = pickRay.Intersects(bb[1]); if (result1.HasValue == true) { ball[1].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, ball[1].position.Y = rand.Next(0, 80); ball[1].position.Z = rand.Next(-(rand.Next(0, 50)), rand.Next(0, } Nullable result2 = pickRay.Intersects(bb[2]); if (result2.HasValue == true) { ball[2].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, ball[2].position.Y = rand.Next(0, 80); ball[2].position.Z = rand.Next(-(rand.Next(0, 50)), rand.Next(0, } }
case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } } base.Draw(gameTime); } /// <summary> /// Äâèæåíèå ìÿ÷åé /// <summary> void MoveBall() { for (int i = 0; ball.Length > i; i++) { if (ball[i].position.Y < -32) { ball[i].position.Y = ball[i].position.Y; } else { ball[i].position.Y -= ball[i].speed; } } }
150));
80)); 150));
80)); 150));
/// <summary> /// Ïðåîáðàçîâûâàåì êîîðäèíàòû /// <summary> Ray GetPickRay() { mouseState = Mouse.GetState(); int mouseX = mouseState.X; int mouseY = mouseState.Y; Vector3 nearsource = new Vector3((float)mouseX, (float)mouseY, 0f); Vector3 farsource = new Vector3((float)mouseX, (float)mouseY, 1f); world = Matrix.CreateTranslation(0, 0, 0); view = Matrix.CreateLookAt(camera, Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); Vector3 nearPoint = graphics.GraphicsDevice.Viewport.Unproject(nearsource, proj, view, world); Vector3 farPoint = graphics.GraphicsDevice.Viewport.Unproject(farsource, proj, view, world);
for (int i = 0; bb.Length > i; i++) { bb[i].Center = ball[i].position; bb[i].Radius = ball[i].radius; }
Nullable result0 = pickRay.Intersects(bb[0]); if (result0.HasValue == true)
80));
}
/// <summary> /// Êëèê ìûøüþ /// <summary> void MouseClick() { mouseState = Mouse.GetState(); cursor.spritePosition.X = mouseState.X; cursor.spritePosition.Y = mouseState.Y;
if (mouseState.LeftButton == ButtonState.Pressed) { Ray pickRay = GetPickRay();
329
Vector3 direction = farPoint - nearPoint; direction.Normalize(); Ray pickRay = new Ray(nearPoint, direction); return pickRay; } } }
Схема работы меню и показ заставок
331
Глава 22 Последние штрихи В отличие от двухмерной игры, созданной нами в предыдущей части книги, в игре «Футбольный стрелок» нет жесткой привязки к определенному размеру экрана. В связи с этим для экрана меню, а также других информационных заставок необ( ходимо предусмотреть универсальный механизм представления графики на экра( не. Способов решения этой задачи очень много, и в каждом конкретном решении можно использовать любые интересные идеи и находки. Достаточно просто за( пустить несколько профессионально сделанных игр и посмотреть, как делают меню и заставки сильные миры сего.
22.1. Схема работы меню и показ заставок Механизм представления заставок почти во всех играх одинаков и сводится к вы( воду того или иного изображения в центре экрана, а все свободное пространство дисплея красится одним из цветов. Меню игры также основано на этой технике, но здесь часто набор доступных команд меню (Новая игра, Помощь, Об игре, Оп( ции…) смещается к краю дисплея (влево, вправо) или к верхней и нижней частям кромок экрана либо располагается в его центре. Это не столь важно, к какой из сторон дисплея смещаются команды, – важен сам механизм смещения команд. Разрешение, установленное на компьютере одного пользователя, может быть од( ним и совсем другим уже на компьютере другого пользователя, поэтому очень важно иметь четкую точку отсчета, от которой вы можете отталкиваться в своих расчетах и производить вывод команд меню и другой графики на экран. В игре «Футбольный стрелок» такой точкой отсчета служит центр экрана. Все основные графические эле( менты заставок и меню устанавливаются в центр экрана. Например, в меню игры от центра экрана происходит расчет вывода курсора, а соответственно и дальнейший переход по командам меню основан на точке в центре дисплея. Подробнее об этом мы поговорим позже в этой главе, а сейчас давайте набросаем на листке бумаги необходи( мые нам компоненты для создания меню и заставок. Обычно последовательность заставок и механизм работы меню традиционен во всех играх. Посмотрите на рис. 22.1, где вашему вниманию предложена схема работы заставок и меню игры «Футбольный стрелок». Первая игровая заставка, появляющаяся на экране монитора в игре «Фут( больный стрелок», – это так называемая титульная заставка. На этой заставке может присутствовать различная информация, но в основном эта информация связана с разработчиком игры, спонсорами и т. д. У нас в игре титульная заставка
Рис. 22.1. Схема работы меню и заставок игры
появляется первой, после чего пользователю будет предложено нажать клавишу Enter для перехода в меню игры. Игровое меню «Футбольного стрелка» содержит пять команд: Игра, Помощь, Об игре, Книги и Выход. Игра – эта команда запускает игровой процесс. Помощь – выбор этой команды откроет новый экран, где пользователь мо( жет ознакомиться с правилами игры и другой полезной информацией об игровом процессе. Возврат с этого экрана в меню игры возможен только по нажатии клавиши Esc. Об игре – эта команда также открывает свой экран, в котором игрок найдет информацию о людях, создавших данную игру. На этом экране обычно присутствует полный перечень всех тех людей, которые внесли свой вклад в создание игры. Возврат с этого экрана в меню игры осуществляется по нажатии клавиши Esc. Книги – эта команда оригинальна только для этой игры, и здесь находится рекламная информация о книгах по программированию игр для Windows и Xbox 360. Возврат с этого экрана в меню игры доступен по нажатии клави( ши Esc. Выход – это последняя команда, и, как видно из названия, ее назначение заключается в закрытии программы и выходе из игры.
332
Титульная заставка
Последние штрихи
Начнем с титульной заставки, потом создадим три заставки: Помощь, Об игре и Книги, затем перейдем к созданию меню игры и к механизму смены игровых состояний в классе Game1.
22.2. Титульная заставка Титульная заставка – это самая первая заставка, которую пользователь видит на своем экране, поэтому важно на этой заставке указать информацию о своей ком( пании или о себе лично. Как правило, все создатели игр на титульной заставке размещают логотип своей компании, а также может быть показана информация о спонсорах или других рекламных партнерах. Если вы делаете игру сами и не имеете своей компании, то разместите на этой заставке информацию, которая впоследствии могла бы идентифицировать ваши игры от других игр. Пользова( тель должен запомнить эту заставку и в дальнейшем (если, конечно, игра будет иметь хоть какой(то спрос) знать, что эта компания или этот программист делает хорошие игры. В игре «Футбольный стрелок» основная задача титульной заставки заключает( ся в рекламе книг по программированию игр для Windows и Xbox 360 (рис. 22.2). В связи с этим на титульную заставку была вынесена информация о книгах, а так( же даны Интернет(адреса, где пользователь смог бы найти больше информации по интересующей его теме. То есть все сделано для продвижения книг на изда(
Рис. 22.2. Титульная заставка игры «Футбольный стрелок»
333
тельском рынке, которые, в свою очередь, двигают эту игру. Рычаги любой рекла( мы необходимо использовать на полную катушку, и уж тем более когда эта рекла( ма бесплатна. Оформление титульной заставки игры может выполняться в любом ключе, здесь все зависит от политики компании или самого программиста. Любые укра( шательства (в меру, конечно) или хитроумные способы представления игры толь( ко приветствуются. Рассмотрим компоненты, из которых состоит титульная за( ставка игры «Футбольный стрелок».
22.2.1. Как сделана титульная заставка Все пространство экрана титульной заставки закрашивается темно(зеленым цве( том. На фоне этого цвета в центре экрана рисуется футбольное поле, на котором располагается некоторая информация о книгах, издательстве и авторе, а также даны обложки самих книг (рис. 22.2). Рисунок футбольного поля – это отдельный графический файл размером 800 × 500 пикселей. В верхней части экрана рисуется титульная надпись игры с текстом: Фут( больный стрелок. Эта надпись сделана отдельным графическим файлом в фор( мате PNG и размером 800 × 80 пикселей. В нижней части экрана более мелким шрифтом рисуется надпись с текстом: Нажмите – Enter, для того чтобы пользо( ватель знал, какую из клавиш на клавиатуре ему нужно нажать для перехода в меню игры. Еще один элемент заставки – это входные отвер( стия пуль. Идея такая: рисуется изображение, напоми( нающее входное отверстие пули (рис. 22.3). Затем в ис( ходном коде программы создается массив данных, содержащий определенное количество таких изобра( жений. В качестве позиций на экране этим изображе( Рис. 22.3. Изображение входного отверстия ниям задаются случайные позиции, и в момент показа пули на экране заставки все отверстия будут раскиданы по экрану случайным образом. В итоге у пользователя со( здастся впечатление, что экран монитора изрешечен выстрелами (рис. 22.2). Последний элемент заставки – это мячик из игры, который выводится в центр экрана и вращается по оси Y. То есть мячик стоит на месте в самом центре экрана и крутится вокруг своей оси с заданной скоростью. Мячик в сцене рисуется послед( ним, и все входные отверстия пуль, даже если попадут в мяч, будут нарисованы под ним. Это простое и самое обыкновенное украшательство заставки титульного лис( та. Таких идей можно придумать много, но главное – не «переборщить» с насы( щенностью графических компонентов. Понятно, что вам хочется показать людям, какой вы хороший программист, но много – это совсем не значит хорошо. Напри( мер, в этой титульной заставке я уже нахожусь где(то на грани того, чтобы пере( брать с насыщенностью игровой сцены для простой титульной заставки…
334
Титульная заставка
Последние штрихи
22.2.2. Разрабатываем класс SplashScreen
335
быть каким угодно. Сначала я попробовал использовать массив из десяти изобра( жений, но этого оказалось маловато. Двадцать изображений тоже особо не впечат( лили, а вот тридцать разбросанных по экрану отверстий выглядели в самый раз, но, конечно, здесь все зависит от разрешения монитора.
Пришла пора поработать над исходным кодом класса, представляющего титуль( ную заставку игры. Для этих целей создается класс SplashScreen, который впоследствии послужит нам прототипом для других классов заставок и основного меню игры. Создаем новый проект с названием Menu. На компакт(диске этот проект нахо( дится в папке Code\Chapter22\Menu. Исходный код класса Menu приведен в лис) тинге 22.1. Для простоты рассмотрения исходного кода я перейду прямо в лис( тинг и по ходу изучения прокомментирую составляющую класса Menu. Так вам будет намного проще и интереснее.
public Texture2D[] hole = new Texture2D[30]; public Vector2[] position = new Vector2[30]; Random rand = new Random();
//========================================================================= /// <summary> /// Ëèñòèíã 22.1 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 22 /// Ïðîåêò: Menu /// Êëàññ: SplashScreen /// Ìåíþ /// <summary> //=========================================================================
private ModelClass ball; static float aspectRatio; static float FOV = MathHelper.PiOver4; static float nearClip = 1.0f; static float farClip = 1000.0f; float angle; Matrix view; Matrix proj; Matrix world; Matrix rotationMatrixY;
#region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion
В последней строке кода мы создаем еще одну дополнительную матрицу, ко( торую впоследствии будем использовать для вращения мяча по оси Y. Для загрузки графики в будущий объект класса SplashScreen создается ме( тод InitializeSplashScreen() с тремя параметрами. Первый параметр content принимает участие в загрузке графики из рабочего каталога игры, а два других x и y будут передавать в этот метод текущие значения ширины и высоты экрана монитора. Эти значения нам понадобятся для определения позиций выво( да отверстий на экран.
namespace Menu { class SplashScreen {
В области глобальных переменных объявляем три объекта, необходимых для загрузки изображений футбольного поля, названия игры и надписи «Нажмите – Enter». Все три объекта представлены классом Texture2D. public Texture2D screen; public Texture2D title; public Texture2D enter;
Теперь нам необходимо объявить массив объектов для хранения графическо( го изображения входного отверстия пули. Количество элементов массива может
Следующий относительно большой блок кода должен быть вам знаком по ра( боте с трехмерными объектами и матрицами. Ничего нового в этих строках нет, за исключением самой последней строки кода.
/// <summary> /// Çàãðóçêà êîìïîíåíòîâ çàñòàâêè /// <summary> public void InitializeSplashScreen(ContentManager content, int x, int y) {
Загружаем в программу все четыре графических изображения. screen = content.Load(“Content\\Textures\\splash”); title = content.Load(“Content\\Textures\\title”); enter = content.Load(“Content\\Textures\\enter”); for (int i = 0; hole.Length > i; i++) { hole[i] = content.Load(“Content\\Textures\\hole”); }
336
Титульная заставка
Последние штрихи
А вот в следующем цикле мы выбираем случайные позиции на экране для всех тридцати отверстий и таким образом как бы раскидываем по экрану отверстия в случайном порядке. В этом случае при каждом новом запуске игры отверстия бу( дут иметь новые позиции, что само по себе неплохо – разнообразие в играх, пусть даже в представлении титульной заставки, нам не помешает.
337
Название игры ставим по ширине в самый центр экрана. По высоте опускаем изображение от верхней части экрана на тридцать пикселей. spriteBatch.Draw(title, new Vector2(x /2 - title.Width/2, 30),
Color.White);
Футбольное поле ставим четко в центр экрана. for (int i = 0; position.Length > i; i++) {
Область вывода отверстий задается по оси X от 20 пикселей до ширины экра( на минус 60 пикселей. Эти самые 60 пикселей примерно равны ширине одного входного отверстия. position[i].X = rand.Next(20, x - 60); position[i].Y = rand.Next(100, y - 60); }
Затем происходят загрузка в игру мяча и выбор позиции на экране, которая находится точно в центре экрана. ball = new ModelClass(); ball.Load(content, “Content\\Models\\SoccerballRed”); ball.position = new Vector3(0, 0, 0);
spriteBatch.Draw(screen, new Vector2(x/2 - screen.Width/2, y/2 – screen.Height/2), Color.White);
Надпись «Нажмите – Enter» по ширине экрана находится в самом центре, а по высоте приподнята на тридцать пикселей от нижней части экрана. spriteBatch.Draw(enter, new Vector2(x / 2 - enter.Width / 2, y - 30 – enter.Height), Color.White); for (int i = 0; hole.Length > i; i++) { spriteBatch.Draw(hole[i], position[i], Color.White); } spriteBatch.End();
Теперь пришло время вывести на экран мяч. graphics.GraphicsDevice.RenderState.DepthBufferEnable = true;
Переменная aspectRatio получает текущие значения ширины и высоты эк( рана и в дальнейшем будет участвовать в матричных расчетах. aspectRatio = (float)x / (float)y; }
Следующий метод DrawScreen() рисует графику на экране и имеет целых пять параметров. Первые два параметра – spriteBatch и graphics – как вы помните, нужны нам для работы с графикой. Параметры x и y передадут в тело метода ширину и высоту дисплея, а последний параметр gameTime позволит нам получать прошедшее время за один такт игры, которое мы будем использовать для вращения мяча по оси Y. /// <summary> /// Ðèñóåì çàñòàâêó /// <summary> public void DrawScreen(SpriteBatch spriteBatch, GraphicsDeviceManager graphics, int x, int y, GameTime gameTime) {
Рисуем на экране все двухмерные изображения. graphics.GraphicsDevice.Clear(Color.DarkGreen); spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
Позиция глаз для мячика удалена на 230 пикселей. Таим образом, мы удалили мяч от себя и в то же время уменьшили его в размерах. При желании можно сам мяч уменьшить, например в 3ds Max, и тогда позиция камеры может быть намного меньше, вплоть до единичного значения по оси Z. Тут все зависит от физического размера самой модели. view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 230.0f), Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip);
Переменная angle получает прошедшее время за один такт, которое мы умно( жаем на взятую с неба цифру 3.0f. Здесь цифра может быть любой, но меньшее значение всегда соответствует меньшей скорости вращения мячика вокруг своей оси, а большая цифра – большей скорости вращения модели. angle += (float)(gameTime.ElapsedGameTime.TotalSeconds * 3.0f);
Затем для вращения мячика именно по оси Y используется метод CreateRotationY(), который возвращает матрицу вращения по оси Y. rotationMatrixY = Matrix.CreateRotationY((float)angle);
338
Заставки Помощь, Об игре и Книги
Последние штрихи
339
Если вы вспомните главу 16, где мы разбирали основы программирования трехмерной графики и, в частности, раздел, связанный с матрицей враще) ния по оси Y, то вам сразу станет понятно, как работает метод CreateRotationY(). Для того чтобы все установки вращения вступили в силу, нужно умножить матрицу вращения на матрицу трансляции и сохранить итоговый результат в мировой матрице. world = Matrix.CreateTranslation(ball.position) * rotationMatrixY; ball.DrawModel(world, view, proj); } } }
При желании можно также вращать мячик сразу по всем трем осям. Для этого достаточно создать еще две матрицы для осей X и Z, умножить их на какое(то чис( ло, а затем элементарно перемножить матрицы между собой, сохранив получен( ное значение в мировой матрице. Например, вот так: angle += (float)(gameTime.ElapsedGameTime.TotalSeconds * 2.0f); Matrix rotX = Matrix.CreateRotationX((float)angle); Matrix rotY = Matrix.CreateRotationY((float)angle); Matrix rotZ = Matrix.CreateRotationZ((float)angle); world = Matrix.CreateTranslation(ball.position) * rotX * rotY * rotZ; ball.DrawModel(world, view, proj);
На этом разработка класса SplashScreen окончена, и мы можем переходить к разработке трех других заставок игры.
22.3. Заставки Помощь, Об игре и Книги Для создания трех оставшихся заставок игры мы возьмем исходный код класса SplashScreen и добавим в него ряд изменений. Основным отличием оставших( ся заставок от титульной заставки (за исключением смены текста на футбольных полях) является наличие не одного мяча, а трех (рис. 22.4). Идея следующая. Вместо того чтобы вращать в центре экрана только один мяч, мы добавим в заставку еще два мяча, а для определения позиций мячей на экране зададим любые ненулевые координаты. Вращать мячи будем по двум осям, а поскольку мы задаем позиции для мячей не в центре экрана, мячики будут пере( мещаться по всему экрану. В этом случае область вращения мячей задается на( чальными позициями объектов. В листинге 22.2 вашему вниманию представлен класс HelpScreen. Этот класс аналогичен двум другим классам AboutScreen и BooksScreen. В этих классах изменяется только текст, размещенный на футбольных полях. Полный исходный код проекта находится в папке Code\Chapter22\Menu.
Рис. 22.4. Пример оформления одной из заставок //========================================================================= /// <summary> /// Ëèñòèíã 22.2 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 22 /// Ïðîåêò: Menu /// Êëàññ: HelpScreen /// Ìåíþ /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace Menu { class HelpScreen
340
{ public Texture2D screen; public Texture2D title; public Texture2D esc; public Texture2D[] hole = new Texture2D[30]; public Vector2[] position = new Vector2[30]; Random rand = new Random(); private ModelClass[] ball = new ModelClass[3]; static float static float static float static float float angle; Matrix Matrix Matrix Matrix Matrix
Создаем меню игры
Последние штрихи
aspectRatio; FOV = MathHelper.PiOver4; nearClip = 1.0f; farClip = 1000.0f;
view; proj; world; rotationMatrixY; rotationMatrixZ;
/// <summary> /// Çàãðóçêà êîìïîíåíòîâ çàñòàâêè /// <summary> public void InitializeHelpScreen(ContentManager content, int x, int y) { screen = content.Load(“Content\\Textures\\help”); title = content.Load(“Content\\Textures\\title”); esc = content.Load(“Content\\Textures\\esc”); for (int i = 0; hole.Length > i; i++) { hole[i] = content.Load(“Content\\Textures\\hole”); }
ball[2].Load(content, “Content\\Models\\SoccerballRed”); ball[2].position = new Vector3(-50, -30, 40); aspectRatio = (float)x / (float)y; } /// <summary> /// Ðèñóåì çàñòàâêó /// <summary> public void DrawScreen(SpriteBatch spriteBatch, GraphicsDeviceManager graphics, int x, int y, GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.DarkGreen); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(title, new Vector2(x / 2 - title.Width / 2, 30), Color.White); spriteBatch.Draw(screen, new Vector2(x / 2 - screen.Width / 2, y / 2 – screen.Height / 2), Color.White); spriteBatch.Draw(esc, new Vector2(x / 2 - esc.Width / 2, y - 30 – esc.Height), Color.White); for (int i = 0; hole.Length > i; i++) { spriteBatch.Draw(hole[i], position[i], Color.White); } spriteBatch.End(); graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 260.0f), Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip);
Здесь мы используем уже две матрицы вращения по осям X и Z. angle += (float)(gameTime.ElapsedGameTime.TotalSeconds * 1.0f); rotationMatrixY = Matrix.CreateRotationY((float)angle); rotationMatrixZ = Matrix.CreateRotationZ((float)angle); for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world * rotationMatrixY * rotationMatrixZ, view, proj); }
for (int i = 0; position.Length > i; i++) { position[i].X = rand.Next(20, x - 60); position[i].Y = rand.Next(100, y - 60); } for (int i = 0; ball.Length > i; i++) { ball[i] = new ModelClass(); }
Позиции для мячей заданы, что называется, с потолка, главным здесь было одно условие: не задавать слишком большие значения по двум осям X и Y, дабы мячики при вращении не вышли из области видимости экрана. ball[0].Load(content, “Content\\Models\\Soccerball”); ball[0].position = new Vector3(-30, 40, -30); ball[1].Load(content, “Content\\Models\\SoccerballGreen”); ball[1].position = new Vector3(50, 30, -50);
341
} } }
22.4. Создаем меню игры Задача книги состоит в том, чтобы показать вам как можно больше различных механизмов реализации игр, в том числе и меню, поэтому в игре «Футбольный стрелок» мы сделаем новое меню, отличное от предыдущей игры. Для формирова( ния игрового меню в новом проекте Menu создается отдельный класс MenuGame.
342
Создаем меню игры
Последние штрихи
Этот класс основан на исходном коде классов HelpScreen, AboutScreen и BooksScreen, но с небольшими дополнениями. Прежде чем переходить к изуче( нию исходного кода игрового меню, давайте сначала рассмотрим, из каких графи( ческих элементов состоит меню, а также проанализируем общий принцип его ра( боты. Для оформления меню используется название игры «Футбольный стрелок», надпись «Нажмите – Enter», три мячика, которые, как и на заставках Помощь, Об игре и Книги, будут летать по экрану, а также четыре новых графических изоб( ражения. Два изображения – это нарисованные девушки, стоящие слева и справа по бокам экрана монитора (для красоты). Еще один новый графический файл – это футбольное поле, но значительно меньшего размера, чем в заставках, которое по( вернуто на 90 градусов вокруг своей оси (рис. 22.5). Последний графический эле( мент меню – это простая прямоугольная текстура размером 220 × 40 пикселей, окрашенная в желтый цвет.
Рис. 22.5. Меню игры
На футбольном поле располагаются пять команд меню: Игра, Помощь, Об игре, Книги и Выход. Все буквы команд меню вырезаны из изображения фут( больного поля редактором Photoshop. Это как если взять листок бумаги и нарисо( вать на нем любое слово, а затем аккуратно вырезать по контуру все буквы этого слова ножницами. Абсолютно идентичный механизм применяется и у нас, а вот зачем это сделано, давайте разбираться.
343
Вы точно играли в игры, в меню которых одна из выбранных команд, напри( мер New Game, красилась одним из цветов, отличным от неактивных в данный момент команд. В этом меню переход по командам происходит по нажатии кла( виш с командами вверх и вниз (Down и Up). В тот момент, когда вы переходите на новую команду, эта строка текста закрашивается одним из цветов, и таким обра( зом вы знаете, какая из команд активна в данный момент. Вот точно такое же меню мы реализуем в игре «Футбольный стрелок». Способов, как всегда, очень много, но мы остановимся на одном из них. Вернемся к листку бумаги. Давайте возьмем простой листок бумаги, покрасим его в темно(зеленый цвет и положим на стол – это будет у нас фон меню. Затем возьмем еще один листок бумаги, меньший по размеру, покрасим его в светло(зе( леный цвет и нарисуем на нем футбольное поле. Затем на этом футбольном поле мы нарисуем команды меню и вырежем их аккуратно ножницами. Положив вто( рой листок бумаги с футбольным полем поверх основного фона, мы увидим, что все команды меню стали хорошо заметны, поскольку они приняли более темный цвет фона. Теперь нам нужно сделать так, чтобы одна из команд (активная в дан( ный момент) закрашивалась в желтый цвет. Для этих целей вырежем прямоугольник, размер которого будет по своей вы( соте на несколько миллиметров больше, чем высота букв в командах, а по шири( не – соответствовать (или даже на пару миллиметров больше) самой длинной ко( манде в меню. После этого закрасим прямоугольник желтым цветом и поместим его между фоном и футбольным полем, получив своего рода подложку для ко( манд меню. При этом прямоугольник должен размещаться точно под одной из команд. Тогда эта команда получит в качестве основного цвета желтый цвет, а все остальные команды – зеленый цвет. То есть мы элементарно подсвечиваем жел( тым цветом активную на данный момент команду. Теперь нам необходимо только расчетливо перемещать этот прямоугольник по командам меню. Для этого необходимо создать простой механизм в классе MenuGame, который будет устанавливать желтый прямоугольник на определен( ную позицию в пространстве под активную в текущий момент команду. Задача несложная, и все расчеты мы будем вести от центра экрана, поскольку заставка футбольного поля привязана именно к этой части экрана. Переходим к исходному коду класса MenuGame, который дан в листинге 22.3. По мере изучения программного кода будем комментировать заслуживающие внимания нововведения. //========================================================================= /// <summary> /// Ëèñòèíã 22.3 /// Èñõîäíûé êîä ê êíèãå: /// “Ïðîãðàììèðîâàíèå êîìïüþòåðíûõ èãð ïîä Windows â XNA Game Studio Express” /// Àâòîð êíèãè: Ãîðíàêîâ Ñ. Ã. /// Ãëàâà 22 /// Ïðîåêò: Menu /// Êëàññ: MenuGame
344
Создаем меню игры
Последние штрихи
/// Ìåíþ /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace Menu { class MenuGame {
В отличие от заставок в меню игры мы загружаем меньшее по размеру фут( больное поле, в котором команды меню вырезаны средствами Photoshop.
Matrix Matrix Matrix Matrix
for (int i = 0; position.Length > i; i++) { position[i].X = rand.Next(200, x - 250); position[i].Y = rand.Next(100, y - 60); } for (int i = 0; ball.Length > i; i++) { ball[i] = new ModelClass(); }
Объект cursor представляет в меню желтую прямоугольную текстуру или подложку меню размером 220 × 40 пикселей. Именно это изображение мы будем передвигать под командами меню, создавая эффект закрашивания команды жел( тым цветом.
ball[0].Load(content, “Content\\Models\\Soccerball”); ball[0].position = new Vector3(-30, 40, -30); ball[1].Load(content, “Content\\Models\\SoccerballGreen”); ball[1].position = new Vector3(50, 30, -50); ball[2].Load(content, “Content\\Models\\SoccerballRed”); ball[2].position = new Vector3(-50, -30, 40); aspectRatio = (float)x / (float)y;
public Texture2D cursor; public Texture2D enter;
public public public public
Texture2D g1; Texture2D g2; Texture2D[] hole = new Texture2D[30]; Vector2[] position = new Vector2[30];
Random rand = new Random(); private ModelClass[] ball = new ModelClass[3]; static float aspectRatio; static float FOV = MathHelper.PiOver4; static float nearClip = 1.0f; static float farClip = 1000.0f; float angle; Matrix view;
proj; world; rotationMatrixY; rotationMatrixZ;
/// <summary> /// Çàãðóçêà êîìïîíåíòîâ çàñòàâêè /// <summary> public void InitializeMenuGameScreen(ContentManager content, int x, int y) { screen = content.Load(“Content\\Textures\\menu”); title = content.Load(“Content\\Textures\\title”); enter = content.Load(“Content\\Textures\\enter”); cursor = content.Load(“Content\\Textures\\cursorMenu”); g1 = content.Load(“Content\\Textures\\g1”); g2 = content.Load(“Content\\Textures\\g2”); for (int i = 0; hole.Length > i; i++) { hole[i] = content.Load(“Content\\Textures\\hole”); }
public Texture2D screen; public Texture2D title;
Два объекта – g1 и g2 – представляют в игре изображение двух красивых на( рисованных девушек, которые располагаются по бокам меню.
345
}
В метод DrawScreen() добавляется еще один новый параметр state. На базе переданного значения этого параметра в методе DrawScreen() посредством оператора switch мы будем выбирать, в каком месте рисовать желтую подложку для меню. /// <summary> /// Ðèñóåì çàñòàâêó /// <summary> public void DrawScreen(SpriteBatch spriteBatch, GraphicsDeviceManager graphics, int x, int y, GameTime gameTime, int state) { graphics.GraphicsDevice.Clear(Color.DarkGreen); spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
346
Смена игровых состояний в классе Game1
Последние штрихи break;
spriteBatch.Draw(title, new Vector2(x / 2 - title.Width / 2, 30), Color.White); spriteBatch.Draw(enter, new Vector2(x / 2 - enter.Width / 2, y - 30enter.Height), Color.White);
case 5: spriteBatch.Draw(cursor, new Vector2(x / 2 - cursor.Width / 2, y / 2 + 80), Color.White); break; }
Выбираем для девушек позиции на экране монитора. Первая девушка рисует( ся с левой стороны дисплея, а вторая – с правой. Очень важно не задавать жесткие числовые значения для всех элементов меню, поскольку разрешение экранов у разных пользователей будет, естественно, разным. Всегда пользуйтесь для сво( их вычислений точкой в центре экрана, углами и четырьмя сторонами монитора. В этом случае компоновка графики в игре будет одинаково выглядеть на всех мо( ниторах.
Если вы не совсем разобрались с расчетами позиций подложки на экране, то возьмите обычный листок бумаги и карандаш. Нарисуйте виртуальный эк) ран монитора и проследите за изменениями позиций желтой текстуры. spriteBatch.Draw(screen, new Vector2(x / 2 - screen.Width / 2, y / 2 – screen.Height / 2), Color.White); for (int i = 0; hole.Length > i; i++) { spriteBatch.Draw(hole[i], position[i], Color.White); } spriteBatch.End();
spriteBatch.Draw(g1, new Vector2(0, y / 2 - g1.Height/2), Color.White); spriteBatch.Draw(g2, new Vector2(x - g2.Width, y / 2 –g2.Height/2), Color.White);
В меню имеются пять команд, соответственно у нас есть пять состояний, или пять вариантов выбора позиций для подложки меню. В классе Game1 с помощью клавиатуры и клавиш «вверх» и «вниз» в игре мы будем переходить по командам меню, а значит, выбирать одну из команд. Каждой команде меню сверху вниз на( значена своя цифра, поэтому выбор одной из команд меню будет соответствовать порядковому числовому значению. Метод DrawScreen() вызывается на каждой итерации игрового цикла, и со( стояние переменной state будет всегда «свежим». А уже на базе полученного значения оператор switch выберет, какой из блоков исходного кода должен ра( ботать в данный момент и в точности в каком месте дисплея должна рисоваться желтая подложка меню. switch (state) { case 1: spriteBatch.Draw(cursor, new Vector2(x / 2 - cursor.Width / 2, y / 2 – 115), Color.White); break; case 2: spriteBatch.Draw(cursor, new Vector2(x / 2 - cursor.Width / 2, y / 2 – 65), Color.White); break; case 3: spriteBatch.Draw(cursor, new Vector2(x / 2 - cursor.Width / 2, y / 2 – 15), Color.White); break; case 4: spriteBatch.Draw(cursor, new Vector2(x / 2 - cursor.Width / 2, y / 2 + 30), Color.White);
347
graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 260.0f), Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); angle += (float)(gameTime.ElapsedGameTime.TotalSeconds * 1.0f); rotationMatrixY = Matrix.CreateRotationY((float)angle); rotationMatrixZ = Matrix.CreateRotationZ((float)angle); for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world * rotationMatrixY * rotationMatrixZ, view, proj); } } } }
22.5. Смена игровых состояний в классе Game1 Для смены игровых состояний в классе Game1 мы применяем структуру CurentGameState и структурную переменную gameState. До этого момента эта переменная нас «выкидывала» сразу в игровой процесс. Теперь пришла пора изменить ее значение, которое теперь будет запускать титульную заставку. CurentGameState gameState = CurentGameState.SplashScreen;
С титульной заставки по нажатии клавиши Enter и смены значения перемен( ной gameState с CurentGameState.SplashScreen на значение CurentGameState.GameScree, в игре включится показ меню. В дальнейшем для смены состояний игры нам достаточно присвоить переменной gameState необходимое
348
Смена игровых состояний в классе Game1
Последние штрихи
значение, и мы выберем то или иное игровое состояние. Теперь поговорим о меха( низме смены состояний и показа заставок.
22.5.1. Создаем объекты для меню и заставок Для вывода на экран меню и заставок нам необходимо в классе Game1 объявить объекты классов SplashScreen, MenuGame, HelpScreen, AboutScreen и BooksScreen. private private private private private
SplashScreen splash; MenuGame menuGame; HelpScreen help; AboutScreen about; BooksScreen books;
А затем и создать эти объекты. splash = new SplashScreen(); menuGame = new MenuGame(); help = new HelpScreen(); about = new AboutScreen(); books = new BooksScreen();
После чего в методе Initialize() класса Game1 мы можем спокойно вызы( вать соответствующие методы классов SplashScreen, MenuGame, HelpScreen, AboutScreen и BooksScreen для загрузки в игру графики. splash.InitializeSplashScreen(content, screenWidth, screenHeight); menuGame.InitializeMenuGameScreen(content, screenWidth, screenHeight); help.InitializeHelpScreen(content, screenWidth, screenHeight); about.InitializeAboutScreen(content, screenWidth, screenHeight); books.InitializeBooksScreen(content, screenWidth, screenHeight);
22.5.2. Обновляем состояние игры в методе Update() Обработка нажатия клавиши Enter в меню и титульной заставке, а также обработ( ка клавиш перехода по меню – это основная и самая главная проблема, которую нам нужно решить. Дело в том, что любое нажатие клавиши на клавиатуре и ее неотпускание ведут к цикличному выполнению определенного условия, назна( ченного на эту клавишу. Например, посмотрите на блок кода, приведенный ниже. if (keyboardState.IsKeyDown(Keys.Down)) { state += 1; }
Если создать такое условие, то нажатие клавиши «вниз» (Keys.Down) и ее неот( пускание ведет к постоянному увеличению переменной state. И даже если вы на(
349
жмете клавишу и моментально отпустите ее, совсем не факт, что вы отпустили эту клавишу в тот момент, когда переменная увеличится всего на одну единицу. Любая задержка нажатия клавиши «вниз» ведет к увеличению переменной state, и просле( дить значение этой переменной и тем более управлять ею просто не возможно. В связи с этим необходимо создать условие, которое вне зависимости от дли( тельности нажатия и удерживания (в данном случае) клавиши «вниз» увеличива( ло переменную state ровно на одну единицу. Именно для этих целей в XNA име( ется метод IsKeyUp(), который на системном уровне регулирует или следит за состоянием нажатой и отпущенной клавиши. Чтобы приведенный выше код работал корректно, а переменная state изме( нялась ровно на одну единицу за одно нажатие клавиши «вниз», необходимо ис( пользовать следующую конструкцию кода. bool downReleased = true; … protected override void Update(GameTime gameTime) { … if(keyboardState.IsKeyDown(Keys.Down) == true && downReleased == true) { state += 1; } … downReleased = keyboardState.IsKeyUp(Keys.Down); base.Update(gameTime); }
В этой версии кода мы имеем дополнительную булеву переменную downReleased, значение которой будет всегда равно true, если клавиша «вниз» не нажата. Как только клавиша «вниз» нажата и не отпущена, значение перемен( ной моментально изменяется на false и блок кода, следующий за конструкцией операторов if/else, не выполняется. Все очень просто. Такой подход в отслежи( вании состояния дополнительной булевой переменной позволяет создать идеаль( ную систему проверки отпускания любой нажатой клавиши. Для работы с нашим меню и заставками нам необходимо в исходном коде класса Game1 создать три булевых переменных для отслеживания нажатий на клавиши Enter, Down и Up. bool downReleased = true; bool upReleased = true; bool enterReleased = true;
А уже в конце метода Update() класса Game1 мы будем следить за отпуска( нием нажатых клавиш следующим образом. downReleased = keyboardState.IsKeyUp(Keys.Down); upReleased = keyboardState.IsKeyUp(Keys.Up); enterReleased = keyboardState.IsKeyUp(Keys.Enter);
350
Последние штрихи
В метод Update() первым в работу включается блок кода, обрабатывающий нажатие клавиши Enter в момент показа титульной заставки игры. protected override void Update(GameTime gameTime) { keyboardState = Keyboard.GetState(); switch(gameState) { case CurentGameState.SplashScreen: { if (keyboardState.IsKeyDown(Keys.Enter)) gameState = CurentGameState.MenuScreen; break; } case CurentGameState.MenuScreen: {
В момент запуска игры значение переменной state у нас равно единице. public int state = 1;
Каждое нажатие на клавиатуре клавиш «вверх» или «вниз» автоматически будет изменять значение переменной state. Если нажата клавиша с командой вниз (Keys.Down), то значение переменной state увеличивается на единицу. if (keyboardState.IsKeyDown(Keys.Down) == true && downReleased == true) { state += 1;
Для того чтобы значение переменной не было больше пяти или не выходило за пределы пяти команд, создана следующая проверка условия. if (state > 5) state = 1; }
В этом случае если state станет больше пяти, то ей будет присвоено значе( ние, равное единице. То есть как только позиция подложки на экране станет боль( ше позиции, отведенной для пятой и последней команды меню, желтая текстура получит позицию, равную первой команде. Таким образом, желтая подложка меню будет перемещаться по командам меню циклично сверху вниз, а в следую( щем блоке кода – и снизу вверх. if (keyboardState.IsKeyDown(Keys.Up) == true && upReleased == true) { state -= 1; if (state < 1) state = 5; }
Смена игровых состояний в классе Game1
351
В этой конструкции кода мы создаем обработку условия для нажатия клави( ши с командой вверх (Keys.Up). Механизм изменения значения переменной state одинаков, как и в случае с командой вниз, с той лишь разницей, что измене( ние state ведется в обратную сторону. В следующем блоке кода происходит обработка нажатия клавиши Enter. В за( висимости от текущего значения переменной state (которая меняется по нажа( тии клавиш «вверх» и «вниз» и соответствует одной из выбранных команд меню) происходит изменение значения gameState, а значит, и выбор нового игрового состояния. if (keyboardState.IsKeyDown(Keys.Enter) && enterReleased true && state == 1) { gameState = CurentGameState.GameScreen; } if (keyboardState.IsKeyDown(Keys.Enter) && enterReleased true && state == 2) { gameState = CurentGameState.HelpScreen; } if (keyboardState.IsKeyDown(Keys.Enter) && enterReleased true && state == 3) { gameState = CurentGameState.AboutScreen; } if (keyboardState.IsKeyDown(Keys.Enter) && enterReleased true && state == 4) { gameState = CurentGameState.BooksScreen; } if (keyboardState.IsKeyDown(Keys.Enter) && enterReleased true && state == 5) { this.Exit(); } break; }
==
==
==
==
==
Для обработки нажатия клавиши Enter можно также использовать более упрощенную схему исходного кода, например вот такую: if (keyboardState.IsKeyDown(Keys.Enter)) { if (enterReleased == true && state == 1) { gameState = CurentGameState.GameScreen; } if (enterReleased == true && state == 2) { gameState = CurentGameState.HelpScreen; }
352
Смена игровых состояний в классе Game1
Последние штрихи
if (enterReleased == true && state == 3) { gameState = CurentGameState.AboutScreen; } if (enterReleased == true && state == 4) { gameState = CurentGameState.BooksScreen; } if (enterReleased == true && state == 5) { this.Exit(); } }
Далее в исходном коде класса Game1 и метода Update() в зависимости от значения структурной переменной gameState происходит обновление состоя( ния игры. case CurentGameState.HelpScreen: { if (keyboardState.IsKeyDown(Keys.Escape)) gameState = CurentGameState.MenuScreen; break; } case CurentGameState.AboutScreen: { if (keyboardState.IsKeyDown(Keys.Escape)) gameState = CurentGameState.MenuScreen; break; } case CurentGameState.BooksScreen: { if (keyboardState.IsKeyDown(Keys.Escape)) gameState = CurentGameState.MenuScreen; break; } case CurentGameState.GameScreen: { if (keyboardState.IsKeyDown(Keys.Escape)) gameState = CurentGameState.MenuScreen; MoveBall(); MouseClick(); break; } case CurentGameState.GameOverScreen: { break;
353
} case CurentGameState.VictoryScreen: { break; } } base.Update(gameTime); }
22.5.3. Обновляем графику в методе Draw() В методе Draw() класса Game1 на основе значения структурной переменной gameState происходит рисование того или иного экрана. Изначально при запус( ке игры переменная gameState равна CurentGameState.SplashScreen, и это значит, что первой на экране рисуется титульная заставка. После нажатия клавиши Enter, обработка нажатия которой происходит в только что рассмотренном методе Update(), значение переменной меняется на CurentGameState.MenuScreen. После чего на экране рисуется меню игры, в котором в зависимости от выбранной команды (вверх или вниз) и нажатия кла( виши Enter происходит изменение состояния всей игры. protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); switch(gameState) { case CurentGameState.SplashScreen: { splash.DrawScreen(spriteBatch, graphics, screenWidth, screenHeight); break; }
В меню игры мы передаем переменную state. В зависимости от ее текущего значения для желтой подложки меню выбирается соответствующая позиция на экране монитора, которая указывает пользователю на активную в данный момент команду меню. case CurentGameState.MenuScreen: { menuGame.DrawScreen(spriteBatch, graphics, screenWidth, screenHeight, gameTime, state); break; } case CurentGameState.HelpScreen: {
354
Добавим в игру логику
Последние штрихи
help.DrawScreen(spriteBatch, graphics, screenWidth, screenHeight, gameTime); break; } case CurentGameState.AboutScreen: { about.DrawScreen(spriteBatch, graphics, screenWidth, screenHeight, gameTime); break; } case CurentGameState.BooksScreen: { books.DrawScreen(spriteBatch, graphics, screenWidth, screenHeight, gameTime); break; } case CurentGameState.GameScreen: { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background, new Vector2(0, -50), Color.White); spriteBatch.End(); graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; view = Matrix.CreateLookAt(camera, Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world, view, proj); } world = Matrix.CreateTranslation(stadium.position); stadium.DrawModel(world, view, proj); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); cursor.DrawSprite(spriteBatch); spriteBatch.End(); break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break;
355
} } base.Draw(gameTime); }
22.6. Добавим в игру логику Закончив проект Menu, мы переходим к краткому обзору финальной версии игры. Полный исходный код игры «Футбольный стрелок» вы найдете на ком( пакт(диске в папке Code\Final3d\Football_arrows. Смысл всей игры заключается в том, чтобы не дать мячикам упасть на футболь( ное поле, но стрелять по мячам можно до бесконечности. Поэтому ключом для окончания уровня будут служить двадцать точных попаданий в каждый мяч. То есть игроку на одном уровне необходимо попасть в каждый мяч как минимум двад( цать раз, и при этом ни один из мячей за это время не должен коснуться земли. Если мяч касается земли, то игра останавливается и пользователю будет пред( ложено пройти текущий уровень заново. Самое интересное заключается в том, что по истечении двадцати попаданий в один мяч этот самый мяч по(прежнему будет падать вниз, и его по(прежнему нужно будет поддерживать на весу. Таким образом, все мячики остаются активными в сцене на протяжении уровня, что при( даст игре динамику. Подсчет попаданий в каждый мячик мы бу( дем вести на табло (рис. 22.6). Это табло разме( щается в левом верхнем углу экрана с небольшим отступом от его краев (по 20 пикселей для каж( дой оси). В табло нарисованы три мяча, которые соответствуют по цвету мячам, используемым в игре. Каждое попадание в мяч увеличит счетчик на единицу, и над каждым мячом будет рисовать( Рис. 22.6. Табло игры ся текущее количество попаданий в цель. По до( стижении цифры двадцать для одного или более мячей на табло дополнительно будет выводиться крестик, который рисуется как раз поверх одного из мячей (рис. 22.6). В результате игрок будет знать, какой из мячей ему нужно подбивать меньше и на каких мячах необходимо сосредоточить свое внимание. Дополнительно в нижней части выводится текущий уровень игры. На каждом новом уровне мы будем увеличивать скорость падения мячей. Для контроля окончания игры по причине выигрыша уровня используется следующая конструкция кода. if (score0 >= 20 && score1 >= 20 && score2 >= 20) { gameState = CurentGameState.VictoryScreen; level += 1; if (level > 8) level = 1; }
356
Добавим в игру логику
Последние штрихи
Как видите, у нас в коде добавляется новый экран VictoryScreen, представлен( ный классом с одноименным названием. Исходный код этого класса фактически идентичен заставкам и меню игры, с той лишь разницей, что на экране присут( ствуют всего три команды: Продолжить, Меню и Выход. Техника обработки этих команд идентична схеме работы меню игры. Этот код нужно вызвать в основном цикле игры, чтобы он обновлялся на каждой итерации. Полный исходный код окончательной версии игры вы найдете на компакт(диске в папке Code\Final3d\ Football_arrows. Если игрок упустит мяч на поле, то в работу включается уже другая конструк( ция кода.
// ïðîâåðÿåì ñîñòîÿíèå ìûøè if(mouseState.LeftButton == ButtonState.Pressed && oldMouseState.LeftButton == ButtonState.Released) { Sound.PlayCue(soundList.Shot); Ray pickRay = GetPickRay(); Nullable result0 = pickRay.Intersects(bb[0]); if(result0.HasValue == true) { ball[0].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, ball[0].position.Y = rand.Next(0, 80); ball[0].position.Z = rand.Next(-(rand.Next(0, 50)), rand.Next(0, score0 += 1; } Nullable result1 = pickRay.Intersects(bb[1]); if (result1.HasValue == true) { ball[1].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, ball[1].position.Y = rand.Next(0, 80); ball[1].position.Z = rand.Next(-(rand.Next(0, 50)), rand.Next(0, score1 += 1; } Nullable result2 = pickRay.Intersects(bb[2]); if (result2.HasValue == true) { ball[2].position.X = rand.Next(-(rand.Next(0, 80)), rand.Next(0, ball[2].position.Y = rand.Next(0, 80); ball[2].position.Z = rand.Next(-(rand.Next(0, 50)), rand.Next(0, score2 += 1; } }
if(ball[i].position.Y < -32) { ball[i].position.Y = ball[i].position.Y; gameState = CurentGameState.GameOverScreen; Sound.PlayCue(soundList.Over); }
Здесь мы имеем также новую заставку GameOverScreen и новый класс GameOverScreen. Этот класс однотипен и похож на все заставки и меню игры, разобраться самостоятельно с ним вам не составит труда. Единственное, на что я еще хочу обратить ваше внимание, – так это на новый механизм обработки на( жатия кнопки мыши в классе Game1. Дело в том, что у мыши ситуация с длительностью удержания нажатой кноп( ки абсолютно аналогична рассмотренной нами ситуации с нажатием клавиш на клавиатуре. В связи с этим нам необходимо добавить в код идентификатор отпус( кания нажатой кнопки мыши. Специального метода у мыши, как у клавиатуры, для этих целей нет, поэтому мы прибегаем к небольшой хитрости. Сначала объявляем в глобальных переменных новый объект, следящий за со( стоянием мыши.
357
80)); 150));
80)); 150));
80)); 150));
Здесь ситуация аналогична работе с клавиатурой, но, как видите, в своем спе( цифичном ракурсе. oldMouseState = mouseState;
MouseState oldMouseState; if (score0 >= 20 && score1 >= 20 && score2 >= 20) { gameState = CurentGameState.VictoryScreen; level += 1; if (level > 8) level = 1; }
А затем в методе MouseClick() проверяем состояние нажатой и отпущенной кнопки мыши: void MouseClick() { mouseState = Mouse.GetState(); cursor.spritePosition.X = mouseState.X; cursor.spritePosition.Y = mouseState.Y; for (int i = 0; bb.Length > i; i++) { bb[i].Center = ball[i].position; bb[i].Radius = ball[i].radius; }
}
Пожалуй, это все значащие нововведения в игре. В финальную версию игры «Футбольный стрелок» еще было добавлено звуковое оформление, которое осно( вано на эффектах из Spacewar Windows Starter Kit, а также код по выводу на экран табло, текста и логика игры. После этого был сформирован инсталляционный па( кет, который вы найдете в папке Final Game\3D. Все эти дополнительные дей( ствия мы изучали еще во второй части главы.
Приложение 1
Приложение 2
Обзор компакт-диска
Интернет-ресурсы
Code – в этой папке находятся исходные коды примеров рассмотренных за время изучения книги. Каждая глава имеет свою папку с одноименным на( званием и содержит вложенную папку с названием изучаемого в этой главе проекта. VCS – здесь вы найдете мастер установки инструментария Visual C# Express через сеть Интернет. Bonus – различный методический материал, связанный с разработкой игр для системы Windows.
В этом приложении собраны интересные ссылки на основные ресурсы Интернета, посвященные так или иначе разработке игр, консольной приставке Xbox 360 и от( части графике. Подборка адресов в Интернете разделена на русскоязычные и анг( лоязычные разделы.
2.1. Русскоязычные ресурсы Подборка русскоязычных ресурсов по теме этой книги. К сожалению, сайтов со схожей тематикой пока не так много, но все, что на данный момент нашлось, есть в предлагаемом перечне.
2.1.1. http://www.xnadev.ru Это первый и пока единственный русскоязычный ресурс, посвященный созданию игр, базирующихся на платформе XNA Framework. Сайт имеет большое коли( чество примеров и статей на различные темы, а также тематический форум, в ко( тором можно пообщаться и даже попросить помощи у своих единомышленников (рис. П.2.1).
2.1.2. http://www.gamedev.ru Старейший из старейших ресурсов, посвященный программированию игр для различных платформ. Кроме огромного количества всевозможных статей и при( меров, сайт обладает особой гордостью под названием: БОЛЬШОЙ ФОРУМ (именно в такой записи) с порядочными и всегда желающим прийти на помощь жителями (рис. П.2.2).
2.1.3. http://dft.ru Этот огромный ресурс нацелен на более профессиональную аудиторию разработ( чиков и содержит немалое количество статей и примеров по созданию игр. На сайте имеется большой тематический форум по всем направлениям игровой индустрии (рис. П.2.3).
360
Русскоязычные ресурсы
ИнтернетJресурсы
Рис. П.2.1
361
Рис. П.2.3
2.1.4. http://www.kriconf.ru Сайт всем известной конференции разработчиков игр, которая проводится каж( дой весной в Москве в гостинице «Космос» (рис. П.2.4). Отличительной особен( ностью сайта является возможность скачать с него все лекции, статьи и выступле( ния, которые имели место на прошедших конференциях. Если у вас нет возможности посетить конференцию on(line, то посетите ее off(line прямо со свое( го компьютера!
2.1.5. http://www.codeplex.com Ресурс посвящен разработке игр на платформе XNA Framework. Сайт создан со( отечественником, но имеет англоязычный интерфейс. На сайте можно найти ис( ходные коды нескольких игровых движков (рис. П.2.5).
2.1.6. http://www.gamedev.kz
Рис. П.2.2
Казахские братья также имеют свой сайт, специализирующийся на разработке игр для различных платформ, в том числе XNA. На сайте имеется некоторое коли( чество статей, примеров, а также форум, где можно пообщаться на различные темы, связанные с разработкой игр (рис. П.2.6).
362
Русскоязычные ресурсы
ИнтернетJресурсы
Рис. П.2.4
363
Рис. П.2.6
2.1.7. http://www.render.ru Направленность сайта – это графика и 3D(моделирование (рис. П.2.7). На ресур( се присутствует огромная подборка статей и примеров, большой форум, а также вы можете скачать абсолютно бесплатно ежемесячный журнал по графике в фор( мате PDF, выпускаемый этим сайтом!
2.1.8. http://www.xboxrussia.ru Сайт для пользователей приставки Xbox 360 (рис. П.2.8). Гордость ресурса – это огромный форум с добрыми людьми и организаторами сайта, которые рады будут ответить на все ваши вопросы. Кроме того, на сайте имеется множество различ( ных инструкций и руководств по настройке Xbox Live, соединения компьютера и приставки, обзоры игр, частные впечатления о прохождении тех же игр и многое другое. Но этот форум для простых пользователей, поэтому не стоит задавать вопросы по программированию игр – на этом сайте люди отдыхают и общаются на различные темы.
2.1.9. http://www.xboxland.net Рис. П.2.5
Еще один ресурс, посвященный использованию приставки Xbox 360 в простых бытовых условиях (рис. П.2.9). На сайте имеются весьма интересные и неорди(
364
Англоязычные ресурсы
ИнтернетJресурсы
Рис. П.2.7
365
Рис. П.2.9
нарные статьи, например о том, как подсоединить жесткий диск приставки Xbox 360 к компьютеру или как сделать распиновку VGA кабеля в домашних ус( ловиях и т. д. Присутствуют обзоры игр и их скриншоты, большой форум и еще ряд, может быть, кому(то полезных услуг.
2.1.10. http://www.dmk]press.ru Сайт издательства ДМК Пресс как всегда предоставит максимум полезной инфор( мации для читателей книг (рис. П.2.10). На сайте вы сможете подобрать интерес( ную вам книгу, узнать о новинках и, конечно, заказать книгу с доставкой на дом.
2.1.11. http://www.gornakov.ru Это официальный сайт автора книги, здесь вы можете познакомиться с полной библиографией писателя, узнать о новинках и анонсах, а также поучаствовать в различных игровых проектах (рис. П.2.11).
2.2. Англоязычные ресурсы Рис. П.2.8
В этом разделе вы найдете ссылки на англоязычные ресурсы, посвященные теме про( граммирования игр в XNA для консольной приставки Xbox 360 и системы Windows.
366
Англоязычные ресурсы
ИнтернетJресурсы
367
2.2.1. http://www.xbox360.com Домашняя англоязычная страница консольной приставки Xbox 360 (рис. П.2.12). Здесь вы сможете создать on(line свой профиль, познакомиться с игровыми новинками, скачать видеоролики игр, прочитать на английском языке различ( ные инструкции и руководства, посвященные как приставке Xbox, так и сервису Xbox Live.
2.2.2. http://creators.xna.com Страница клуба разработчиков игр для консольной приставки Xbox 360 и систе( мы Windows (рис. П.2.13). На этом сайте имеется большое количество исходных кодов и примеров. Дополнительно на сайте можно скачать все имеющиеся на дан( ный момент Starter Kits, а также получить любую информацию, связанную с раз( работкой игр в XNA Game Studio Express. Это один из первых ресурсов, рекомен( дуемых для посещения!
2.2.3. http://www.xna.com Рис. П.2.10
Рис. П.2.11
Домашняя страница XNA Framework с большим обилием ссылок и документации (рис. П.2.14).
Рис. П.2.12
368
Англоязычные ресурсы
ИнтернетJресурсы
369
2.2.4. http://www.microsoft.com/xna Еще одна тематическая страница корпорации Microsoft, посвященная XNA Fra( mework (рис. П.2.15).
2.2.5. http://www.msdn.com/xna Домашняя страница Microsoft, посвященная разработке игр с использованием DirectX и XNA Framework (рис. П.2.16). На этом сайте всегда можно найти послед( ние версии SDK DirectX, студии разработки игр и другие инструментальные средства компании Microsoft.
2.2.6. http://blogs.msdn.com/xna Страница команды разработчиков XNA Game Studio Express (рис. П.2.17). С этой страницы по соответствующим ссылкам вы можете перейти на частные страницы каждого разработчика, на которых вы можете найти исходные коды, примеры, связанные со студией разработки игр.
2.2.7. http://forum.microsoft.com Рис. П.2.13
Большой англоязычный форум по всем продуктам и технологиям компании Microsoft (рис. П.2.18). В том числе имеется большой раздел о студии XNA Game
Рис. П.2.14
Рис. П.2.15
370
Англоязычные ресурсы
ИнтернетJресурсы
Рис. П.2.16
371
Рис. П.2.18
Studio Express. Здесь присутствует огромное количество участников форума со всего мира!
2.2.8. http://www.ziggyware.com Отличный англоязычный сайт с огромным количеством примеров, готовых про( грамм, исходных кодов, уроков, статей и ссылок по XNA Game Studio Express (рис. П.2.19). Фаворит из большого количества подобных сайтов, рекомендую к посещению! Обязательно!
2.2.9. http://www.xna101.net Уроки по программированию игр в XNA Game Studio Express. Здесь на базе одной трехмерной игры разбираются возможности XNA (рис. П.2.20). Для бесплатного скачивания доступны все рассмотренные исходные коды.
2.2.10. http://xbox360homebrew.com
Рис. П.2.17
Сайт организован командой единомышленников и помогает людям научиться про( граммировать в XNA Game Studio Express для приставки Xbox 360 (рис. П.2.21). На сайте имеется большая подборка статей и исходных кодов для бесплатного ис( пользования.
372
Англоязычные ресурсы
ИнтернетJресурсы
Рис. П.2.19
373
Рис. П.2.21
2.2.11. http://www.garagegames.com Коммерческая организация, предлагающая к продаже свой 2D( и 3D(движок, ориентированный для работы в XNA Game Studio Express (рис. П.2.22). Стои( мость лицензии на движок небольшая.
2.2.12. http://learnxna.com Еще один сайт, ориентированный на начинающих программистов (рис. П.2.23). На сайте имеется большая подборка видеоуроков и ссылок на аналогичные ресур( сы по XNA.
2.2.13. http://www.turbosquid.com/xna В книге не раз уже упоминался этот ресурс (рис. П.2.24). Раздел XNA этого сайта содержит набор моделей в формате FBX, а также другие графические элементы, необходимые для создания домашних, и не только, игр.
2.2.14. http://www.xnaresources.com Рис. П.2.20
Небольшой сайт, посвященный теме этой книги (рис. П.2.25). Содержит уроки, исходные коды и множество ссылок в Интернете на аналогичные ресурсы.
374
Англоязычные ресурсы
ИнтернетJресурсы
Рис. П.2.22
Рис. П.2.24
Рис. П.2.23
Рис. П.2.25
375
376
Англоязычные ресурсы
ИнтернетJресурсы
2.2.15. http://www.xnadevelopment.com Тоже небольшой и домашний сайт, освещающий тему программирования игр в XNA Game Studio Express (рис. П.2.26). На сайте присутствуют уроки и несколько статей по разработке игр.
2.2.16. http://xnamagic.com Первый трехмерный коммерческий движок, созданный специально для XNA Framework (рис. П.2.27). На данный момент доступна для бесплатного скачива( ния бета(версия движка.
2.2.17. http://blog.tehone.com Англоязычный блог, посвященный теме создания игр в XNA Game Studio Express (рис. П.2.28).
2.2.18. http://www.student.dtu.dk Трехмерный движок для XNA Framework (рис. П.2.29). Рис. П.2.27
Рис. П.2.26
Рис. П.2.28
377
378
Англоязычные ресурсы
ИнтернетJресурсы
Рис. П.2.29
Рис. П.2.30
2.2.19. http://www.dhpoware.com Еще один сайт, направленный на разработку игр для различных платформ, в том числе XNA (рис. П.2.30).
2.2.20. http://www.nuclex.org Сайт с подборкой уроков, статей и исходных кодов по XNA (рис. П.2.31).
2.2.21. http://www.xnaportal.com Портал ссылок на сайты по темам XNA Framework и XNA Game Studio Express (рис. П.2.32).
2.2.22. http://www.xnatutorial.com Набор статей и уроков по XNA Game Studio Express (рис. П.2.33). На сайте вы также найдете большое количество ссылок в Интернете на аналогичные ресурсы.
Рис. П.2.31
379
380
ИнтернетJресурсы
Предметный указатель H HLSL, 263
M Managed DirectX, 21
S
Рис. П.2.32
Smart Install Maker, 236 компиляция установочного пакета, 247 страница Диалоги, 243 Интерфейс, 242 Информация, 236 Требования, 241 Файлы, 238 Ярлыки, 245 удаление программы, 246
V Visual C# Express запуск пректа, 47 компиляция, 47 ошибки, 48 распространение, 32 регистрация, 41 сборка проекта, 48 создание проекта, 45 сохранение проекта, 46 установка, 34
X
Рис. П.2.33
XACT создание проекта, 196 сохранение, 200 формат, 196 Xbox 360, 21 XNA Framework, 22
XNA Game Studio Express новости, 56 установка, 53 шаблоны, 58 XNAExtras, 152
Z Z(буфер, 104 3D, 268
В Вершина, 255
Д Движение под углом, 110 Добавить изображение, 80
З Задний буфер, 73
И Изображение анимированное, 93 неанимированное, 93 последовательность вывода, 108 Интерполяция, 268 Искусственный интеллект, 109 Источник света параллельный, 262 прожекторный, 262 точечный, 262
К Класс AudioEngine, 204 BitmapFont, 153 BoundingSphere, 308 Game1, 90
382
Программирование компьютерных игр под Windows в XNA Game Studio Express
GameOverScreen, 356 GraphicsAdapter, 270 HelpScreen, 338 Menu, 168 MenuGame, 341 ModelClass, 290 Program, 63 Random, 115 Sound, 201 SplashScreen, 335 Sprite, 87 SpriteBatch, 82 Texture2D, 82, 334 WaveBank, 204 Конвейер графический, 267 Координаты, 255
М Матрица, 257 вращения, 259 масштабирования, 260 мировая, 259 проекции, 261 трансляции, 260 Меню, 165, 330 Метод Begin(), 84 Clear(), 71 Collisions(), 144 CreateLookAt(), 284 CreateRotationY(), 337 CuePlay(), 206 Draw(), 70, 83, 175 DrawAnimationSprite(), 96 DrawMenu(), 171 DrawModel(), 283 DrawString(), 158 End(), 84 Exit(), 75 GetPickRay(), 307 Initialize(), 69, 83, 119, 269 InitializeSplashScreen(), 335 Intersects(), 146 IsKeyDown(), 127
IsKeyUp(), 349 Keyboard.GetState(), 75 LevelSelect(), 222 Load(), 82 LoadGraphicsContent(), 82, 92, 118 Main(), 65 Mouse.GetState(), 185 MouseClick(), 299, 356 MoveBall(), 299 MovePlatform(), 126 MoveSprite(), 115, 119 NewGame(), 172 Next(), 115 PlayCue(), 204 rand.Next(), 297 Run(), 65 Sound.Initialize(), 212 Sound.PlayCue(), 213 Sound.Update(), 212 Stop(), 205 UnloadGraphicsContent(), 69 Update(), 70, 165, 173 UpdateFrame(), 120 UpdateMouse(), 184 Модель, 256 загрузка, 279
Н Нормаль, 262
П Пауза, 131 Переменная angle, 337 aspectRatio, 281, 283, 336 background, 323 camera, 317 cursorState, 167 downReleased, 349 endScore, 216 farClip, 281 FOV, 281 gameState, 219, 271, 347 level, 219
Предметный указатель levelState, 219 nearClip, 281 positionModel, 282 radius, 305 speed, 295 tempLevel, 219 totalScore, 216 Подсчет очков, 149 Полигон, 255 Проект иконка, 63 имя, 61 создание, 61 структура, 62 Пространство имен, 65
Р Растеризация, 268
С Система координат, 78 3D, 253
Скорость, 110 Спрайт, 78 Столкновение, 136 Структура BoundingBox, 136 CurentGameState, 347 Matrix, 280 MouseState, 183 soundList, 205 Сцена, 261
У Уровни абстракции, 20 XNA Framework, 23
Ф Фон, 103, 321
Ш Шейдер, 263 Шрифт, 154
383
Книги издательства «ДМК Пресс» можно заказать в торгово(изда( тельском холдинге «АЛЬЯНС(КНИГА» наложенным платежом, вы( слав открытку или письмо по почтовому адресу: 123242, Москва, а/я 20 или по электронному адресу: [email protected]. При оформлении заказа следует указать адрес (полностью), по ко( торому должны быть высланы книги; фамилию, имя и отчество полу( чателя. Желательно также указать свой телефон и электронный адрес. Эти книги вы можете заказать и в Internet(магазине: www.alians kniga.ru. Оптовые закупки: тел. (495) 2589194, 2589195; электронный ад( рес books@alianskniga.ru.
Горнаков С. Г.
Программирование компьютерных игр под Windows в XNA Game Studio Express Главный редактор
Мовчан Д. А.
dm@dmk]press.ru
Литературный редактор Корректор Верстка Дизайн обложки
Бронер П. Е. Синяева Г. И. Чаннова А. А. Мовчан А. Г.
Подписано в печать ***.2007. Формат 70×100 1/16 . Гарнитура «Петербург». Печать офсетная. Усл. печ. л. ***. Тираж *000 экз. № Издательство ДМК Пресс. 123007, Москва, 1(й Силикатный пр(д, д. 14 Web(сайт издательства: www.dmk(press.ru Internet(магазин: www.abook.ru Электронный адрес издательства: books@dmk(press.ru