Китай |
Программирование простой игры в DirectX
Размещение кода создания и отображения препятствий в движке игры
Имея созданные нами базовые возможности по управлению препятствиями можно приступить к их добавлению в основном классе приложения DodgerGame.
- Добавьте в секцию #region класса DodgerGame объявление ссылки для поддержки списка текущих препятствий в сцене и константу для задания высоты препятствий
#region Секция переменных-членов класса DodgerGame .......................................... // Ссылка на объект автомобиля private Car car = null; // Ссылка на препятствия private Obstacles obstacles; // Высота препятствий private const float OBSTACLE_HEIGHT = Car.HEIGHT * 0.85f; #endregionЛистинг 17.46. Добавление переменной и константы в класс DodgerGame
- Добавьте в обработчик события сброса устройства OnDeviceReset() класса DodgerGame создание экземпляра класса-коллекции препятствий
// Обработчик события сброса устройства private void OnDeviceReset(object sender, EventArgs e) { .............................................. // Загрузка Mesh-файла и создание объекта автомобиля car = new Car(device); // Создание объекта-коллекции препятствий obstacles = new Obstacles(); }Листинг 17.47. Создание объекта-коллекции в обработчике OnDeviceReset() класса DodgerGame
- Добавьте в класс 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)); } }Листинг 17.48. Функция AddObstacles() генерации препятствий в классе DodgerGame
Эта функция вначале вычисляет число препятствий, которое необходимо добавить в данной дорожной секции. Долее проверяется, чтобы между препятствиями имеелось достаточное пространство для размещения автомобиля. После этого в цикле функция случайным образом помещает очередное препятствие на участке дороги слева или справа и добавляет его к списку препятствий.
Теперь осталось сделать три вещи перед тем, как препятствия появятся в сцене:
- Добавить вызов функции 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); }Листинг 17.49. Функция LoadDefaultGameOptions() сброса опций
Уничтожение объектов препятствий происходит в два этапа. Сначала перебираются все ссылки на каждый объект препятствия и производится открепление каждой текущей ссылки. Освобожденные участки памяти на управляемой куче, ранее занимаемые Mesh -объектами препятствий, теперь не будут числиться за формой и станут "добычей" сборщика мусора. Затем динамический массив, содержащий освобожденные ссылки, освобождается и от самих ссылок, устанавливая начальную размерность.
- Мы хотим устанавливать сброс характеристик игры только при начале новой игры, поэтому разместите вызов функции LoadDefaultGameOptions() в конце метода InitializeGraphics() класса DodgerGame
// Функция создания и инициализации объектов DirectX public void InitializeGraphics() { ..................................................... // Прямой вызов обработчика this.OnDeviceReset(device, null); // Сброс переменных игры при новой игре LoadDefaultGameOptions(); }Листинг 17.50. Вызов функции 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); } }Листинг 17.51. Вызов функции LocationObstacle() вычисления новых позиций препятствий
Теперь необходимо отобразить препятствия на сцене, для этого
- Добавьте в метод 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(); }Листинг 17.52. Добавление вызова функции отображения препятствий в DodgerGame.OnPaint()
- Запустите приложение
Препятствия появляются на дороге только один раз при запуске приложения потому, что мы их изначально генерируем только единыжды в функции 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); } }Листинг 17.53. Добавление препятствий в функции OnFrameUpdate()
Теперь при смене секций дороги сразу будут генерироваться и новые препятствия для будущей секции.
Прежде, чем запускать игру, введем возможность ее останавливать. Для имитации остановки игры введем обработчик щелчка на клиентской области формы левой кнопкой мыши. В самом обработчике можно устанавливать весовую переменную 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; } }Листинг 17.54. Обработчик приостановки игры в классе DodgerGame
- Постройте приложение, примерный снимок экрана будет выглядеть так
На данном этапе мы достигли следующей функциональности:
- Дорога наезжает на зрителя
- Автомобиль управляется стрелками
- Препятствия генерируются случайно
- Случайной формы
- Случайного цвета
- В случайном месте
- Препятствия проходят сквозь автомобиль
- Перепятствия не вращаются
- Движение останавливается при одинарном щелчке мышью на форме
Добавление вращений к препятствиям
Добавим несколько переменных к классу Obstacle, чтобы управлять вращением препятствий для демонстрации возможностей DirectX.
public class Obstacle { #region Секция характеристик препятствий ............................................ // Переменные для вращения препятствий private float rotation = 0; private float rotationspeed = 0.0f; private Vector3 rotationVector; #endregion ............................................ }Листинг 17.55. Переменные для управления вращением препятствий в классе 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()); }Листинг 17.56. Определение скорости и осей вращения препятствий в конструкторе Obstacle()
// Вычисление положения препятствия public void LocationObstacle(float elapsedTime, float speed) { position.Z += (speed * elapsedTime); rotation += rotationspeed * elapsedTime; }Листинг 17.57. Вычисление поворота в функции LocationObstacle()
// Рисование препятствия 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); }Листинг 17.58. Поворот препятствий перед рисованием
- Запустите приложение
Теперь препятствия вращаются вокруг случайных осей мировых координат и со случайной скоростью.
Оформление атрибутики игры
Добавим в движок игры необходимый код для начала игры, подсчета времени игры, подсчета очков с учетом сложности игры.
#region Секция переменных-членов класса DodgerGame .......................................................... // Переменные игры private bool isGameOver = true; // Стоп-игра, перекур! private int gameOverTick = 0; // Для обеспечения задержки private bool hasGameStarted = false; // Игра идет private int score = 0; // Очки #endregionЛистинг 17.59. Добавление переменных с информацией об игре в класс DodgerGame
- Добавьте в функцию 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); }Листинг 17.60. Сброс переменных в функции LoadDefaultGameOptions()
Теперь нам нужно ввести код для обнаружения того, что препятствие миновало автомобиль. При этом мы должны будем увеличивать очки игроку, но будем, также, увеличивать скорость дороги и горизонтального перемещения автомобиля для повышения трудности продолжения игры. Чуть позже мы введем проверку столкновения, при обнаружении которого сразу остановим игру. Если столкновений не будет, то игра не остановится, препятствие будет считаться пройденным успешно, а очки и скорость увеличатся.
- Добавьте в функцию 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); } }Листинг 17.61. При проезде препятствия увеличить очки и скорость
Как только препятствие миновало заднюю границу автомобиля, оно считается прошедшим, за это увеличиваются очки. Но такие препятствия нужно сразу удалять, чтобы не считалось, что они все еще находятся в зоне риска столкновения с автомобилем. Удаление препятствий из основной коллекции препятствий выполняется с помощью временной коллекции removeObstacles.
- Запустите приложение
Подождите немного и убедитесь, что теперь с каждым проездом препятствия скорость дороги и скорость горизонтального перемещения автомобиля постепенно нарастают.
Теперь нужно проверить те препятствия из оставшихся в списке, которые только подходят к автомобилю. Если какое-то из них пересекло переднюю границу автомобиля и находится с той-же стороны дороги, что и автомобиль, то значит автомобиль врезался в одно из препятствий.
// Обнаружение столкновений автомобиля с препятствием 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; } }Листинг 17.62. Функция IsHittingCar() обнаружения столкновения с препятствием
Мы отлавливаем момент, когда автомобиль находится в той же самой глубине секции и с той же самой стороны дороги, что и препятствие. Теперь необходимо внести эту проверку в движок игры.
- Добавьте в конце функции 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); } }Листинг 17.63. Проверка столкновений в движке игры
- Запустите приложение
Все должно работать так, как мы задумали.