[Главная страница] [Предыдыущая часть] [Следующая часть] [Приложения к этой части]
Да уж давно бы пора! - подумают некоторые. Ну ладно, не злитесь, в этой главе мы разработаем и претворим в жизнь самую настоящую 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