Программирование простой игры в DirectX
Добавление кода создания дороги
Для создания дороги мы воспользуемся готовым файлом road.x, который находится в прилагаемом каталоге Source. Вначале мы создадим ссылки уровня класса на Mesh -объект, материалы и текстуру. Затем код загрузки внешнего Mesh -файла road.x мы упакуем в статическую общедоступную функцию LoadMesh(), которая будет возвращать значение ссылки на загруженный Mesh -объект, материалы и текстуру. Статической и общедоступной мы сделаем функцию LoadMesh() для того, чтобы она была доступна через имя класса DodgerGame из различных классов нашего приложения, которые нам еще предстоит создать, и временная переменная, возвращаемая функцией, сохраняла бы свое значение в пределах приложения.
- Поместите в начале класса DodgerGame в созданную нами ранее секцию #region объявления переменных-ссылок
#region Секция переменных-членов класса DodgerGame // Ссылка на устройство Device device; // Ссылки на элементы дороги private Mesh roadMesh = null; private Material[] roadMaterials = null; private Texture[] roadTextures = null; #endregionЛистинг 17.13. Размещение переменных-ссылок в секции класса DodgerGame
- Командой Project/Add Existing Item добавьте в проект из прилагаемого к данной лабораторной работе каталога Source Mesh -файл дороги road.x и файл ее текстуры road1.jpg
- В панели Solution Explorer выделите скопированные файлы road.x и road1.jpg и, выполнив для них команду Properties контекстного меню, установите свойство оболочки Copy to Output Directory в значение Copy if newer (копировать в выходной каталог bin при обновлении элемента), чтобы файлы находились в одном каталоге с исполнимой сборкой приложения
- Добавьте в конец класса DodgerGame функцию LoadMesh() загрузки Mesh -файла дороги, как показано ниже
// Функция загрузки Mesh-файла дороги public static Mesh LoadMesh(Device device, string file, ref Material[] meshMaterials, ref Texture[] meshTextures) { ExtendedMaterial[] mtrl; // Загружаем Mesh-файл Mesh tempMesh = Mesh.FromFile(file, MeshFlags.Managed, device, out mtrl); // Если имеется материал, сохраняем его if (mtrl != null && mtrl.Length > 0) { // Создаем массивы meshMaterials = new Material[mtrl.Length]; meshTextures = new Texture[mtrl.Length]; // Сохраняем каждый материал и текстуру for (int i = 0; i < mtrl.Length; i++) { meshMaterials[i] = mtrl[i].Material3D; if (mtrl[i].TextureFilename != null && mtrl[i].TextureFilename != string.Empty) { // Пытаемся загрузить файл с текстурой meshTextures[i] = TextureLoader.FromFile(device, @".\" // Текущий каталог размещения сборки + mtrl[i].TextureFilename); } } } return tempMesh; }Листинг 17.14. Функция загрузки Mesh-файла дороги
- Добавьте вызов функции LoadMesh() в конце обработчика события сброса устройства OnDeviceReset()
// Обработчик события сброса устройства private void OnDeviceReset(object sender, EventArgs e) { ..................................................... // Включить освещение сцены device.RenderState.Lighting = true; // Загрузка Mesh-файла и создание объекта дороги roadMesh = LoadMesh(device, @".\road.x", ref roadMaterials, ref roadTextures); }Листинг 17.15. Вызов функции загрузки файла road.x и создания объектов
Использование ключевых слов ref в определении и вызове функции LoadMesh() гарантируют, что данные будут передаваться по ссылке, а не по значению. Неинтерпретируемая строка @".\road.x" указывает, что файл road.x находится в текущем каталоге исполнимой сборки.
- Создайте функцию DrawRoad() рендеринга (отображения) секции дороги в классе DodgerGame следующего содержания
// Функция отображения секции дороги private void DrawRoad(float x, float y, float z) { device.Transform.World = Matrix.Translation(x, y, z); for(int i = 0; i < roadMaterials.Length; i++) { device.Material = roadMaterials[i]; device.SetTexture(0, roadTextures[i]); roadMesh.DrawSubset(i); } }Листинг 17.16. Функция отображения секции дороги DrawRoad()
Эта функция рисует дорогу в соответствующем месте, отображая материалы, текстуру и сам объект. Мы будем использовать эту функцию так, чтобы рисовать последовательные перемещения дороги сверху вниз, и тем самым создавать иллюзию перемещения автомобиля на этой дороге, а сам автомобиль будет перемещаться только по горизонтали экрана. Для управления процессом перемещения дороги нам понадобятся некоторые переменные и константы.
- Добавьте в секцию размещения переменных-членов класса DodgerGame объявления следующих величин
#region Секция переменных-членов класса DodgerGame // Ссылка на устройство Device device; // Ссылки на элементы дороги private Mesh roadMesh = null; private Material[] roadMaterials = null; private Texture[] roadTextures = null; // Объявления переменных-характеристик для управления дорогой public const float ROAD_LOCATION_LEFT = 2.5F; public const float ROAD_LOCATION_RIGHT = -2.5F; private const float ROAD_SIZE = 100.0F; private const float MAXIMUM_ROAD_SPEED = 250.0F; private const float ROAD_SPEED_INCREMENT = 0.5F; // Глубина рисуемой дороги private float RoadDepth0 = 0.0F; private float RoadDepth1 = -100.0F; private float RoadSpeed = 30.0F; #endregionЛистинг 17.17. Определение характеристик дороги
Константа ROAD_SIZE устанавливает длину дороги, а константы ROAD_LOCATION_LEFT и ROAD_LOCATION_RIGHT определяют расстояние краев дороги от центральной линии. Константа MAXIMUM_ROAD_SPEED определяет предельную скорость перемещения дороги в процессе игры. Константа ROAD_SPEED_INCREMENT призвана варьировать текущую скорость с начальным значением RoadSpeed. Загружаемый Mesh -объект дороги имеет высоту 100 единиц и переменные глубины различаются как раз на эту величину. После рисования одной секции мы сразу же нарисуем вторую секцию непосредственно с конца первой.
- Поместите в функцию OnPaint() два вызова функции рисования с разной глубиной двух секций дороги друг за дружкой (фигурные скобки не обязательны)
// Код организации рендеринга protected override void OnPaint(PaintEventArgs e) { // Очистка экрана device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.CornflowerBlue, 1.0F, 0); // Формирование о отображение сцены device.BeginScene(); { // Отображение двух секций дороги с разной глубиной DrawRoad(0.0F, 0.0F, RoadDepth0); DrawRoad(0.0F, 0.0F, RoadDepth1); } device.EndScene(); device.Present(); this.Invalidate(); }Листинг 17.18. Вызовы функции рисования двух секций дороги
- Постройте приложение и убедитесь, что дорога отображается в окне формы
Фильтрация текстуры дороги
Мы видим, что на данном этапе асфальт дороги смотрится не сплошным, а сильно пикселизованным. Причина в способе определения DirectX цвета пиксела. Когда один элемент текстуры (тексел) охватывает больше одного пиксела на экране, то пикселы расчитываются фильтром растяжения. Если же несколько элементов текстуры перекрывают отдельный пиксел, пиксел расчитывается фильтром сжатия. Заданный по умолчанию фильтр растяжения и сжатия, называемый точечным фильтром, попросту использует самый близкий элемент текстуры как цвет для соответствующего пиксела. Это и вызывает эффект пикселизации.
Существуют различные способы для фильтрации текстур, однако не каждое устройство может поддерживать их. На данном этапе нам понадобится фильтр, который может интерполировать элементы текстуры дороги, чтобы отобразить ее более гладкой.
// Обработчик события сброса устройства private void OnDeviceReset(object sender, EventArgs e) { // Установка камеры device.Transform.Projection = Matrix.PerspectiveFovLH( (float)Math.PI / 4, // Угол зрения равен 45 градусов (float)this.Width / (float)this.Height, // Форматное соотношение сторон 1.0F, // Ближний план 1000.0F); // Дальний план device.Transform.View = Matrix.LookAtLH( new Vector3(0.0F, 9.5F, 17.0F), // Положение камеры new Vector3(), // Положение объекта текущее new Vector3(0, 1, 0)); // Направление камеры // Установка фильтрации текстуры // Установка фильтра сжатия - анизотропный, если поддерживается // видеокартой, иначе - линейный (если тоже поддерживается) if (device.DeviceCaps.TextureFilterCaps.SupportsMinifyAnisotropic) device.SamplerState[0].MinFilter = TextureFilter.Anisotropic; else if (device.DeviceCaps.TextureFilterCaps.SupportsMinifyLinear) device.SamplerState[0].MinFilter = TextureFilter.Linear; // Установка фильтра растяжения - анизотропный, если поддерживается // видеокартой, иначе - линейный (если тоже поддерживается) if (device.DeviceCaps.TextureFilterCaps.SupportsMagnifyAnisotropic) device.SamplerState[0].MagFilter = TextureFilter.Anisotropic; else if (device.DeviceCaps.TextureFilterCaps.SupportsMagnifyLinear) device.SamplerState[0].MagFilter = TextureFilter.Linear; // Установка освещения // Поддерживаются ли несколько полноценных источников освещения if ((device.DeviceCaps.VertexProcessingCaps.SupportsDirectionAllLights) && (device.DeviceCaps.MaxActiveLights > 1)) { // Устанавливаем первый источник device.Lights[0].Type = LightType.Directional; device.Lights[0].Diffuse = Color.White; device.Lights[0].Direction = new Vector3(1, -1, -1); device.Lights[0].Commit(); device.Lights[0].Enabled = true; // Устанавливаем второй источник device.Lights[1].Type = LightType.Directional; device.Lights[1].Diffuse = Color.White; device.Lights[1].Direction = new Vector3(-1, 1, -1); device.Lights[1].Commit(); device.Lights[1].Enabled = true; } else { // Источники не поддерживаются, устанавливаем общий свет device.RenderState.Ambient = Color.White; // Настраиваем общий свет device.Lights[0].Type = LightType.Directional; device.Lights[0].Diffuse = Color.White; device.Lights[0].Direction = new Vector3(1, -1, -1); device.Lights[0].Commit(); device.Lights[0].Enabled = true; } // Включить освещение сцены device.RenderState.Lighting = true; // Загрузка Mesh-файла и создание объекта дороги roadMesh = LoadMesh(device, @".\road.x", ref roadMaterials, ref roadTextures); }Листинг 17.19. Код фильтрации текстуры в функции OnDeviceReset()
В приведенном коде вначале выясняется, способна ли видеокарта поддерживать анизотропную фильтрацию текстуры при сжатии или растяжении. Если нет, то проверяется поддержка линейной фильтрации для этих режимов. И наконец, если ничего не поддерживается, то фильтрация не устанавливается в устройстве.
- Откомпилируйте проект и убедитесь, что дорога приобрела более размытую текстуру (если на Вашем компьютере есть соответствующая поддержка)
Некоторая шероховатость на поверхности дороги все - же осталась, но это к лучшему! - более реалистичным будет казаться эффект перемещения дороги, к которому мы сейчас приступим.
Имитация движения дороги
Скорость перемещения дороги, если не принять специальных мер, зависит от частоты смены кадров на конкретном компьютере. Это значит, что на разных компьютерах скорость будет разной. Чтобы сделать скорость независимой от конкретных компьютеров, нужно привязать ее к некоторой независимой от частоты кадров величине. Такой величиной является тактовое время таймера высокой точности (с точностью до 1 миллисекунды), которое можно извлечь из класса Utility. Этот класс не является библиотечным, но поставляется дополнительно с инструменальными средствами разработчика DirectX SDK. Исходный код этого класса находится в файле Utility.cs и прилагается к данной лабораторной работе.
- Выполните команду меню оболочки Project/Add Existing Item и в открывшемся диалоговом окне добавьте к проекту файл Utility.cs, находящийся в каталоге Source данной лабораторной работы
Оболочка сама скопирует файл в каталог размещения проекта, где находится файл Dodger.sln.
#region Секция переменных-членов класса DodgerGame // Ссылка на устройство Device device; // Ссылки на элементы дороги private Mesh roadMesh = null; private Material[] roadMaterials = null; private Texture[] roadTextures = null; // Объявления переменных-характеристик для управления дорогой public const float ROAD_LOCATION_LEFT = 2.5F; public const float ROAD_LOCATION_RIGHT = -2.5F; private const float ROAD_SIZE = 100.0F; private const float MAXIMUM_ROAD_SPEED = 250.0F; private const float ROAD_SPEED_INCREMENT = 0.5F; // Глубина рисуемой дороги private float RoadDepth0 = 0.0F; private float RoadDepth1 = -100.0F; private float RoadSpeed = 30.0F; // Точное время private float elapsedTime = Utility.Timer(DirectXTimer.GetElapsedTime); #endregionЛистинг 17.20. Объявление ссылки на точное время в классе DodgerGame
- Добавьте к классу DodgerGame функцию обновления кадра с именем OnFrameUpdate()
// Функция обновления кадра, привязанная // к таймеру повышенной точности private void OnFrameUpdate() { // Извлекаем точное время elapsedTime = Utility.Timer(DirectXTimer.GetElapsedTime); // Вычисляем текущую глубину дороги RoadDepth0 += RoadSpeed * elapsedTime; RoadDepth1 += RoadSpeed * elapsedTime; // Проверяем необходимость смены секций дороги if (RoadDepth0>75.0F) { RoadDepth0 = RoadDepth1 - 100.0F; } if (RoadDepth1>75.0F) { RoadDepth1 = RoadDepth0 - 100.0F; } }Листинг 17.21. Функция движения и смены секций дороги
- Поместите в функцию OnPaint() код вызова функции OnFrameUpdate() вычисления новых параметров глубины рисования дороги перед секцией формирования и отображением сцены
// Код организации рендеринга protected override void OnPaint(PaintEventArgs e) { // Очистка экрана device.Clear(ClearFlags.Target | ClearFlags.ZBuffer, Color.CornflowerBlue, 1.0F, 0); // Вычисление новых параметров рисования дороги OnFrameUpdate(); // Формирование о отображение сцены device.BeginScene(); { // Отображение двух секций дороги с разной глубиной DrawRoad(0.0F, 0.0F, RoadDepth0); DrawRoad(0.0F, 0.0F, RoadDepth1); } device.EndScene(); device.Present(); this.Invalidate(); }Листинг 17.22. Вызов функции вычисления новых параметров дороги
- Запустите приложение и убедитесь, что дорога наезжает на зрителя