Как создать игру с астероидами на Silverlight - Обработка клавиатурных событий

ОГЛАВЛЕНИЕ

Обработка клавиатурных событий

Одной из проблем, возникших при разработке игры, была обработка клавиатурных событий. Вскоре выяснилось, что нельзя полагаться на стандартное событие KeyDown, поскольку оно не возбуждалось должным образом. Игра должна была очень быстро реагировать на нажатие клавиши пользователем. Стандартная обработка событий не подходила. В результате поиска в интернете был найден класс KeyState. Класс KeyState является статическим классом, отвечающим за обработку всех событий отпускания клавиши и нажатия клавиши. Он хранит состояние клавиш, которые были нажаты, и обеспечивает более быструю реакцию игры. Для проверки, была ли нажата клавиша, вызывается метод GetKeyState. Чтобы подключить класс KeyState, надо вызвать метод HookEvents. Код ниже заставляет корабль двигаться по экрану.

if(KeyState.GetKeyState(Key.Up) == true) {
   ship.Thrust();
}
else {
   ship.Drift();
}

Во фрагменте выше проверяется, нажал ли пользователь стрелку вверх на клавиатуре. Если да – вызывается метод Thrust(создать тягу). Thrust похож на метод  MoveForward астероида, не считая того, что он отображает маленькое пламя за кораблем, дающее иллюзию зажигания ракетного двигателя.

Динамический XAML

В оригинальной игре с астероидами есть три разных размера астероидов. Не хотелось делать отдельный класс для маленького, среднего и большого астероида, поэтому был найден способ динамического создания XAML(расширяемый язык разметки приложений) при выполнении. К сожалению, Silverlight не очень хорошо подходит для такого варианта, но все же удалось заставить его работать.

Идея здесь заключается в динамическом создании астероида посредством заполнения атрибута дата Data элемента Path. Атрибут Data определяет, как рисовать астероид с помощью синтаксиса разметки траектории. По сути, это мини-язык, применяемый для описания геометрических траекторий. Объяснить синтаксис разметки траектории непросто. Он столь же загадочен, как и регулярные выражения, но хорошие учебники по нему есть в MSDN.

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

public Asteroid(AsteroidSize size, Canvas parent )

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

public string GetPathData()
{
   int radius = (int)_size * BASE_RADIUS;
   string pathData = String.Empty;
   for (int i = 0; i < 18; i++)
   {
      float degrees = i * 20;
      Point pt = CreatePointFromAngle(degrees,
                   radius * (rand.Next(70,99) * .01));
      if (degrees == 0) {
         pathData += string.Format("M{0},{1} L",
                       (int)pt.X + radius, (int)pt.Y + radius);
      }
      else{
         pathData += string.Format("{0},{1} ",
                       (int)pt.X + radius, (int)pt.Y + radius);
      }
   }
   pathData += "z";
   return String.Format("<Path xmlns='http://schemas.microsoft.com/" +
                        "winfx/2006/xaml/presentation' xmlns:x='http://schemas." +
                        "microsoft.com/winfx/2006/xaml\' Data='{0}'/>",
                        pathData);
}

Обнаружение столкновений

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

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

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

Вероятно, есть более изящные или надежные способы обнаружения столкновений.

// <summary>
/// Определяет, сталкиваются ли два элемента,
/// с помощью 2-ходовой проверки (пересечение прямоугольников, затем проверка совпадения)
/// </summary>
/// <param name="control1">Контейнерный управляющий элемент для первого элемента</param>
/// <param name="controlElem1">Первый элемент</param>
/// <param name="control2"> Контейнерный управляющий элемент для второго элемента</param>
/// <param name="controlElem2">Второй элемент</param>
/// <returns>Истина, если объекты сталкиваются, иначе ложь</returns>
public static bool CheckCollision(FrameworkElement control1,
              FrameworkElement controlElem1, FrameworkElement control2,
              FrameworkElement controlElem2) {
   // сначала проверяется, сталкиваются ли прямоугольники спрайтов
   Rect rect1 = UserControlBounds(control1);
   Rect rect2 = UserControlBounds(control2);
   rect1.Intersect(rect2);

   if(rect1 == Rect.Empty) {
      // нет столкновения - уходи!
      return false;
   } else {
     bool bCollision = false;
     Point ptCheck = new Point();
     // теперь производится более точная проверка совпадения пикселей
     for(int x = Convert.ToInt32(rect1.X); x <
         Convert.ToInt32(rect1.X + rect1.Width); x++) {
        for(int y = Convert.ToInt32(rect1.Y); y <
            Convert.ToInt32(rect1.Y + rect1.Height); y++) {
           ptCheck.X = x;
           ptCheck.Y = y;
           List<UIElement> hits = (List<UIElement>)
             System.Windows.Media.VisualTreeHelper.
             FindElementsInHostCoordinates(ptCheck, control1);
           if(hits.Contains(controlElem1)) {
              // есть совпадение в первом управляющем элементе,
              // проверяется, есть ли аналогичное совпадение во втором элементе
              List<UIElement> hits2 = (List<UIElement>)
                System.Windows.Media.VisualTreeHelper.
                FindElementsInHostCoordinates(ptCheck, control2);
              if(hits2.Contains(controlElem2)) {
                 bCollision = true;
                 break;
              }
     }
   }
   if(bCollision)
     break;
   }
   return bCollision;
  }
}

Интересные особенности

В Silverlight и XAML нет наследования и полиморфизма. Поэтому пришлось повторять блоки кода во многих классах, потому что не удалось выяснить, как сделать их многократно используемыми.