Прежде чем перейти к созданию трехмерной игры, нам стоит уделить внимание оптимизации ранее изученного механизма смены игровых состояний в приложении. Как вы помните, для смены игровых состояний в двухмерной игре мы использовали конструкцию управляющих операторов if/else. Теперь пришло время создать более действенный и профессиональный подход смены игровых состояний, основанный на использовании структур и структурных переменных.
16.1. Улучшаем смену игровых состояний
В чем смысл методики смены игровых состояний, основанных на структурных данных? Необходимо сформировать структуру данных, содержащую определенный набор переменных. Как вы знаете, структура – это совокупность переменных, предназначенных для хранения различной информации. Объявление структуры приводит к созданию набора данных, которые впоследствии может использовать любой объект структуры или структурная переменная. Создать структуру в нашем контексте можно следующим образом:
Как видите, названия переменных соответствуют определенным фазам игровых состояний. У нас эти переменные будут означать следующее: * SplashScreen – первая игровая заставка, появляющаяся при старте игры; * MenuScreen – меню игры; * Help – экран, объясняющий правила игры; * AboutScreen – экран с информацией об игре; * GameScreen – игровой процесс; * GameOverScreen – экран проигрыша игры; * VictoryScreen – экран выигрыша игры. Дополнительно можно ввести любое количество переменных для любого количества фаз игровых состояний. Далее при старте игры создается структурная переменная и инициализируется одним из значений структуры, например:
Это значит, что в данный момент структурная переменная gameState ассоциируется исключительно со значением SplashScreen. Затем в игровом цикле необходимо просто создать проверку значения переменной gameState и в соответствии с ее текущим значением выбрать определенные действия.
switch(gameState) { case CurentGameState.SplashScreen: { // показать заставку break; } case CurentGameState.MenuScreen: { // показать меню break; } case CurentGameState.HelpScreen: { // показать экран с правилами игры break; } case CurentGameState.AboutScreen: { // показать экран об игре break; } case CurentGameState.GameScreen: { // старт игры break; } case CurentGameState.GameOverScreen: { // конец игры break; } case CurentGameState.VictoryScreen: { // экран выигрыша игры break; } }
Элегантное и в то же время действенное решение смены игровых состояний. Этот подход мы будем использовать в нашей трехмерной игре. На компактдиске в папке Code\Chapter16\GameState находится новый проект под названием GameState. В этом проекте используется вышеописанная схема. Ниже в листинге 16.1 предложен исходный код класса Game1, иллюстрирующий работу смены игровых состояний на базе структуры CurentGameState. На данном этапе в исходном коде структурная переменная gameState представляет фазу игрового процесса. Так будет продолжаться до тех пор, пока мы недойдем до главы, рассказывающей о создании меню и других игровых заставок.
//========================================================================= /// /// Листинг 16.1 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Автор книги: Горнаков С. Г. /// Глава 16 /// Проект: GameState /// Класс: Game1 /// Смена игровых состояний /// //=========================================================================
#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, HelpScreen, AboutScreen, GameScreen, GameOverScreen, VictoryScreen } CurentGameState gameState = CurentGameState.GameScreen; GraphicsDeviceManager graphics; ContentManager content; int screenWidth, screenHeight; /// /// Конструктор /// public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; } /// /// Инициализация /// protected override void Initialize() { base.Initialize(); } /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { } } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.HelpScreen: { break; } case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { if(GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } } base.Update(gameTime); } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(new Color(159, 147, 207)); switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } } base.Draw(gameTime); } } }
Загружаем в игру модель
Если вы работаете сами на себя и не умеете создавать 3D-модели в графических редакторах, то вопрос выбора и поиска модели будет для вас одним из главных. Для простых и, скажем так, домашних игр трехмерная графика может быть какой угодно, но для игр, которые делаются на продажу, качество 3D-моделей – дело первостепенной важности. Вы можете покупать модели, можете найти единомышленника, готового сотрудничать на энтузиазме или близких к тому отношениях, или поискать бесплатные модели в Интернете. В использовании бесплатных моделей есть один важный нюанс. Не все модели в Интернете бесплатны, даже если на одном из сайтов об этом так говорят. Владельцы этих сайтов могут и не знать, что модели, предлагаемые для свободного скачивания, на самом деле далеко не бесплатны. В этом вопросе нужно соблюдать осторожность, а лучше связаться с автором графики и узнать точно, на каком типе лицензии распространяются его модели.
17.1. Рисуем модель на экране телевизора
В качестве основной модели для нашей трехмерной игры послужит футбольный мячик. Как вы помните из первой главы, идея заключается в том, чтобы постараться не упустить мячик или несколько мячей на поле, подбивая их все время курсором мыши. Модель футбольного мяча была взята с сайта Turbosquid.com. Это бесплатная модель без текстуры, но с материалом, сделана 3D-мастером, редставленным на сайте под именем RP3D (рис. 17.3). Модель распространяется в нескольких форматах, в том числе в формате 3DS. Для преобразования модели из формата 3DS в формат Х-файла применялась программа 3D Exploration. Но вы можете использовать для преобразования моделей из одного формата в другой любую удобную вам программу или утилиту. Таких бесплатных программ очень много в Интернете. Для более профессиональных модельеров советую скачать с сайта компании Autodesk плагин к программе 3ds Max версии 8 и ниже, который позволяет прямо из 3ds Max сохранять модель в формате FBX (рис. 17.4). Ниже в тексте дана прямая ссылка на ряд утилит для различных операционных систем. К сожалению, согласно лицензионному соглашению эти программы нельзя размещать на цифровых носителях и распространять с какими бы то ни было печатными изданиями. Все утилиты небольших размеров (от 1 до 2 Мбайт), поэтому скачать их самостоятельно вам не составит труда.
Рис. 17.3. Модель мячика, взятая с сайта
Рис. 17.4. Страница компании Autodesk
17.1.1. Механизм загрузки модели в игру
Алгоритм по загрузке модели в игру довольно простой. Первым делом необходимо явно загрузить в проект модель и все текстуры к этой модели, если таковые имеются. Название текстур и текстурные координаты для одной конкретной модели прописываются во внутренних свойствах модели. Поэтому некоторые текстуры могут быть жестко привязаны к папке с определенным названием. Здесь все зави- сит от модельера, который делал эту модель. Например, в Starter Kit Spacewar студии XNA Game Studio Express для всех моделей задан каталог Models, а для текстур этих моделей определена папка Textures. В соответствии с этим, если вы используете в своих проектах графику из Starter Kit Spacewar, модели и текстуры у вас должны располагаться в папках с точно такими же названиями. Если изменить базовое местонахождение моделей или текстур, то в момент компиляции программы студия разработки игр выдаст вам ошибку, из контекста которой будет следовать, что компилятор не нашел того или иного графического компонента. Определившись с расположением модели в проекте, нужно объявить и создать объект системного класса Model, затем загрузить в него модель из файла и нарисовать ее на экране. Сейчас давайте попрактикуемся и перейдем к рассмотрению исходного кода нового проекта LoadingModel, направленного на загрузку модели в игру и вывод ее на экран. Исходный код всего проекта находится на компакт-диске в папке Code\Chapter17\ LoadingModel. Отправной точкой для нового проекта у нас, как всегда, является предыдущий проект, поэтому возьмите последний исходный код класса Game1 и приступайте к его модификации. Именно в классе Game1 на этом этапе работ развернутся все действия. Начнем с того, что в области глобальных переменных класса Game1 объявим три матрицы.
Matrix world; Matrix view; Matrix proj;
Как видно из названий матриц, это те самые три матрицы (мировая, видовая и проекционная), которые используются для представления объектов в трехмерном пространстве. Дополнительно структура Matrix в XNA Framework имеет большой набор встроенных методов, с помощью которых можно выполнить любые матричные преобразования. Далее в области глобальных переменных класса Game1 создаются четыре новые переменные.
Все четыре переменные принимают участие в матричных расчетах, и это часть давно отлаженного механизма, передающегося по наследству от старых версий DirectX. Первая переменная aspectRatio в исходном коде представляет так называемый коэффициент сжатия, на базе которого ведется расчет перспективы модели для правильного соотношения геометрических размеров в пространстве. Следующая переменная FOV призвана определять поле зрения. Здесь типичное значение равно MathHelper.PiOver4. Две оставшиеся переменные nearClip и farClip позволяют задать соответственно переднюю и заднюю области отсечения и участвуют в проекционных расчетах. Затем в исходном коде у нас следует объявление объекта model класса Model, который будет представлять мячик в игре.
private Model model; private Vector3 positionModel
Переменная positionModel необходима для выбора позиции модели на экране. В методе Initialize() мы задаем позицию модели в пространстве нулевыми значениями.
positionModel = new Vector3(0, 0, 0);
Нулевые значения для позиции модели при нахождении камеры в центре экрана устанавливают модель четко в центр экрана. Помните, в главе 15 мы говорили о преобразовании системы координат модели в свою локальную, или объектную, систему координат. Так вот, используя в исходном коде три ранее созданные матрицы (мировую, видовую и проекционную), мы создадим для объекта свою локальную систему координат. После этих действий все свои установки объектов на позиции в трехмерном мире вам придется вести относительно локальной системы координат объекта и позиции камеры (о которой мы поговорим позже в этой главе). Если камера стоит в центре экрана (X = 0 и Y = 0, по оси Z камера либо приближается, либо удаляется от центра экрана) и у объекта нулевые координаты, то местоположение объекта всегда будет точно в центре телевизора (рис. 17.5).
Рис. 17.5. Объектная система координат
Переходим в тело метода Initialize(). Здесь появляется новая запись, инициализирующая переменную aspectRatio значением, равным делению половины ширины экрана на половину высоты экрана.
Переходим к методу LoadGraphicsContent(). Строка кода по загрузке модели из рабочего каталога программы выглядит следующим образом:
model = content.Load(«Content\\Models\\Soccerball»);
Как видите, такая запись практически идентична записи по загрузке спрайтового изображения, но вместо ключевого слова используется слово . Подобная команда дается для Content Pipeline на выполнение загрузки в программу уже трехмерной модели, а не текстуры. Модель мячика находится в каталоге рабочего проекта в папке Content\Models (рис. 17.6). Здесь схема поиска модели в каталоге проекта у загрузчика Content Pipeline стандартна.
Рис. 17.6. Расположение модели в каталоге проекта
17.1.2. Метод DrawModel()
Для рисования или представления модели на экране телевизора в исходном коде класса Game1 создан метод DrawModel().
Этот метод на первый взгляд кажется сложным, но на самом деле это отлаженный механизм построения модели в пространстве и вывода ее на экран. В этот метод в качестве параметра передается модель и в первых двух строках происходит считывание и преобразование всех вершин модели в один массив данных. Затем происходит установка всех трех матриц. Мировая матрица получает позицию модели в пространстве. В матрице вида происходит установка камеры методом CreateLookAt(), где первый параметр как раз и определяет позицию камеры. В нашем случае мы удаляем камеру на 150 пикселей по оси Z в сторону ваших глаз. Два оставшихся параметра метода CreateLookAt() – типично используемые величины. По окончании этой главы самостоятельно попробуйте изменять все значения матрицы вида, чтобы уяснить суть этих преобразований. Матрица проекции для своих расчетов использует четыре переменные, о назначении которых мы уже упоминали. Последний блок кода метода DrawModel() задействует механизм языка программирования С# и оператор foreach для цикличного перебора данных массива, или, как в нашем случае, коллекции данных. Оператор foreach позаимствован из языка программирования Visual Basic и позволяет циклично извлекать каждый элемент коллекции данных по очереди, помещая извлеченный элемент в очередной объект массива, который вынесен в заголовок этого оператора.
foreach (ModelMesh mesh in m.Meshes)
В результате этот механизм позволяет получить все компоненты мэша, объединить их воедино и сформировать трехмерную модель. Следующая строка кода
foreach (BasicEffect effect in mesh.Effects)
делает ту же самую операцию по извлечению коллекции данных, но уже в отношении шейдерных данных, а точнее вершинного потока, поступающего во входные данные видеоадаптера. В данном контексте используется простой эффект BasicEffect, который дает возможность работать с шейдерами в автоматическом режиме. В этом случае система сама рассчитывает освещение, материал, нормали, трансформацию и другие виды преобразований в автоматическом режиме. Работать с шейдерами очень сложно, и освещение этой темы выходит за рамки книги, поэтому в программе применяется более простой в использовании класс BasicEffect. Далее сам метод DrawModel() мы вызываем в методе Draw(), отвечающем за рисование графики на экране телевизора.
case CurentGameState.GameScreen: { graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; DrawModel(model); break; }
Здесь, думается, все понятно, за исключением самой верхней строки кода этого блока.
На самом деле запись этой строки кода сейчас не имеет какой-либо смысловой нагрузки. Эта запись нам понадобится в следующих главах, когда мы добавим на экран вывод спрайтовых изображений. Дело в том, что использование в программе объектов класса SpriteBatch несколько изменяет настройки видеоадаптера, перестраивая эти настройки под работу с двухмерной графикой. Поэтому очень важно возвращать некоторые настройки назад. В данном случае необходимо вновь включить буфер глубины, который SpriteBatch отключает для работы с двухмерной графикой. В противном случае ваша модель на экране будет представлена с большими визуальными дефектами. Кстати, может понадобиться и больше дополнительных настроек для одновременной работы с моделями и двухмерной графикой. Теперь обратитесь к полному исходному коду класса Game1, который представлен в листинге 17.1. По обыкновению все новшества в коде выделены жирным шрифтом. В следующем разделе мы усовершенствуем работу программы и создадим отдельный класс для работы с трехмерными моделями.
//========================================================================= /// /// Листинг 17.1 /// Глава 17 /// Проект: LoadingModel /// Класс: Game1 /// Загрузка модели /// //=========================================================================
#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 { { SplashScreen, MenuScreen, HelpScreen, AboutScreen, GameScreen, GameOverScreen, VictoryScreen } CurentGameState gameState = CurentGameState.GameScreen; GraphicsDeviceManager graphics; ContentManager content; int screenWidth, screenHeight; Matrix view; Matrix proj; Matrix world; static float aspectRatio; static float FOV = MathHelper.PiOver4; static float nearClip = 1.0f; static float farClip = 1000.0f; private Model model; private Vector3 positionModel;
/// /// Конструктор /// public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; } /// /// Инициализация /// protected override void Initialize() { aspectRatio = (float)screenWidth / (float)screenHeight; positionModel = new Vector3(0, 0, 0); base.Initialize(); } /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { model = content.Load(«Content\\Models\\Soccerball»); } } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { … } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(new Color(159, 147, 207)); switch (gameState) { case CurentGameState.SplashScreen: { break; } case CurentGameState.MenuScreen: { break; } case CurentGameState.AboutScreen: { break; } case CurentGameState.GameScreen: { graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; private enum CurentGameState DrawModel(model); break; } case CurentGameState.GameOverScreen: { break; } case CurentGameState.VictoryScreen: { break; } } base.Draw(gameTime); } /// /// Рисуем модель /// 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; effect.View = view; effect.Projection = proj; } mesh.Draw(); } } } }
17.2. Класс ModelClass
В шестой главе, когда мы только начинали работать с двухмерной графикой, был создан один общий класс для работы со спрайтами. В этой главе мы также создадим такой класс, но уже для представления трехмерных моделей. Для реализации этой задачи формируем новый проект LoadingModelClass на базе предыдущего примера, а в проект добавляем дополнительный класс ModelClass (См. листинг 17.2).
//========================================================================= /// /// Листинге 17.2 /// Глава 17 /// Проект: LoadingModelClass /// Класс: ModelClass /// Загружаем в игру модель через класс /// //=========================================================================
#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); } /// /// Загрузка модели в игру /// public void Load(ContentManager content, String stringModel) { model = content.Load(stringModel); } /// /// Рисуем модель на экране /// 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. Перейдем к программному коду этого класса.
17.3. Создаем объект класса LoadingModelClass
Теперь необходимо создать в исходном коде класса Game1 объект ball класса ModelClass и загрузить в него модель мячика. После этого нужно установить матрицы и вызвать метод ball.DrawModel(world, view, proj) в цикле прорисовки графики на экране. В листинге 17.3 представлен полный исходный код обновленного класса Game1. Весь проект LoadingModelClass находится на компакт-диске в папке Code\Chapter17\LoadingModelClass.
//========================================================================= /// /// Листинг 17.3 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Автор книги: Горнаков С. Г. /// Глава 17 /// Проект: LoadingModelClass /// Класс: Game1 /// Загружаем в игру модель через класс /// //========================================================================= #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 { … } CurentGameState gameState = CurentGameState.GameScreen; GraphicsDeviceManager graphics; ContentManager content; int screenWidth, screenHeight; Matrix view; Matrix proj; Matrix world; static float aspectRatio; static float FOV = MathHelper.PiOver4; static float nearClip = 1.0f; static float farClip = 1000.0f; private ModelClass ball; /// /// Конструктор /// public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; ball = new ModelClass(); } /// /// Инициализация /// protected override void Initialize() { aspectRatio = (float)screenWidth / (float)screenHeight; base.Initialize(); } /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { ball.Load(content, «Content\\Models\\Soccerball»); ball.position = new Vector3(0, 0, 0); } } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { if (unloadAllContent == true) { content.Unload(); } } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { … } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(new Color(159, 147, 207)); switch (gameState) { 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); } } }