Share via


Создание и исследование трехмерных лабиринтов с помощью Silverlight 5

Грэг Дункан

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

Сегодняшний проект не только познакомит нас с созданием и генерацией случайного лабиринта, но теперь, используя новые трехмерные возможности Silverlight 5, мы можем также изучать наши создания…

Создание трехмерного лабиринта с помощью Silverlight 5.0

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

Лабиринт на этом видео целиком выполнен с помощью 3D API Silverlight 5.0 и полностью совместим с битами RTW. Лабиринт случайным образом генерируется каждый раз, когда вы загружаете программу, и вы можете управлять движением с помощью клавиатуры.

Генерируем лабиринт

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

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

...

 public void Generate()
  {
      ThreadPool.QueueUserWorkItem(
          o =>
          {
              int progress = 0;

              Stack<Cell> stack = new Stack<Cell>();
              this.Reset();

              Cell cell = this.GetRandom(0);
              cell.Type = CellType.Enter;
              cell.Visited = true;

              while (true)
              {
                  Cell adiacent = this.GetAdiacentNonVisited(cell);

                  if (adiacent != null)
                  {
                      stack.Push(cell);
                      adiacent.Visited = true;
                      cell = adiacent;

                      progress++;

                      this.OnGenerationProgressChanged((int)(progress * 100.0 / (this.Width * this.Height)));
                  }
                  else
                  {
                      if (stack.Count > 0)
                          cell = stack.Pop();
                      else
                          break;
                  }
              }

              cell = this.GetRandom(this.Height - 1);
              cell.Type = CellType.Exit;

              this.OnGenerationCompleted();
          });
  }

Создание трехмерной картинки

После того, как лабиринт рассчитан, время нарисовать его, используя Silverlight 3D API. Отрисовка осуществляется путем создания плана квадрата, представляющего пол лабиринта и последующих итераций по всем ячейкам и созданием оставшихся стен.

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

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

...

 private void AddWall(List<VertexPositionColor> edges, float xOffset, float zOffset, float xSize, float zSize)
  {
      var wall = new List<VertexPositionColor>();

      Vector3 topLeftFront = new Vector3(xOffset, this.Height, zOffset + zSize);
      Vector3 bottomLeftFront = new Vector3(xOffset, 0.0f, zOffset + zSize);
      Vector3 topRightFront = new Vector3(xOffset + xSize, this.Height, zOffset + zSize);
      Vector3 bottomRightFront = new Vector3(xOffset + xSize, 0.0f, zOffset + zSize);
      Vector3 topLeftBack = new Vector3(xOffset, this.Height, zOffset);
      Vector3 topRightBack = new Vector3(xOffset + xSize, this.Height, zOffset);
      Vector3 bottomLeftBack = new Vector3(xOffset, 0.0f, zOffset);
      Vector3 bottomRightBack = new Vector3(xOffset + xSize, 0.0f, zOffset);

      Color c1 = Color.FromNonPremultiplied(200, 200, 200, 255);
      Color c2 = Color.FromNonPremultiplied(150, 150, 150, 255);
      Color c3 = Color.FromNonPremultiplied(100, 100, 100, 255);

      // Front face
      wall.Add(new VertexPositionColor(topRightFront, c1));
      wall.Add(new VertexPositionColor(bottomLeftFront, c1));
      wall.Add(new VertexPositionColor(topLeftFront, c1));
      wall.Add(new VertexPositionColor(topRightFront, c1));
      wall.Add(new VertexPositionColor(bottomRightFront, c1));
      wall.Add(new VertexPositionColor(bottomLeftFront, c1));

      // Back face 
      wall.Add(new VertexPositionColor(bottomLeftBack, c1));
      wall.Add(new VertexPositionColor(topRightBack, c1));
      wall.Add(new VertexPositionColor(topLeftBack, c1));
      wall.Add(new VertexPositionColor(bottomRightBack, c1));
      wall.Add(new VertexPositionColor(topRightBack, c1));
      wall.Add(new VertexPositionColor(bottomLeftBack, c1));

      // Top face
      wall.Add(new VertexPositionColor(topLeftBack, c2));
      wall.Add(new VertexPositionColor(topRightBack, c2));
      wall.Add(new VertexPositionColor(topLeftFront, c2));
      wall.Add(new VertexPositionColor(topRightBack, c2));
      wall.Add(new VertexPositionColor(topRightFront, c2));
      wall.Add(new VertexPositionColor(topLeftFront, c2));

      // Left face
      wall.Add(new VertexPositionColor(bottomLeftFront, c3));
      wall.Add(new VertexPositionColor(bottomLeftBack, c3));
      wall.Add(new VertexPositionColor(topLeftFront, c3));
      wall.Add(new VertexPositionColor(topLeftFront, c3));
      wall.Add(new VertexPositionColor(bottomLeftBack, c3));
      wall.Add(new VertexPositionColor(topLeftBack, c3));

      // Right face 
      wall.Add(new VertexPositionColor(bottomRightBack, c3));
      wall.Add(new VertexPositionColor(bottomRightFront, c3));
      wall.Add(new VertexPositionColor(topRightFront, c3));
      wall.Add(new VertexPositionColor(bottomRightBack, c3));
      wall.Add(new VertexPositionColor(topRightFront, c3));
      wall.Add(new VertexPositionColor(topRightBack, c3));

      edges.AddRange(wall);
  }

Передвиженияигрокаисоздание сплошных стен

Удивительно впервые взглянуть на нарисованный лабиринт, но еще более потрясающе двигаться внутри него с помощью клавиатуры. Каждый раз когда свойство объекта Observer изменяется, эти изменения отражаются наружу с помощью события Change. Событие вызывает обновление положения камеры и перерисовывает сцену. Используя стрелки «вверх» и «вниз» можно двигаться вперед и назад, а с помощью стрелок «вправо» и «влево» можно менять направление движения на соответствующие направления. Кроме того, можно использовать клавиши PageUp и PageDown для наклона камеры вниз и вверх, как если бы наблюдатель наклонял голову.

Чтобы постоянно перехватывать события клавиатуры я использовал прием. К этому пришлось прибегнуть, поскольку Silverlight не повторяет автоматически события клавиатуры. Используя класс KeyboardState, я создал проверочное условие для изменения состояния KeyUp и KeyDown отслеживаемых клавиш. Затем таймер изменяет свойства наблюдателя в соответствии с нажатой клавишей. Такой прием позволяет также использовать сразу более одной клавиши, давая возможность наблюдателю поворачивать в момент, когда он движется вперед.

...

Работа с 3D

Работа с 3D в Silverlight очень интересна, но чтобы достичь хороших результатов требуется глубокое знание 3D-математики. Каждый раз, когда вы получаете что-то реально эффективное благодаря использованию API низкого уровня, интерфейс программирования становится слишком трудным. Так что если вам нужно работать с 3D, я предлагаю использовать платформу высокого уровня, абстрагирующую API и позволяющую мыслить в терминах сплошных фигур, а не коллекции границ. Я полагаю, Балдер привел прекрасный наглядный пример https://balder.codeplex.com/.

Скачать: https://www.silverlightplayground.org/assets/sources/SLPG.Maze.zip (270kb)

Вот снимок решения:

clip_image002

clip_image004

И решение, запущенное на моем ноутбуке (которое у меня заработало с первого раза без проблем). clip_image006

Если вы размышляете над тем как поиграть с новыми возможностями 3D, доступными в Silverlight 5 или являетесь фанатом лабиринтов, этот проект может заинтересовать вас…