ПОНЯТНО О Visual Basic NET (том 2)

Вторая часть – управляем машиной


Если с первой частью все было относительно просто, то про вторую стоит поговорить подробнее. Фактически, нам нужно будет создавать автомобиль, как раньше мы создавали будильник. Не имея программистского опыта, мы попытаемся использовать житейский опыт касательно того, как автомобиль устроен. Причем применительно к задачам проекта. Так, цвет сиденья в этом смысле нам не очень важен. А важно нам управлять скоростью и направлением движения (этим в обычном автомобиле занимаются руль и педали газа и тормоза). А еще важно, чтобы автомобиль чувствовал, «куда он въехал» (газон, ограждение, финиш) и вел себя соответственно (этим мы займемся в третьей части).

Для начала поместим на форму маленький PictureBox. Это ваша машина. Так и назовем его – Машина. Поставим задачу управлять им с клавиатуры как написано в задании на игру:

Направление движения может быть только горизонтальным и вертикальным, наискосок машина не движется. Выбор направления – это клавиши со стрелками. Тормоз – клавиша Ctrl. Газ – клавиша пробела. Щелчок по клавише газа или тормоза увеличивает или уменьшает скорость на какое-то значение.

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

Код второй части. Приведу код, который заведует управлением машиной. Этим кодом нужно дополнить первую часть программы, чтобы получилась работающая версия проекта. Вслед за кодом я привожу пояснения. Из процедур, входящих в первую часть, я здесь перепишу только две:  Form1_Load  и  Кнопка_начинай_сначала_Click.

Dim Цвет_фона As Color = Color.White                                       'Цвет фона при рисовании машины

Dim Исходная_машина As New Bitmap("Машина.BMP")           'Создаем исходное изображение машины

'Определяем 4 рабочих изображения машины:

Dim Машина_налево, Машина_вверх, Машина_направо, Машина_вниз As Bitmap                    

Dim x, y As Short            'Горизонтальная и вертикальная координаты автомобиля


Dim Шаг As Short           'Шаг  численно равен перемещению автомобиля по форме  на каждом такте таймера
Dim Газ As Boolean = False                                'Нажимаем ли мы на газ


Dim Тормоз As Boolean = False                         'Нажимаем ли мы на тормоз
Enum типРуль                                        'Куда едем
    вверх
    влево
    вниз
    вправо
End Enum
Dim Руль As типРуль
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
     '……………здесь расположены уже знакомые нам предыдущие строки процедуры……………
    Создаем_изображения_машины()
    Машина.BackColor = Color.Transparent
    Me.KeyPreview = True                                       'Чтобы машина отзывалась на клавиатуру
    txtВремя.ReadOnly = True
End Sub
Private Sub Кнопка_начинай_сначала_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)  _
Handles Кнопка_начинай_сначала.Click
     '……………здесь расположены уже знакомые нам предыдущие строки процедуры……………
    Ставим_машину_на_старт()
    txtВремя.Focus()
End Sub
Sub Создаем_изображения_машины()
    Создаем_изображение(Машина_налево, RotateFlipType.RotateNoneFlipNone)
    Создаем_изображение(Машина_вверх, RotateFlipType.Rotate90FlipNone)
    Создаем_изображение(Машина_направо, RotateFlipType.Rotate180FlipNone)
    Создаем_изображение(Машина_вниз, RotateFlipType.Rotate270FlipNone)
End Sub
Sub Создаем_изображение(ByRef Автомобиль As Bitmap, ByVal Поворот As RotateFlipType)
    Автомобиль = New Bitmap(Исходная_машина)     'Порождаем новый Bitmap из исходного
    Автомобиль.RotateFlip(Поворот)                            'Поворачиваем его
    Автомобиль.MakeTransparent(Цвет_фона)           'Делаем фон автомобиля прозрачным
End Sub
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
    Изменяем_скорость()
    Выбираем_куда_ехать_и_делаем_шаг()
    Машина.Left = x     : Машина.Top = y                'Передвигаем PictureBox


End Sub
Sub Ставим_машину_на_старт()
    x = X_старта      : y = Y_старта     ' Координаты машины приравниваются координатам точки старта
    Шаг = 0                                           'На старте стоим, а не едем
    Руль = типРуль.влево                   'Это чтобы машина знала, что когда стартуем, нужно ехать влево
    Машина.Image = Машина_налево                        'Ориентируем машину налево
End Sub
Sub Изменяем_скорость()
    If Газ Then Шаг = Шаг + 1
    If Тормоз Then
        Шаг = Шаг – 2                       'Потому, что тормоз действует быстрее газа
        'В результате быстрого торможения скорость может стать отрицательной, что и предотвращается:
        If Шаг < 0 Then Шаг = 0
    End If
    'Чтобы во время набора скорости и торможения приходилось без перерыва жать на педаль:
    Газ = False    : Тормоз = False
End Sub
Sub Выбираем_куда_ехать_и_делаем_шаг()
    Select Case Руль
        Case типРуль.вверх    : Машина.Image = Машина_вверх         : y = y - Шаг
        Case типРуль.вниз            : Машина.Image = Машина_вниз           : y = y + Шаг
        Case типРуль.влево    : Машина.Image = Машина_налево             : x = x - Шаг
        Case типРуль.вправо : Машина.Image = Машина_направо            : x = x + Шаг
    End Select
End Sub
'Обработка события - нажатия клавиши на клавиатуре для управления автомобилем:
Private Sub Form1_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs)  _
Handles MyBase.KeyDown
    Select Case e.KeyCode
        Case Keys.Left                   : Руль = типРуль.влево
        Case Keys.Right          : Руль = типРуль.вправо
        Case Keys.Up              : Руль = типРуль.вверх
        Case Keys.Down          : Руль = типРуль.вниз
        Case Keys.Space        : Газ = True
        Case Keys.ControlKey        : Тормоз = True
    End Select
End Sub
Создаем изображения машины. Пустой PictureBox передвигать по форме неинтересно, поэтому рисуем в каком-нибудь графическом редакторе маленькую машинку (вид сверху) точно так же, как мы рисовали летающую тарелку для мультфильма. Сохраняем ее.


Во время гонок машина может ехать в 4 направлениях: влево, вправо, вверх, вниз. Значит, для придания естественности игре (чтобы машина не ехала «боком»), мы должны иметь 4 ориентации машины, между которыми будем в нужные моменты переключаться. Поэтому нам нужны 4 одинаковых изображения машины, повернутые в разные стороны. Для этого вовсе не нужно рисовать 4 рисунка и сохранять 4 файла (хотя и это можно, конечно). Вспомним, что метод RotateFlip умеет поворачивать картинки под прямыми углами. Его и используем.
Обращение к процедуре Создаем_изображения_машины мы находим, естественно, в процедуре Form1_Load, так как создать эти изображения достаточно один раз. Здесь же мы видим, как следующая строка делает прозрачным фон элемента управления Машина. Тело процедуры Создаем_изображения_машины, как видите, состоит из 4 операторов, каждый из которых создает одно из 4 изображений. Каждый из этих операторов есть в свою очередь обращение к процедуре Создаем_изображение. Рассмотрим ее получше.
В верхней части окна кода мы видим строки:
Dim Исходная_машина As New Bitmap("Машина.BMP")           'Создаем исходное изображение машины
'Определяем 4 рабочих изображения машины:
Dim Машина_налево, Машина_вверх, Машина_направо, Машина_вниз As Bitmap                    
Первая из них на основе нарисованной нами машины создает объект Исходная_машина типа Bitmap, который послужит исходным материалом для упомянутых 4 изображений. Каждое из них в дальнейшем получится поворотом картинки Исходная_машина на нужный угол. Тут же объявляются (но не создаются) объекты типа Bitmap для этих 4 изображений.
У процедуры Создаем_изображение два параметра: Первый из них (Автомобиль) как раз и является тем изображением, которое создает процедура. Он имеет тип Bitmap и обозначен словом ByRef, а не ByVal, так как в процессе работы изображение и создается и меняется. Второй параметр (Поворот) имеет тип перечисления RotateFlipType, которое как раз и используется при повороте картинок. Когда я рисовал машину, я нарисовал ее глядящей влево, отсюда становятся понятны значения второго параметра в обращении к процедурам.


Для формирования требуемого изображения машины нужно проделать 3 вещи:
  • Создать его из исходной картинки машины

  • Повернуть на нужный угол

  • Сделать фон прозрачным

  • Именно эти 3 вещи делают 3 оператора процедуры. Взгляните на них. Если вы не привыкли еще к параметрам объектного типа или вообще не понимаете текста этих операторов, я «перепишу» их по-другому. Например, для случая выполнения 2-го оператора процедуры Создаем_изображения_машины я просто для наглядности подставлю в них вместо параметров их значения:
        Машина_вверх = New Bitmap(Исходная_машина)     'Порождаем новый Bitmap из исходного
        Машина_вверх.RotateFlip(RotateFlipType.Rotate90FlipNone)                       'Поворачиваем его
        Машина_вверх.MakeTransparent(Цвет_фона)            'Делаем фон автомобиля прозрачным
    Управляем машиной. Поместим на форму таймер. Настроим его интервал в пределах от 10 до 100 (по вкусу) На каждом импульсе таймера автомобиль должен будет проделать весь цикл своего функционирования. Из этого цикла нам для нормального передвижения машины достаточно пока озаботиться тремя вещами. Автомобиль должен:
    • Изменить или не изменить скорость в соответствии с приказами клавиатуры.

    • Изменить или не изменить направление движения в соответствии с приказами клавиатуры

    • Сделать очередной шаг в нужном направлении.

    • Эти три вещи как раз и выполняются операторами, которые мы видим в процедуре Timer1_Tick. Разберемся в них, но сначала заглянем в верхнюю части окна кода, где мы видим объявления переменных  x, y, Шаг, Газ, Тормоз и Руль. Зачем нам нужны последние три переменные? Нельзя ли попроще: управлять движением объектов с клавиатуры безо всяких переменных. Попытаться можно, и программа поначалу получится короче. Но с ростом сложности проекта будут расти неудобства. Например, машина не будет знать направления своего движения, а без этого трудно будет запрограммировать отскок от ограждения.
      С учетом вышесказанного проглядите процедуру Form1_KeyDown. Она проста и в комментариях не нуждается.


      События, связанные с клавиатурой, имеются у многих элементов управления. Почему мы выбрали события формы? Выбор какого-нибудь элемента управления был бы для нашей игры неудобен. Если мы для программирования реакции автомобиля на нажатия клавиш выберем, например, процедуру Private Sub Button1_KeyDown, то во время гонки мы не сможем щелкать по другим кнопкам, кроме Button1, так как иначе Button1 выйдет из фокуса и автомобиль перестанет реагировать на клавиши.
      Изменяем скорость. Теперь посмотрим, как регулируется скорость. Для этого заглянем в процедуру Изменяем_скорость. Действие ее полностью определяется значением переменных Газ и Тормоз. Если это газ, то на данном такте таймера шаг, а значит и скорость, возрастет на 1. Если тормоз – упадет на 2 (потому что тормоз обычно действует сильнее газа). Отрицательный шаг в результате торможения означал бы задний ход, что неестественно, поэтому в программе это предотвращается.
      Как видите, значения переменных Газ и Тормоз в конце процедуры принудительно приравниваются False. Это значит, что щелчок по клавише газа или тормоза приводит только к однократному увеличению или уменьшению скорости. Ведь на следующем тике таймера переменные Газ и Тормоз будут иметь значение False, а значит процедура не приведут к изменению шага. Значит на следующем такте таймера скорость не изменится. Чтобы она изменилась, нам нужно еще раз нажать на клавишу газа или тормоза. Обычно поступают по-другому – просто удерживают клавишу нажатой, при этом событие KeyDown возникает несколько раз в секунду и скорость меняется достаточно быстро. Это соответствует механике реального автомобиля – чтобы набирать скорость, нужно непрерывно и усиленно нажимать на педаль газа, а чтобы тормозить – тормоза.
      Выбираем куда ехать и делаем шаг. Именно эта процедура задает ориентацию машины и направление движения. Загляните в нее. Ее дело – чувствовать одно из 4 значений переменной Руль и в соответствии с этим значением делать две вещи: поворачивать машину в нужном направлении и менять в этом направлении ее координату. Само же изображение автомобиля на экране прыгнет на указанную координату мгновением позже, на последней строке процедуры Timer1_Tick.


      В этой процедуре каждая строка оператора Select Case присваивает свойству Image нашего элемента управления Машина значение одного из 4 изображений машины, а именно как раз того, которого требует нажатие клавиши со стрелкой на клавиатуре. При этом в выбранном направлении изменяется и координата x или y. В результате мы видим, что автомобиль глядит в ту же сторону, куда он едет.
      Последняя строка процедуры Timer1_Tick перемещает элемент управления Машина на экране в соответствии с вычисленными координатами x и y.
      Ставим машину на старт. Когда мы нажимаем кнопку Начинаем сначала, машина, где бы она ни была и что бы ни делала, должна прыгнуть на старт и замереть, глядя влево. Именно это поведение обеспечивают все пять операторов процедуры Ставим_машину_на_старт. И именно поэтому обращение к этой процедуре включено в процедуру Кнопка_начинай_сначала_Click.
      Чтобы машина реагировала на клавиатуру. Я уже говорил ранее, что поскольку один какой-нибудь элемент управления на форме всегда находится в фокусе, до процедуры Form1_KeyDown дело не дойдет. Это значит, что наша машина не будет реагировать на клавиши. Для борьбы с этим в процедуре Form1_Load свойство формы KeyPreview установлено в True. Это означает приказ компьютеру при нажатии на клавишу вызывать событие формы, а уж потом другого объекта. Но этого недостаточно. И вот почему. При запуске проекта фокус автоматически устанавливается на кнопку «Начинаем сначала». Первое нажатие во время гонки на клавишу со стрелкой приводит не к выбору направления машиной, а к перескакиванию фокуса с кнопки на другой объект формы. Если же вы вместо этого нажимаете на пробел, то машина дает газ, но радоваться тоже рано. Кнопки воспринимают пробел и Enter, как приказ на нажатие, и поэтому кнопка «Начинаем сначала» тут же сама собой нажимается и получается, что немедленно после старта газоны перерисовываются, а машина снова прыгает на старт.
      Чтобы прекратить это безобразие, поместим на форму текстовое поле. Позже оно нам пригодиться для отображения счетчика времени, а сейчас у нас другая забота: увести от вредной кнопки фокус куда-нибудь подальше. Текстовое поле ведет себя смирно, назовем его txtВремя и включим в процедуру оператор, уводящий фокус с кнопки на это поле:


          txtВремя.Focus()
      Помогло. При нажатии клавиш со стрелками фокус не покидает текстовое поле. Но осталась одна шероховатость. Если в текстовом поле есть текст, то при нажатии клавиш со стрелками текстовый курсор будет слоняться по текстовому полю влево-вправо, а при нажатии пробела – вставлять в текст пробел. Чтобы от этого избавиться и чтобы предотвратить в дальнейшем возможность нечаянно с клавиатуры испортить показания счетчика времени, помещаем в процедуру Form1_Load оператор
          txtВремя.ReadOnly = True
      Это означает, что содержимое текстового поля можно только читать, но вручную не менять. Изменения возможны только программным путем.
      Теперь все нормально.
      Результат. После ввода всего вышеприведенного кода у вас при каждом нажатии на кнопку Начинаем сначала должны рисоваться поле, старт, финиш и новая конфигурация газонов, машина должна нормально вставать на старт, а при нажатии нужных клавиш должна нормально ускоряться, тормозить и ездить по всей форме во всех направлениях, не разбирая пока, где асфальт, где преграды и все остальное.

      Содержание раздела