Основы Direct Draw на Visual Basic

[Главная страница] [Предыдыущая часть] [Следующая часть] [Приложения к этой части]


... Это не Фича, это - Бага!
некий ]PD[-System

Часть третья: Творим!

Да уж давно бы пора! - подумают некоторые. Ну ладно, не злитесь, в этой главе мы разработаем и претворим в жизнь самую настоящую DirectX'овую игруху!

Не будем очень оригинальны и сделаем PingPong; ну знаете, такая штука, где ездят две ракетки и пинают мячик стараясь чтобы противник по нему не попал. Так вот, мы постараемся сделать полностью (ну или почти) работающий ПингПонг для одного игрока против компьютера. Как подключить второго игрока я надеюсь вам будет абсолютно понятно - это останется в качестве упражнения. Кроме того, для упрощения задачи я пока не буду рассматривать вывод текстовых сообщений - это в раздел эффектов в следующую часть. А для усложнения задачи - выработаем такой алгоритм, в котором поле у нас будет представлено в виде матрицы в которой есть элементы 0 - свободно и 1 - блок. От блоков мячик соответственно отскакивает. Это сделано для того, чтобы мяч мог передвигаться независимо от расположения блоков, вам надо будет только поменять ссответствующее значение в матрице. Это уже будет нечто действительно похожее на движок и вы можете делать различные игровые поля.

Итак, приступаем

Что сначала?

Ну с самого начала, еще даже перед тем как начать рисовать графику, надо решить один Заместо скрин-шота такой маленький вопросик: а какой у нас будет ПингПонг: вертикальный или горизонтальный? Я выбрал вертикальный; не спрашивайте почему, вы всегда сможете сделать другой.

Разрешение экрана будет 640x480x16, а размер матрицы - 12*16, то есть одна клетка - 40*40 пикселей.

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

Процесс этот утомительный только по одной причине: кропотливость. Сначала надо выбрать размерность каждого объекта: бита будет размером 80*20, мяч - 20*20, а блок - 40*40, чтобы занимал одну клетку. Размеры мяча и биты абсолютно произвольные, но если вы захотите взять другие, учтите, что придется исправлять все нижеследующие математические расчеты.

Итак, зайдите куда-нибудь типа FreeHand8 и сотворите шедевр в стиле Малевича! Каждый объект рисуйте по отдельности и сохраняйте в 24-разрядном файле BMP. Не забывайте, что потребуется две разных биты. Нарисовали - вперед в Painbrush! Выбирайте пункт меню "Вставить из файла" и скомпонуйте все спрайты на одном рисунке. Затем подгоните размер рисунка под границы спрайтов (рисунок не может быть больше, чем разрешение экрана, иначе он не поместится в буфере). Да! И не забудьте, что нам потребуется "прозрачный" цвет. Я взял для этого черный (RGB(0,0,0)), поэтому контуры на рисунках спрайтов у меня вроде как черные, а на самом деле нет (RGB(12,12,12)).Примерное расположение спрайтов

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

Примечание: как всегда я где-то напортачил, поэтому размерности спрайтов у меня с погрешностями +/- 1 пиксел.

Первые аккорды кода

Давным давно, еще в школе, нам преподавали бейсик (простой!). Поэтому я еще некоторое время то и дело боролся с искушением свалить все в одну кучу. Это не только непрактично, но и просто как-то неэстетично и плохо выглядит, поэтому давайте обсудим структуру программы.

Заместо того, чтобы вводить кучу переменных, для описания объектов используем типы BallInf и BetInf, описывающие соответственно мячик и биту. Создадим экземпляр мячика Ball и два экземпляра биты Bet1 и Bet2 "As BetInf".

Модуль mdlDirectXу нас уже есть (ведь есть, правда?! Если нет, то сюда), поэтому помимо формы frmBall код будет размещаться в модуле mdlBall, в котором будут функции и процедуры, выполняющие основные расчеты и размещение объектов на экране. В моих принципах освобождать модули кода форм от лишних процедур, не относящихся непосредственно к обработке их событий поэтому я жестоко и беспощадно БУДУ убирать все служебные процедуры в другие модули. Можете теперь в меня чем-нибудь кинуть.

Программу будем писать постепенно, создавая сначала костяк, а потом наращивая все более и более завороченные фичи (не баги!).

Для начала, создаем новый проект, добавляем в него нужные библиотеки того DirectX, с каким хотите работать - на данном этапе это не принципиально. Я использовал DirectX 6, благо он есть у всех. Дадим имя проекту - VBPong, переименуем главную форму в frmBall, добавим все вышеперечисленные модули и сохраним все хозяйство в одну новую папку. В нее же надо перекопировать графический файл, который мы предусмотрительно приготовили заранее.

Теперь, все готово и можно приступать к написанию кода.

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

Dim bEnd as Boolean  'Когда этот флаг True - это сигнал к выходу из программы

Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer)
  Select Case KeyCode
  Case vbKeyEscape   'Выйти из программы
    bEnd=True
  End Select
End Sub

'При выходе надо уничтожать все объекты DirectX
Private Sub Form_Unload(Cancel As Integer)
  mdlDirectX.DestroyDirectX
End Sub

Private Sub Form_Load()
  bEnd = False

 
  'Инициализация DirectDraw
  mdlDirectX.DDrawInit 640, 480, 16, Me
  Set lpDDSPic = CreateDDSFromBitmap(lpDD, App.Path & "\tball.bmp")  'Создаем буфер с нашей графикой
  mdlDirectX.SetColorKey 0, 0, 0  'Прозрачный цвет - черный

  'Следующие строки назовем "Регион 1". Сюда будут помещаться начальные определения игры
  '------- Пока пусто --------
  '------- Конец региона 1 --------

  'Теперь, создаем главный цикл прорисовки экрана
  While bEnd = False
    DoEvents 'Будем жалостливы к системе
    Call ClearBuffer(lpDDSBack) 'Чистим полотно

    ' Это место назовем "Регион 2". Здесь надо разместить последовательность рисования всех
    ' объектов на очередном витке цикла
    ' ------ Пока пусто ----------
    ' ------- Конец региона 2 ---------

    'Теперь, завершающие действия витка
    Call mdlDirectX.WaitForVerticalBlank 'Подождем до начала цикла обновления
    Call lpDDSFront.Flip(Nothing, DDFLIP_WAIT) ' Переводим задний буфер на передний
  Wend
  Unload Me
End Sub

И на этом код формы пока заканчивается. Внимательно присмотрясь к тому что будет делать программа, вы увидите, что после инициализации, она войдет в цикл, в котором будет находиться до самого своего конца. Идея такова, чтобы в этом цикле заставить программу отображать все нужные нам объекты каждый раз на заданных местах. Места же эти каждый раз перевычисляются на очередном витке цикла. Таким образом, вычисления сводятся к определению новых координат объекта, заданию структуры RECT, о которой говорилось в прошлой главе, и применению метода BltFast. Все эти действия помещаются в "Регионе 2" и будут занимать все дальнейшее время нашей работы в этой части. Теперь я думаю вам стало ясно, почему я был категорически против размещения всей программы в модуле кода формы. Почти все действия, относящиеся к DirectX мы уже сделали и теперь приступаем к математике.

Добавим функциональности

Переходим в модуль mdlBall. Сперва, как я уже говорил, определим структуры BallInf и BetInf, а также создадим нужные переменные.

Типы выглядят так:

Type BetInf
  X As Integer  'координата на оси X
  SpeedX As Integer 'Приращение следующего шага на оси X
End Type

Type BallInf
  X As Integer 'Координаты на осях
  Y As Integer
  SpeedX As Integer 'Приращения на осях
  SpeedY As Integer
End Type

'Две биты и мяч
Public Bet1 As BetInf 
Private Bet2 As BetInf
Private Ball As BallInf

'Игровое поле
Private Map(11, 15)  

Инициализация закончена. Текущее положение ракетки зависит от координаты X. Y у обеих ракеток постоянная - у нижней - 460, у верхней - 0. Чтобы посчитать новое положение ракетки исходя из старого, надо прибавить к координате X приращение SpeedX. Если приращение отрицательное, то бита двигается влево и наоборот. Bet1 управляется игроком, поэтому приращение задается нажатием клавиши "влево" или "вправо". Если клавиша отпускается, то приращение равно нулю. Приращение Bet2 вычисляется компьютером.

Положение мяча задается координатами X и Y. У него также существуют два осевых приращения.

Карта размера 16*12 (16 - по-горизонтали, 12 - по-вертикали). Учтите, что массив начинается с нуля и сначала идут строки.

Давайте напишем функцию, которая сбрасывает положения мяча и бит до начальных.

Public Sub Reset()
  Bet1.X = 320
  Bet1.SpeedX = 0

  Bet2.X = 320
  Bet2.SpeedX = 0

  Ball.X = 320
  Ball.Y = 240
  Ball.SpeedY = -5
  Ball.SpeedX = GetRandomSpeedX
End Sub

Здесь используется дополнительная функция GetRandomSpeedX. Она возвращает случайное приращение по оси X, колеблющееся от -5 до 5. Вот как она описывается.

Private Function GetRandomSpeedX() As Integer
  Randomize
  GetRandomSpeedX = Int(Rnd * 10) - 5
End Function

Теперь, функция, которая инициализирует карту. Ее нужно вызывать только один раз, когда загружается уровень. Вы можете сами поэкспериментировать, дописыая новые уровни.

Public Sub LoadMap(ByVal Level As Integer)
  Dim i As Integer
  Select Case Level
  Case 1 'Всего лишь две вертикальные стенки по бокам
    For i = 0 To 11
      Map(i, 0) = 1
      Map(i, 15) = 1
    Next i
  End Select
End Sub

Мы всего-лишь задали матрицу. Нам еще понадобится процедура, которая будет рисовать карту каждый виток цикла по этой матрице.

Public Sub DrawMap()
  Dim i As Integer, j As Integer
  For i = 0 to 11
    For j = 0 to 15
      If Map(i,j)=1 then
        ' Если встретили в матрице значение 1, переводим на это место на экране красный блок
        rc.Top = 0
        rc.Left = 102
        rc.Right = rc.Left + 40
        rc.bottom = 40
        Call lpDDSBack.BltFast(40 * j, 40 * i, lpDDSPic, rc, DDBLTFAST_WAIT Or DDBLTFAST_SRCCOLORKEY)
      End If
    Next j
  Next i
End Sub

Теперь, процедура, которая будет рисовать биты. В параметре ей указывается какая бита будет рисоваться, 1-нижняя, 2-верхняя

Public Sub DrawBet(ByVal Num As Integer)
  Dim BY As Integer, BX As Integer 

  If Num = 1 Then
    rc.Left = 0
    Bet1.X = Bet1.X + Bet1.SpeedX  'Новая координата X
    If Bet1.X + 80 > 600 Then Bet1.X = 600 - 80  'Проверяем на боковые стенки, которые д. б. всегда
    If Bet1.X < 40 Then Bet1.X = 40
    BX = Bet1.X
    BY = 460
  Else
    ' Поставьте здесь ремарку пока не напишете функцию DoBetAI
    DoBetAI  'Вычисляем приращение по X биты компьютера

    rc.Left = 142
    Bet2.X = Bet2.X + Bet2.SpeedX
    If Bet2.X + 80 > 600 Then Bet2.X = 600 - 80
    If Bet2.X < 40 Then Bet2.X = 40
    BX = Bet2.X
    BY = 0
  End If 

  'Заполняем недостающие элементы структуры RECT и рисуем биту на заднем буфере
  rc.Top=0
  rc.bottom = rc.Top + 20
  rc.Right = rc.Left + 80
  Call lpDDSBack.BltFast(BX, BY, lpDDSPic, rc, DDBLTFAST_WAIT Or DDBLTFAST_SRCCOLORKEY)
End Sub

Отлично, теперь "приделаем" нашей нижней бите управление. Доведите процедуру обработки события формы frmBall_KeyDown до следующего состояния, а затем допишите обработку события frmBall_KeyUp:

Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer)
  Select Case KeyCode
  Case vbKeyEscape 'Выход
    bEnd = True
  Case vbKeyLeft 'Управление первого игрока
    Bet1.SpeedX = -5
  Case vbKeyRight
    Bet1.SpeedX = 5
  End Select
End Sub

Private Sub Form_KeyUp(KeyCode As Integer, Shift As Integer)
  Bet1.SpeedX = 0
End Sub

Добавим в регион 1 следующее:

mdlBall.Reset
mdlBall.LoadMap 1

А в регион 2 это:

mdlBall.DrawMap
mdlBall.DrawBet 1

Запустите программу. Вы увидите, что уровень уже нарисован и вы можете двигать свою битку в пределах стенок. Эти стенки по определению должны быть всегда, учитывайте это. Выходите из программы как и раньше, с помощью Escape.

Свободный полет

Осталось самое сложное - запрограммировать мячик, чтобы он летал и отскакивал от тех мест, которые в массиве помечены единичкой. Я сделал это вот каким нехитрым образом. Итак, представьте себе двухмерный мячик. В процессе полета он может находиться целиком в одном секторе (каждый сектор - 40*40, если помните, а мяч - 20*20), в двух секторах одновременно, в трех не может, а вот в четырех - запросто. Теперь представьте, что у мячика есть крайние точки, так называемые - "контрольные точки". Можно сказать, что если одна из этих точек находится в каком-то секторе, то некая часть мячика тоже находится в этом секторе. Таким образом, если контрольная точка находится в секторе, который помечен 0, то все нормально, однако, если контрольная точка попадает в сектор с 1, то он уже занят блоком и мячик должен отскочить. В какую сторону? Посмотрите на такой рисунок:

Четыре контрольных точки

Вы видите на нем четыре контрольных точки, отмеченных красным, а также их координаты в системе координат мяча. Если мяч попал в занятый сектор точкой номер 1, то очевидно, что он стукнулся о преграду, находящуюся сверху него и должен отсочить вниз. Это легко задать формулой SpeedY=-SpeedY. Если "просигналила" точка номер два, то мяч отскакивает влево и т. д. При этом, мяч нужно "отпозиционировать" то-есть поставить просигналившую точку ровно на границу с занятым сектором, чтобы мяч не "залипал" там.

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

Хорош этот способ или нет, решайте сами. По крайней мере он работает.

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

Private Function CheckPoint() As Integer
  Dim Ret As Integer
  Ret = 0
  'Проверяем точки
  If Map(Int((Ball.Y + 10) / 40), Int(Ball.X / 40)) <> 0 Then Ret = 4
  If Map(Int((Ball.Y + 20) / 40), Int((Ball.X + 10) / 40)) <> 0 Then Ret = 3
  If Map(Int((Ball.Y + 10) / 40), Int((Ball.X + 20) / 40)) <> 0 Then Ret = 2
  If Map(Int(Ball.Y / 40), Int((Ball.X + 10) / 40)) <> 0 Then Ret = 1
  CheckPoint = Ret
End Function

Функция будет всегда выдавать только одну точку с приоритетом по часовой стрелке.

Настал момент рисования мяча.

Public Sub DrawBall()
  MoveBall 'Вычисляем новые координаты мяча
  rc.Top = 0
  rc.Left = 81
  rc.bottom = 21
  rc.Right = 102
  ' Рисуем его
  Call lpDDSBack.BltFast(Ball.X, Ball.Y, lpDDSPic, rc, DDBLTFAST_WAIT Or DDBLTFAST_SRCCOLORKEY)
End Sub

Private Sub MoveBall()
  Dim Pt As Integer
  'Делем следующий шаг
  Ball.X = Ball.X + Ball.SpeedX
  Ball.Y = Ball.Y + Ball.SpeedY
  Pt = CheckPoint 'И проеряем, где после этого оказался мяч
  'Проверяем попадание мяча одной из контрольных точек в занятый сектор
  If Pt > 0 Then
    Select Case Pt
    Case 1 'Верх
      Ball.SpeedY = -Ball.SpeedY 'Изменение осевой скорости
      Ball.Y = Int(Ball.Y / 40) * 40 + 40 'Нормализация осевой координаты
    Case 2 'Право
      Ball.SpeedX = -Ball.SpeedX
      Ball.X = Int((Ball.X + 20) / 40) * 40 - 20
    Case 3 'Низ
      Ball.SpeedY = -Ball.SpeedY
      Ball.Y = Int((Ball.Y + 20) / 40) * 40 - 20
    Case 4 'Лево
      Ball.SpeedX = -Ball.SpeedX
      Ball.X = Int(Ball.X / 40) * 40 + 40
    End Select 'Дополнительных точек пока нет
  Else
    'Нижняя ракетка
    If Ball.Y + 20 > 460 Then
       If (Ball.X + 15) >= Bet1.X And (Ball.X + 5) <= (Bet1.X + 80) Then
         'Отбили мяч
         Ball.SpeedY = -Ball.SpeedY
         Ball.SpeedX = GetDirectSpeedX
         Ball.Y = 460 - 20
       Else
         'Мяч упал
         Reset
       End If
     End If
    'Верхняя ракетка
    If Ball.Y < 20 Then
      If (Ball.X + 15) >= Bet2.X And (Ball.X + 5) <= (Bet2.X + 80) Then
        Ball.SpeedY = -Ball.SpeedY
        Ball.SpeedX = GetRandomSpeedX 
        Ball.Y = 20
      Else
        Reset
      End If
    End If
  End If
End Sub

Private Function GetDirectSpeedX() As Integer
  'Направление мяча по X в зависимости от того, каким местом биты отбил его игрок
  GetDirectSpeedX = Int(((Ball.X + 10) - (Bet1.X + 40)) / 8)
End Function

Наконец, последняя функция - вычисление приращения по X ракетки компьютера в зависимости от позиции мяча. Все очень просто. Компьютер всегда старается, чтобы левый и правый край мяча попадал в поле его ракетки.

Private Sub DoBetAI()
  If Ball.X <= Bet2.X + 10 Then Bet2.SpeedX = -5
  If Ball.X + 20 >= Bet2.X + 80 Then Bet2.SpeedX = 5
  If Ball.X > Bet2.X And Ball.X + 20 < Bet2.X + 80 Then Bet2.SpeedX = 0
End Sub

Громкие апплодисменты! Написание модуля завершено. Теперь добавьте в регион две cтроки:

mdlBall.DrawBet 2
mdlBall.DrawBall

И запустите программу (Не забудьте снять ремарку с вызова функции DoBetAI). Работает? Ура!

Надеюсь, теперь вам немного стало ясно, как делаются игрушки на DirectX. Если нет - то это нормально. Просто надо упражняться, упражняться и еще раз упражняться... Не помню кто сказал...

На этом части три пришел конец. В следующей части поговорим об эффектах.

Приложения

Пример - проект VBPong ~25кб Загрузить

Приятного программирования, Antiloop