Практикум прикладного программирования на C# в среде VS.NET 2005

Программирование простой игры в DirectX

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

Файлы к лабораторной работе Вы можете скачать здесь.

В настоящей лабораторной работе мы используем средства управляемого DirectX и построим простую игру. Для любого коммерческого варианта игры нужно вначале построить подробный план, в котором тщательно продумать все существенные детали и игру в целом. Обычно для этого применяется язык диаграмм UML. Но наша игра простая и цели наши другие.

Программу построим из трех основных классов:

  • Основной класс формы - DodgerGame (движок игры)
  • Класс управления автомобилем - Car
  • Класс управления препятствиями - Obstacle
  • Создание заготовки приложения

  • Выберите каталог для размещения проекта и командой File/New/Project создайте новый пустой проект C# с именем Dodger
  • Выполните команду меню Project/Add Reference и добавьте к проекту ссылки на следующие библиотечные сборки
  • System.dll
  • System.Drawing.dll
  • System.Windows.Forms.dll
  • Microsoft.DirectX.dll
  • Microsoft.DirectX.Direct3D.dll
  • Microsoft.Directx.Direct3DX.dll
  • Примечание. В трех последних сборках выбирайте версии в соответствии с рисунком

  • Командой меню Project/Add New Item добавьте к проекту класс DodgerGame, который будет играть роль движка нашей игры
  • Удалите из проекта автоматически сгенерированные при добавлении класса ненужные ссылки на библиотечные сборки
  • Добавьте в начало файла DodgerGame.cs соответствующие using -директивы
  • using System;
    using System.Drawing;
    using System.Windows.Forms;
    using Microsoft.DirectX;
    using Microsoft.DirectX.Direct3D;
  • Сделайте класс DodgerGame наследником библиотечного класса Form (отметьте, что пиктограмма класса в панели Solution Explorer сразу изменилась) и пропишите конструктор класса DodgerGame следующим образом
  • public class DodgerGame : Form
      {
        public DodgerGame()
        {
          this.Size = new Size(800, 600); // Размер окна формы
          this.Text = "Lab37. Игра Dodger"; // Заголовок окна
        }
      }
  • Добавьте в проект командой Project/Add New Item новый класс с именем AppEntry, в котором разместите точку входа приложения
  • Отредактированный класс должен быть таким
  • namespace Dodger
    {
      public class AppEntry
      {
        static void Main()
        {
          using (DodgerGame frm = new DodgerGame())
          {
            System.Windows.Forms.Application.Run(frm);
          }
        }
      }
    }

    Инструкция using в данном применении заставит сборщик мусора выполниться сразу же после завершения ее работы.

  • Установите командой Project/Dodger Properties в окне свойств проекта оконный режим работы приложения
  • Установите режим Release работы компилятора и постройте проект. Должна появиться пустая форма
  • На этом подготовительные операции по созданию заготовки проекта окончены. Теперь приступим к программированию самой игры с использованием классов управляемого DirectX.

    Программирование движка игры

    Движком игры у нас будет класс DodgerGame. Именно в нем мы будем организовывать отображение сцены и управление игрой. Визуально этот класс представлен окном формы, поскольку наследует базовый класс Form.

    Создание устройства в главном классе

  • Добавьте в конец конструктора класса DodgerGame код определения стиля окна формы
  • public DodgerGame()
        {
          this.Size = new Size(800, 600); // Размер окна формы
          this.Text = "Lab37. Игра Dodger"; // Заголовок окна
          this.SetStyle(ControlStyles.AllPaintingInWmPaint 
            | ControlStyles.Opaque, true);
        }
  • Создайте в начале класса DodgerGame перед конструктором секцию для размещения переменных-членов класса и объявите в ней ссылку на устройство
  • public class DodgerGame : Form
      {
        #region Секция переменных-членов класса DodgerGame
      
        // Ссылка на устройство
        Device device;
          
        #endregion
      
        public DodgerGame()
        {
        ....................................
        }
      }
  • Добавьте в класс DodgerGame функцию InitializeGraphics() для инициализации объектов DirectX
  • // Функция создания и инициализации объектов DirectX
        public void InitializeGraphics()
        {
          // Установить параметры представления
          PresentParameters presentParams = new PresentParameters();
          presentParams.Windowed = true;
          presentParams.SwapEffect = SwapEffect.Discard;
      
          // Включить буфер глубины устройства
          presentParams.AutoDepthStencilFormat = DepthFormat.D16;
          presentParams.EnableAutoDepthStencil = true;
            
          // Установить адаптер программную поддержку по умолчанию
          int adapterOrdinal = Manager.Adapters.Default.Adapter;
          CreateFlags flags = CreateFlags.SoftwareVertexProcessing;
            
          // Проверка возможности использования прямой аппаратной поддержки
          Caps caps = Manager.GetDeviceCaps(adapterOrdinal, DeviceType.Hardware);
            
          // Возможна ли прямая аппаратная поддержка вершинного буфера
          if(caps.DeviceCaps.SupportsHardwareTransformAndLight)
            flags = CreateFlags.HardwareVertexProcessing;
              
          // Возможна ли прямая аппаратная поддержка устройства
          if(caps.DeviceCaps.SupportsPureDevice)
            flags |= CreateFlags.PureDevice;
              
          // Создать устройство
          device = new Device(adapterOrdinal, DeviceType.Hardware, this, flags, presentParams);
        }
  • Добавьте в функцию Main() класса AppEntry вызов функции инициализации объектов DirectX
  • public class AppEntry
      {
        static void Main()
        {
          using (DodgerGame frm = new DodgerGame())
          {
            frm.InitializeGraphics();
            System.Windows.Forms.Application.Run(frm);
          }
        }
      }

    Сначала мы создаем и устанавливаем параметры представления устройства, в том числе включаем буфер глубины. Затем извлекаем и сохраняем свободный порядковый номер устройства и устанавливаем изначально программный способ обработки вершин. Затем мы выясняем, способна ли поддерживать видеокарта текущего компьютера прямое аппаратное вычисление вершин графических объектов. При обнаружении возможностей аппаратной обработки вершин проверяем возможность создания реального устройства и настраиваем переменную flags с помощью поразрядной операции OR. Способы определения возможностей аппаратной поддержки приведены в лабораторной работе №11.

  • Для восстановления состояния устройства после его сброса добавьте в конец функции InitializeGraphics() код регистрации события DeviceReset объекта устройства ( код добавляйте вручную, чтобы оболочка создала правильный обработчик)
  • // Функция создания и инициализации объектов DirectX
        public void InitializeGraphics()
        {
          .........................................................
                // Создать устройство
                device = new Device(adapterOrdinal, DeviceType.Hardware, this, flags, presentParams);
        
                // Подписаться на событие регистрации сброса устройства
                device.DeviceReset += new EventHandler(device_DeviceReset);
        }

    Оболочка создаст обработчик события с автоматически сгенерированным именем. Переименуйте обработчик на OnDeviceReset

    // Функция создания и инициализации объектов DirectX
        public void InitializeGraphics()
        {
          .......................................................
          device.DeviceReset += new EventHandler(OnDeviceReset);
        }
      
        // Обработчик события сброса устройства
        private void OnDeviceReset(object sender, EventArgs e)
        {
    
        }
  • Добавьте в конец функции InitializeGraphics() принудительный вызов обработчика при запуске приложения
  • public void InitializeGraphics()
        {
          .........................................................
                // Подписаться на событие регистрации сброса устройства
                device.DeviceReset += new EventHandler(OnDeviceReset);
        
                // Прямой вызов обработчика
                this.OnDeviceReset(device, null);
        }

    Установка камеры и освещения сцены

  • Заполните обработчик события сброса устройства следующим кодом
  • // Обработчик события сброса устройства
        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.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;
        }

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

    Добавление кода запуска рендеринга

    Переопределим в классе DodgerGame наследуемый из Form метод OnPaint() и поместим в него код организации рендеринга. Операцию переопределения метода OnPaint() выполните вручную!!! и начните с ввода ключевого слова override

  • Добавьте вручную (чтобы оболочка правильно сгенерировала аргументы метода) в класс DodgerGame код переопределения метода OnPaint()
  • // Код организации рендеринга
            protected override void OnPaint(PaintEventArgs e)
            {
                // Очистка экрана
                device.Clear(ClearFlags.Target | ClearFlags.ZBuffer,
                    Color.CornflowerBlue, 1.0F, 0);
        
                // Формирование о отображение сцены
                device.BeginScene();
                device.EndScene();
            
                device.Present();
                this.Invalidate();
            }
  • Запустите приложение, должно получиться пустое окно с голубым фоном.
  • Приступим к созданию первого игрового объекта "Дорога" (road).

    Добавление кода создания дороги

    Для создания дороги мы воспользуемся готовым файлом 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
  • Командой 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;
            }
  • Добавьте вызов функции LoadMesh() в конце обработчика события сброса устройства OnDeviceReset()
  • // Обработчик события сброса устройства
        private void OnDeviceReset(object sender, EventArgs e)
        {
          .....................................................
      
          // Включить освещение сцены
          device.RenderState.Lighting = true;
      
          // Загрузка Mesh-файла и создание объекта дороги
          roadMesh = LoadMesh(device, @".\road.x", ref roadMaterials, ref roadTextures);
        }

    Использование ключевых слов 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);
          }
        }

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

  • Добавьте в секцию размещения переменных-членов класса 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

    Константа 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();
            }
  • Постройте приложение и убедитесь, что дорога отображается в окне формы
  • Фильтрация текстуры дороги

    Мы видим, что на данном этапе асфальт дороги смотрится не сплошным, а сильно пикселизованным. Причина в способе определения DirectX цвета пиксела. Когда один элемент текстуры (тексел) охватывает больше одного пиксела на экране, то пикселы расчитываются фильтром растяжения. Если же несколько элементов текстуры перекрывают отдельный пиксел, пиксел расчитывается фильтром сжатия. Заданный по умолчанию фильтр растяжения и сжатия, называемый точечным фильтром, попросту использует самый близкий элемент текстуры как цвет для соответствующего пиксела. Это и вызывает эффект пикселизации.

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

  • Добавьте в функцию OnDeviceReset() перед установкой освещения следующий код фильтрации текстуры
  • // Обработчик события сброса устройства
            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);
            }

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

  • Откомпилируйте проект и убедитесь, что дорога приобрела более размытую текстуру (если на Вашем компьютере есть соответствующая поддержка)
  • Некоторая шероховатость на поверхности дороги все - же осталась, но это к лучшему! - более реалистичным будет казаться эффект перемещения дороги, к которому мы сейчас приступим.

    Имитация движения дороги

    Скорость перемещения дороги, если не принять специальных мер, зависит от частоты смены кадров на конкретном компьютере. Это значит, что на разных компьютерах скорость будет разной. Чтобы сделать скорость независимой от конкретных компьютеров, нужно привязать ее к некоторой независимой от частоты кадров величине. Такой величиной является тактовое время таймера высокой точности (с точностью до 1 миллисекунды), которое можно извлечь из класса Utility. Этот класс не является библиотечным, но поставляется дополнительно с инструменальными средствами разработчика DirectX SDK. Исходный код этого класса находится в файле Utility.cs и прилагается к данной лабораторной работе.

  • Выполните команду меню оболочки Project/Add Existing Item и в открывшемся диалоговом окне добавьте к проекту файл Utility.cs, находящийся в каталоге Source данной лабораторной работы
  • Оболочка сама скопирует файл в каталог размещения проекта, где находится файл Dodger.sln.

  • Включите в секцию #region переменных класса объявление ссылки на точное время и ее инициализацию
  • #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
  • Добавьте к классу 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;
                }
        }
  • Поместите в функцию 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();
            }
  • Запустите приложение и убедитесь, что дорога наезжает на зрителя
  • Создание автомобиля

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

    Добавление к проекту нового класса Car

  • Выполните команду Project/Add Class и добавьте к проекту файл с именем Car.cs нового пустого класса
  • Удалите из проекта лишние ссылки, автоматически добавленные оболочкой при создании нового файла (хотя это и не обязательно!)
  • Скопируйте из начала файла DodgerGame объявления using используемых библиотечных пространств имен и разместите их также в начале файла Car.cs, чтобы можно было использовать короткие имена библиотечных классов
  • using System;
    using System.Drawing;
    using System.Windows.Forms;
    using Microsoft.DirectX;
    using Microsoft.DirectX.Direct3D;
        
    namespace Dodger
    {
        class Car
        {
        }
    }
  • Создайте в начале класса Car секцию #region для размещения переменных-членов этого класса
  • using System;
    using System.Drawing;
    using System.Windows.Forms;
    using Microsoft.DirectX;
    using Microsoft.DirectX.Direct3D;
        
    namespace Dodger
    {
        class Car
        {
            #region Секция переменных-характеристик автомобиля
        
            // Константы
            public const float HEIGHT = 2.5f;// Высота автомобиля
            public const float DEPTH = 3.0f;// Глубина автомобиля
            public const float SPEED_INCREMENT = 0.1f;// Приращение скорости бокового перемещения
            private const float SCALE = 0.85f; // Отношение размера автомобиля к ширине дороги
        
            // Переменные
            private float carLocation = DodgerGame.ROAD_LOCATION_LEFT;// Текущее положение слева
            private float carDiameter;// Диаметр для расчета столкновений с препятствием
            private float carSpeed = 10.0f;// Текущая боковая скорость автомобиля
            private bool movingLeft = false;// Направление перемещения влево
            private bool movingRight = false;// Направление перемещения вправо
        
            // Ссылки для Mesh-объекта автомобиля
            private Mesh carMesh = null;
            private Material[] carMaterials = null;
            private Texture[] carTextures = null;
        
            #endregion
        }
    }

    Данные константы и переменные представляют собой все характеристики, необходимые для управления автомобилем.

  • Добавьте в класс Car конструктор с параметрами, который при создании автомобиля автоматически загружает или устанавливает все его характеристики, необходимые для рендеринга
  • // Параметризованный конструктор класса
            public Car(Device device)
            {
                // Создать и загрузить Mesh-объект "Автомобиль",
                // используя ранее разработанный метод класса DodgerGame
                carMesh = DodgerGame.LoadMesh(device, @".\car.x", 
                    ref carMaterials, ref carTextures);
        
                // Мы должны вычислить сферу ограничения для нашего автомобиля
                VertexBuffer vb = carMesh.VertexBuffer;
                try
                {
                    // Замкнуть вершинный буфер перед вычислением
                    GraphicsStream stm = vb.Lock(0, 0, LockFlags.None);
                    Vector3 center; // Мы не будем использовать центр, 
                                    //но ссылка на него требуется
                    float radius = Geometry.ComputeBoundingSphere(stm,
                        carMesh.NumberVertices, carMesh.VertexFormat, out center);
        
                    // Вычислим диаметр автомобиля
                    carDiameter = (radius * 2) * SCALE;
                }
                finally
                {
                    // Независимо от результата попытки
                    // отмыкаем и устанавливаем вершинный буфер
                    vb.Unlock();
                    vb.Dispose();
                }
            }

    Здесь мы использовали статический метод класса DodgerGame для создания объекта автомобиля и загрузки в него Mesh-информации из файла car.x, только с другими ссылками. Мы вычислили граничную сферу, которая полностью включает все точки автомобиля, полученные из вершинного буфера. При использовании вершинного буфера автомобиля его необходимо замкнуть - сделать недоступным другим потокам. В блоке finally, который будет выполнен всегда и последним, мы разблокируем и освобождаем вершинный буфер.

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

  • Добавьте в класс Car файла Car.cs функцию DrawCar() рисования автомобиля
  • // Функция прорисовки автомобиля
            public void DrawCar(Device device)
            {
                // Автомобиль является слишком большим, предварительно уменьшим его
                device.Transform.World = Matrix.Scaling(SCALE, SCALE, SCALE)
                    * Matrix.Translation(carLocation, HEIGHT, DEPTH);
        
                for (int i = 0; i < carMaterials.Length; i++)
                {
                    device.Material = carMaterials[i];
                    device.SetTexture(0, carTextures[i]);
                    carMesh.DrawSubset(i);
                }
            }

    Здесь мы использовали такой-же код, как и при отображении дороги, но с предварительным масштабированием автомобиля.

    Добавление свойств в класс Car

    Ранее в секции #region объявления переменных класса Car мы создали ссылочные поля с доступом private, которые будут видны только внутри этого класса.

    // Переменные
        private float carLocation = DodgerGame.ROAD_LOCATION_LEFT;// Текущее полощение слева
        private float carDiameter;// Диаметр для расчета столкновений с препятствием
        private float carSpeed = 10.0f;// Текущая боковая скорость автомобиля
        private bool movingLeft = false;// Направление перемещения влево
        private bool movingRight = false;// Направление перемещения вправо

    Но эти переменные являются управляемыми характеристиками автомобиля и должны изменяться внутри класса управления игрой DodgerGame. Поэтому упакуем их в общедоступные свойства, используя возможности языка C#.

  • Добавьте в секцию #region класса Car общедоступные свойства на базе введенных ранее закрытых полей
  • #region Секция переменных-характеристик автомобиля
        
            // Константы
            public const float HEIGHT = 2.5f;// Высота автомобиля
            public const float DEPTH = 3.0f;// Глубина автомобиля
            public const float SPEED_INCREMENT = 0.1f;// Приращение скорости бокового перемещения
            private const float SCALE = 0.85f; // Отношение размера автомобиля к ширине дороги
        
            // Переменные
            private float carLocation = DodgerGame.ROAD_LOCATION_LEFT;// Текущее положение слева
            private float carDiameter;// Диаметр для расчета столкновений с препятствием
            private float carSpeed = 10.0f;// Текущая боковая скорость автомобиля
            private bool movingLeft = false;// Направление перемещения влево
            private bool movingRight = false;// Направление перемещения вправо
        
            // Ссылки для Mesh-объекта автомобиля
            private Mesh carMesh = null;
            private Material[] carMaterials = null;
            private Texture[] carTextures = null;
        
            // Общедоступные свойства для характеристик класса Car
            public float Location // Текущее положение слева
            {
                get { return carLocation; }
                set { carLocation = value; }
            }
        
            public float Diameter // Диаметр для расчета столкновений с препятствием
            {
                get { return carDiameter; }
            }
        
            public float Speed // Текущая боковая скорость автомобиля
            {
                get { return carSpeed; }
                set { carSpeed = value; }
            }
        
            public bool IsMovingLeft // Направление перемещения влево
            {
                get { return movingLeft; }
                set { movingLeft = value; }
            }
        
            public bool IsMovingRight // Направление перемещения вправо
            {
                get { return movingRight; }
                set { movingRight = value; }
            }
        
            #endregion

    Создание объекта Car в движке игры

    Теперь у нас есть класс Car и нужно разместить код создания и использования его экземпляра (объекта) в основном движке игры - классе DodgerGame.

  • Поместите в секцию #region переменных класса DodgerGame ссылку на объект класса Car
  • #region Секция переменных-членов класса DodgerGame
        ..................................................
        // Ссылка на объект автомобиля
        private Car car = null;
        #endregion

    Поскольку мы используем ссылку на устройство в конструкторе класса Car, то объект устройства должен быть создан прежде создания объекта автомобиля. Это так и есть, поскольку устройство мы создаем в функции InitializeGraphics() самым первым.

    С учетом того, что устройство может сбрасываться и каждый раз после этого нужно восстанавливать все его характеристики, код создания автомобиля нужно разместить в обработчике сброса устройства OnDeviceReset() класса DodgerGame.

  • Поместите в классе DodgerGame код создания объекта автомобиля в конце обработчика OnDeviceReset() после создания объекта дороги
  • // Обработчик события сброса устройства
            private void OnDeviceReset(object sender, EventArgs e)
            {
                // Установка камеры
                ............................................
        
                // Включить освещение сцены
                device.RenderState.Lighting = true;
        
                // Загрузка Mesh-файла и создание объекта дороги
                roadMesh = LoadMesh(device, @".\road.x", 
                    ref roadMaterials, ref roadTextures);
        
                // Загрузка Mesh-файла и создание объекта автомобиля
                car = new Car(device);
            }

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

  • Добавьте в функцию OnPaint() класса DodgerGame, сразу после двух вызовов функции отображения дороги, код рисования автомобиля
  • // Код организации рендеринга
            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);
        
                    // Отображение автомобиля
                    car.DrawCar(device);
                }
                device.EndScene();
        
                device.Present();
                this.Invalidate();
            }
  • Добавьте к проекту из прилагаемого каталога Source файл car.x командой Project/Add Existing Item
  • В панели Solution Explorer вызовите для файла car.x через контекстное меню панель Properties и установите свойство оболочки Copy to Output Directory в значение Copy if newer
  • Постройте приложение и убедитесь, что появился неподвижный относительно сцены автомобиль на движущейся дороге, прижатый к ее левой стороне
  • Клавиатурное управление автомобилем

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

  • В классе DodgerGame переопределите виртуальный метод OnKeyDown() формы, унаследованный от ее библиотечного базового класса Control (выполните переопределение заготовки метода вручную с ввода ключевого слова override для правильного создания оболочкой аргументов метода)
  • // Переопределение виртуального метода OnKeyDown() в классе DodgerGame
            protected override void OnKeyDown(KeyEventArgs e)
            {
                base.OnKeyDown(e);
            }
  • Заполните тело метода следующим кодом
  • // Переопределение виртуального метода OnKeyDown() в классе DodgerGame
            protected override void OnKeyDown(KeyEventArgs e)
            {
                // Выход по Esc - завершение работы приложения
                if (e.KeyCode == Keys.Escape)
                    this.Close();
        
                // Перемещение влево
                if (e.KeyCode == Keys.Left || e.KeyCode == Keys.NumPad4)
                {
                    car.IsMovingLeft = true;
                    car.IsMovingRight = false;
                }
        
                // Перемещение вправо
                if (e.KeyCode == Keys.Right || e.KeyCode == Keys.NumPad6)
                {
                    car.IsMovingRight = true;
                    car.IsMovingLeft = false;
                }
        
                // Остановить перемещение автомобиля
                if (e.KeyCode == Keys.Down || e.KeyCode == Keys.NumPad2)
                {
                    car.IsMovingLeft = false;
                    car.IsMovingRight = false;
                }
            }

    Здесь мы отлавливаем и обрабатываем нажатия клавиш со стрелками для перемещения автомобиля влево или вправо и устанавливаем флаги необходимости этого перемещения. Также предусмотрены клавиши завершения работы приложения и сброса флагов для остановки перемещения. Коды нажатых клавиш мы сравниваем с полями перечисления System.Windows.Forms.Keys, в котором имеются именованные коды для всех клавиш клавиатуры.

  • Добавьте в класс Car функцию LocationCar() для пересчета новых горизонтальных позиций автомобиля в зависимости от установленных характеристик его перемещения
  • // Управление перемещением автомобиля
            public void LocationCar(float elapsedTime)
            {
                // Перемещаем влево
                if (movingLeft)
                {
                    this.Location += carSpeed * elapsedTime;
        
                    // Если достигли левого края дороги...
                    if (this.Location>= DodgerGame.ROAD_LOCATION_LEFT)
                    {
                        movingLeft = false;
                        this.Location = DodgerGame.ROAD_LOCATION_LEFT;
                    }
                }
        
                // Перемещаем вправо
                if (movingRight)
                {
                    this.Location -= carSpeed * elapsedTime;
        
                    // Если достигли правого края дороги...
                    if (this.Location <= DodgerGame.ROAD_LOCATION_RIGHT)
                    {
                        movingRight = false;
                        this.Location = DodgerGame.ROAD_LOCATION_RIGHT;
                    }
                }
            }
  • Добавьте в класс Car функцию IncrementSpeed() для увеличения скорости горизонтального перемещения автомобиля
  • // Функция увеличения скорости горизонтального перемещения автомобиля
            public void IncrementSpeed()
            {
                carSpeed += SPEED_INCREMENT;
            }
  • Вставьте вызов функции LocationCar() в конец метода OnFrameUpdate() класса DodgerGame
  • // Функция обновления кадра, привязанная
            // к таймеру повышенной точности
            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;
                }
        
                // Вычисление координаты нового положения автомобиля
                car.LocationCar(elapsedTime);
            }
  • Запустите приложение и убедитесь, что положение автомобиля управляется стрелками обеих клавиатур и все закрепленные за клавишами функциональности выполняются
  • Добавление препятствий

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

    Добавление к приложению нового класса Obstacle (препятствия)

  • В панели Solution Explorer щелкните правой кнопкой мыши на файле Car.cs и через контекстное меню выполните команду Copy
  • Щелкните правой кнопкой мыши на узле с именем проекта и выполните команду Paste
  • Переименуйте полученную копию файла в файл Obstacle.cs
  • Выполните команду меню оболочки Window/Close All Documents, чтобы закрыть все окна редактируемых файлов
  • В панели Solution Explorer двойным щелчком на файле Obstacle.cs откройте его на редактирование, переименуйте класс в Obstacle и очистите от лишнего кода
  • using System;
    using System.Drawing;
    using System.Windows.Forms;
    using Microsoft.DirectX;
    using Microsoft.DirectX.Direct3D;
        
    namespace Dodger
    {
        public class Obstacle
        {
        }
    }

    Добавление переменных и констант в класс Obstacle

  • Добавьте в класс Obstacle переменные и константы характеристик препятствий, упакованные в секцию #region
  • using System;
    using System.Drawing;
    using System.Windows.Forms;
    using Microsoft.DirectX;
    using Microsoft.DirectX.Direct3D;
        
    namespace Dodger
    {
        public class Obstacle
        {
            #region Секция характеристик препятствий
        
            private const int NUMBER_MESH_TYPES = 5;
            private const float OBJECT_LENGTH = 3.0f;
            private const float OBJECT_RADIUS = OBJECT_LENGTH / 2.0f;
            private const int OBJECT_STACKS_SLICES = 18;
        
            // Цвета препятствий
            private static readonly Color[] ObstacleColors = 
          { 
            Color.Red, Color.Blue, Color.Green, 
            Color.Bisque, Color.Cyan, Color.DarkKhaki,
            Color.OldLace, Color.PowderBlue, Color.DarkTurquoise, 
            Color.Azure, Color.Violet, Color.Tomato,
            Color.Yellow, Color.Purple, Color.AliceBlue, 
            Color.Honeydew, Color.Crimson, Color.Firebrick 
          };
        
            // Ссылки для Mesh-информации
            private Mesh obstacleMesh = null;
            private Material obstacleMaterial;
            private Vector3 position;
            private bool isTeapot;
        
            #endregion
        }
    }

    Первая константа NUMBER_MESH_TYPES расчитана на пять примитивных Mesh -объектов (сфера, куб, тор, цилиндр, заварочный чайник - sphere, cube, torus, cylinder, teapot), которые можно сгенерировать классом Mesh. Большинство этих типов будут иметь или параметр длины (константа OBJECT_LENGTH ), или параметр радиуса (константа OBJECT_RADIUS ), которыми можно варьировать для управления размером препятствия. Последняя константа OBJECT_STACKS_SLICES отвечает за дополнительные параметры примитивов (пачки, наборы, сектора, кольца, треугольники и т.д.), которые управляют числом встроенных полигонов. При увеличении значения этой константы растет число используемых полигонов, а следовательно, и качество картинки, но увеличиваются вычислительные затраты.

    Далее идет массив цветов для Mesh -объектов, которыми мы будем отображать сгенерированные препятствия. В последней секции этого кода мы заготовили ссылки для адресации препятствий.

    Генерация одного препятствия в конструкторе класса

  • Добавьте в класс Obstacle следующий параметризованный конструктор
  • // Параметризованный конструктор
            public Obstacle(Device device, float x, float y, float z)
            {
                // Сохраняем нашу позицию
                position = new Vector3(x, y, z);
        
                // Пока это - не заварочный чайник
                isTeapot = false;
        
                // Создать новое препятствие
                switch (Utility.Rnd.Next(NUMBER_MESH_TYPES))
                {
                    case 0:
                        obstacleMesh = Mesh.Sphere(device,
                            OBJECT_RADIUS,
                            OBJECT_STACKS_SLICES,
                            OBJECT_STACKS_SLICES);
                        break;
                    case 1:
                        obstacleMesh = Mesh.Box(device,
                            OBJECT_LENGTH,
                            OBJECT_LENGTH,
                            OBJECT_LENGTH);
                        break;
                    case 2:
                        obstacleMesh = Mesh.Teapot(device);
                        isTeapot = true;
                        break;
                    case 3:
                        obstacleMesh = Mesh.Cylinder(device,
                            OBJECT_RADIUS,
                            OBJECT_RADIUS,
                            OBJECT_LENGTH,
                            OBJECT_STACKS_SLICES,
                            OBJECT_STACKS_SLICES);
                        break;
                    case 4:
                        obstacleMesh = Mesh.Torus(device,
                            OBJECT_RADIUS / 3.0f,
                            OBJECT_RADIUS / 2.0f,
                            OBJECT_STACKS_SLICES,
                            OBJECT_STACKS_SLICES);
                        break;
                }
        
                // Установить цвет препятствия
                obstacleMaterial = new Material();
                Color objColor = ObstacleColors[Utility.Rnd.Next(ObstacleColors.Length)];
                obstacleMaterial.Ambient = objColor;
                obstacleMaterial.Diffuse = objColor;
            }

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

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

    Создание одного препятствия

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

  • Добавьте в класс Obstacle функцию вычисления позиции препятствия LocationObstacle()
  • // Вычисление положения препятствия
        public void LocationObstacle(float elapsedTime, float speed)
        {
          position.Z += (speed * elapsedTime);
        }
  • Добавьте к классу Obstacle свойство только для чтения с именем Depth, которое будет возвращать координату глубины препятствия в мировых координатах
  • // Свойство глубины класса Obstacle
        public float Depth
        {
          get { return position.Z; }
        }
  • Добавьте к классу Obstacle функцию DrawObstacle() рисования сгенерированного препятствия
  • // Рисование препятствия
            public void DrawObstacle(Device device)
            {
                if (isTeapot)
                {
                    device.Transform.World =
                        Matrix.Scaling(OBJECT_RADIUS, OBJECT_RADIUS, OBJECT_RADIUS) *
                        Matrix.Translation(position);
                }
                else
                {
                    device.Transform.World =
                        Matrix.Translation(position);
                }
        
                device.Material = obstacleMaterial;
                device.SetTexture(0, null);
                obstacleMesh.DrawSubset(0);
            }

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

    Теперь нужно позаботиться об освобождении закрепленных за Mesh -препятствиями ресурсов при их удалении. Для этого создадим в классе Obstacle соответствующую секцию #region

  • Добавьте в конец класса Obstacle следующий код
  • #region Освобождение ресурсов для сборщика мусора
        
            // Для принудительного вызова при смене препятствий
            public void Dispose()
            {
                obstacleMesh.Dispose();
                System.GC.SuppressFinalize(this);
            }
        
            // Для автоматического вызова при завершении игры
            ~Obstacle() // Деструктор
            {
                this.Dispose();
            }
        
            #endregion

    Здесь мы вызываем метод Dispose() класса Mesh, а затем инициируем работу сборщика мусора для возвращения системе освобожденной памяти. Игра может продолжаться долго, а сгенерированные Mesh -препятствия требуют много ресурсов. Поэтому, такая мера будет своевременно возвращать системе память, исключив ее возможную утечку. То же самое происходит автоматически и при завершении игры, когда вызывается деструктор класса препятствий.

    Создание группы препятствий

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

  • Добавьте в файл Obstacle.cs после класса Obstacle новый класс с именем Obstacles, наследующий интерфейс System.Collections.IEnumerable библиотечной сборки mscorlib
  • // Класс-коллекция для хранения данных о препятствиях
        public class Obstacles : IEnumerable
        {
            // Создать объект динамического массива 
            // с автоматическим изменением длины
            private ArrayList obstacleList = new ArrayList();
        
            // Создание свойства возможности адресации 
            // элементов коллекции по индексу
            public Obstacle this[int index]
            {
                get
                {
                    return (Obstacle)obstacleList[index];
                }
            }
        
            // Переопределение интерфейсного метода сброса курсора 
            // нумератора в начало динамического массива 
            public IEnumerator GetEnumerator()
            {
                return obstacleList.GetEnumerator();
            }
        
            // Добавление препятствия в динамический массив
            public void Add(Obstacle obstacle)
            {
                obstacleList.Add(obstacle);
            }
        
            // Удаление препятствия из динамического массива
            public void Remove(Obstacle obstacle)
            {
                obstacleList.Remove(obstacle);
            }
        
            // Очистка коллекции препятствий
            public void Clear()
            {
                obstacleList.Clear();
            }
        }

    В данном коде мы используем библиотечный класс ArrayList для создания динамического массива и упаковки его в класс-коллекцию. Коллекции в C# означают то же самое, что шаблоны в C++. Мы создали класс-коллекцию на основе интерфейсного класса IEnumerable. В нашем классе мы создали свойство для прямого индексированного доступа к хранимым в коллекции объектам препятствий, а также заложили с помощью интерфейсного класса IEnumerable возможность доступа к препятствиям методом перебора с использованием цикла foreach языка C#. Мы также упаковали в своем классе три виртуальных метода поэлементного добавления ( Add ), поэлементного удаления ( Remove ) и групповой очистки ( Clear ) класса ArrayList.

  • Для доступности сокращенных имен ArrayList и IEnumerable известите компилятор об используемом библиотечном пространстве имен System.Collections соответствующей using -директивой в начале файла Obstacle.cs
  • using System;
    using System.Drawing;
    using System.Windows.Forms;
    using Microsoft.DirectX;
    using Microsoft.DirectX.Direct3D;
    using System.Collections;
      
    namespace Dodger
    {
    ...................................................
    }

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

    Интерфейс - это чистая синтаксическая конструкция, которая предназначена для установки правил взаимодействия отдельных частей программы. Интерфейсы никогда не содержат поля данных и в них не бывает реализаций методов по умолчанию. Каждый член интерфейса (будь то свойство или метод) автоматически становится абстрактным. Кроме того, в C# множественное наследование классов не поддерживается, зато множественное наследование интерфейсов - обычное дело.

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

    Размещение кода создания и отображения препятствий в движке игры

    Имея созданные нами базовые возможности по управлению препятствиями можно приступить к их добавлению в основном классе приложения DodgerGame.

  • Добавьте в секцию #region класса DodgerGame объявление ссылки для поддержки списка текущих препятствий в сцене и константу для задания высоты препятствий
  • #region Секция переменных-членов класса DodgerGame
        
            ..........................................
        
            // Ссылка на объект автомобиля
            private Car car = null;
        
            // Ссылка на препятствия
            private Obstacles obstacles;
            // Высота препятствий
            private const float OBSTACLE_HEIGHT = Car.HEIGHT * 0.85f;
        
            #endregion
  • Добавьте в обработчик события сброса устройства OnDeviceReset() класса DodgerGame создание экземпляра класса-коллекции препятствий
  • // Обработчик события сброса устройства
            private void OnDeviceReset(object sender, EventArgs e)
            {
                ..............................................
        
                // Загрузка Mesh-файла и создание объекта автомобиля
                car = new Car(device);
        
                // Создание объекта-коллекции препятствий
                obstacles = new Obstacles();
            }
  • Добавьте в класс DodgerGame функцию AddObstacles() заполнения последующей дорожной секции новыми препятствиями
  • // Добавление препятствий в коллекцию Obstacles
            private void AddObstacles(float minDepth)
            {
                // Вычислить возможное число препятствий
                int numberToAdd = (int)((ROAD_SIZE / car.Diameter - 1) / 2.0f);
                // Получить зазор между препятствиями, достаточный для автомобиля
                float minSize = ((ROAD_SIZE / numberToAdd) - car.Diameter) / 2.0f;
        
                // Генерировать препятствия
                for (int i = 0; i < numberToAdd; i++)
                {
                    // Случайным образом сгенерировать размер в допустимом диапазоне
                    float depth = minDepth - ((float)Utility.Rnd.NextDouble() * minSize);
                    // Поправка глубины
                    depth -= (i * (car.Diameter * 2));
        
                    // Выбрать левую или правую стороны дороги
                    float location = (Utility.Rnd.Next(50)>25) 
                        ? ROAD_LOCATION_LEFT : ROAD_LOCATION_RIGHT;
        
                    // Добавить препятствие в коллекцию
                    obstacles.Add(new Obstacle(device, location, OBSTACLE_HEIGHT, depth));
                }
            }

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

    Теперь осталось сделать три вещи перед тем, как препятствия появятся в сцене:

  • Добавить вызов функции AddObstacles() генерации препятствий
  • Убедиться, что функция обновления вызывается для каждого препятствия в сцене
  • Отобразить препятствие на экране
  • Кроме этого, нужно предусмотреть функцию для установки в начальное положение всех переменных-характеристик для новой игры, поскольку начальные значения они принимают только при запуске приложения, а при повторной игре их нужно сбрасывать.

  • Добавьте в класс DodgerGame функцию LoadDefaultGameOptions() установки переменных игры в начальные значения
  • // Установка переменных игры в начальные значения
            private void LoadDefaultGameOptions()
            {
                // Характеристики дороги
                RoadDepth0 = 0.0f;
                RoadDepth1 = -100.0f;
                RoadSpeed = 30.0f;
        
                // Характеристики автомобиля
                car.Location = ROAD_LOCATION_LEFT;
                car.Speed = 10.0f;
                car.IsMovingLeft = false;
                car.IsMovingRight = false;
        
                // Удаляем препятствия предыдущей игры
                foreach (Obstacle o in obstacles)
                {
                    // Сначала освобождаем каждый ресурс 
                    // по отдельности, затем удаляем
                    o.Dispose();
                }
                obstacles.Clear();
        
                // Заполняем новыми препятствиями
                AddObstacles(RoadDepth1);
        
                // Стартуем наш таймер
                Utility.Timer(DirectXTimer.Start);
            }

    Уничтожение объектов препятствий происходит в два этапа. Сначала перебираются все ссылки на каждый объект препятствия и производится открепление каждой текущей ссылки. Освобожденные участки памяти на управляемой куче, ранее занимаемые Mesh -объектами препятствий, теперь не будут числиться за формой и станут "добычей" сборщика мусора. Затем динамический массив, содержащий освобожденные ссылки, освобождается и от самих ссылок, устанавливая начальную размерность.

  • Мы хотим устанавливать сброс характеристик игры только при начале новой игры, поэтому разместите вызов функции LoadDefaultGameOptions() в конце метода InitializeGraphics() класса DodgerGame
  • // Функция создания и инициализации объектов DirectX
            public void InitializeGraphics()
            {
                .....................................................
        
                // Прямой вызов обработчика
                this.OnDeviceReset(device, null);
        
                // Сброс переменных игры при новой игре
                LoadDefaultGameOptions();
            }
  • Для вычисления новой позиции препятствий при каждой смене кадра добавьте вызов функции LocationObstacle() в конец метода OnFrameUpdate() класса DodgerGame
  • // Функция обновления кадра, привязанная
            // к таймеру повышенной точности
            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;
                }
        
                // Вычисление координаты нового положения автомобиля
                car.LocationCar(elapsedTime);
        
                // Вычисление координаты нового положения препятствий
                foreach (Obstacle o in obstacles)
                {
                    o.LocationObstacle(elapsedTime, RoadSpeed);
                }
            }

    Теперь необходимо отобразить препятствия на сцене, для этого

  • Добавьте в метод OnPaint() класса DodgerGame после кода отображения автомобиля код отображения препятствий
  • // Код организации рендеринга
            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);
        
                    // Отображение автомобиля
                    car.DrawCar(device);
        
                    // Отображение препятствий
                    foreach (Obstacle o in obstacles)
                    {
                        o.DrawObstacle(device);
                    }
                }
                device.EndScene();
        
                device.Present();
                this.Invalidate();
            }
  • Запустите приложение
  • Препятствия появляются на дороге только один раз при запуске приложения потому, что мы их изначально генерируем только единыжды в функции LoadDefaultGameOptions() вызовом AddObstacles(RoadDepth1) в самом начале игры. Но после смены секций дороги создание новых препятствий не повторяется. Исправим это, разместив такой же вызов функции AddObstacles() создания препятствий в соответствующее место функции OnFrameUpdate()

  • Добавьте в код смены секций дороги функции OnFrameUpdate() класса DodgerGame вызов функции AddObstacles() создания препятствий
  • // Функция обновления кадра, привязанная
        // к таймеру повышенной точности
        private void OnFrameUpdate()
        {
          // Извлекаем точное время
          elapsedTime = Utility.Timer(DirectXTimer.GetElapsedTime);
      
          // Вычисляем текущую глубину
          RoadDepth0 += RoadSpeed * elapsedTime;
          RoadDepth1 += RoadSpeed * elapsedTime;
      
          // Проверяем необходимость смены секций дороги
          if(RoadDepth0>75.0F)
          {
            RoadDepth0 = RoadDepth1 - 100.0F;
            AddObstacles(RoadDepth0);
          }
          if(RoadDepth1>75.0F)
          {
            RoadDepth1 = RoadDepth0 - 100.0F;
            AddObstacles(RoadDepth1);
          }
      
          // Вычисление координаты нового положения автомобиля
          car.LocationCar(elapsedTime);
      
          // Вычисление координаты нового положения препятствий
          foreach(Obstacle o in obstacles)
          {
            o.LocationObstacle(elapsedTime, RoadSpeed);
          }
        }

    Теперь при смене секций дороги сразу будут генерироваться и новые препятствия для будущей секции.

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

  • Наберите в конце функции InitializeGraphics() код регистрации события щелчка и создания обработчика (код набирайте вручную, чтобы оболочка грамотно довершила работу)
  • Заполните заготовку обработчика, сгенерированную оболочкой, следующим кодом
  • // Приостановка движения
            static bool stopGame = true;
            void DodgerGame_Click(object sender, EventArgs e)
            {
                if (stopGame)
                {
                    Utility.Timer(DirectXTimer.Stop);
                    stopGame = false;
                }
                else
                {
                    Utility.Timer(DirectXTimer.Start);
                    stopGame = true;
                }
            }
  • Постройте приложение, примерный снимок экрана будет выглядеть так
  • На данном этапе мы достигли следующей функциональности:

  • Дорога наезжает на зрителя
  • Автомобиль управляется стрелками
  • Препятствия генерируются случайно
  • Случайной формы
  • Случайного цвета
  • В случайном месте
  • Препятствия проходят сквозь автомобиль
  • Перепятствия не вращаются
  • Движение останавливается при одинарном щелчке мышью на форме
  • Добавление вращений к препятствиям

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

  • Добавьте в секцию #region переменных-полей класса Obstacle следующие определения переменных
  • public class Obstacle
        {
            #region Секция характеристик препятствий
        
            ............................................
                
            // Переменные для вращения препятствий
            private float rotation = 0;
            private float rotationspeed = 0.0f;
            private Vector3 rotationVector;
        
            #endregion
        
            ............................................
        }

    Скорость вращения и оси вращения должны выбираться случайным образом.

  • Добавьте в конец конструктора класса Obstacle следующий код
  • // Параметризованный конструктор
            public Obstacle(Device device, float x, float y, float z)
            {
                ..................................................
        
                // Установить цвет препятствия
                obstacleMaterial = new Material();
                Color objColor = ObstacleColors[Utility.Rnd.Next(ObstacleColors.Length)];
                obstacleMaterial.Ambient = objColor;
                obstacleMaterial.Diffuse = objColor;
        
                // Параметры вращения препятствий
                rotationspeed = (float)Utility.Rnd.NextDouble() * (float)Math.PI;
                rotationVector = new Vector3((float)Utility.Rnd.NextDouble(),
                    (float)Utility.Rnd.NextDouble(), (float)Utility.Rnd.NextDouble());
            }
  • Добавьте переменную вращения в функцию LocationObstacle() класса Obstacle
  • // Вычисление положения препятствия
        public void LocationObstacle(float elapsedTime, float speed)
        {
          position.Z += (speed * elapsedTime);
          rotation += rotationspeed * elapsedTime;
        }
  • Добавьте в функцию рисования DrawObstacle() класса Obstacle левый матричный множитель
  • // Рисование препятствия
            public void DrawObstacle(Device device)
            {
                if (isTeapot)
                {
                    device.Transform.World = 
                        Matrix.RotationAxis(rotationVector, rotation) *
                        Matrix.Scaling(OBJECT_RADIUS, OBJECT_RADIUS, OBJECT_RADIUS) *
                        Matrix.Translation(position);
                }
                else
                {
                    device.Transform.World =
                        Matrix.RotationAxis(rotationVector, rotation) *
                        Matrix.Translation(position);
                }
        
                device.Material = obstacleMaterial;
                device.SetTexture(0, null);
                obstacleMesh.DrawSubset(0);
            }
  • Запустите приложение
  • Теперь препятствия вращаются вокруг случайных осей мировых координат и со случайной скоростью.

    Оформление атрибутики игры

    Добавим в движок игры необходимый код для начала игры, подсчета времени игры, подсчета очков с учетом сложности игры.

  • Добавьте в секцию #region класса DodgerGame объявления переменных-полей
  • #region Секция переменных-членов класса DodgerGame
        ..........................................................  
        // Переменные игры
        private bool isGameOver = true; // Стоп-игра, перекур!
        private int gameOverTick = 0; // Для обеспечения задержки
        private bool hasGameStarted = false; // Игра идет
        private int score = 0; // Очки
        #endregion
  • Добавьте в функцию LoadDefaultGameOptions() класса DodgerGame сброс счета игры
  • // Установка переменных игры в начальные значения
            private void LoadDefaultGameOptions()
            {
                // Характеристики дороги
                RoadDepth0 = 0.0f;
                RoadDepth1 = -100.0f;
                RoadSpeed = 30.0f;
        
                // Характеристики автомобиля
                car.Location = ROAD_LOCATION_LEFT;
                car.Speed = 10.0f;
                car.IsMovingLeft = false;
                car.IsMovingRight = false;
        
                // Сбрасываем очки 
                score = 0;
        
                // Удаляем препятствия предыдущей игры
                foreach (Obstacle o in obstacles)
                {
                    // Сначала освобождаем каждый ресурс 
                    // по отдельности, затем удаляем
                    o.Dispose();
                }
                obstacles.Clear();
        
                // Заполняем новыми препятствиями
                AddObstacles(RoadDepth1);
        
                // Стартуем наш таймер
                Utility.Timer(DirectXTimer.Start);
            }

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

  • Добавьте в функцию OnFrameUpdate() класса DodgerGame сразу после кода проверки необходимости смены секций дороги и генерирования новых препятствий следующий код
  • // Функция обновления кадра, привязанная
            // к таймеру повышенной точности
            private void OnFrameUpdate()
            {
                // Извлекаем точное время
                elapsedTime = Utility.Timer(DirectXTimer.GetElapsedTime);
        
                // Вычисляем текущую глубину дороги
                RoadDepth0 += RoadSpeed * elapsedTime;
                RoadDepth1 += RoadSpeed * elapsedTime;
        
                // Проверяем необходимость смены секций дороги
                if (RoadDepth0>75.0F)
                {
                    RoadDepth0 = RoadDepth1 - 100.0F;
                    AddObstacles(RoadDepth0);
                }
                if (RoadDepth1>75.0F)
                {
                    RoadDepth1 = RoadDepth0 - 100.0F;
                    AddObstacles(RoadDepth1);
                }
        
                // Преверяем, что препятствие миновало автомобиль
                // Увеличиваем очки и усложняем игру
                Obstacles removeObstacles = new Obstacles();
                foreach (Obstacle o in obstacles)
                {
                    if (o.Depth>car.Diameter - (Car.DEPTH / 2))
                    {
                        removeObstacles.Add(o);
                        // Увеличить скорость дороги
                        RoadSpeed += ROAD_SPEED_INCREMENT;
        
                        // Проверить, чтобы скорость не зашкалила
                        if (RoadSpeed>= MAXIMUM_ROAD_SPEED)
                        {
                            RoadSpeed = MAXIMUM_ROAD_SPEED;
                        }
        
                        // Увеличить скорость горизонтальных перемещений автомобиля
                        car.IncrementSpeed();
        
                        // Увеличить очки за количество миновавших препятствий
                        score += (int)(RoadSpeed * (RoadSpeed / car.Speed));
                    }
                }
        
                // Удалить миновавшие препятствия с помощью временного списка
                foreach (Obstacle o in removeObstacles)
                {
                    obstacles.Remove(o);
                    o.Dispose();// Вызвать сборщик мусора (упаковать)
                }
                removeObstacles.Clear();// Очистить временный список
        
                // Вычисление координаты нового положения автомобиля
                car.LocationCar(elapsedTime);
        
                // Вычисление координаты нового положения препятствий
                foreach (Obstacle o in obstacles)
                {
                    o.LocationObstacle(elapsedTime, RoadSpeed);
                }
            }

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

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

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

  • Добавьте в класс Obstacle ( не перепутайте с классом Obstacles!!!) новый метод IsHittingCar()
  • // Обнаружение столкновений автомобиля с препятствием
            public bool IsHittingCar(float carLocation, float carDiameter)
            {
                if (position.Z > (Car.DEPTH - (carDiameter / 2.0f)))
                {
                    // Проверка столкновения на правой стороне дороги
                    if ((carLocation < 0)  (position.X < 0))
                        return true;
        
                    // Проверка столкновения на левой стороне дороги
                    if ((carLocation > 0)  (position.X > 0))
                        return true;
                }
        
                return false;
            }
        }

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

  • Добавьте в конце функции OnFrameUpdate() класса DodgerGame, перед вычислением нового положения автомобиля и препятствий, следующий код
  • // Функция обновления кадра, привязанная
            // к таймеру повышенной точности
            private void OnFrameUpdate()
            {
                ..................................................
        
                // Удалить миновавшие препятствия с помощью временного списка
                foreach (Obstacle o in removeObstacles)
                {
                    obstacles.Remove(o);
                    o.Dispose();// Вызвать сборщик мусора (упаковать)
                }
                removeObstacles.Clear();// Очистить временный список
        
                // Проверка столкновения и остановка игры
                foreach (Obstacle o in obstacles)
                {
                    if (o.IsHittingCar(car.Location, car.Diameter))
                    {
                        // Если обнаружено столкновение - закончить игру!
                        isGameOver = true; // Стоп-игра, перекур!
                        gameOverTick = System.Environment.TickCount;
                        // Останавливаем таймер
                        Utility.Timer(DirectXTimer.Stop);
                    }
                }
        
                // Вычисление координаты нового положения автомобиля
                car.LocationCar(elapsedTime);
        
                // Вычисление координаты нового положения препятствий
                foreach (Obstacle o in obstacles)
                {
                    o.LocationObstacle(elapsedTime, RoadSpeed);
                }
            }
  • Запустите приложение
  • Все должно работать так, как мы задумали.

    Начало новой игры

    Игра останавливается, если автомобиль врезается в препятствие. Нам нужно задать возможность начать новую игру. Задействуем для этих целей клавишу пробела Space. Прежде всего, когда игра остановлена, нужно прекратить выполнение функции OnFrameUpdate() класса DodgerGame.

  • Поместите в начало функции OnFrameUpdate() класса DodgerGame признак остановки игры
  • // Функция обновления кадра, привязанная
        // к таймеру повышенной точности
        private void OnFrameUpdate()
        {
          // Если игра остановлена, не выполнять функцию
          if(isGameOver || !hasGameStarted)
            return;
      
          // Извлекаем точное время
          elapsedTime = Utility.Timer(DirectXTimer.GetElapsedTime);
          .....................................................
        }
  • Добавьте в переопределение функции OnKeyDown() класса DodgerGame код перезапуска игры по нажатию клавиши пробела Space
  • // Переопределение виртуального метода OnKeyDown() в классе DodgerGame
            protected override void OnKeyDown(KeyEventArgs e)
            {
                .........................................................
        
                // Space - перезапуск игры
                if (e.KeyCode == Keys.Space  isGameOver)
                {
                    LoadDefaultGameOptions();
                    isGameOver = false;
                    hasGameStarted = true;
                }
            }

    При желании, можно удалить вызов функции LoadDefaultGameOptions() из метода InitializeGraphics(), поскольку игра все-равно начнется только после нажатия клавиши Space, где вызов этой функции теперь тоже размещен.

    Давайте просто для тренировки создадим небольшую паузу между окончанием текущей игры и возможностью начала новой, чтобы все состояния игры физически успели восстановиться. Для этого поместим в функцию OnKeyDown() код, который на время 1000 миллисекунд (1 секунда) сделает недоступными все клавиши, кроме Escape.

  • Поместите в начало функции OnKeyDown() класса DodgerGame сразу после секции завершения игры следующий код
  • // Переопределение виртуального метода OnKeyDown() в классе DodgerGame
            protected override void OnKeyDown(KeyEventArgs e)
            {
                // Выход по Esc - завершение работы приложения
                if (e.KeyCode == Keys.Escape)
                    this.Close();
        
                // Блокировка клавиатуры
                if (System.Environment.TickCount - gameOverTick < 1000)
                    return;
        
                // Перемещение влево
                .....................................................
            }
  • Запустите приложение и убедитесь, что все работает как надо
  • Разработка табло результатов

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

    В дополнительной библиотеке Microsoft.Directx.Direct3DX.dll есть класс Microsoft.DirectX.Direct3D.Font, который можно использовать для отображения текста (классы одного и того же пространства имен могут физически быть размещены в разных библиотечных файлах). Но и в библиотеке System.Drawing.dll также есть класс System.Drawing.Font. Посколько в нашем приложении подключены ссылки на обе библиотеки, то без указания полного имени класса (с указанием пространства имен), компилятор не сможет самостоятельно определить, какой класс имеется ввиду. Но полное имя класса получится длинным, что может быть не всегда практично. Лучший способ - ввести короткий псевдоним нужного пространства имен, и использовать его для идентификации класса вместо длинной цепочки пространств имен.

    Поскольку мы собираемся использовать в нашей игре для отображения текста класс Microsoft.DirectX.Direct3D.Font, то для однозначного указания именно этого класса введем сокращенный псевдоним его пространства имен. Назовем псевдоним именем Direct3D

  • Добавьте в начало файла DodgerGame.cs после блока подключения пространств имен следующий код объявления псевдонима
  • using System;
    using System.Drawing;
    using System.Windows.Forms;
    using Microsoft.DirectX;
    using Microsoft.DirectX.Direct3D;
    using Direct3D = Microsoft.DirectX.Direct3D;
      
    namespace Dodger
    {
        class DodgerGame : Form
        {
    
            ..............................................
    
        }
    }

    Теперь класс Font можно однозначно определять в нашем приложении, если использовать для него сокращенный псевдоним, который мы только что создали.

    Создаваемые шрифты могут быть различного цвета, но должны имет один размер и семейство (стиль). При желании можно отображать несколько различных типов текста. Выберем для нашей игры два типа текста, которые будут поддерживаться двумя экземплярами класса Font.

  • Добавьте в секцию #region объявления переменных класса DodgerGame следующие ссылки
  • #region Секция переменных-членов класса
        ...............................................
          
            // Ссылки на шрифты 
        private Direct3D.Font scoreFont = null;
        private Direct3D.Font gameFont = null;
          
        #endregion

    Ссылки на объекты шрифтов мы заготовили, теперь нужно создать сами объекты.

  • Добавьте в конец функции InitializeGraphics() ( чтобы этот код размещался после создания устройства ) класса DodgerGame следующий код, используя псевдоним пространства имен Direct3D для однозначной адресации класса Font
  • // Функция создания и инициализации объектов DirectX
        public void InitializeGraphics()
        {
          ............................................................
            
                // Создать устройство
                device = new Device(adapterOrdinal, DeviceType.Hardware, 
                    this, flags, presentParams);
          ............................................................
            
                // Создание шрифтов
                scoreFont = new Direct3D.Font(device,
                    new System.Drawing.Font("Arial", 12.0f, 
                    FontStyle.Bold));
        
                gameFont = new Direct3D.Font(device,
                    new System.Drawing.Font("Arial", 36.0f, 
                    FontStyle.Bold | FontStyle.Italic));
        }

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

  • Добавьте в метод OnPaint() класса DodgerGame следующий код
  • // Код организации рендеринга
            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);
        
                    // Отображение автомобиля
                    car.DrawCar(device);
        
                    // Отображение препятствий
                    foreach (Obstacle o in obstacles)
                    {
                        o.DrawObstacle(device);
                    }
        
                    // Визуализация табло
                    if (hasGameStarted)
                    {
                        // Отображение очков
                        scoreFont.DrawText(string.Format("Текущие очки: {0}", score),
                            new Rectangle(5, 5, 0, 0), 
                            DrawTextFormat.NoClip, Color.Yellow);
                    }
                    if (isGameOver)
                    {
                        // Если игра закончена, уведомить игрока
                        if (hasGameStarted)
                        {
                            gameFont.DrawText("Вы проиграли. Ха-ха-ха!",
                                new Rectangle(25, 45, 0, 0), 
                                DrawTextFormat.NoClip, Color.Red);
                        }
                        if ((System.Environment.TickCount - gameOverTick)>= 1000)
                        {
                            // Вывести подсказку - как начать игру, если после 
                            // остановки предыдущей игры прошло более 1 секунды
                            gameFont.DrawText("Нажмите клавишу Space",
                                new Rectangle(25, 130, 0, 0), 
                                DrawTextFormat.NoClip, Color.WhiteSmoke);
                        }
                    }
                }
                device.EndScene();
        
                device.Present();
                this.Invalidate();
            }
  • Запустите игру и полюбуйтесь на выполненную работу
  • Добавление текстовых надписей и запись лучших результатов в системный реестр

    Теперь у нас есть работоспособный вариант игры. Но Родина должна знать своих героев. Желательно для этого предусмотреть возможность сохранения нескольких игроков с лучшими достижениями.

  • Для сохранения лучших результатов добавьте в главное пространство имен Dodger файла DodgerGame.cs (перед или после класса DodgerGame ) простую структуру
  • namespace Dodger
    {
      public class DodgerGame : Form
      {
            ...............................................
      } 
      
      // Структура для сохранения лучших достижений игры
      public struct HighScore
      {
        private int realScore; // Результат
        private string playerName; // Игрок
      
        // Свойства для доступа к закрытым членам
        public int Score
        {
          get
          {
            return realScore;
          }
      
          set
          {
            realScore = value;
          }
        }
          
        public string Name
        {
          get
          {
            return playerName;
          }
          set
          {
            playerName = value;
          }
        }
      }
    }

    После объявления структуры нужно в движке игры создать массив (список) для требуемого количества игроков. Мы будем сохранять результаты трех лучших игроков.

  • Добавьте в конец секции #region объявления переменных класса DodgerGame следующий код
  • #region Секция переменных-членов класса DodgerGame
        
            ...............................................
            
            // Ссылки на шрифты 
            private Direct3D.Font scoreFont = null;
            private Direct3D.Font gameFont = null;
        
            // Информация о трех лучших достижениях
            private HighScore[] highScores = new HighScore[3];
            private string defaultHighScoreName = string.Empty;
        
            #endregion

    Теперь создадим три отдельных функции. Первая функция CheckHighScore() проверит текущий счет и сравнит его с имеющимся, чтобы внести при необходимости новое лучшее значение в список. Вторая функция SaveHighScores() сохранит информацию о наилучших результатах в системный реестр перед завершением работы приложения, а третья функция LoadHighScores() будет вызывать эти значения из системного реестра при запуске приложения.

  • Добавьте код перечисленных функций в конец класса DodgerGame
  • public class DodgerGame : Form
      {
           ...............................................................
           
        // Функции поддержки лучших результатов в истории игры
        // Если текущий игрок побеждает, то запоминаем его
        // индекс и просим ввести свое имя
        private void CheckHighScore()
        {
          int index = -1;// Сбрасываем текущий индекс
          for (int i = highScores.Length - 1; i>= 0; i--)
          {
            if (score>= highScores[i].Score) // Нашли, кого заменить
            {
              index = i;
            }
          }
      
          // Делаем замену, если индекс отличен от -1
          if (index>= 0)
          {
            // Освобождаем найденную позицию 
            for (int i = highScores.Length - 1; i>index ; i--)
            {
              highScores[i] = highScores[i-1];
            }
            // Сохраняем на освободившемся месте текущий результат
            highScores[index].Score = score;
            highScores[index].Name = Input.InputBox("Вы получили высокий счет!!!", 
              "Просим ввести ваше имя:", defaultHighScoreName);
          }
        }
      
        // Сохранение лучших результатов в системном реестре
        public void SaveHighScores()
        {
          Microsoft.Win32.RegistryKey key = 
            Microsoft.Win32.Registry.CurrentUser.CreateSubKey(
            "Software\\MDXBoox\\Dodger");
      
          try // Делаем попытку
          {
            for(int i = 0; i < highScores.Length; i++)
            {
              key.SetValue(string.Format("Player{0}", i), highScores[i].Name);
              key.SetValue(string.Format("Score{0}", i), highScores[i].Score);
            }
            key.SetValue("PlayerName", defaultHighScoreName);
          }
          finally // Выполняем при любом исходе
          {
            if (key != null)
            {
              key.Close(); // Обязательно закрываем ключ
            }
          }
        }
      
        // Загрузка списка лучших результатов из системного реестра
        private void LoadHighScores()
        {
          Microsoft.Win32.RegistryKey key = 
            Microsoft.Win32.Registry.CurrentUser.CreateSubKey(
            "Software\\MDXBoox\\Dodger");
      
          try // Делаем попытку
          {
            for(int i = 0; i < highScores.Length; i++)
            {
              highScores[i].Name = (string)key.GetValue(
                string.Format("Player{0}", i),  string.Empty);
      
              highScores[i].Score = (int)key.GetValue(
                string.Format("Score{0}", i),  0);
            }
            defaultHighScoreName = (string)key.GetValue(
              "PlayerName", System.Environment.UserName);
          }
          finally // Выполняем при любом исходе
          {
            if (key != null)
            {
              key.Close(); // Обязательно закрываем ключ
            }
          }
        }
      }

    Мы пытаемся сохранить сведения в узле HKEY_LOCAL_MACHINE системного реестра. Если к нему нет доступа, измените код для записи в HKEY_CURRENT_USER.

    Если теперь попытаться откомпилировать приложение, то компилятор скажет, что не найден класс Input в подключенных к проекту пространствах имен. Его нет среди библиотечных классов C#. Это дополнительный пользовательский класс (делали умные люди - проклятые капиталисты), который находится в отдельном файле InputBox.cs каталога Source, прилагаемого к данной лабораторной работе. Он имитирует работу одноименного класса библиотеки для Visual Basic фирмы Microsoft.

  • Выполните команду Project/Add Existing Item меню оболочки, найдите в прилагаемом каталоге Source файл InputBox.cs и присоедините его к проекту. Оболочка физически скопирует его в место размещения проекта вместе с файлом ресурсов
  • Откомпилируйте проект командой Project/Build Dodger, теперь компилятор успокоился
  • Теперь главной задачей является правильное размещение вызовов наших функций поддержки лучших достижений. Проверка того, входит ли текущий результат в тройку лучших, должна производится сразу после завершения текущей игры.

  • Добавьте в секцию кода проверки столкновения и остановки игры функции OnFrameUpdate() класса DodgerGame следующий код
  • // Функция обновления кадра, привязанная
            // к таймеру повышенной точности
            private void OnFrameUpdate()
            {   
    
                ......................................................
        
                // Удалить миновавшие препятствия с помощью временного списка
                foreach (Obstacle o in removeObstacles)
                {
                    obstacles.Remove(o);
                    o.Dispose();// Вызвать сборщик мусора (упаковать)
                }
                removeObstacles.Clear();// Очистить временный список
        
                // Проверка столкновения и остановка игры
                foreach (Obstacle o in obstacles)
                {
                    if (o.IsHittingCar(car.Location, car.Diameter))
                    {
                        // Если обнаружено столкновение - закончить игру!
                        isGameOver = true; // Стоп-игра, перекур!
                        gameOverTick = System.Environment.TickCount;
                        // Останавливаем таймер
                        Utility.Timer(DirectXTimer.Stop);
        
                        // Проверить результат среди лучших игроков
                        CheckHighScore();
                    }
                }
        
                // Вычисление координаты нового положения автомобиля
                car.LocationCar(elapsedTime);
        
                // Вычисление координаты нового положения препятствий
                foreach (Obstacle o in obstacles)
                {
                    o.LocationObstacle(elapsedTime, RoadSpeed);
                }
            }

    Запись рекордов в системный реестр должна выполняться перед завершением работы приложения непосредственно после выхода из цикла сообщений Windows.

  • Откройте через панель Solution Explorer на редактирование файл AppEntry.cs приложения и после цикла сообщений добавьте вызов функции сохранения результатов
  • using System;
        
    namespace Dodger
    {
        class AppEntry
        {
            static void Main()
            {
                using (DodgerGame frm = new DodgerGame())
                {
                    frm.InitializeGraphics();
                    System.Windows.Forms.Application.Run(frm);
        
                    // Запись результатов в системный реестр
                    frm.SaveHighScores();
                }
            }
        }
    }

    Чтение результатов из системного реестра нужно выполнять сразу после запуска приложения, поэтому вызов функции чтения разместим в конструкторе класса DodgerGame.

  • Поместите в конец конструктора DodgerGame() класса DodgerGame код чтения достижений из системного реестра
  • public DodgerGame()
            {
                this.Size = new Size(800, 600); // Размер окна формы
                this.Text = "Lab37. Игра Dodger"; // Заголовок окна
                this.SetStyle(ControlStyles.AllPaintingInWmPaint
                    | ControlStyles.Opaque, true);
        
                // Чтение лучших результатов из системного реестра
                this.LoadHighScores();
            }

    И последнее действие в данной марафонской лабораторной работе - вывести лучшие достижения на экран.

  • Добавьте в функцию OnPain() класса DodgerGame в секцию показа результатов следующий код
  • // Код организации рендеринга
            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);
        
                    // Отображение автомобиля
                    car.DrawCar(device);
        
                    // Отображение препятствий
                    foreach (Obstacle o in obstacles)
                    {
                        o.DrawObstacle(device);
                    }
        
                    // Визуализация табло
                    if (hasGameStarted)
                    {
                        // Отображение очков
                        scoreFont.DrawText(string.Format("Текущие очки: {0}", score),
                            new Rectangle(5, 5, 0, 0), 
                            DrawTextFormat.NoClip, Color.Yellow);
                    }
                    if (isGameOver)
                    {
                        // Если игра закончена, уведомить игрока
                        if (hasGameStarted)
                        {
                            gameFont.DrawText("Вы проиграли. Ха-ха-ха!",
                                new Rectangle(25, 45, 0, 0), 
                                DrawTextFormat.NoClip, Color.Red);
                        }
                        if ((System.Environment.TickCount - gameOverTick) >= 1000)
                        {
                            // Вывести подсказку - как начать игру, если после 
                            // остановки предыдущей игры прошло более 1 секунды
                            gameFont.DrawText("Нажмите клавишу Space",
                                new Rectangle(25, 130, 0, 0), 
                                DrawTextFormat.NoClip, Color.WhiteSmoke);
                        }
        
                        // Показ лучших достижений
                        gameFont.DrawText("Лучшие результаты: ", 
                            new Rectangle(25, 205, 0, 0),
                            DrawTextFormat.NoClip, Color.Red);
        
                        for (int i = 0; i < highScores.Length; i++)
                        {
                            gameFont.DrawText(string.Format("Игрок: {0} : {1}", 
                                highScores[i].Name,
                                highScores[i].Score),
                                new Rectangle(25, 270 + (i * 55), 0, 0), 
                                DrawTextFormat.NoClip, Color.Yellow);
                        }
                    }
                }
                device.EndScene();
        
                device.Present();
                this.Invalidate();
            }
  • Запустите игру и проверьте ее функциональные возможности
  • Должно внешне быть примерно такое

    Уважаемые Студенты-программисты (и примкнувшие к ним КОЧЕГАРЫ)!!! Сделайте все своими ручками. Попытайтесь разобраться в коде. У вас впереди будет много блестящих программ, но эти "кривые" - самые первые, поэтому и запомнятся вам как самые родные ( или двоюродные ).

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