Движем ловца – вторая ступень проекта
Сейчас мы должны запрограммировать ловца полностью. Но сначала нужно очень точно продумать его поведение, определить до мелочей все, что он должен уметь делать. Выпишем все его умения:
А.
По приходе импульса от таймера он должен:
- Проверить, не наткнулся ли он на бортик поля, и если да, то … Что? Мы не придумали еще. Надо придумать. Пуст он должен отскочить в исходное положение и остановиться.
- В противном случае сдвинуться на некоторый шаг вверх, вниз, влево или вправо, подчиняясь соответствующим клавишам клавиатуры, или стоять на месте (клавиша Ctrl).
- Разбудить ловца и заставить его выполнить свою процедуру Действие, в частности вычислить свое положение (координаты) на форме и переместить свое изображение в вычисленное место.
- Увеличить на 1 счетчик времени (импульсов таймера) на форме.
- Установить в 0 счетчик времени.
- Заставить ловца выполнить свою процедуру Начальная_установка, то есть прыгнуть в точку старта ловца.
В.
При нажатии кнопки Начинай сначала он должен возвращаться в исходное положение.
Все. Вы спросите – а почему так мало, а как же умение ловить шары, ради которого ловец и создан? Я отвечу: В нашем случае проще запрограммировать «исчезалки», чем «ловилки». Не поняли? – Поясню. Наткнувшись на шар, ловец у нас не будет предпринимать никаких действий по его «поимке». Наоборот, шар, наткнувшись на ловца, потрудится добровольно исчезнуть. Со стороны – никакой разницы.
Запрограммируем все действия, перечисленные в пункте А, в процедуре класса clsЛовец, которую назовем Действие. Запрограммируем все действия, перечисленные в пункте В, в процедуре Начальная_установка класса clsЛовец, которую мы уже частично написали.
Таймер. Свои действия ловец должен производить по импульсам таймера. Но у класса нет ни одного элемента управления, значит и таймера тоже нет. Таймером нашего проекта будет таймер, принадлежащий форме. Поместите его на форму. Задайте ему интервал = 10. Щелкните по нему дважды – в окне кода формы появится заготовка процедуры. Напомню, что эта процедура выполняется на каждом импульсе таймера. Значит это и будет главная процедура нашего проекта, задающая ритм всем объектам и элементам управления.
Перечислю действия, которые должна выполнять эта процедура, пока шаров в проекте нет:
Перечислю действия, которые должны выполняться при нажатии на кнопку «Начинай сначала»:
Программа. Вот как выглядит наш проект на второй ступени. На третьей ступени добавится класс шара, к стандартному модулю и коду формы добавятся строки, касающиеся шаров. А модуль clsЛовец я привожу целиком, в окончательном варианте.
Стандартный модуль: Остался неизменным.
Модуль формы:
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Форма = Me
KeyPreview = True 'Чтобы форма реагировала на клавиатуру
Счетчик_времени.ReadOnly = True 'Чтобы нельзя было вручную менять показания счетчика
Ловец = New clsЛовец 'Создаем объект Ловец класса clsЛовец
Начальная_установка()
End Sub
Private Sub Начальная_установка()
Счетчик_времени.Text = 0 'Обнуляем счетчик времени
Счетчик_времени.Focus() 'Чтобы фокус ушел с кнопки
Ловец.Начальная_установка() 'Ловец встает в исходную позицию
End Sub
Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
Ловец.Действие()
Счетчик_времени.Text = Счетчик_времени.Text + 1 'Счетчик времени на форме увеличивается на 1
End Sub
Private Sub Form1_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs) Handles MyBase.KeyDown
Ловец.Реакция_на_клавиатуру(e)
End Sub
Private Sub Начинай_сначала_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles Начинай_сначала.Click
Начальная_установка()
End Sub
Модуль класса clsЛовец (останется неизменным):
Public Class clsЛовец
Private x As Double 'Горизонтальная координата ловца на форме
'Свойство - горизонтальная координата ловца на форме
Public ReadOnly Property Xл() As Double
Get
Return x
End Get
End Property
Private y As Double 'Вертикальная координата ловца на форме
'Свойство - вертикальная координата ловца на форме
Public ReadOnly Property Yл() As Double
Get
Return y
End Get
End Property
Private Enum типРуль ' Направление движения ловца или состояние неподвижности
вверх
влево
вниз
вправо
стоп
End Enum
Private Руль As типРуль
Public Sub New()
Форма.pictЛовец.Width = Размер_ловца
Форма.pictЛовец.Height = Размер_ловца
End Sub
Public Sub Начальная_установка() 'Ловец встает в исходную позицию и останавливается
Руль = типРуль.стоп
x = Форма.Поле.Left + Форма.Поле.Width * 1 / 4
y = Форма.Поле.Top + Форма.Поле.Height / 2
Ставим_изображение_ловца_на_место()
End Sub
Private Sub Ставим_изображение_ловца_на_место()
Форма.pictЛовец.Left = x
Форма.pictЛовец.Top = y
End Sub
Public Sub Действие() 'Главная процедура ловца, выполняется один раз на каждом импульсе таймера
'Если ловец врезается в бортик, то отскакивает в исходное положение и останавливается:
If Ловец_у_бортика() Then Начальная_установка()
Выбираем_куда_ехать_и_делаем_шаг() 'Ловец слушается клавиатуру
End Sub
Private Function Ловец_у_бортика() As Boolean
'ЕСЛИ ловец находится у потолка ИЛИ у пола, ИЛИ у левой стены ИЛИ у правой, ТО:
If y < Форма.Поле.Top Or y + Размер_ловца > Форма.Поле.Top + Форма.Поле.Height _
Or x < Форма.Поле.Left Or x + Размер_ловца > Форма.Поле.Left + Форма.Поле.Width Then
Return True
Else
Return False
End If
End Function
Private Sub Выбираем_куда_ехать_и_делаем_шаг()
Dim dx As Double = 1 'Шаг ловца по горизонтали и вертикали между двумя импульсами таймера
Dim dy As Double = 1
Select Case Руль
Case типРуль.вверх : y = y - dy
Case типРуль.вниз : y = y + dy
Case типРуль.влево : x = x - dx
Case типРуль.вправо : x = x + dx
Case типРуль.стоп 'Поскольку никуда идти не надо, постольку ничего не делаем
End Select
Ставим_изображение_ловца_на_место()
End Sub
Public Sub Реакция_на_клавиатуру(ByVal e As System.Windows.Forms.KeyEventArgs)
Select Case e.KeyCode
Case Keys.Left : Руль = типРуль.влево
Case Keys.Right : Руль = типРуль.вправо
Case Keys.Up : Руль = типРуль.вверх
Case Keys.Down : Руль = типРуль.вниз
Case Keys.ControlKey : Руль = типРуль.стоп
End Select
End Sub
End Class
Запустите проект. Проверьте, правильно ли движется ловец. Для его движения не нужно держать палец на клавише, достаточно щелкнуть.
А теперь пояснения.
Начну с клавиатуры. Она почти полностью копирует работу клавиатуры в игре «Гонки» (тех, кто подзабыл работу с клавиатурой, отсылаю туда – 14.4.4). Для того, чтобы проект мог реагировать на клавиатуру, в коде формы появились три строки:
KeyPreview = True 'Чтобы форма реагировала на клавиатуру
Счетчик_времени.ReadOnly = True 'Чтобы нельзя было вручную менять показания счетчика
Счетчик_времени.Focus() 'Чтобы фокус ушел с кнопки
Причины необходимости таких строк подробно объяснены в 14.4.4.
Отличие от Гонок состоит в том, что теперь у нас появился объект и в соответствии с принципом инкапсуляции обработку нажатия на клавишу клавиатуры я перенес в него. Раньше вся обработка происходила бы в процедуре Form1_KeyDown формы, а теперь я там оставил только обращение к процедуре ловца Реакция_на_клавиатуру. Чтобы не плодить ловцу лишних полей, я снабдил эту процедуру параметром e, который несет в себе всю информацию про нажатие клавиши.
В результате работы этой процедуры переменная перечислимого типа Руль запоминает, какая клавиша была нажата, и движение ловца поэтому осуществляется далее в соответствующем направлении без необходимости удерживать эту клавишу нажатой.
Процедуры таймера Timer1_Tick и кнопки Начинай_сначала_Click предельно кратки и полностью соответствуют описанию их работы, приведенной чуть выше в этом подразделе.
С пояснениями кода формы покончено. Перейдем к классу clsЛовец. Здесь добавлений много. В начальную установку ловца добавилась строка
Руль = типРуль.стоп
Она нужна потому, что начальная установка может застигнуть ловца в любой момент, чаще всего тогда, когда он находится в движении. Так вот, это нужно, чтобы он успокоился.
Главная процедура ловца Действие в нашем случае сводится к выполнению двух дел:
Проверке, не врезался ли ловец в бортик, а если врезался – к возврату его в исходное положение и остановке. Проверку осуществляет булевская функция Ловец_у_бортика, а возврат – уже рассмотренная нами процедура Начальная_установка.
Опрашиванию переменной Руль и перемещению изображения ловца в соответствующем направлении. Всем этим занимается процедура Выбираем_куда_ехать_и_делаем_шаг. Она работает совершенно аналогично своей тезке из Гонок (см. 14.4.4).
Получается, что нам осталось разобрать только булевскую функцию Ловец_у_бортика. Эта функция примет значение True, если ловец в своих путешествиях по полю врежется в бортик. Факт столкновения определяется сравнением координат ловца и координат каждого из 4 бортиков. В операторе If эти 4 сравнения отделены друг от друга знаком логической функции Or.
Координаты и шаг ловца и шаров я сделал дробного типа, а не целого. Причина в том, что скорости ловца и шаров вы можете захотеть сделать маленькими. Тогда может понадобиться шаг меньший 1.
Процесс работы проекта. Если вам пока непонятно, как работает проект, разберем последовательность вызова процедур и функций в процессе работы проекта. Эту последовательность я буду разбирать в хронологическом порядке, то есть в том, в котором вызываются процедуры и функции после запуска проекта.
После выполнения процедуры Form1_Load, которое мы разобрали ранее, на игровой площадке наступает покой до прихода первого импульса от таймера.
И вот импульс грянул. Заработала процедура Timer1_Tick и первым делом запустила процедуру Ловец.Действие. Давайте пока не будем в нее заглядывать, вообразим, что там ничего существенного не произошло, и пойдем дальше. Следующая строка увеличивает счетчик времени. Вместо 0 в счетчике на форме появляется 1. На этом процедура Timer1_Tick заканчивает свою работу. Все замирает до тех пор, пока через 10 тысячных долей секунды не придет следующий импульс от таймера.
Предположим, вы за это время еще не успели прикоснуться к клавиатуре. Вот пришел новый импульс. Заработала процедура Timer1_Tick и запустила процедуру Ловец.Действие. Рассмотрим, как она работает. Ее тело состоит из двух строк, вызывающих процедуры и функции с интуитивно ясными именами. Первой вызывается функция Ловец_у_бортика. Очевидно, что поскольку ловец пока далеко от бортиков, то эта функция принимает значение False.
Далее выполняется процедура Выбираем_куда_ехать_и_делаем_шаг. Руль у нас после начальной установки находится в положении стоп и ничто его оттуда не вывело, значит оператор Select Case не меняет ни x ни y. Следовательно процедура Ставим_изображение_ловца_на_место не сдвигает изображение ловца из начального положения.
На этом вторая строка процедуры Ловец.Действие завершается, а с ней и вся процедура. VB возвращается в процедуру Timer1_Tick, которая увеличивает счетчик времени еще на 1.
Ждем следующего импульса. Пусть до момента его прихода мы успели нажать на клавиатуре стрелку вправо, желая направить ловца направо. Немедленно сработала процедура Form1_KeyDown в модуле формы. Она вызвала процедуру ловца Реакция_на_клавиатуру, которая присвоила переменной Руль значение вправо. Импульса все нет.
Вот пришел импульс. Опять процедура Timer1_Tick направляет нас в процедуру Ловец.Действие, та – в процедуру Выбираем_куда_ехать_и_делаем_шаг. Поскольку руль повернут направо, вычисляется новое значение x, которое на dx больше предыдущего. Согласно новому значению x процедура Ставим_изображение_ловца_на_место смещает изображение ловца чуть вправо.
Ждем следующего импульса и так далее. Вот и вся механика ловца.
Теперь поговорим об инкапсуляции. В проекте я старался максимально придерживаться принципа инкапсуляции, все что можно я делал Private. Посмотрим по порядку.
Объекты Форма, Ловец и константу Размер_ловца я объявил в стандартном модуле. Потому что все они будут нужны не в одном, а в нескольких модулях. Естественно, я сделал их Public.
В форме все процедуры объявлены Private. Потому что в нашем проекте форма – это тот пульт, из которого осуществляется управление проектом. Ее процедуры предназначены, чтобы управлять, а не управляться. Чего не скажешь об элементах управления, которые видны снаружи формы и управляются из классов.
Теперь поглядите повнимательнее в код ловца. Координаты ловца на форме x и y я сделал Private, но поскольку знаю, что они понадобятся шару (который должен знать их величину, чтобы вовремя исчезнуть при столкновении), я создал два соответствующих свойства только для чтения: Xл и Yл.
Наводит на размышление тот факт, что методами класса стали именно те процедуры, которые должны вызываться из формы в ответ на какие-то события формы. Их просто нельзя было сделать Private. Это Начальная_установка (реагирует на загрузку формы и на кнопку Начинай сначала), Действие (реагирует на таймер) и Реакция_на_клавиатуру (реагирует на клавиатуру). О конструкторе я не говорю. Если его сделать Private, то нельзя будет создать объект.
Поведение ловца определяется процедурами и функцией, определенными в коде ловца. Именно они обеспечивают механику его работы. Никакая процедура и функция снаружи класса в этой механике не участвует. Ловец самодостаточен. Снаружи осуществляется только запуск трех методов объекта. Именно поэтому они сделаны Public, иначе их снаружи и запустить было бы нельзя. Но вся механика этих методов, опять же, находится внутри ловца, так что никакого нарушения суверенитета нет. Остальные процедуры и функция сделаны Private, они снаружи и не видны и недоступны.
Обратите внимание, что разные модули могут использовать одноименные компоненты. Например, процедура Начальная_установка. Проблемы в этом нет, так как если процедура задана, как Private, то из других модулей она просто не видна, а если даже Public, то перед своим именем она будет требовать имени хозяина.
Вот и все о второй ступени проекта.