Файлы к лабораторной работе Вы можете скачать здесь.
В настоящей лабораторной работе мы используем средства управляемого и построим простую игру. Для любого
коммерческого варианта игры нужно вначале построить подробный
план, в котором тщательно продумать все существенные детали
и игру в целом. Обычно для этого применяется язык диаграмм . Но наша игра простая и цели наши другие.
Программу построим из трех основных классов:
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 работы компилятора и постройте
проект. Должна появиться пустая формаНа этом подготовительные операции по созданию заготовки
проекта окончены. Теперь приступим к программированию самой
игры с использованием классов управляемого .
Движком игры у нас будет класс 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);
}
}
}
Сначала мы создаем и устанавливаем параметры представления
устройства, в том числе включаем буфер глубины. Затем извлекаем
и сохраняем свободный порядковый номер устройства и устанавливаем
изначально с помощью поразрядной операции 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() выполните вручную!!! и начните с ввода ключевого слова
Добавьте вручную (чтобы оболочка правильно сгенерировала аргументы
метода) в класс 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);
}
В приведенном коде вначале выясняется, способна ли видеокарта
поддерживать анизотропную фильтрацию
Откомпилируйте
проект и убедитесь, что дорога приобрела более 
Некоторая шероховатость на поверхности дороги все - же осталась, но это к лучшему! - более реалистичным будет казаться эффект перемещения дороги, к которому мы сейчас приступим.
Скорость перемещения дороги, если не принять специальных
мер, зависит от частоты смены кадров на конкретном компьютере.
Это значит, что на разных компьютерах скорость будет разной.
Чтобы сделать скорость независимой от конкретных компьютеров,
нужно привязать ее к некоторой независимой от частоты кадров
величине. Такой величиной является тактовое время . Этот класс не является
библиотечным, но поставляется дополнительно с инструменальными
средствами разработчика . Исходный
код этого класса находится в файле и
прилагается к данной лабораторной работе.
Выполните
команду меню оболочки 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();
}
Запустите
приложение и убедитесь, что дорога наезжает на зрителяИмея движущуюся дорогу, приступим к программированию автомобиля. Для этого создадим отдельный класс, в котором разместим всю необходимую для управления автомобилем функциональность. В процессе рендеринга автомобиль должен оставаться неподвижным относительно перемещающейся сцены, за исключением перемещений влево-вправо, когда он будет объезжать препятствия, которые мы создадим позднее.
Выполните
команду 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, только с другими ссылками. Мы вычислили
граничную сферу, которая полностью включает все точки автомобиля,
полученные из вершинного буфера. При использовании вершинного
буфера автомобиля его необходимо замкнуть - сделать недоступным
другим потокам. В блоке ,
который будет выполнен всегда и последним, мы разблокируем
и освобождаем вершинный буфер.
Далее необходимо добавить метод, выполняющий прорисовку нашего автомобиля. В качестве аргумента нужно передать в этот метод ссылку на устройство, которое должно быть одним для всей сцены.
Добавьте
в класс 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);
}
}
Здесь мы использовали такой-же код, как и при отображении дороги, но с предварительным масштабированием автомобиля.
Ранее в секции # мы
создали ссылочные поля с доступом 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
Теперь у нас есть класс и нужно разместить
код создания и использования его экземпляра (объекта) в
основном движке игры - классе DodgerGame.
Поместите
в секцию #region DodgerGame ссылку
на объект класса Car #region Секция переменных-членов класса DodgerGame
..................................................
// Ссылка на объект автомобиля
private Car car = null;
#endregion
Поскольку мы используем ссылку на устройство в конструкторе
класса , то объект устройства должен
быть создан прежде создания объекта автомобиля. Это так и
есть, поскольку устройство мы создаем в функции 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);
}
Запустите
приложение и убедитесь, что положение автомобиля управляется
стрелками обеих клавиатур и все закрепленные за клавишами
функциональности выполняютсяНа данном этапе мы создали движущуюся дорогу и управляемый
автомобиль (хоть управление и примитивное, но поучительное
с точки зрения кода). У нас получился автомобильный
симулятор - создается полное ощущение
движения автомобиля. Теперь мы создадим препятствия, которые
пользователь нашей игры должен объезжать. Ранее мы предусмотрели
в параметризованном конструкторе автомобиля создание сферы,
чтобы можно было контролировать моменты столкновения автомобиля
с препятствиями при неудачном их объезде. Весь код создания
препятствий упакуем в новом классе с именем (препятствия).
В
панели 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 переменные и константы
характеристик препятствий, упакованные в секцию #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 -объектов (сфера,
куб, тор, цилиндр, заварочный чайник - 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;
}
Вначале решим задачу для правильного отображения одного препятствия. Поскольку сгенерированное препятствие должно быть неподвижным относительно движущейся дороги, необходимо организовать его перемещение при рисовании с такой-же скоростью, что и дороги.
Добавьте
в класс 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 Освобождение ресурсов для сборщика мусора
// Для принудительного вызова при смене препятствий
public void Dispose()
{
obstacleMesh.Dispose();
System.GC.SuppressFinalize(this);
}
// Для автоматического вызова при завершении игры
~Obstacle() // Деструктор
{
this.Dispose();
}
#endregion
Здесь мы вызываем метод класса 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();
}
}
В данном коде мы используем библиотечный класс для
создания .
В нашем классе мы создали свойство для прямого индексированного доступа
к хранимым в возможность
доступа к препятствиям языка
C#. Мы также упаковали в своем классе три Add ), поэлементного
удаления ( )
и групповой очистки ( ) класса .
Для
доступности сокращенных имен ArrayList и IEnumerable известите
компилятор об используемом библиотечном пространстве имен System.Collections соответствующей using -директивой
в начале файла Obstacle .csusing System;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using System.Collections;
namespace Dodger
{
...................................................
}
Интерфейс -
это не более чем именованный набор членов все
интерфейсные классы начинаются с символа 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;
}
}
Постройте
приложение, примерный снимок экрана будет выглядеть так
На данном этапе мы достигли следующей функциональности:
Добавим несколько переменных к классу ,
чтобы управлять вращением препятствий для демонстрации возможностей .
Добавьте
в секцию #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);
}
}
Запустите
приложениеВсе должно работать так, как мы задумали.
Игра останавливается, если автомобиль врезается в препятствие.
Нам нужно задать возможность начать новую игру. Задействуем
для этих целей клавишу пробела .
Прежде всего, когда игра остановлена, нужно прекратить выполнение
функции 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(), поскольку игра
все-равно начнется только после нажатия клавиши ,
где вызов этой функции теперь тоже размещен.
Давайте просто для тренировки создадим небольшую паузу между
окончанием текущей игры и возможностью начала новой, чтобы
все состояния игры физически успели восстановиться. Для этого
поместим в функцию OnKeyDown() код, который
на время 1000 миллисекунд (1 секунда) сделает недоступными
все клавиши, кроме .
Поместите
в начало функции 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. есть
класс Microsoft.,
который можно использовать для отображения текста (классы
одного и того же пространства имен могут физически быть размещены
в разных библиотечных файлах). Но и в библиотеке System.Drawing. также
есть класс System.Drawing.Font.
Посколько в нашем приложении подключены ссылки на обе библиотеки,
то без указания полного имени класса (с указанием пространства
имен), компилятор не сможет самостоятельно определить, какой
класс имеется ввиду. Но полное
Поскольку мы собираемся использовать в нашей игре для отображения
текста класс Microsoft.
то для однозначного указания именно этого класса введем
сокращенный
Добавьте
в начало файла 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() будет
вызывать эти значения из системного реестра при запуске приложения.
Добавьте
код перечисленных функций в конец класса DodgerGamepublic 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
фирмы 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();
}
Запустите
игру и проверьте ее функциональные возможностиДолжно внешне быть примерно такое


Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.