Статьи

Как написать игрушку на VB (3)

Когда я начинал, писать первую статью о создании игрушек на VB, я не ожидал, что это вызовет такой бурный отклик. В мою задачу входила только демонстрация возможностей самого VB6. Как с помощью стандартных функций и немного API создать (и достаточно быстро) интересное приложение, а то, что это была игрушка -  так это чисто маркетинговый ход - заинтересовать читателя. Однако, игроделов оказалось достаточно много, и мой пример привлек к себе внимания даже больше, чем я сам ожидал. Ну что ж, сегодня третья статья. Она будет посвящена созданию гонок по реке на скутерах. Сразу же хочу поблагодарить Данилу Беляева за предоставленные к этой игрушке карты.

Само создание этой игрушки можно условно разделить на три части: 

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

Шаг 1. Создайте новый проект: River. Основную форму назовите frmMain. Ее свойства:
AutoRedraw =True, Caption="River", KeyPreview=True, ScaleMode=3 (Pixel), StartUpPosition=2 (Center Screen).
Сразу же добавим модуль для хранения API-функций, глобальных переменных и процедур - mdlDeclare.

Чтобы наш скутер мог поворачиваться во всех направлениях нам необходимо 8 его изображений: 4 основных и 4 промежуточных. 

NB! При создании собственной игрушки Вы можете увеличить количество промежуточных позиций для более плавного поворота скутера.

За основу я взял стандартную иконку 32 х 32 пиксела и нарисовал остальные позиции. Теперь на форме разместим 8 Image, со следующими свойствами:
Name=imgSrc, Index=от 0 до 7, Picture - по одной позиции скутера в каждый Image, Visible=False.
И один Image (Name=imgDest) для непосредственного отображения скутера. Еще нам понадобится таймер:
Name=tmrSpeed, Enabled=False, Interval=100

Займемся кодами. Нам потребуются 2 переменные уровня формы: NumPos As Integer - для определения позиции (направления) скутера и lSpeed As Long - для сохранения текущей скорости (смещения в пикселах за единицу времени).

При загрузке формы определимся с начальными значениями:
Private Sub Form_Load()
    lSpeed = 0
    NumPos = 0
    tmrSpeed.Enabled = True
End Sub
Последние две строки, позже мы перенесем в другие части программы.

Теперь займемся клавишами:
Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer)
Select Case KeyCode
    'смена направления
    Case vbKeyLeft
        NumPos = NumPos + 1
        If NumPos > 7 Then NumPos = 0
        imgDest.Picture = imgSrc(NumPos).Picture
    Case vbKeyRight
        NumPos = NumPos - 1
        If NumPos < 0 Then NumPos = 7
        imgDest.Picture = imgSrc(NumPos).Picture
    'смена скорости
    Case vbKeyUp
        lSpeed = lSpeed + 1
    Case vbKeyDown
        lSpeed = lSpeed - 1
End Select
End Sub

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

Далее все эти данные принимаются событием таймера и, в зависимости от номера картинки сдвигают скутер в нужном направлении.
Private Sub tmrSpeed_Timer()
Select Case NumPos ' движение корабля, в зависимости от номера картинки
    Case 0
        imgDest.Move imgDest.Left, imgDest.Top - lSpeed
    Case 1
        imgDest.Move imgDest.Left - lSpeed, imgDest.Top - lSpeed
    Case 2
        imgDest.Move imgDest.Left - lSpeed, imgDest.Top
    Case 3
        imgDest.Move imgDest.Left - lSpeed, imgDest.Top + lSpeed
    Case 4
        imgDest.Move imgDest.Left, imgDest.Top + lSpeed
    Case 5
        imgDest.Move imgDest.Left + lSpeed, imgDest.Top + lSpeed
    Case 6
        imgDest.Move imgDest.Left + lSpeed, imgDest.Top
    Case 7
        imgDest.Move imgDest.Left + lSpeed, imgDest.Top - lSpeed
End Select
End Sub

Ну вот, теперь можно отдохнуть и "погонять" наш скутер по форме.

Шаг 2. Займемся созданием карт. Для своей игрушки я выбрал размеры 650 х 550 пикселов, хотя никто Вам не запрещает создавать карты других размеров.
Нарисовать карту можно просто в Paint'е, а если есть талант и умение, то в Photoshop'е. Единственным условием для карты является окраска трассы (реки) одним цветом, без переходов. Для начала создадим массив пикселей карты за вычетом цвета трассы. Аналогичное действие я описывал в статье Опыт создания "вырезанных" форм (Часть 2). Функцию lGetRegion я взял оттуда, нисколько не изменив. Далее нам понадобятся 2 API-функции:
CreateRectRgn - для получения региона скутера и CombineRgn - для определения пересечения регионов берега и скутера. Очень часто, используя API-функции, мы пользуемся ими как процедурами. Но они ведь ФУНКЦИИ, т.е. возвращают нам какое-либо значение после выполнения. В частности, функция CombineRgn возвращает следующие значения, которые мы внесем в глобальные константы модуля:
Public Const NULLREGION = 1
Public Const SIMPLEREGION = 2
Public Const COMPLEXREGION = 3
Public Const ERROR = 0

Для исключения переполнения стека нам понадобится еще одна функция (на этот раз мы будем использовать ее как процедуру) - DeleteObject. Смотрим, что у нас получилось: мы определяем массив точек берегов, а затем в событии таймера и скутера. После чего проверяем значение функции CombineRgn и, если она равна объединению двух регионов, следовательно произошло столкновение с берегом и мы сбрасываем скорость до нуля.
Private Sub Form_Load()
    ...
    Rgn_Coast = lGetRegion(Me, vbWhite))' здесь мы пока исключаем белый цвет
    'чуть позже мы будем помещать сюда истинное значение цвета реки, в 
    'зависимости его от цвета карты

End Sub

Private Sub tmrSpeed_Timer()
    Dim Itog&
    Select Case NumPos ' движение корабля, в зависимости от номера картинки
    ...
    End Select

'получение массива точек корабля
Rgn_Ship = CreateRectRgn(imgDest.Left, imgDest.Top, _
    imgDest.Left + imgDest.Width, imgDest.Top + imgDest.Height)
'проверка на столкновение с берегом
Itog = CombineRgn(Rgn_Ship, Rgn_Ship, Rgn_Coast, RGN_AND)

If Itog = COMPLEXREGION Then
    lSpeed = 0
End If
DeleteObject Rgn_Ship
DeleteObject Itog

End Sub

Теперь необходимо заняться финишем. Этот этап состоит из 2-х частей: 1-я - рисование не карте финишной линии с определением координат в пикселах. 2-я - вывод кодов, отлавливающих пересечение скутером финишной черты. 
Собственно говоря, рисование на карте финишной черты служит только для остановки движения скутера, для правильной обработки кодов. Опытным путем подобрана толщина финишной черты для данных размеров скутера, и она составляет 5 пиксел. Теперь, имея координаты финиша (для каждой карты они будут свои) -добавим код:
Private Sub Form_Load()
    ...
    Rgn_Coast = lGetRegion(Me, vbWhite))
    Rgn_Finish = CreateRectRgn(642, 450, 647, 537)
End Sub

Private Sub tmrSpeed_Timer()
    Dim Itog&, Finish&
Select Case NumPos ' движение корабля, в зависимости от номера картинки
...
End Select

'получение массива точек корабля
Rgn_Ship = CreateRectRgn(imgDest.Left, imgDest.Top, _
    imgDest.Left + imgDest.Width, imgDest.Top + imgDest.Height)
'проверка на столкновение с берегом
Itog = CombineRgn(Rgn_Ship, Rgn_Ship, Rgn_Coast, RGN_AND)
'проверка на прохождение финиша
Finish = CombineRgn(Rgn_Ship, Rgn_Ship, Rgn_Finish, RGN_AND)
If Itog = COMPLEXREGION Then
    lSpeed = 0
ElseIf Finish = SIMPLEREGION Then
    lSpeed = 0
    tmrSpeed.Enabled = False
End If
DeleteObject Rgn_Ship
DeleteObject Itog
DeleteObject Finish
End Sub

Усложняем форму, упрощая ее для пользователя - добавляем меню с подменю. Структура представлена ниже в таблице:

Caption Name Index Shortcut Enabled
Игра mnuPlay      
- Загрузить трассу mnuSubPlay 0 F3 True
- Новая mnuSubPlay 1 F2 False
- Лучшие результаты mnuSubPlay 2 F5 False
- Выход mnuSubPlay 3 F12 True

Прежде чем заняться внесением кодов для меню, сделаем еще одно усложнение - создадим ini-файл для хранения настроек конкретной карты. А чтобы не вводить пользователя в искушение дадим ему другое расширение - *.map. Последний раз я описывал как работать с ini-файлами в статье Работа с INI-файлами. Здесь мы будем пользоваться только свойствами на чтение. Поэтому нам достаточно будет лишь одной функции, которую мы разместим в модуле:
'Получение значений из файла
Public Function MapEntries(key$) As String
Dim KeyValue$
Dim characters As Long

KeyValue$ = String$(128, 0)
characters = GetPrivateProfileStringByKeyName("Options", _
    key$, "", KeyValue$, 127, FileName)

If characters > 1 Then
    KeyValue$ = Left$(KeyValue$, characters)
End If

MapEntries = KeyValue$
End Function

Сама же структура файла настроек *.map будет выглядеть следующим образом (естественно значения для каждой карты будут свои)
[Options]
MapName=Trassa04
MinusColor=&H00F1BC4D
ShipX=80
ShipY=1
ShipOrient=4
FinishX1=642
FinishY1=450
FinishX2=647
FinishY2=537

Вернемся к кодам меню. Перед этим удалим из Form_Load все строчки за исключением одной.
Private Sub Form_Load()
    lSpeed = 0
End Sub

Вначале займемся загрузкой карты

Private Sub mnuSubPlay_Click(Index As Integer)
Select Case Index
    Case 0 'открытие карты и загрузка стартовых параметров
        FindFile
        If FileName = vbNullString Then Exit Sub
        
        Me.Picture = LoadPicture(App.Path & "\" & MapEntries("MapName") & ".bmp")
        Rgn_Coast = lGetRegion(Me, (MapEntries("MinusColor")))
        Rgn_Finish = CreateRectRgn(MapEntries("FinishX1"), MapEntries("FinishY1"), _
        MapEntries("FinishX2"), MapEntries("FinishY2"))
        mnuSubPlay(1).Enabled = True
        mnuSubPlay(2).Enabled = True
    Case 1 'Новая игра (загрузка параметров корабля)
    Case 2 'Таблица рекордов
    Case 3 'Выход
        Unload Me
End Select
End Sub

Процедура FindFile так же неоднократно описывалась мной в различных статьях, она служит для использования API-функции GetOpenFileName, для открытия стандартного диалогового окна открытия файлов.
'Открытие диалогового окна
Private Sub FindFile()
    Dim OFName As OpenFilename
    OFName.lStructSize = Len(OFName)
    OFName.hwndOwner = Me.hWnd
    OFName.hInstance = App.hInstance
    OFName.lpstrFilter = "MAP-файлы (*.map)" + Chr$(0) + "*.map"
    OFName.lpstrFile = Space$(254)
    OFName.nMaxFile = 255
    OFName.lpstrFileTitle = Space$(254)
    OFName.nMaxFileTitle = 255
    OFName.lpstrTitle = "Открытие файлов карт"
    OFName.flags = 0
    OFName.nFilterIndex = 0
    If GetOpenFileName(OFName) Then
        FileName = OFName.lpstrFile
    End If
End Sub

Далее подгружаем картинку на форму, определяем регионы для берегов и финишной черты, и разблокируем меню для запуска игры и открытия формы рекордов данной карты.
Займемся стартом новой игры:
Private Sub mnuSubPlay_Click(Index As Integer)
Select Case Index
    Case 0 'открытие карты и загрузка стартовых параметров
        ...
    Case 1 'Новая игра (загрузка параметров корабля)
        NumPos = MapEntries("ShipOrient")
        With imgDest
            .Picture = imgSrc(NumPos).Picture
            .Move MapEntries("ShipX"), MapEntries("ShipY")
            .Visible = True
        End With
        
        lSpeed = 0
        tmrSpeed.Enabled = True
        TimeStart = Timer
        Me.Caption = "Start!"
    Case 2 'Таблица рекордов
    Case 3 'Выход
        Unload Me
End Select
End Sub

Здесь все просто: получаем номер позиции скутера (направление), загружаем соответствующую картинку и устанавливаем на старт, делая видимым. Обнуляем скорость и включаем секундомер движения. Здесь же у нас появляется новая переменная уровня формы TimeStart, которая нам понадобится в дальнейшем для вычисления времени прохождения трассы. Таблицей рекордов займемся в третьем шаге, а сейчас приведем в окончательный вид процедуру работы таймера и закончим на этом с шагом вторым. Полностью данная процедура выглядит так:
Private Sub tmrSpeed_Timer()
    Dim Itog&, Finish&
    Select Case NumPos ' движение корабля, в зависимости от номера картинки
        Case 0
            imgDest.Move imgDest.Left, imgDest.Top - lSpeed
        Case 1
            imgDest.Move imgDest.Left - lSpeed, imgDest.Top - lSpeed
        Case 2
            imgDest.Move imgDest.Left - lSpeed, imgDest.Top
        Case 3
            imgDest.Move imgDest.Left - lSpeed, imgDest.Top + lSpeed
        Case 4
            imgDest.Move imgDest.Left, imgDest.Top + lSpeed
        Case 5
            imgDest.Move imgDest.Left + lSpeed, imgDest.Top + lSpeed
        Case 6
            imgDest.Move imgDest.Left + lSpeed, imgDest.Top
        Case 7
            imgDest.Move imgDest.Left + lSpeed, imgDest.Top - lSpeed
    End Select
    
    'получение массива точек корабля
    Rgn_Ship = CreateRectRgn(imgDest.Left, imgDest.Top, _
        imgDest.Left + imgDest.Width, imgDest.Top + imgDest.Height)
    'проверка на столкновение с берегом
    Itog = CombineRgn(Rgn_Ship, Rgn_Ship, Rgn_Coast, RGN_AND)
    'проверка на прохождение финиша
    Finish = CombineRgn(Rgn_Ship, Rgn_Ship, Rgn_Finish, RGN_AND)
    
    If Itog = COMPLEXREGION Then
        lSpeed = 0
    ElseIf Finish = SIMPLEREGION Then
        lSpeed = 0
        tmrSpeed.Enabled = False
        TimeFinish = Timer
        dTotalTime = Format(TimeFinish - TimeStart, "00.00")
        Me.Caption = "Total Time: " & dTotalTime & " sec"
        'frmRecords.Show vbModal
    End If
    
DeleteObject Rgn_Ship
DeleteObject Itog
DeleteObject Finish

End Sub

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

Шаг 3. Улучшаем интерфейс. Принцип создания формы, выводящей таблицу рекордов я достаточно подробно, в свое время, описал в статье Классы - это просто. Здесь я остановлюсь лишь на некоторых небольших коррекциях в классе и коллекции, которые необходимы для подгонки этой заготовки для данного конкретного приложения. В остальном все взято без каких-либо изменений.

Итак, класс Resultat. Здесь изменения коснулись только типа для третьего свойства (Result). В нашей игрушке результат прохождения трассы выражается в секундах с сотыми долями. Поэтому для этого свойства изменим тип на Double. Тоже самое необходимо проделать в коллекции Resultats для последних аргументов в процедурах Add и Sort. Кроме того процедура Sort должна сортировать записи от меньшего к большему. Это легко сделать поменяв знак больше на меньше в проверочной строке
If NewResult.Result < Me.Item(i).Result Then

На этом все изменения в классах закончены. Перейдем к форме. Добавим новую форму в наш проект:
Name=frmRecords, AutoRedraw=True, BorderStyle=1 (Fixed Single), Caption="Records", Font=Courier New Cyr, Bold, 10, StartUpPosition=1 (Center Owner).

Расположим на форме справа текстовое поле и три кнопки. Для текстового поля: Name=txtPlayer, MaxLenght=10, Text="Player". Первая кнопка: Name=cmdAdd, Caption="Add Result". Вторая: Name=cmdClear, Caption="Clear All". Третья: Name=cmdClose, Caption="Close".

В кодах объявим две переменные уровня формы:
Private R As Resultats
Private sPath As String

Кнопка добавления записи. Вначале добавим к коллекции имя игрока (из текстового поля), текущую дату и количество секунд, за сколько была пройдена трасса. Напечатаем все результаты на форме. И заблокируем текстовое поле и кнопку добавления для исключения повторного внесения записей.
Private Sub cmdAdd_Click()
    R.Add txtPlayer.Text, Date, dTotalTime
    R.PrintCls Me
    cmdAdd.Enabled = False
    txtPlayer.Enabled = False
End Sub

Кнопка очистки списка рекордов удаляет все записи и очищает надписи на форме.
Private Sub cmdClear_Click()
    R.Clear
    R.PrintCls Me
End Sub

Кнопка закрытия формы - просто закрывает ее
Private Sub cmdClose_Click()
    Unload Me
End Sub

Теперь посмотрим что происходит при открытии формы. Вначале мы инициализируем переменную с типом коллекции. Устанавливаем количество отображаемых на форме количества записей. Собираем в одну переменную полный путь к файлу рекордов. Так как карты могут быть самыми разными по сложности, то у нас для каждой карты будет храниться свой файл рекордов. Здесь самое место установить обработчик ошибок. Вообще-то он нам нужен только для того чтобы отлавливать ошибку №53 (отсутствие файла). В этом случае мы открываем данный на запись, в результате чего он автоматически создается и снова возвращаемся к месту, где у нас произошла ошибка. Далее идет стандартная процедура считывания из файла значений в переменные, которые потом добавляются в коллекцию..
Private Sub Form_Load()
    Dim sPlayer As String, dDate As Date, dResult As Double
    
    Set R = New Resultats
    R.Max = 10
    sPath = App.Path & "\" & MapEntries("MapName") & ".res"
    
    On Error GoTo LocalErr
    Open sPath For Input As #1
        Do While Not EOF(1)
            Input #1, sPlayer, dDate, dResult
            R.Add sPlayer, dDate, dResult
        Loop
    Close #1
    
    R.PrintCls Me
    
    Exit Sub
LocalErr:
Select Case Err.Number
    Case 53
        Open sPath For Output As #1
        Close #1
        Resume
    Case Else
        MsgBox Err.Number & " - " & Err.Description
End Select
End Sub

Последняя процедура = это процедура закрытия формы. Здесь мы записываем коллекцию в файл рекордов и на выходе уничтожаем переменную с типом коллекции, тем самым освобождая память.
Private Sub Form_Unload(Cancel As Integer)
    Dim i%
    
    Open sPath For Output As #1
        For i = 1 To R.Count
            Write #1, R.Item(i).Player, R.Item(i).CurDate, R.Item(i).Result
        Next
    Close #1
    
    Set R = Nothing
End Sub

Остались мелочи. В игровой форме (frmMain) дописать коды для подменю Лучшие результаты и снять комментарий в процедуре таймера с вывода таблицы рекордов.
Private Sub mnuSubPlay_Click(Index As Integer)
    Select Case Index
    ...
        Case 2 'Таблица рекордов
            With frmRecords
                .cmdAdd.Enabled = False
                .cmdClear.Enabled = False
                .txtPlayer.Enabled = False
                .Show vbModal
            End With
    ...
End Sub

Создание игрушки закончено. Успехов Вам в гонках на трассах!


Назад

Скачать пример

Hosted by uCoz